Compare commits

...

6 commits

Author SHA1 Message Date
Francis Cao
1229663814 Merge branch 'analytics' of https://github.com/umami-software/umami into dev
Some checks failed
Create docker images (cloud) / Build, push, and deploy (push) Has been cancelled
Node.js CI / build (push) Has been cancelled
2026-01-29 10:24:15 -08:00
Francis Cao
e57239de1e Fix share parameter logic 2026-01-29 10:13:08 -08:00
Mike Cao
abfb78bb98 Fixed mobile menus.
Some checks failed
Node.js CI / build (push) Has been cancelled
2026-01-29 09:23:53 -08:00
Mike Cao
9b310dacef Remove events section from share page overview
Share page overview should look the same as normal app overview

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 01:00:23 -08:00
Mike Cao
fd2e2047cd Restrict x-umami-client-* headers to cloud mode only.
Some checks failed
Create docker images (cloud) / Build, push, and deploy (push) Has been cancelled
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
2026-01-23 20:52:39 -08:00
Mike Cao
fbd0564133 Add x-umami-client-* headers as highest priority source for client detection. 2026-01-23 20:04:01 -08:00
6 changed files with 21 additions and 32 deletions

View file

@ -42,7 +42,7 @@ export function MobileNav() {
{({ close }) => { {({ close }) => {
return ( return (
<> <>
<NavMenu padding="3" onItemClick={close} border="bottom"> <NavMenu padding="3" onItemClick={close} border="bottom" width="100%">
<NavButton /> <NavButton />
{links.map(link => { {links.map(link => {
return ( return (

View file

@ -1,15 +1,13 @@
import { Grid, Heading, Row, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen'; import { Grid, Heading, Row, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
import { GridRow } from '@/components/common/GridRow'; import { GridRow } from '@/components/common/GridRow';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { EventsChart } from '@/components/metrics/EventsChart';
import { MetricsTable } from '@/components/metrics/MetricsTable'; import { MetricsTable } from '@/components/metrics/MetricsTable';
import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic'; import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic';
import { WorldMap } from '@/components/metrics/WorldMap'; import { WorldMap } from '@/components/metrics/WorldMap';
export function WebsitePanels({ websiteId }: { websiteId: string }) { export function WebsitePanels({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { pathname } = useNavigation();
const tableProps = { const tableProps = {
websiteId, websiteId,
limit: 10, limit: 10,
@ -18,7 +16,6 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) {
metric: formatMessage(labels.visitors), metric: formatMessage(labels.visitors),
}; };
const rowProps = { minHeight: '570px' }; const rowProps = { minHeight: '570px' };
const isSharePage = pathname.includes('/share/');
return ( return (
<Grid gap="3"> <Grid gap="3">
@ -116,25 +113,6 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) {
<WeeklyTraffic websiteId={websiteId} /> <WeeklyTraffic websiteId={websiteId} />
</Panel> </Panel>
</GridRow> </GridRow>
{isSharePage && (
<GridRow layout="two-one" {...rowProps}>
<Panel>
<Heading size="2">{formatMessage(labels.events)}</Heading>
<Row border="bottom" marginBottom="4" />
<MetricsTable
websiteId={websiteId}
type="event"
title={formatMessage(labels.event)}
metric={formatMessage(labels.count)}
limit={15}
filterLink={false}
/>
</Panel>
<Panel gridColumn={{ xs: 'span 1', md: 'span 2' }}>
<EventsChart websiteId={websiteId} />
</Panel>
</GridRow>
)}
</Grid> </Grid>
); );
} }

View file

@ -129,7 +129,7 @@ export function ShareNav({
const items = allItems const items = allItems
.map(section => ({ .map(section => ({
label: section.label, label: section.label,
items: section.items.filter(item => parameters[item.id] !== false), items: section.items.filter(item => parameters[item.id] === true),
})) }))
.filter(section => section.items.length > 0); .filter(section => section.items.length > 0);
@ -144,12 +144,12 @@ export function ShareNav({
position={isMobile ? undefined : 'fixed'} position={isMobile ? undefined : 'fixed'}
padding="3" padding="3"
width={isMobile ? '100%' : collapsed ? '60px' : '240px'} width={isMobile ? '100%' : collapsed ? '60px' : '240px'}
maxHeight="100vh" maxHeight="100dvh"
height="100vh" height="100dvh"
border={isMobile ? undefined : 'right'} border={isMobile ? undefined : 'right'}
borderColor={isMobile ? undefined : '4'} borderColor={isMobile ? undefined : '4'}
> >
<Row as="header" gap alignItems="center" paddingY="3" justifyContent="space-between"> <Row as="header" gap alignItems="center" justifyContent="space-between">
{!collapsed && ( {!collapsed && (
<a href={logoUrl} target="_blank" rel="noopener" style={{ marginLeft: 12 }}> <a href={logoUrl} target="_blank" rel="noopener" style={{ marginLeft: 12 }}>
<Row alignItems="center" gap> <Row alignItems="center" gap>

View file

@ -9,7 +9,7 @@ export function MobileMenuButton(props: DialogProps) {
<Menu /> <Menu />
</Icon> </Icon>
</Button> </Button>
<Modal placement="left"> <Modal placement="left" offset="80px">
<Dialog variant="sheet" {...props} style={{ width: 'auto' }} /> <Dialog variant="sheet" {...props} style={{ width: 'auto' }} />
</Modal> </Modal>
</DialogTrigger> </DialogTrigger>

View file

@ -10,6 +10,16 @@ import { safeDecodeURIComponent } from '@/lib/url';
const MAXMIND = 'maxmind'; const MAXMIND = 'maxmind';
const PROVIDER_HEADERS = [ const PROVIDER_HEADERS = [
// Umami custom headers (cloud mode only)
...(process.env.CLOUD_MODE
? [
{
countryHeader: 'x-umami-client-country',
regionHeader: 'x-umami-client-region',
cityHeader: 'x-umami-client-city',
},
]
: []),
// Cloudflare headers // Cloudflare headers
{ {
countryHeader: 'cf-ipcountry', countryHeader: 'cf-ipcountry',
@ -66,13 +76,13 @@ function decodeHeader(s: string | undefined | null): string | undefined | null {
return Buffer.from(s, 'latin1').toString('utf-8'); return Buffer.from(s, 'latin1').toString('utf-8');
} }
export async function getLocation(ip: string = '', headers: Headers, hasPayloadIP: boolean) { export async function getLocation(ip: string = '', headers: Headers, skipHeaders: boolean) {
// Ignore local ips // Ignore local ips
if (!ip || (await isLocalhost(ip))) { if (!ip || (await isLocalhost(ip))) {
return null; return null;
} }
if (!hasPayloadIP && !process.env.SKIP_LOCATION_HEADERS) { if (!skipHeaders && !process.env.SKIP_LOCATION_HEADERS) {
for (const provider of PROVIDER_HEADERS) { for (const provider of PROVIDER_HEADERS) {
const countryHeader = headers.get(provider.countryHeader); const countryHeader = headers.get(provider.countryHeader);
if (countryHeader) { if (countryHeader) {

View file

@ -1,6 +1,7 @@
import ipaddr from 'ipaddr.js'; import ipaddr from 'ipaddr.js';
export const IP_ADDRESS_HEADERS = [ export const IP_ADDRESS_HEADERS = [
...(process.env.CLOUD_MODE ? ['x-umami-client-ip'] : []), // Umami custom header (cloud mode only)
'true-client-ip', // CDN 'true-client-ip', // CDN
'cf-connecting-ip', // Cloudflare 'cf-connecting-ip', // Cloudflare
'fastly-client-ip', // Fastly 'fastly-client-ip', // Fastly
@ -81,7 +82,7 @@ export function getIpAddress(headers: Headers) {
} }
if (header === 'forwarded') { if (header === 'forwarded') {
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/); const match = ip.match(/for=(\[?[0-9a-fA-F:.]+]?)/);
if (match) { if (match) {
return resolveIp(match[1]); return resolveIp(match[1]);