mirror of
https://github.com/umami-software/umami.git
synced 2026-05-30 06:47:25 +00:00
performance updates checkpoint
This commit is contained in:
@@ -1,72 +1,105 @@
|
||||
-- Create Performance
|
||||
CREATE TABLE umami.website_performance
|
||||
(
|
||||
website_id UUID,
|
||||
session_id UUID,
|
||||
visit_id UUID,
|
||||
url_path String,
|
||||
lcp Nullable(Decimal(10, 1)),
|
||||
inp Nullable(Decimal(10, 1)),
|
||||
cls Nullable(Decimal(10, 4)),
|
||||
fcp Nullable(Decimal(10, 1)),
|
||||
ttfb Nullable(Decimal(10, 1)),
|
||||
created_at DateTime('UTC')
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
PARTITION BY toYYYYMM(created_at)
|
||||
ORDER BY (website_id, toStartOfHour(created_at), session_id)
|
||||
SETTINGS index_granularity = 8192;
|
||||
-- Add performance columns to website_event
|
||||
ALTER TABLE umami.website_event ADD COLUMN lcp Nullable(Decimal(10, 1)) AFTER twclid;
|
||||
ALTER TABLE umami.website_event ADD COLUMN inp Nullable(Decimal(10, 1)) AFTER lcp;
|
||||
ALTER TABLE umami.website_event ADD COLUMN cls Nullable(Decimal(10, 4)) AFTER inp;
|
||||
ALTER TABLE umami.website_event ADD COLUMN fcp Nullable(Decimal(10, 1)) AFTER cls;
|
||||
ALTER TABLE umami.website_event ADD COLUMN ttfb Nullable(Decimal(10, 1)) AFTER fcp;
|
||||
|
||||
-- Performance hourly aggregation
|
||||
CREATE TABLE umami.website_performance_hourly
|
||||
(
|
||||
website_id UUID,
|
||||
url_path String,
|
||||
lcp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))),
|
||||
lcp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))),
|
||||
lcp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))),
|
||||
inp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))),
|
||||
inp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))),
|
||||
inp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))),
|
||||
cls_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 4))),
|
||||
cls_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 4))),
|
||||
cls_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 4))),
|
||||
fcp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))),
|
||||
fcp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))),
|
||||
fcp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))),
|
||||
ttfb_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))),
|
||||
ttfb_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))),
|
||||
ttfb_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))),
|
||||
sample_count SimpleAggregateFunction(sum, UInt64),
|
||||
created_at DateTime('UTC')
|
||||
)
|
||||
ENGINE = AggregatingMergeTree
|
||||
PARTITION BY toYYYYMM(created_at)
|
||||
ORDER BY (website_id, toStartOfHour(created_at), url_path)
|
||||
SETTINGS index_granularity = 8192;
|
||||
-- Update materialized view to exclude performance events from view counts
|
||||
DROP TABLE umami.website_event_stats_hourly_mv;
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.website_performance_hourly_mv
|
||||
TO umami.website_performance_hourly
|
||||
CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv
|
||||
TO umami.website_event_stats_hourly
|
||||
AS
|
||||
SELECT
|
||||
website_id,
|
||||
url_path,
|
||||
quantileState(0.5)(lcp) as lcp_p50,
|
||||
quantileState(0.75)(lcp) as lcp_p75,
|
||||
quantileState(0.95)(lcp) as lcp_p95,
|
||||
quantileState(0.5)(inp) as inp_p50,
|
||||
quantileState(0.75)(inp) as inp_p75,
|
||||
quantileState(0.95)(inp) as inp_p95,
|
||||
quantileState(0.5)(cls) as cls_p50,
|
||||
quantileState(0.75)(cls) as cls_p75,
|
||||
quantileState(0.95)(cls) as cls_p95,
|
||||
quantileState(0.5)(fcp) as fcp_p50,
|
||||
quantileState(0.75)(fcp) as fcp_p75,
|
||||
quantileState(0.95)(fcp) as fcp_p95,
|
||||
quantileState(0.5)(ttfb) as ttfb_p50,
|
||||
quantileState(0.75)(ttfb) as ttfb_p75,
|
||||
quantileState(0.95)(ttfb) as ttfb_p95,
|
||||
count() as sample_count,
|
||||
toStartOfHour(created_at) as created_at
|
||||
FROM umami.website_performance
|
||||
GROUP BY website_id, url_path, created_at;
|
||||
session_id,
|
||||
visit_id,
|
||||
hostnames as hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
entry_url,
|
||||
exit_url,
|
||||
url_paths as url_path,
|
||||
url_query,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
utm_content,
|
||||
utm_term,
|
||||
referrer_domain,
|
||||
page_title,
|
||||
gclid,
|
||||
fbclid,
|
||||
msclkid,
|
||||
ttclid,
|
||||
li_fat_id,
|
||||
twclid,
|
||||
event_type,
|
||||
event_name,
|
||||
views,
|
||||
min_time,
|
||||
max_time,
|
||||
tag,
|
||||
distinct_id,
|
||||
timestamp as created_at
|
||||
FROM (SELECT
|
||||
website_id,
|
||||
session_id,
|
||||
visit_id,
|
||||
arrayFilter(x -> x != '', groupArray(hostname)) hostnames,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
argMinState(url_path, created_at) entry_url,
|
||||
argMaxState(url_path, created_at) exit_url,
|
||||
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
|
||||
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
|
||||
arrayFilter(x -> x != '', groupArray(utm_source)) utm_source,
|
||||
arrayFilter(x -> x != '', groupArray(utm_medium)) utm_medium,
|
||||
arrayFilter(x -> x != '', groupArray(utm_campaign)) utm_campaign,
|
||||
arrayFilter(x -> x != '', groupArray(utm_content)) utm_content,
|
||||
arrayFilter(x -> x != '', groupArray(utm_term)) utm_term,
|
||||
arrayFilter(x -> x != '' and x != hostname, groupArray(referrer_domain)) referrer_domain,
|
||||
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
|
||||
arrayFilter(x -> x != '', groupArray(gclid)) gclid,
|
||||
arrayFilter(x -> x != '', groupArray(fbclid)) fbclid,
|
||||
arrayFilter(x -> x != '', groupArray(msclkid)) msclkid,
|
||||
arrayFilter(x -> x != '', groupArray(ttclid)) ttclid,
|
||||
arrayFilter(x -> x != '', groupArray(li_fat_id)) li_fat_id,
|
||||
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
|
||||
event_type,
|
||||
if(event_type = 2, groupArray(event_name), []) event_name,
|
||||
sumIf(1, event_type NOT IN (2, 5)) views,
|
||||
min(created_at) min_time,
|
||||
max(created_at) max_time,
|
||||
arrayFilter(x -> x != '', groupArray(tag)) tag,
|
||||
distinct_id,
|
||||
toStartOfHour(created_at) timestamp
|
||||
FROM umami.website_event
|
||||
GROUP BY website_id,
|
||||
session_id,
|
||||
visit_id,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
event_type,
|
||||
distinct_id,
|
||||
timestamp);
|
||||
|
||||
@@ -34,6 +34,12 @@ CREATE TABLE umami.website_event
|
||||
ttclid String,
|
||||
li_fat_id String,
|
||||
twclid String,
|
||||
--performance
|
||||
lcp Nullable(Decimal(10, 1)),
|
||||
inp Nullable(Decimal(10, 1)),
|
||||
cls Nullable(Decimal(10, 4)),
|
||||
fcp Nullable(Decimal(10, 1)),
|
||||
ttfb Nullable(Decimal(10, 1)),
|
||||
--events
|
||||
event_type UInt32,
|
||||
event_name String,
|
||||
@@ -209,7 +215,7 @@ FROM (SELECT
|
||||
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
|
||||
event_type,
|
||||
if(event_type = 2, groupArray(event_name), []) event_name,
|
||||
sumIf(1, event_type != 2) views,
|
||||
sumIf(1, event_type NOT IN (2, 5)) views,
|
||||
min(created_at) min_time,
|
||||
max(created_at) max_time,
|
||||
arrayFilter(x -> x != '', groupArray(tag)) tag,
|
||||
@@ -282,79 +288,6 @@ JOIN (SELECT event_id, string_value as currency
|
||||
ON c.event_id = ed.event_id
|
||||
WHERE positionCaseInsensitive(data_key, 'revenue') > 0;
|
||||
|
||||
-- Create Performance
|
||||
CREATE TABLE umami.website_performance
|
||||
(
|
||||
website_id UUID,
|
||||
session_id UUID,
|
||||
visit_id UUID,
|
||||
url_path String,
|
||||
lcp Nullable(Decimal(10, 1)),
|
||||
inp Nullable(Decimal(10, 1)),
|
||||
cls Nullable(Decimal(10, 4)),
|
||||
fcp Nullable(Decimal(10, 1)),
|
||||
ttfb Nullable(Decimal(10, 1)),
|
||||
created_at DateTime('UTC')
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
PARTITION BY toYYYYMM(created_at)
|
||||
ORDER BY (website_id, toStartOfHour(created_at), session_id)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
-- Performance hourly aggregation
|
||||
CREATE TABLE umami.website_performance_hourly
|
||||
(
|
||||
website_id UUID,
|
||||
url_path String,
|
||||
lcp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))),
|
||||
lcp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))),
|
||||
lcp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))),
|
||||
inp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))),
|
||||
inp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))),
|
||||
inp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))),
|
||||
cls_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 4))),
|
||||
cls_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 4))),
|
||||
cls_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 4))),
|
||||
fcp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))),
|
||||
fcp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))),
|
||||
fcp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))),
|
||||
ttfb_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))),
|
||||
ttfb_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))),
|
||||
ttfb_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))),
|
||||
sample_count SimpleAggregateFunction(sum, UInt64),
|
||||
created_at DateTime('UTC')
|
||||
)
|
||||
ENGINE = AggregatingMergeTree
|
||||
PARTITION BY toYYYYMM(created_at)
|
||||
ORDER BY (website_id, toStartOfHour(created_at), url_path)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.website_performance_hourly_mv
|
||||
TO umami.website_performance_hourly
|
||||
AS
|
||||
SELECT
|
||||
website_id,
|
||||
url_path,
|
||||
quantileState(0.5)(lcp) as lcp_p50,
|
||||
quantileState(0.75)(lcp) as lcp_p75,
|
||||
quantileState(0.95)(lcp) as lcp_p95,
|
||||
quantileState(0.5)(inp) as inp_p50,
|
||||
quantileState(0.75)(inp) as inp_p75,
|
||||
quantileState(0.95)(inp) as inp_p95,
|
||||
quantileState(0.5)(cls) as cls_p50,
|
||||
quantileState(0.75)(cls) as cls_p75,
|
||||
quantileState(0.95)(cls) as cls_p95,
|
||||
quantileState(0.5)(fcp) as fcp_p50,
|
||||
quantileState(0.75)(fcp) as fcp_p75,
|
||||
quantileState(0.95)(fcp) as fcp_p95,
|
||||
quantileState(0.5)(ttfb) as ttfb_p50,
|
||||
quantileState(0.75)(ttfb) as ttfb_p75,
|
||||
quantileState(0.95)(ttfb) as ttfb_p95,
|
||||
count() as sample_count,
|
||||
toStartOfHour(created_at) as created_at
|
||||
FROM umami.website_performance
|
||||
GROUP BY website_id, url_path, created_at;
|
||||
|
||||
-- Create session_replay
|
||||
CREATE TABLE umami.session_replay
|
||||
(
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
'use client';
|
||||
import { Column, Grid, Row, Text } from '@umami/react-zen';
|
||||
import {
|
||||
Column,
|
||||
Grid,
|
||||
Heading,
|
||||
ListItem,
|
||||
Row,
|
||||
Select,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs,
|
||||
Text,
|
||||
} from '@umami/react-zen';
|
||||
import { colord } from 'colord';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { BarChart } from '@/components/charts/BarChart';
|
||||
import { GridRow } from '@/components/common/GridRow';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||
import { useLocale, useMessages, useResultQuery } from '@/components/hooks';
|
||||
import { ListTable } from '@/components/metrics/ListTable';
|
||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||
import { PerformanceCard } from '@/components/metrics/PerformanceCard';
|
||||
import { renderDateLabels } from '@/lib/charts';
|
||||
import { CHART_COLORS, WEB_VITALS_THRESHOLDS } from '@/lib/constants';
|
||||
@@ -39,8 +52,15 @@ function formatMetricValue(metric: string, value: number): string {
|
||||
return `${Math.round(value)} ms`;
|
||||
}
|
||||
|
||||
const PERCENTILES = [
|
||||
{ id: 'p50', label: 'p50 — Median' },
|
||||
{ id: 'p75', label: 'p75 — 75th Percentile' },
|
||||
{ id: 'p95', label: 'p95 — 95th Percentile' },
|
||||
] as const;
|
||||
|
||||
export function Performance({ websiteId, startDate, endDate, unit }: PerformanceProps) {
|
||||
const [selectedMetric, setSelectedMetric] = useState<string>('lcp');
|
||||
const [selectedPercentile, setSelectedPercentile] = useState<'p50' | 'p75' | 'p95'>('p75');
|
||||
const { t, labels } = useMessages();
|
||||
const { locale, dateLocale } = useLocale();
|
||||
|
||||
@@ -121,6 +141,19 @@ export function Performance({ websiteId, startDate, endDate, unit }: Performance
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<Grid columns="280px" gap>
|
||||
<Select
|
||||
label="Percentile"
|
||||
value={selectedPercentile}
|
||||
onChange={(value: string) => setSelectedPercentile(value as 'p50' | 'p75' | 'p95')}
|
||||
>
|
||||
{PERCENTILES.map(({ id, label }) => (
|
||||
<ListItem key={id} id={id}>
|
||||
{label}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
{data && (
|
||||
<Column gap>
|
||||
@@ -129,7 +162,7 @@ export function Performance({ websiteId, startDate, endDate, unit }: Performance
|
||||
<PerformanceCard
|
||||
key={metric}
|
||||
metric={metric}
|
||||
value={Number(data.summary?.[metric]?.p75 || 0)}
|
||||
value={Number(data.summary?.[metric]?.[selectedPercentile] || 0)}
|
||||
label={t(labels[metric]) || metric.toUpperCase()}
|
||||
formatValue={(n: number) => formatMetricValue(metric, n)}
|
||||
onClick={() => setSelectedMetric(metric)}
|
||||
@@ -162,20 +195,84 @@ export function Performance({ websiteId, startDate, endDate, unit }: Performance
|
||||
/>
|
||||
</Column>
|
||||
</Panel>
|
||||
<Panel>
|
||||
<ListTable
|
||||
title={t(labels.pages)}
|
||||
metric={t(labels[selectedMetric]) || selectedMetric.toUpperCase()}
|
||||
data={data.pages?.map(
|
||||
({ urlPath, p75, count }: { urlPath: string; p75: number; count: number }) => ({
|
||||
label: urlPath,
|
||||
count: Number(p75),
|
||||
percent: 0,
|
||||
}),
|
||||
)}
|
||||
renderLabel={({ label }: { label: string }) => <Text>{label}</Text>}
|
||||
/>
|
||||
</Panel>
|
||||
<GridRow layout="two">
|
||||
<Panel>
|
||||
<Tabs>
|
||||
<Heading size="2xl">{t(labels.pages)}</Heading>
|
||||
<TabList>
|
||||
<Tab id="path">{t(labels.path)}</Tab>
|
||||
<Tab id="title">{t(labels.pageTitle)}</Tab>
|
||||
</TabList>
|
||||
<TabPanel id="path">
|
||||
<ListTable
|
||||
metric={t(labels[selectedMetric]) || selectedMetric.toUpperCase()}
|
||||
showPercentage={false}
|
||||
data={data.pages?.map(({ name, p50, p75, p95 }: any) => ({
|
||||
label: name,
|
||||
count: Number({ p50, p75, p95 }[selectedPercentile]),
|
||||
percent: 0,
|
||||
}))}
|
||||
renderLabel={({ label }: { label: string }) => <Text>{label}</Text>}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel id="title">
|
||||
<ListTable
|
||||
metric={t(labels[selectedMetric]) || selectedMetric.toUpperCase()}
|
||||
showPercentage={false}
|
||||
data={data.pageTitles?.map(({ name, p50, p75, p95 }: any) => ({
|
||||
label: name,
|
||||
count: Number({ p50, p75, p95 }[selectedPercentile]),
|
||||
percent: 0,
|
||||
}))}
|
||||
renderLabel={({ label }: { label: string }) => <Text>{label}</Text>}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Panel>
|
||||
<Panel>
|
||||
<Tabs>
|
||||
<Heading size="2xl">{t(labels.environment)}</Heading>
|
||||
<TabList>
|
||||
<Tab id="device">{t(labels.device)}</Tab>
|
||||
<Tab id="browser">{t(labels.browser)}</Tab>
|
||||
</TabList>
|
||||
<TabPanel id="device">
|
||||
<ListTable
|
||||
metric={t(labels[selectedMetric]) || selectedMetric.toUpperCase()}
|
||||
showPercentage={false}
|
||||
data={data.devices?.map(({ name, p50, p75, p95 }: any) => ({
|
||||
label: name,
|
||||
count: Number({ p50, p75, p95 }[selectedPercentile]),
|
||||
percent: 0,
|
||||
}))}
|
||||
renderLabel={({ label }: { label: string }) => (
|
||||
<Row gap="2" alignItems="center">
|
||||
<TypeIcon type="device" value={label} />
|
||||
<Text>{label}</Text>
|
||||
</Row>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel id="browser">
|
||||
<ListTable
|
||||
metric={t(labels[selectedMetric]) || selectedMetric.toUpperCase()}
|
||||
showPercentage={false}
|
||||
data={data.browsers?.map(({ name, p50, p75, p95 }: any) => ({
|
||||
label: name,
|
||||
count: Number({ p50, p75, p95 }[selectedPercentile]),
|
||||
percent: 0,
|
||||
}))}
|
||||
renderLabel={({ label }: { label: string }) => (
|
||||
<Row gap="2" alignItems="center">
|
||||
<TypeIcon type="browser" value={label} />
|
||||
<Text>{label}</Text>
|
||||
</Row>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Panel>
|
||||
</GridRow>
|
||||
</Column>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { json, unauthorized } from '@/lib/response';
|
||||
import { reportResultSchema } from '@/lib/schema';
|
||||
import { canViewWebsite } from '@/permissions';
|
||||
import { getPerformance, type PerformanceParameters } from '@/queries/sql/reports/getPerformance';
|
||||
import { getPerformanceMetrics } from '@/queries/sql/reports/getPerformanceMetrics';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { auth, body, error } = await parseRequest(request, reportResultSchema);
|
||||
@@ -20,7 +21,19 @@ export async function POST(request: Request) {
|
||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
||||
const filters = await getQueryFilters(body.filters, websiteId);
|
||||
|
||||
const data = await getPerformance(websiteId, parameters as PerformanceParameters, filters);
|
||||
const [{ chart, summary }, pages, pageTitles, devices, browsers] = await Promise.all([
|
||||
getPerformance(websiteId, parameters as PerformanceParameters, filters),
|
||||
getPerformanceMetrics(websiteId, parameters as PerformanceParameters, filters, 'url_path', 500),
|
||||
getPerformanceMetrics(
|
||||
websiteId,
|
||||
parameters as PerformanceParameters,
|
||||
filters,
|
||||
'page_title',
|
||||
500,
|
||||
),
|
||||
getPerformanceMetrics(websiteId, parameters as PerformanceParameters, filters, 'device'),
|
||||
getPerformanceMetrics(websiteId, parameters as PerformanceParameters, filters, 'browser', 500),
|
||||
]);
|
||||
|
||||
return json(data);
|
||||
return json({ chart, summary, pages, pageTitles, devices, browsers });
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { parseRequest } from '@/lib/request';
|
||||
import { badRequest, forbidden, json, serverError } from '@/lib/response';
|
||||
import { anyObjectParam, urlOrPathParam } from '@/lib/schema';
|
||||
import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url';
|
||||
import { createSession, saveEvent, savePerformance, saveSessionData } from '@/queries/sql';
|
||||
import { createSession, saveEvent, saveSessionData } from '@/queries/sql';
|
||||
|
||||
interface Cache {
|
||||
websiteId: string;
|
||||
@@ -284,11 +284,21 @@ export async function POST(request: Request) {
|
||||
const currentUrl = new URL(url, base);
|
||||
const urlPath = currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname;
|
||||
|
||||
await savePerformance({
|
||||
await saveEvent({
|
||||
websiteId: sourceId,
|
||||
sessionId,
|
||||
visitId,
|
||||
urlPath,
|
||||
pageTitle: safeDecodeURIComponent(title),
|
||||
eventType: EVENT_TYPE.performance,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
lcp,
|
||||
inp,
|
||||
cls,
|
||||
|
||||
@@ -68,7 +68,12 @@ export function ListTable({
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<Grid alignItems="center" justifyContent="space-between" paddingLeft="2" columns="1fr 100px">
|
||||
<Grid
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
paddingLeft="2"
|
||||
columns={`1fr ${showPercentage ? '100px' : '150px'}`}
|
||||
>
|
||||
<Text weight="bold">{title}</Text>
|
||||
<Text weight="bold" align="center">
|
||||
{metric}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
.card {
|
||||
transition: box-shadow 0.2s;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 15%, transparent);
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
box-shadow: 0 0 0 2px var(--base-color-primary);
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--primary) 40%, transparent),
|
||||
0 0 6px color-mix(in srgb, var(--primary) 15%, transparent);
|
||||
}
|
||||
|
||||
.good .rating {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useSpring } from '@react-spring/web';
|
||||
import { Column, Text } from '@umami/react-zen';
|
||||
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { WEB_VITALS_THRESHOLDS } from '@/lib/constants';
|
||||
import { formatNumber } from '@/lib/format';
|
||||
@@ -33,7 +31,6 @@ export const PerformanceCard = ({
|
||||
}: PerformanceCardProps) => {
|
||||
const { t, labels } = useMessages();
|
||||
const rating = getRating(metric, value);
|
||||
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
|
||||
|
||||
return (
|
||||
<Column
|
||||
@@ -52,7 +49,7 @@ export const PerformanceCard = ({
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="4xl" weight="bold" wrap="nowrap">
|
||||
<AnimatedDiv title={value?.toString()}>{props?.x?.to(x => formatValue(x))}</AnimatedDiv>
|
||||
{formatValue(value)}
|
||||
</Text>
|
||||
<Text size="sm" className={styles.rating}>
|
||||
{t(labels[rating === 'needs-improvement' ? 'needsImprovement' : rating])}
|
||||
|
||||
@@ -114,6 +114,7 @@ export const EVENT_TYPE = {
|
||||
customEvent: 2,
|
||||
linkEvent: 3,
|
||||
pixelEvent: 4,
|
||||
performance: 5,
|
||||
} as const;
|
||||
|
||||
export const ENTITY_TYPE = {
|
||||
|
||||
@@ -53,6 +53,13 @@ export interface SaveEventArgs {
|
||||
ttclid?: string;
|
||||
lifatid?: string;
|
||||
twclid?: string;
|
||||
|
||||
// Performance
|
||||
lcp?: number;
|
||||
inp?: number;
|
||||
cls?: number;
|
||||
fcp?: number;
|
||||
ttfb?: number;
|
||||
}
|
||||
|
||||
export async function saveEvent(args: SaveEventArgs) {
|
||||
@@ -89,6 +96,11 @@ async function relationalQuery({
|
||||
ttclid,
|
||||
lifatid,
|
||||
twclid,
|
||||
lcp,
|
||||
inp,
|
||||
cls,
|
||||
fcp,
|
||||
ttfb,
|
||||
}: SaveEventArgs) {
|
||||
const websiteEventId = uuid();
|
||||
|
||||
@@ -119,6 +131,11 @@ async function relationalQuery({
|
||||
eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
|
||||
tag,
|
||||
hostname,
|
||||
lcp,
|
||||
inp,
|
||||
cls,
|
||||
fcp,
|
||||
ttfb,
|
||||
createdAt,
|
||||
},
|
||||
});
|
||||
@@ -186,6 +203,11 @@ async function clickhouseQuery({
|
||||
ttclid,
|
||||
lifatid,
|
||||
twclid,
|
||||
lcp,
|
||||
inp,
|
||||
cls,
|
||||
fcp,
|
||||
ttfb,
|
||||
}: SaveEventArgs) {
|
||||
const { insert, getUTCString } = clickhouse;
|
||||
const { sendMessage } = kafka;
|
||||
@@ -221,12 +243,17 @@ async function clickhouseQuery({
|
||||
tag: tag,
|
||||
distinct_id: distinctId,
|
||||
created_at: getUTCString(createdAt),
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
hostname,
|
||||
browser: browser,
|
||||
os: os,
|
||||
device: device,
|
||||
screen: screen,
|
||||
language: language,
|
||||
hostname: hostname,
|
||||
lcp: lcp,
|
||||
inp: inp,
|
||||
cls: cls,
|
||||
fcp: fcp,
|
||||
ttfb: ttfb,
|
||||
};
|
||||
|
||||
if (kafka.enabled) {
|
||||
|
||||
@@ -68,7 +68,7 @@ async function relationalQuery(
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
group by prefix,
|
||||
@@ -163,7 +163,7 @@ async function clickhouseQuery(
|
||||
VIDEO_DOMAINS,
|
||||
)}]) != 0 or position(lower(utm_medium), 'video') > 0 then concat(prefix, 'Video')
|
||||
when referrer_domain != hostname and referrer_domain != '' then 'referral'
|
||||
else '' end AS name,
|
||||
else '' end AS "name",
|
||||
session_id,
|
||||
visit_id,
|
||||
count(*) c,
|
||||
@@ -174,7 +174,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
and name != ''
|
||||
${filterQuery}
|
||||
group by prefix, name, session_id, visit_id
|
||||
|
||||
@@ -45,7 +45,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${dateQuery}
|
||||
${filterQuery}),
|
||||
|
||||
@@ -123,7 +123,7 @@ async function clickhouseQuery(
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
group by 1, 2
|
||||
|
||||
@@ -58,7 +58,7 @@ async function relationalQuery(
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by 1, 2
|
||||
|
||||
@@ -103,7 +103,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by session_id, visit_id
|
||||
) as t;
|
||||
@@ -127,7 +127,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by session_id, visit_id
|
||||
) as t;
|
||||
|
||||
@@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by time
|
||||
order by 1
|
||||
@@ -63,7 +63,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by time
|
||||
order by time
|
||||
@@ -78,7 +78,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by time
|
||||
order by time
|
||||
|
||||
@@ -24,7 +24,6 @@ export * from './pageviews/getPageviewExpandedMetrics';
|
||||
export * from './pageviews/getPageviewMetrics';
|
||||
export * from './pageviews/getPageviewStats';
|
||||
export * from './performance/getPerformanceStats';
|
||||
export * from './performance/savePerformance';
|
||||
export * from './replays/getReplayChunks';
|
||||
export * from './replays/getSessionReplays';
|
||||
export * from './replays/saveRecording';
|
||||
|
||||
@@ -68,7 +68,7 @@ async function relationalQuery(
|
||||
from website_event
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
group by visit_id
|
||||
) x
|
||||
on x.visit_id = website_event.visit_id
|
||||
@@ -100,7 +100,7 @@ async function relationalQuery(
|
||||
${entryExitQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
group by ${column}, website_event.session_id, website_event.visit_id
|
||||
@@ -149,7 +149,7 @@ async function clickhouseQuery(
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
group by visit_id) x
|
||||
ON x.visit_id = website_event.visit_id`;
|
||||
}
|
||||
@@ -177,7 +177,7 @@ async function clickhouseQuery(
|
||||
${entryExitQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
and name != ''
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
|
||||
@@ -63,7 +63,7 @@ async function relationalQuery(
|
||||
from website_event
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
order by visit_id, created_at ${order}
|
||||
) x
|
||||
on x.visit_id = website_event.visit_id
|
||||
@@ -81,7 +81,7 @@ async function relationalQuery(
|
||||
${entryExitQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
group by 1
|
||||
@@ -127,7 +127,7 @@ async function clickhouseQuery(
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
group by visit_id) x
|
||||
ON x.visit_id = website_event.visit_id`;
|
||||
}
|
||||
@@ -141,7 +141,7 @@ async function clickhouseQuery(
|
||||
${entryExitQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
group by x
|
||||
@@ -180,7 +180,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
${groupByQuery}) as g
|
||||
|
||||
@@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by 1
|
||||
order by 1
|
||||
@@ -70,7 +70,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by t
|
||||
) as g
|
||||
@@ -90,7 +90,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by t
|
||||
) as g
|
||||
|
||||
@@ -23,8 +23,11 @@ async function relationalQuery(
|
||||
websiteId: string,
|
||||
filters: QueryFilters,
|
||||
): Promise<PerformanceStatsResult> {
|
||||
const { rawQuery } = prisma;
|
||||
const { startDate, endDate } = filters;
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
const result = await rawQuery(
|
||||
`
|
||||
@@ -35,11 +38,15 @@ async function relationalQuery(
|
||||
percentile_cont(0.75) within group (order by fcp) as fcp,
|
||||
percentile_cont(0.75) within group (order by ttfb) as ttfb,
|
||||
count(*) as count
|
||||
from performance
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
`,
|
||||
{ websiteId, startDate, endDate },
|
||||
queryParams,
|
||||
);
|
||||
|
||||
return result?.[0] || { lcp: 0, inp: 0, cls: 0, fcp: 0, ttfb: 0, count: 0 };
|
||||
@@ -49,8 +56,8 @@ async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
filters: QueryFilters,
|
||||
): Promise<PerformanceStatsResult> {
|
||||
const { rawQuery } = clickhouse;
|
||||
const { startDate, endDate } = filters;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
|
||||
|
||||
const result = await rawQuery<PerformanceStatsResult>(
|
||||
`
|
||||
@@ -61,11 +68,14 @@ async function clickhouseQuery(
|
||||
quantile(0.75)(fcp) as fcp,
|
||||
quantile(0.75)(ttfb) as ttfb,
|
||||
count() as count
|
||||
from website_performance
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
`,
|
||||
{ websiteId, startDate, endDate },
|
||||
queryParams,
|
||||
);
|
||||
|
||||
return result?.[0] || { lcp: 0, inp: 0, cls: 0, fcp: 0, ttfb: 0, count: 0 };
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import kafka from '@/lib/kafka';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export interface SavePerformanceArgs {
|
||||
websiteId: string;
|
||||
sessionId: string;
|
||||
visitId: string;
|
||||
urlPath: string;
|
||||
lcp?: number;
|
||||
inp?: number;
|
||||
cls?: number;
|
||||
fcp?: number;
|
||||
ttfb?: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export async function savePerformance(args: SavePerformanceArgs) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(data: SavePerformanceArgs) {
|
||||
const { websiteId, sessionId, visitId, urlPath, lcp, inp, cls, fcp, ttfb, createdAt } = data;
|
||||
|
||||
await prisma.client.performance.create({
|
||||
data: {
|
||||
id: uuid(),
|
||||
websiteId,
|
||||
sessionId,
|
||||
visitId,
|
||||
urlPath: urlPath?.substring(0, 500),
|
||||
lcp,
|
||||
inp,
|
||||
cls,
|
||||
fcp,
|
||||
ttfb,
|
||||
createdAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function clickhouseQuery(data: SavePerformanceArgs) {
|
||||
const { websiteId, sessionId, visitId, urlPath, lcp, inp, cls, fcp, ttfb, createdAt } = data;
|
||||
const { insert, getUTCString } = clickhouse;
|
||||
const { sendMessage } = kafka;
|
||||
|
||||
const message = {
|
||||
website_id: websiteId,
|
||||
session_id: sessionId,
|
||||
visit_id: visitId,
|
||||
url_path: urlPath?.substring(0, 500),
|
||||
lcp: lcp ?? null,
|
||||
inp: inp ?? null,
|
||||
cls: cls ?? null,
|
||||
fcp: fcp ?? null,
|
||||
ttfb: ttfb ?? null,
|
||||
created_at: getUTCString(createdAt),
|
||||
};
|
||||
|
||||
if (kafka.enabled) {
|
||||
await sendMessage('performance', message);
|
||||
} else {
|
||||
await insert('website_performance', [message]);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ export interface PerformanceParameters {
|
||||
|
||||
export interface PerformanceResult {
|
||||
chart: { t: string; p50: number; p75: number; p95: number }[];
|
||||
pages: { urlPath: string; p75: number; count: number }[];
|
||||
summary: {
|
||||
lcp: { p50: number; p75: number; p95: number };
|
||||
inp: { p50: number; p75: number; p95: number };
|
||||
@@ -39,7 +38,11 @@ async function relationalQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<PerformanceResult> {
|
||||
const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters;
|
||||
const { getDateSQL, rawQuery } = prisma;
|
||||
const { getDateSQL, rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
const chart = await rawQuery(
|
||||
`
|
||||
@@ -48,29 +51,17 @@ async function relationalQuery(
|
||||
percentile_cont(0.5) within group (order by ${metric}) as p50,
|
||||
percentile_cont(0.75) within group (order by ${metric}) as p75,
|
||||
percentile_cont(0.95) within group (order by ${metric}) as p95
|
||||
from performance
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
group by t
|
||||
order by t
|
||||
`,
|
||||
{ websiteId, startDate, endDate },
|
||||
);
|
||||
|
||||
const pages = await rawQuery(
|
||||
`
|
||||
select
|
||||
url_path as "urlPath",
|
||||
percentile_cont(0.75) within group (order by ${metric}) as p75,
|
||||
count(*) as count
|
||||
from performance
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
group by url_path
|
||||
order by p75 desc
|
||||
limit 100
|
||||
`,
|
||||
{ websiteId, startDate, endDate },
|
||||
{ ...queryParams, startDate, endDate },
|
||||
);
|
||||
|
||||
const summaryResult = await rawQuery(
|
||||
@@ -92,11 +83,15 @@ async function relationalQuery(
|
||||
percentile_cont(0.75) within group (order by ttfb) as ttfb_p75,
|
||||
percentile_cont(0.95) within group (order by ttfb) as ttfb_p95,
|
||||
count(*) as count
|
||||
from performance
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
`,
|
||||
{ websiteId, startDate, endDate },
|
||||
{ ...queryParams, startDate, endDate },
|
||||
).then(result => result?.[0]);
|
||||
|
||||
const summary = {
|
||||
@@ -128,7 +123,7 @@ async function relationalQuery(
|
||||
count: Number(summaryResult?.count || 0),
|
||||
};
|
||||
|
||||
return { chart, pages, summary };
|
||||
return { chart, summary };
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
@@ -137,38 +132,26 @@ async function clickhouseQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<PerformanceResult> {
|
||||
const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters;
|
||||
const { getDateSQL, rawQuery } = clickhouse;
|
||||
const { getDateSQL, rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
|
||||
|
||||
const chart = await rawQuery<{ t: string; p50: number; p75: number; p95: number }[]>(
|
||||
`
|
||||
select
|
||||
${getDateSQL('created_at', unit, timezone)} t,
|
||||
${getDateSQL('created_at', 'minute', timezone)} t,
|
||||
quantile(0.5)(${metric}) as p50,
|
||||
quantile(0.75)(${metric}) as p75,
|
||||
quantile(0.95)(${metric}) as p95
|
||||
from website_performance
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
group by t
|
||||
order by t
|
||||
`,
|
||||
{ websiteId, startDate, endDate },
|
||||
);
|
||||
|
||||
const pages = await rawQuery<{ urlPath: string; p75: number; count: number }[]>(
|
||||
`
|
||||
select
|
||||
url_path as "urlPath",
|
||||
quantile(0.75)(${metric}) as p75,
|
||||
count() as count
|
||||
from website_performance
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
group by url_path
|
||||
order by p75 desc
|
||||
limit 100
|
||||
`,
|
||||
{ websiteId, startDate, endDate },
|
||||
{ ...queryParams, startDate, endDate },
|
||||
);
|
||||
|
||||
const summaryResult = await rawQuery<any>(
|
||||
@@ -190,11 +173,14 @@ async function clickhouseQuery(
|
||||
quantile(0.75)(ttfb) as ttfb_p75,
|
||||
quantile(0.95)(ttfb) as ttfb_p95,
|
||||
count() as count
|
||||
from website_performance
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
`,
|
||||
{ websiteId, startDate, endDate },
|
||||
{ ...queryParams, startDate, endDate },
|
||||
).then(result => result?.[0]);
|
||||
|
||||
const summary = {
|
||||
@@ -226,5 +212,5 @@ async function clickhouseQuery(
|
||||
count: Number(summaryResult?.count || 0),
|
||||
};
|
||||
|
||||
return { chart, pages, summary };
|
||||
return { chart, summary };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
import type { PerformanceParameters } from './getPerformance';
|
||||
|
||||
export interface PerformanceMetricsData {
|
||||
name: string;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export async function getPerformanceMetrics(
|
||||
...args: [
|
||||
websiteId: string,
|
||||
parameters: PerformanceParameters,
|
||||
filters: QueryFilters,
|
||||
column: string,
|
||||
limit?: number,
|
||||
]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
parameters: PerformanceParameters,
|
||||
filters: QueryFilters,
|
||||
column: string,
|
||||
limit?: number,
|
||||
): Promise<PerformanceMetricsData[]> {
|
||||
const { startDate, endDate, metric = 'lcp' } = parameters;
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
${column} as "name",
|
||||
percentile_cont(0.5) within group (order by ${metric}) as p50,
|
||||
percentile_cont(0.75) within group (order by ${metric}) as p75,
|
||||
percentile_cont(0.95) within group (order by ${metric}) as p95,
|
||||
count(*) as count
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
group by ${column}
|
||||
order by p75 desc
|
||||
${limit ? `limit ${limit}` : ''}
|
||||
`,
|
||||
{ ...queryParams, startDate, endDate },
|
||||
);
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
parameters: PerformanceParameters,
|
||||
filters: QueryFilters,
|
||||
column: string,
|
||||
limit?: number,
|
||||
): Promise<PerformanceMetricsData[]> {
|
||||
const { startDate, endDate, metric = 'lcp' } = parameters;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
|
||||
|
||||
return rawQuery<PerformanceMetricsData[]>(
|
||||
`
|
||||
select
|
||||
${column} as "name",
|
||||
quantile(0.5)(${metric}) as p50,
|
||||
quantile(0.75)(${metric}) as p75,
|
||||
quantile(0.95)(${metric}) as p95,
|
||||
count() as count
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
group by ${column}
|
||||
order by p75 desc
|
||||
${limit ? `limit ${limit}` : ''}
|
||||
`,
|
||||
{ ...queryParams, startDate, endDate },
|
||||
);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ async function relationalQuery(
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by name, website_event.session_id, website_event.visit_id
|
||||
${includeCountry ? ', country' : ''}
|
||||
@@ -138,7 +138,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
and name != ''
|
||||
${filterQuery}
|
||||
group by name, session_id, visit_id
|
||||
|
||||
@@ -57,7 +57,7 @@ async function relationalQuery(
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by 1
|
||||
${includeCountry ? ', 3' : ''}
|
||||
@@ -101,7 +101,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by x
|
||||
${includeCountry ? ', country' : ''}
|
||||
@@ -120,7 +120,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by x
|
||||
${includeCountry ? ', country' : ''}
|
||||
|
||||
@@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by 1
|
||||
order by 1
|
||||
@@ -70,7 +70,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by t
|
||||
) as g
|
||||
@@ -90,7 +90,7 @@ async function clickhouseQuery(
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by t
|
||||
) as g
|
||||
|
||||
Reference in New Issue
Block a user