feat(api): split performance and UTM analytics datasets
This commit is contained in:
1 parent
8c17658ac1
commit
e202b740a1
24 files changed
+397
-257
No files matched your search
@@ -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,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));
|
||||
}
|
||||
@@ -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']),
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
File renamed without changes.
@@ -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;
|
||||
}
|
||||
@@ -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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user