This commit is contained in:
daniyil kotov 2026-02-02 23:20:21 -05:00 committed by GitHub
commit 56d32ddaca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 95 additions and 30 deletions

View file

@ -34,10 +34,10 @@ export async function GET(
getEventMetrics(websiteId, { type: 'event' }, filters),
getPageviewMetrics(websiteId, { type: 'path' }, filters),
getPageviewMetrics(websiteId, { type: 'referrer' }, filters),
getSessionMetrics(websiteId, { type: 'browser' }, filters),
getSessionMetrics(websiteId, { type: 'os' }, filters),
getSessionMetrics(websiteId, { type: 'device' }, filters),
getSessionMetrics(websiteId, { type: 'country' }, filters),
getSessionMetrics(websiteId, { type: 'browser' }, filters).then(r => r.data),
getSessionMetrics(websiteId, { type: 'os' }, filters).then(r => r.data),
getSessionMetrics(websiteId, { type: 'device' }, filters).then(r => r.data),
getSessionMetrics(websiteId, { type: 'country' }, filters).then(r => r.data),
]);
const zip = new JSZip();

View file

@ -4,10 +4,9 @@ import { useApi } from '../useApi';
import { useDateParameters } from '../useDateParameters';
import { useFilterParameters } from '../useFilterParameters';
export type WebsiteMetricsData = {
x: string;
y: number;
}[];
export type WebsiteMetricsData =
| { x: string; y: number }[]
| { data: { x: string; y: number }[]; total: number };
export function useWebsiteMetricsQuery(
websiteId: string,

View file

@ -40,21 +40,25 @@ export function MetricsTable({
const filteredData = useMemo(() => {
if (data) {
let items = data as any[];
// Handle both old format (array) and new format ({ data, total })
const items = Array.isArray(data) ? data : data.data;
const total = Array.isArray(data) ? undefined : data.total;
let filtered = items as any[];
if (dataFilter) {
if (Array.isArray(dataFilter)) {
items = dataFilter.reduce((arr, filter) => {
filtered = dataFilter.reduce((arr, filter) => {
return filter(arr);
}, items);
}, filtered);
} else {
items = dataFilter(items);
filtered = dataFilter(filtered);
}
}
items = percentFilter(items);
filtered = percentFilter(filtered, total);
return items.map(({ x, y, z, ...props }) => ({ label: x, count: y, percent: z, ...props }));
return filtered.map(({ x, y, z, ...props }) => ({ label: x, count: y, percent: z, ...props }));
}
return [];
}, [data, dataFilter, limit, type]);

View file

@ -32,10 +32,16 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) {
type: 'country',
});
const metrics = useMemo(
() => (data || mapData ? percentFilter((data || mapData) as any[]) : []),
[data, mapData],
);
const metrics = useMemo(() => {
const source = data || mapData;
if (!source) return [];
// Handle both old format (array) and new format ({ data, total })
const items = Array.isArray(source) ? source : source.data;
const total = Array.isArray(source) ? undefined : source.total;
return percentFilter(items, total);
}, [data, mapData]);
const getFillColor = (code: string) => {
if (code === 'AQ') return;

View file

@ -1,7 +1,7 @@
export const percentFilter = (data: any[]) => {
export const percentFilter = (data: any[], total?: number) => {
if (!Array.isArray(data)) return [];
const total = data.reduce((n, { y }) => n + y, 0);
return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props }));
const sum = total ?? data.reduce((n, { y }) => n + y, 0);
return data.map(({ x, y, ...props }) => ({ x, y, z: sum ? (y / sum) * 100 : 0, ...props }));
};
export const paramFilter = (data: any[]) => {

View file

@ -12,9 +12,14 @@ export interface SessionMetricsParameters {
offset?: number | string;
}
export interface SessionMetricsResult {
data: { x: string; y: number; country?: string }[];
total: number;
}
export async function getSessionMetrics(
...args: [websiteId: string, parameters: SessionMetricsParameters, filters: QueryFilters]
) {
): Promise<SessionMetricsResult> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@ -25,7 +30,7 @@ async function relationalQuery(
websiteId: string,
parameters: SessionMetricsParameters,
filters: QueryFilters,
) {
): Promise<SessionMetricsResult> {
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = prisma;
@ -45,7 +50,8 @@ async function relationalQuery(
column = `lower(left(${type}, 2))`;
}
return rawQuery(
// Get the data with limit
const data = await rawQuery(
`
select
${column} x,
@ -67,14 +73,35 @@ async function relationalQuery(
`,
{ ...queryParams, ...parameters },
FUNCTION_NAME,
);
) as { x: string; y: number; country?: string }[];
// Get total unique sessions
const totalResult = await rawQuery(
`
select count(distinct website_event.session_id) as total
from website_event
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and website_event.event_type != 2
${filterQuery}
`,
queryParams,
FUNCTION_NAME,
) as { total: number }[];
return {
data,
total: Number(totalResult[0]?.total) || 0,
};
}
async function clickhouseQuery(
websiteId: string,
parameters: SessionMetricsParameters,
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
): Promise<SessionMetricsResult> {
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = clickhouse;
@ -88,10 +115,11 @@ async function clickhouseQuery(
column = `lower(left(${type}, 2))`;
}
let sql = '';
let dataSql = '';
let totalSql = '';
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
sql = `
dataSql = `
select
${column} x,
count(distinct session_id) y
@ -109,8 +137,18 @@ async function clickhouseQuery(
limit ${limit}
offset ${offset}
`;
totalSql = `
select count(distinct session_id) as total
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type != 2
${filterQuery}
`;
} else {
sql = `
dataSql = `
select
${column} x,
uniq(session_id) y
@ -128,7 +166,25 @@ async function clickhouseQuery(
limit ${limit}
offset ${offset}
`;
totalSql = `
select uniq(session_id) as total
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type != 2
${filterQuery}
`;
}
return rawQuery(sql, { ...queryParams, ...parameters }, FUNCTION_NAME);
const [data, totalResult] = await Promise.all([
rawQuery(dataSql, { ...queryParams, ...parameters }, FUNCTION_NAME) as Promise<{ x: string; y: number; country?: string }[]>,
rawQuery(totalSql, queryParams, FUNCTION_NAME) as Promise<{ total: number }[]>,
]);
return {
data,
total: Number(totalResult[0]?.total) || 0,
};
}