Skip to content
Open
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/1.bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ body:
- type: input
attributes:
label: Which Umami version are you using? (if relevant)
description: 'For example: Chrome, Edge, Firefox, etc'
description: 'For example: 2.18.0, 2.15.1, 1.39.0, etc'
- type: input
attributes:
label: Which browser are you using? (if relevant)
Expand Down
5 changes: 3 additions & 2 deletions scripts/data-migrations/convert-utm-clid-columns.sql
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ FROM (SELECT event_id, website_id, session_id,
(regexp_matches(url_query, '(?:[&?]|^)utm_medium=([^&]+)', 'i'))[1] AS utm_medium,
(regexp_matches(url_query, '(?:[&?]|^)utm_source=([^&]+)', 'i'))[1] AS utm_source,
(regexp_matches(url_query, '(?:[&?]|^)utm_term=([^&]+)', 'i'))[1] AS utm_term
FROM "website_event") url
FROM "website_event"
WHERE url_query IS NOT NULL) url
WHERE we.event_id = url.event_id
and we.session_id = url.session_id
and we.website_id = url.website_id;
Expand All @@ -45,4 +46,4 @@ SET fbclid = LEFT(SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[
utm_medium = LEFT(SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_medium=[^&]+'), '=', -1), '&', 1), 255),
utm_source = LEFT(SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_source=[^&]+'), '=', -1), '&', 1), 255),
utm_term = LEFT(SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_term=[^&]+'), '=', -1), '&', 1), 255)
WHERE 1 = 1;
WHERE url_query IS NOT NULL;
13 changes: 8 additions & 5 deletions src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Button, Text, Icon, Icons } from 'react-basics';
import { useLocale, useMessages, useTeamUrl } from '@/components/hooks';
import useDashboard from '@/store/dashboard';
import Link from 'next/link';
import { useMemo } from 'react';
import { Button, Icon, Icons, Text } from 'react-basics';
import { firstBy } from 'thenby';
import Link from 'next/link';
import WebsiteChart from './WebsiteChart';
import useDashboard from '@/store/dashboard';
import WebsiteHeader from './WebsiteHeader';
import WebsiteMetrics from './WebsiteMetrics';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
import { useMessages, useLocale, useTeamUrl } from '@/components/hooks';

export default function WebsiteChartList({
websites,
Expand Down Expand Up @@ -46,7 +47,9 @@ export default function WebsiteChartList({
</Button>
</Link>
</WebsiteHeader>
<WebsiteMetricsBar websiteId={id} showChange={true} />
<WebsiteMetrics websiteId={id}>
<WebsiteMetricsBar websiteId={id} showChange={true} />
</WebsiteMetrics>
{showCharts && <WebsiteChart websiteId={id} />}
</div>
) : null;
Expand Down
11 changes: 7 additions & 4 deletions src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use client';
import { usePathname } from 'next/navigation';
import FilterTags from '@/components/metrics/FilterTags';
import { useNavigation } from '@/components/hooks';
import FilterTags from '@/components/metrics/FilterTags';
import { FILTER_COLUMNS } from '@/lib/constants';
import { usePathname } from 'next/navigation';
import WebsiteChart from './WebsiteChart';
import WebsiteExpandedView from './WebsiteExpandedView';
import WebsiteHeader from './WebsiteHeader';
import WebsiteMetrics from './WebsiteMetrics';
import WebsiteMetricsBar from './WebsiteMetricsBar';
import WebsiteTableView from './WebsiteTableView';
import { FILTER_COLUMNS } from '@/lib/constants';

export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
const pathname = usePathname();
Expand All @@ -27,7 +28,9 @@ export default function WebsiteDetailsPage({ websiteId }: { websiteId: string })
<>
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
<FilterTags websiteId={websiteId} params={params} />
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} sticky={true} />
<WebsiteMetrics websiteId={websiteId} showFilter={true} sticky={true}>
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
</WebsiteMetrics>
<WebsiteChart websiteId={websiteId} />
{!view && <WebsiteTableView websiteId={websiteId} />}
{view && <WebsiteExpandedView websiteId={websiteId} />}
Expand Down
66 changes: 66 additions & 0 deletions src/app/(main)/websites/[websiteId]/WebsiteMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useMessages, useSticky } from '@/components/hooks';
import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
import useStore, { setWebsiteDateCompare } from '@/store/websites';
import classNames from 'classnames';
import { ReactNode } from 'react';
import { Dropdown, Item } from 'react-basics';
import WebsiteFilterButton from './WebsiteFilterButton';
import styles from './WebsiteMetrics.module.css';

export function WebsiteMetrics({
websiteId,
sticky,
compareMode = false,
showFilter = false,
children,
}: {
websiteId: string;
sticky?: boolean;
compareMode?: boolean;
showFilter?: boolean;
children?: ReactNode;
}) {
const { formatMessage, labels } = useMessages();
const { ref, isSticky } = useSticky({ enabled: sticky });
const dateCompare = useStore(state => state[websiteId]?.dateCompare);

const items = [
{ label: formatMessage(labels.previousPeriod), value: 'prev' },
{ label: formatMessage(labels.previousYear), value: 'yoy' },
];

return (
<div
ref={ref}
className={classNames(styles.container, {
[styles.sticky]: sticky,
[styles.isSticky]: sticky && isSticky,
})}
>
<div>{children}</div>
<div className={styles.actions}>
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
<WebsiteDateFilter websiteId={websiteId} showAllTime={!compareMode} />
{compareMode && (
<div className={styles.vs}>
<b>VS</b>
<Dropdown
className={styles.dropdown}
items={items}
value={dateCompare || 'prev'}
renderValue={value => items.find(i => i.value === value)?.label}
alignment="end"
onChange={(value: any) => setWebsiteDateCompare(websiteId, value)}
>
{items.map(({ label, value }) => (
<Item key={value}>{label}</Item>
))}
</Dropdown>
</div>
)}
</div>
</div>
);
}

export default WebsiteMetrics;
84 changes: 20 additions & 64 deletions src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { Dropdown, Item } from 'react-basics';
import classNames from 'classnames';
import { useDateRange, useMessages, useSticky } from '@/components/hooks';
import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
import { useDateRange, useMessages } from '@/components/hooks';
import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats';
import MetricCard from '@/components/metrics/MetricCard';
import MetricsBar from '@/components/metrics/MetricsBar';
import { formatShortTime, formatLongNumber } from '@/lib/format';
import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats';
import useStore, { setWebsiteDateCompare } from '@/store/websites';
import WebsiteFilterButton from './WebsiteFilterButton';
import styles from './WebsiteMetricsBar.module.css';
import { formatLongNumber, formatShortTime } from '@/lib/format';
import useStore from '@/store/websites';

export function WebsiteMetricsBar({
websiteId,
sticky,
showChange = false,
compareMode = false,
showFilter = false,
}: {
websiteId: string;
sticky?: boolean;
Expand All @@ -26,7 +19,6 @@ export function WebsiteMetricsBar({
const { dateRange } = useDateRange(websiteId);
const { formatMessage, labels } = useMessages();
const dateCompare = useStore(state => state[websiteId]?.dateCompare);
const { ref, isSticky } = useSticky({ enabled: sticky });
const { data, isLoading, isFetched, error } = useWebsiteStats(
websiteId,
compareMode && dateCompare,
Expand Down Expand Up @@ -76,60 +68,24 @@ export function WebsiteMetricsBar({
]
: [];

const items = [
{ label: formatMessage(labels.previousPeriod), value: 'prev' },
{ label: formatMessage(labels.previousYear), value: 'yoy' },
];

return (
<div
ref={ref}
className={classNames(styles.container, {
[styles.sticky]: sticky,
[styles.isSticky]: sticky && isSticky,
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
{metrics.map(({ label, value, prev, change, formatValue, reverseColors }) => {
return (
<MetricCard
key={label}
value={value}
previousValue={prev}
label={label}
change={change}
formatValue={formatValue}
reverseColors={reverseColors}
showChange={!isAllTime && (compareMode || showChange)}
showPrevious={!isAllTime && compareMode}
/>
);
})}
>
<div>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
{metrics.map(({ label, value, prev, change, formatValue, reverseColors }) => {
return (
<MetricCard
key={label}
value={value}
previousValue={prev}
label={label}
change={change}
formatValue={formatValue}
reverseColors={reverseColors}
showChange={!isAllTime && (compareMode || showChange)}
showPrevious={!isAllTime && compareMode}
/>
);
})}
</MetricsBar>
</div>
<div className={styles.actions}>
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
<WebsiteDateFilter websiteId={websiteId} showAllTime={!compareMode} />
{compareMode && (
<div className={styles.vs}>
<b>VS</b>
<Dropdown
className={styles.dropdown}
items={items}
value={dateCompare || 'prev'}
renderValue={value => items.find(i => i.value === value)?.label}
alignment="end"
onChange={(value: any) => setWebsiteDateCompare(websiteId, value)}
>
{items.map(({ label, value }) => (
<Item key={value}>{label}</Item>
))}
</Dropdown>
</div>
)}
</div>
</div>
</MetricsBar>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client';
import WebsiteHeader from '../WebsiteHeader';
import WebsiteMetricsBar from '../WebsiteMetricsBar';
import FilterTags from '@/components/metrics/FilterTags';
import { useNavigation } from '@/components/hooks';
import FilterTags from '@/components/metrics/FilterTags';
import { FILTER_COLUMNS } from '@/lib/constants';
import WebsiteChart from '../WebsiteChart';
import WebsiteHeader from '../WebsiteHeader';
import WebsiteMetrics from '../WebsiteMetrics';
import WebsiteMetricsBar from '../WebsiteMetricsBar';
import WebsiteCompareTables from './WebsiteCompareTables';

export function WebsiteComparePage({ websiteId }) {
Expand All @@ -21,7 +22,9 @@ export function WebsiteComparePage({ websiteId }) {
<>
<WebsiteHeader websiteId={websiteId} />
<FilterTags websiteId={websiteId} params={params} />
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showFilter={true} />
<WebsiteMetrics websiteId={websiteId} compareMode={true} showFilter={true}>
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} />
</WebsiteMetrics>
<WebsiteChart websiteId={websiteId} compareMode={true} />
<WebsiteCompareTables websiteId={websiteId} />
</>
Expand Down
49 changes: 22 additions & 27 deletions src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,36 @@
import { useMessages } from '@/components/hooks';
import useWebsiteSessionStats from '@/components/hooks/queries/useWebsiteSessionStats';
import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
import MetricCard from '@/components/metrics/MetricCard';
import MetricsBar from '@/components/metrics/MetricsBar';
import { formatLongNumber } from '@/lib/format';
import { Flexbox } from 'react-basics';

export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetched, error } = useWebsiteSessionStats(websiteId);

return (
<Flexbox direction="row" justifyContent="space-between" style={{ minHeight: 120 }}>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
<MetricCard
value={data?.visitors?.value}
label={formatMessage(labels.visitors)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.visits?.value}
label={formatMessage(labels.visits)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.pageviews?.value}
label={formatMessage(labels.views)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.events?.value}
label={formatMessage(labels.events)}
formatValue={formatLongNumber}
/>
</MetricsBar>
<WebsiteDateFilter websiteId={websiteId} />
</Flexbox>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
<MetricCard
value={data?.visitors?.value}
label={formatMessage(labels.visitors)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.visits?.value}
label={formatMessage(labels.visits)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.pageviews?.value}
label={formatMessage(labels.views)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.events?.value}
label={formatMessage(labels.events)}
formatValue={formatLongNumber}
/>
</MetricsBar>
);
}

Expand Down
17 changes: 10 additions & 7 deletions src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'use client';
import WebsiteHeader from '../WebsiteHeader';
import EventsDataTable from './EventsDataTable';
import EventsMetricsBar from './EventsMetricsBar';
import EventsChart from '@/components/metrics/EventsChart';
import { useMessages } from '@/components/hooks';
import { GridRow } from '@/components/layout/Grid';
import EventsChart from '@/components/metrics/EventsChart';
import EventsTable from '@/components/metrics/EventsTable';
import { useMessages } from '@/components/hooks';
import { Item, Tabs } from 'react-basics';
import { useState } from 'react';
import { Item, Tabs } from 'react-basics';
import WebsiteHeader from '../WebsiteHeader';
import WebsiteMetrics from '../WebsiteMetrics';
import EventProperties from './EventProperties';
import EventsDataTable from './EventsDataTable';
import EventsMetricsBar from './EventsMetricsBar';

export default function EventsPage({ websiteId }) {
const [label, setLabel] = useState(null);
Expand All @@ -22,7 +23,9 @@ export default function EventsPage({ websiteId }) {
return (
<>
<WebsiteHeader websiteId={websiteId} />
<EventsMetricsBar websiteId={websiteId} />
<WebsiteMetrics websiteId={websiteId}>
<EventsMetricsBar websiteId={websiteId} />
</WebsiteMetrics>
<GridRow columns="two-one">
<EventsChart websiteId={websiteId} focusLabel={label} />
<EventsTable
Expand Down
Loading