feat(api): split performance and UTM analytics datasets

This commit is contained in:
Mike Cao committed 2026-09-09 18:55:35 -07:00
1 parent 8c17658ac1
commit e202b740a1
24 files changed
+397 -257

No files matched your search

+2 -2
View File
@@ -2,8 +2,8 @@ import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { reportResultSchema } from '@/lib/schema';
import { canViewWebsiteSection } from '@/permissions';
import { getPerformance, type PerformanceParameters } from '@/queries/sql/reports/getPerformance';
import { getPerformanceMetrics } from '@/queries/sql/reports/getPerformanceMetrics';
import { getPerformance, type PerformanceParameters } from '@/queries/sql/performance/getPerformance';
import { getPerformanceMetrics } from '@/queries/sql/performance/getPerformanceMetrics';
export async function POST(request: Request) {
const { auth, body, error } = await parseRequest(request, reportResultSchema);
+3 -3
View File
@@ -3,12 +3,12 @@ import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { reportResultSchema } from '@/lib/schema';
import { canViewWebsiteSection } from '@/permissions';
import { getRevenueChart, type RevenuParameters } from '@/queries/sql/reports/getRevenueChart';
import { getRevenueChart, type RevenuParameters } from '@/queries/sql/revenue/getRevenueChart';
import {
getRevenueMetrics,
type RevenueMetricsResult,
} from '@/queries/sql/reports/getRevenueMetrics';
import { getRevenueStats } from '@/queries/sql/reports/getRevenueStats';
} from '@/queries/sql/revenue/getRevenueMetrics';
import { getRevenueStats } from '@/queries/sql/revenue/getRevenueStats';
export async function POST(request: Request) {
const { auth, body, error } = await parseRequest(request, reportResultSchema);
@@ -0,0 +1,19 @@
import { performanceChartQuerySchema } from '@/lib/analytics-schema';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { canViewWebsiteSection } from '@/permissions';
import type { PerformanceParameters } from '@/queries/sql/performance/getPerformance';
import { getPerformanceChart } from '@/queries/sql/performance/getPerformanceChart';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const { auth, query, error } = await parseRequest(request, performanceChartQuerySchema);
if (error) return error();
const { websiteId } = await params;
if (!(await canViewWebsiteSection(auth, websiteId, 'performance'))) return unauthorized();
const filters = await getQueryFilters(query, websiteId);
const parameters = { ...query, ...filters } as PerformanceParameters;
return json(await getPerformanceChart(websiteId, parameters, filters));
}
@@ -0,0 +1,28 @@
import { performanceMetricsQuerySchema } from '@/lib/analytics-schema';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { canViewWebsiteSection } from '@/permissions';
import type { PerformanceParameters } from '@/queries/sql/performance/getPerformance';
import { getPerformanceMetrics } from '@/queries/sql/performance/getPerformanceMetrics';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const { auth, query, error } = await parseRequest(request, performanceMetricsQuerySchema);
if (error) return error();
const { websiteId } = await params;
if (!(await canViewWebsiteSection(auth, websiteId, 'performance'))) return unauthorized();
const filters = await getQueryFilters(query, websiteId);
const parameters = { ...query, ...filters } as PerformanceParameters;
const columns = { path: 'url_path', title: 'page_title', device: 'device', browser: 'browser' };
return json(
await getPerformanceMetrics(
websiteId,
parameters,
filters,
columns[query.type],
query.limit ?? (query.type === 'device' ? undefined : 500),
),
);
}
@@ -0,0 +1,19 @@
import { performanceStatsQuerySchema } from '@/lib/analytics-schema';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { canViewWebsiteSection } from '@/permissions';
import type { PerformanceParameters } from '@/queries/sql/performance/getPerformance';
import { getPerformanceStats } from '@/queries/sql/performance/getPerformanceStats';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const { auth, query, error } = await parseRequest(request, performanceStatsQuerySchema);
if (error) return error();
const { websiteId } = await params;
if (!(await canViewWebsiteSection(auth, websiteId, 'performance'))) return unauthorized();
const filters = await getQueryFilters(query, websiteId);
const parameters = { ...query, ...filters } as PerformanceParameters;
return json(await getPerformanceStats(websiteId, parameters, filters));
}
@@ -3,7 +3,7 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { filterParams, withDateRange } from '@/lib/schema';
import { canViewWebsiteSection } from '@/permissions';
import { getRevenueChart, type RevenuParameters } from '@/queries/sql/reports/getRevenueChart';
import { getRevenueChart, type RevenuParameters } from '@/queries/sql/revenue/getRevenueChart';
export async function GET(
request: Request,
@@ -3,8 +3,8 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
import { badRequest, json, unauthorized } from '@/lib/response';
import { filterParams, withDateRange } from '@/lib/schema';
import { canViewWebsiteSection } from '@/permissions';
import type { RevenuParameters } from '@/queries/sql/reports/getRevenueChart';
import { getRevenueMetrics, type RevenueMetricType } from '@/queries/sql/reports/getRevenueMetrics';
import type { RevenuParameters } from '@/queries/sql/revenue/getRevenueChart';
import { getRevenueMetrics, type RevenueMetricType } from '@/queries/sql/revenue/getRevenueMetrics';
const revenueMetricType = z.enum(['country', 'region', 'referrer', 'channel']);
@@ -3,7 +3,7 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { filterParams, pagingParams, searchParams, withDateRange } from '@/lib/schema';
import { canViewWebsiteSection } from '@/permissions';
import { getRevenueSessions } from '@/queries/sql/reports/getRevenueSessions';
import { getRevenueSessions } from '@/queries/sql/revenue/getRevenueSessions';
export async function GET(
request: Request,
@@ -4,8 +4,8 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { filterParams, withDateRange } from '@/lib/schema';
import { canViewWebsiteSection } from '@/permissions';
import type { RevenuParameters } from '@/queries/sql/reports/getRevenueChart';
import { getRevenueStats } from '@/queries/sql/reports/getRevenueStats';
import type { RevenuParameters } from '@/queries/sql/revenue/getRevenueChart';
import { getRevenueStats } from '@/queries/sql/revenue/getRevenueStats';
export async function GET(
request: Request,
@@ -0,0 +1,17 @@
import { utmMetricsQuerySchema } from '@/lib/analytics-schema';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { canViewWebsiteSection } from '@/permissions';
import { getUTM } from '@/queries/sql/utm/getUTM';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const { auth, query, error } = await parseRequest(request, utmMetricsQuerySchema);
if (error) return error();
const { websiteId } = await params;
if (!(await canViewWebsiteSection(auth, websiteId, 'utm'))) return unauthorized();
const filters = await getQueryFilters(query, websiteId);
return json(await getUTM(websiteId, { ...filters, column: query.type }, filters));
}
+13
View File
@@ -83,3 +83,16 @@ export const goalDefinitionSchema = z.object({
description: z.string().max(500).optional(),
parameters: goalParametersSchema,
});
export const performanceStatsQuerySchema = analyticsSchema({});
export const performanceChartQuerySchema = analyticsSchema({
metric: z.enum(['lcp', 'inp', 'cls', 'fcp', 'ttfb']).optional(),
});
export const performanceMetricsQuerySchema = analyticsSchema({
metric: z.enum(['lcp', 'inp', 'cls', 'fcp', 'ttfb']).optional(),
type: z.enum(['path', 'title', 'device', 'browser']),
limit: z.coerce.number().int().min(1).max(500).optional(),
});
export const utmMetricsQuerySchema = analyticsSchema({
type: z.enum(['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']),
});
+1 -1
View File
@@ -41,7 +41,7 @@ export * from './breakdown/getBreakdown';
export * from './funnels/getFunnel';
export * from './journeys/getJourney';
export * from './retention/getRetention';
export * from './reports/getUTM';
export * from './utm/getUTM';
export * from './sessions/createSession';
export * from './sessions/getLinkedDistinctIds';
export * from './sessions/getLinkedSessionIds';
@@ -0,0 +1,68 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { getPerformance } from './getPerformance';
import { getPerformanceChart } from './getPerformanceChart';
import { getPerformanceStats } from './getPerformanceStats';
const mocks = vi.hoisted(() => ({
backend: 'prisma',
rawQuery: vi.fn(),
parseFilters: vi.fn(() => ({
queryParams: {},
filterQuery: 'and filtered',
cohortQuery: '',
joinSessionQuery: '',
})),
}));
vi.mock('@/lib/db', () => ({
PRISMA: 'prisma',
CLICKHOUSE: 'clickhouse',
runQuery: (queries: any) => queries[mocks.backend](),
}));
vi.mock('@/lib/prisma', () => ({
default: {
rawQuery: mocks.rawQuery,
parseFilters: mocks.parseFilters,
getDateSQL: () => 'bucket',
},
}));
vi.mock('@/lib/clickhouse', () => ({
default: {
rawQuery: mocks.rawQuery,
parseFilters: mocks.parseFilters,
getDateSQL: () => 'bucket',
},
}));
const parameters = {
startDate: new Date(1000),
endDate: new Date(2000),
unit: 'day',
timezone: 'UTC',
metric: 'inp',
};
describe.each(['prisma', 'clickhouse'])('%s performance datasets', backend => {
beforeEach(() => {
mocks.backend = backend;
mocks.rawQuery.mockReset().mockResolvedValue([]);
});
test('chart executes only the selected metric time series', async () => {
expect(await getPerformanceChart('website', parameters, {})).toEqual({ chart: [] });
expect(mocks.rawQuery).toHaveBeenCalledTimes(1);
const sql = mocks.rawQuery.mock.calls[0][0];
expect(sql).toContain('inp');
expect(sql).toContain('group by t');
expect(sql).not.toContain('lcp_p50');
expect(sql).toContain('and filtered');
});
test('stats executes only the summary and preserves empty-result defaults', async () => {
const stats = await getPerformanceStats('website', parameters, {});
expect(stats.count).toBe(0);
expect(stats.lcp).toEqual({ p50: 0, p75: 0, p95: 0 });
expect(mocks.rawQuery).toHaveBeenCalledTimes(1);
expect(mocks.rawQuery.mock.calls[0][0]).not.toContain('group by t');
});
test('legacy composition preserves the chart and summary shape', async () => {
const result = await getPerformance('website', parameters, {});
expect(mocks.rawQuery).toHaveBeenCalledTimes(2);
expect(result).toMatchObject({ chart: [], summary: { count: 0 } });
});
});
@@ -0,0 +1,34 @@
import type { QueryFilters } from '@/lib/types';
import { getPerformanceChart } from './getPerformanceChart';
import { getPerformanceStats } from './getPerformanceStats';
export interface PerformanceParameters {
startDate: Date;
endDate: Date;
unit: string;
timezone: string;
metric: string;
}
export interface PerformanceResult {
chart: { t: string; p50: number; p75: number; p95: number }[];
summary: {
lcp: { p50: number; p75: number; p95: number };
inp: { p50: number; p75: number; p95: number };
cls: { p50: number; p75: number; p95: number };
fcp: { p50: number; p75: number; p95: number };
ttfb: { p50: number; p75: number; p95: number };
count: number;
};
}
// Compatibility composition for the legacy report endpoint.
export async function getPerformance(
...args: [websiteId: string, parameters: PerformanceParameters, filters: QueryFilters]
): Promise<PerformanceResult> {
const [{ chart }, summary] = await Promise.all([
getPerformanceChart(...args),
getPerformanceStats(...args),
]);
return { chart, summary };
}
@@ -0,0 +1,81 @@
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, PerformanceResult } from './getPerformance';
export async function getPerformanceChart(
...args: [websiteId: string, parameters: PerformanceParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
parameters: PerformanceParameters,
filters: QueryFilters,
): Promise<Pick<PerformanceResult, 'chart'>> {
const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters;
const { getDateSQL, rawQuery, parseFilters } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
const chart = await rawQuery(
`
select
${getDateSQL('website_event.created_at', unit, timezone)} t,
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 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
`,
{ ...queryParams, startDate, endDate },
);
return { chart };
}
async function clickhouseQuery(
websiteId: string,
parameters: PerformanceParameters,
filters: QueryFilters,
): Promise<Pick<PerformanceResult, 'chart'>> {
const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters;
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,
quantile(0.5)(${metric}) as p50,
quantile(0.75)(${metric}) as p75,
quantile(0.95)(${metric}) as p95
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
`,
{ ...queryParams, startDate, endDate },
);
return { chart };
}
@@ -3,16 +3,11 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
export interface PerformanceStatsResult {
lcp: number;
inp: number;
cls: number;
fcp: number;
ttfb: number;
count: number;
}
import type { PerformanceParameters, PerformanceResult } from './getPerformance';
export async function getPerformanceStats(...args: [websiteId: string, filters: QueryFilters]) {
export async function getPerformanceStats(
...args: [websiteId: string, parameters: PerformanceParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@@ -21,22 +16,34 @@ export async function getPerformanceStats(...args: [websiteId: string, filters:
async function relationalQuery(
websiteId: string,
parameters: PerformanceParameters,
filters: QueryFilters,
): Promise<PerformanceStatsResult> {
): Promise<PerformanceResult['summary']> {
const { startDate, endDate } = parameters;
const { rawQuery, parseFilters } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
const result = await rawQuery(
const summaryResult = await rawQuery(
`
select
percentile_cont(0.75) within group (order by lcp) as lcp,
percentile_cont(0.75) within group (order by inp) as inp,
percentile_cont(0.75) within group (order by cls) as cls,
percentile_cont(0.75) within group (order by fcp) as fcp,
percentile_cont(0.75) within group (order by ttfb) as ttfb,
percentile_cont(0.5) within group (order by lcp) as lcp_p50,
percentile_cont(0.75) within group (order by lcp) as lcp_p75,
percentile_cont(0.95) within group (order by lcp) as lcp_p95,
percentile_cont(0.5) within group (order by inp) as inp_p50,
percentile_cont(0.75) within group (order by inp) as inp_p75,
percentile_cont(0.95) within group (order by inp) as inp_p95,
percentile_cont(0.5) within group (order by cls) as cls_p50,
percentile_cont(0.75) within group (order by cls) as cls_p75,
percentile_cont(0.95) within group (order by cls) as cls_p95,
percentile_cont(0.5) within group (order by fcp) as fcp_p50,
percentile_cont(0.75) within group (order by fcp) as fcp_p75,
percentile_cont(0.95) within group (order by fcp) as fcp_p95,
percentile_cont(0.5) within group (order by ttfb) as ttfb_p50,
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 website_event
${cohortQuery}
@@ -46,27 +53,68 @@ async function relationalQuery(
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
`,
queryParams,
);
{ ...queryParams, startDate, endDate },
).then(result => result?.[0]);
return result?.[0] || { lcp: 0, inp: 0, cls: 0, fcp: 0, ttfb: 0, count: 0 };
const summary = {
lcp: {
p50: Number(summaryResult?.lcp_p50 || 0),
p75: Number(summaryResult?.lcp_p75 || 0),
p95: Number(summaryResult?.lcp_p95 || 0),
},
inp: {
p50: Number(summaryResult?.inp_p50 || 0),
p75: Number(summaryResult?.inp_p75 || 0),
p95: Number(summaryResult?.inp_p95 || 0),
},
cls: {
p50: Number(summaryResult?.cls_p50 || 0),
p75: Number(summaryResult?.cls_p75 || 0),
p95: Number(summaryResult?.cls_p95 || 0),
},
fcp: {
p50: Number(summaryResult?.fcp_p50 || 0),
p75: Number(summaryResult?.fcp_p75 || 0),
p95: Number(summaryResult?.fcp_p95 || 0),
},
ttfb: {
p50: Number(summaryResult?.ttfb_p50 || 0),
p75: Number(summaryResult?.ttfb_p75 || 0),
p95: Number(summaryResult?.ttfb_p95 || 0),
},
count: Number(summaryResult?.count || 0),
};
return summary;
}
async function clickhouseQuery(
websiteId: string,
parameters: PerformanceParameters,
filters: QueryFilters,
): Promise<PerformanceStatsResult> {
): Promise<PerformanceResult['summary']> {
const { startDate, endDate } = parameters;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
const result = await rawQuery<PerformanceStatsResult>(
const summaryResult = await rawQuery<any>(
`
select
quantile(0.75)(lcp) as lcp,
quantile(0.75)(inp) as inp,
quantile(0.75)(cls) as cls,
quantile(0.75)(fcp) as fcp,
quantile(0.75)(ttfb) as ttfb,
quantile(0.5)(lcp) as lcp_p50,
quantile(0.75)(lcp) as lcp_p75,
quantile(0.95)(lcp) as lcp_p95,
quantile(0.5)(inp) as inp_p50,
quantile(0.75)(inp) as inp_p75,
quantile(0.95)(inp) as inp_p95,
quantile(0.5)(cls) as cls_p50,
quantile(0.75)(cls) as cls_p75,
quantile(0.95)(cls) as cls_p95,
quantile(0.5)(fcp) as fcp_p50,
quantile(0.75)(fcp) as fcp_p75,
quantile(0.95)(fcp) as fcp_p95,
quantile(0.5)(ttfb) as ttfb_p50,
quantile(0.75)(ttfb) as ttfb_p75,
quantile(0.95)(ttfb) as ttfb_p95,
count() as count
from website_event
${cohortQuery}
@@ -75,8 +123,37 @@ async function clickhouseQuery(
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
`,
queryParams,
);
{ ...queryParams, startDate, endDate },
).then(result => result?.[0]);
return result?.[0] || { lcp: 0, inp: 0, cls: 0, fcp: 0, ttfb: 0, count: 0 };
const summary = {
lcp: {
p50: Number(summaryResult?.lcp_p50 || 0),
p75: Number(summaryResult?.lcp_p75 || 0),
p95: Number(summaryResult?.lcp_p95 || 0),
},
inp: {
p50: Number(summaryResult?.inp_p50 || 0),
p75: Number(summaryResult?.inp_p75 || 0),
p95: Number(summaryResult?.inp_p95 || 0),
},
cls: {
p50: Number(summaryResult?.cls_p50 || 0),
p75: Number(summaryResult?.cls_p75 || 0),
p95: Number(summaryResult?.cls_p95 || 0),
},
fcp: {
p50: Number(summaryResult?.fcp_p50 || 0),
p75: Number(summaryResult?.fcp_p75 || 0),
p95: Number(summaryResult?.fcp_p95 || 0),
},
ttfb: {
p50: Number(summaryResult?.ttfb_p50 || 0),
p75: Number(summaryResult?.ttfb_p75 || 0),
p95: Number(summaryResult?.ttfb_p95 || 0),
},
count: Number(summaryResult?.count || 0),
};
return summary;
}
-216
View File
@@ -1,216 +0,0 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
export interface PerformanceParameters {
startDate: Date;
endDate: Date;
unit: string;
timezone: string;
metric: string;
}
export interface PerformanceResult {
chart: { t: string; p50: number; p75: number; p95: number }[];
summary: {
lcp: { p50: number; p75: number; p95: number };
inp: { p50: number; p75: number; p95: number };
cls: { p50: number; p75: number; p95: number };
fcp: { p50: number; p75: number; p95: number };
ttfb: { p50: number; p75: number; p95: number };
count: number;
};
}
export async function getPerformance(
...args: [websiteId: string, parameters: PerformanceParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
parameters: PerformanceParameters,
filters: QueryFilters,
): Promise<PerformanceResult> {
const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters;
const { getDateSQL, rawQuery, parseFilters } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
const chart = await rawQuery(
`
select
${getDateSQL('website_event.created_at', unit, timezone)} t,
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 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
`,
{ ...queryParams, startDate, endDate },
);
const summaryResult = await rawQuery(
`
select
percentile_cont(0.5) within group (order by lcp) as lcp_p50,
percentile_cont(0.75) within group (order by lcp) as lcp_p75,
percentile_cont(0.95) within group (order by lcp) as lcp_p95,
percentile_cont(0.5) within group (order by inp) as inp_p50,
percentile_cont(0.75) within group (order by inp) as inp_p75,
percentile_cont(0.95) within group (order by inp) as inp_p95,
percentile_cont(0.5) within group (order by cls) as cls_p50,
percentile_cont(0.75) within group (order by cls) as cls_p75,
percentile_cont(0.95) within group (order by cls) as cls_p95,
percentile_cont(0.5) within group (order by fcp) as fcp_p50,
percentile_cont(0.75) within group (order by fcp) as fcp_p75,
percentile_cont(0.95) within group (order by fcp) as fcp_p95,
percentile_cont(0.5) within group (order by ttfb) as ttfb_p50,
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 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}
`,
{ ...queryParams, startDate, endDate },
).then(result => result?.[0]);
const summary = {
lcp: {
p50: Number(summaryResult?.lcp_p50 || 0),
p75: Number(summaryResult?.lcp_p75 || 0),
p95: Number(summaryResult?.lcp_p95 || 0),
},
inp: {
p50: Number(summaryResult?.inp_p50 || 0),
p75: Number(summaryResult?.inp_p75 || 0),
p95: Number(summaryResult?.inp_p95 || 0),
},
cls: {
p50: Number(summaryResult?.cls_p50 || 0),
p75: Number(summaryResult?.cls_p75 || 0),
p95: Number(summaryResult?.cls_p95 || 0),
},
fcp: {
p50: Number(summaryResult?.fcp_p50 || 0),
p75: Number(summaryResult?.fcp_p75 || 0),
p95: Number(summaryResult?.fcp_p95 || 0),
},
ttfb: {
p50: Number(summaryResult?.ttfb_p50 || 0),
p75: Number(summaryResult?.ttfb_p75 || 0),
p95: Number(summaryResult?.ttfb_p95 || 0),
},
count: Number(summaryResult?.count || 0),
};
return { chart, summary };
}
async function clickhouseQuery(
websiteId: string,
parameters: PerformanceParameters,
filters: QueryFilters,
): Promise<PerformanceResult> {
const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters;
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,
quantile(0.5)(${metric}) as p50,
quantile(0.75)(${metric}) as p75,
quantile(0.95)(${metric}) as p95
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
`,
{ ...queryParams, startDate, endDate },
);
const summaryResult = await rawQuery<any>(
`
select
quantile(0.5)(lcp) as lcp_p50,
quantile(0.75)(lcp) as lcp_p75,
quantile(0.95)(lcp) as lcp_p95,
quantile(0.5)(inp) as inp_p50,
quantile(0.75)(inp) as inp_p75,
quantile(0.95)(inp) as inp_p95,
quantile(0.5)(cls) as cls_p50,
quantile(0.75)(cls) as cls_p75,
quantile(0.95)(cls) as cls_p95,
quantile(0.5)(fcp) as fcp_p50,
quantile(0.75)(fcp) as fcp_p75,
quantile(0.95)(fcp) as fcp_p95,
quantile(0.5)(ttfb) as ttfb_p50,
quantile(0.75)(ttfb) as ttfb_p75,
quantile(0.95)(ttfb) as ttfb_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}
`,
{ ...queryParams, startDate, endDate },
).then(result => result?.[0]);
const summary = {
lcp: {
p50: Number(summaryResult?.lcp_p50 || 0),
p75: Number(summaryResult?.lcp_p75 || 0),
p95: Number(summaryResult?.lcp_p95 || 0),
},
inp: {
p50: Number(summaryResult?.inp_p50 || 0),
p75: Number(summaryResult?.inp_p75 || 0),
p95: Number(summaryResult?.inp_p95 || 0),
},
cls: {
p50: Number(summaryResult?.cls_p50 || 0),
p75: Number(summaryResult?.cls_p75 || 0),
p95: Number(summaryResult?.cls_p95 || 0),
},
fcp: {
p50: Number(summaryResult?.fcp_p50 || 0),
p75: Number(summaryResult?.fcp_p75 || 0),
p95: Number(summaryResult?.fcp_p95 || 0),
},
ttfb: {
p50: Number(summaryResult?.ttfb_p50 || 0),
p75: Number(summaryResult?.ttfb_p75 || 0),
p95: Number(summaryResult?.ttfb_p95 || 0),
},
count: Number(summaryResult?.count || 0),
};
return { chart, summary };
}
File renamed without changes.