Skip to content

Commit 1ad3c63

Browse files
committed
fix: perf issues with high memory usage in summary time series queries
1 parent 7404a19 commit 1ad3c63

File tree

18 files changed

+975
-98
lines changed

18 files changed

+975
-98
lines changed

apps/api/src/query/builders/summary.ts

Lines changed: 17 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,7 @@ export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
8585
e.session_id,
8686
e.anonymous_id,
8787
e.event_name,
88-
toTimeZone(e.time, {timezone:String}) as normalized_time,
89-
sa.session_referrer as referrer,
90-
sa.session_utm_source as utm_source,
91-
sa.session_utm_medium as utm_medium,
92-
sa.session_utm_campaign as utm_campaign,
93-
sa.session_country as country,
94-
sa.session_device_type as device_type,
95-
sa.session_browser_name as browser_name,
96-
sa.session_os_name as os_name
88+
toTimeZone(e.time, {timezone:String}) as normalized_time
9789
FROM analytics.events e
9890
${helpers.sessionAttributionJoin('e')}
9991
WHERE
@@ -318,15 +310,7 @@ export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
318310
e.session_id,
319311
e.anonymous_id,
320312
e.event_name,
321-
toTimeZone(e.time, {timezone:String}) as normalized_time,
322-
sa.session_referrer as referrer,
323-
sa.session_utm_source as utm_source,
324-
sa.session_utm_medium as utm_medium,
325-
sa.session_utm_campaign as utm_campaign,
326-
sa.session_country as country,
327-
sa.session_device_type as device_type,
328-
sa.session_browser_name as browser_name,
329-
sa.session_os_name as os_name
313+
toTimeZone(e.time, {timezone:String}) as normalized_time
330314
FROM analytics.events e
331315
${helpers.sessionAttributionJoin('e')}
332316
WHERE
@@ -356,12 +340,6 @@ export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
356340
sql: `
357341
WITH ${sessionAttributionCTE}
358342
${baseEventsQuery}
359-
hour_range AS (
360-
SELECT arrayJoin(arrayMap(
361-
h -> toStartOfHour(toTimeZone(toDateTime(concat({startDate:String}, ' 00:00:00')) + (h * 3600), {timezone:String})),
362-
range(toUInt32(dateDiff('hour', toDateTime(concat({startDate:String}, ' 00:00:00')), toDateTime(concat({endDate:String}, ' 23:59:59'))) + 1))
363-
)) AS datetime
364-
),
365343
session_details AS (
366344
SELECT
367345
session_id,
@@ -389,9 +367,9 @@ export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
389367
GROUP BY event_hour
390368
)
391369
SELECT
392-
formatDateTime(hr.datetime, '%Y-%m-%d %H:00:00') as date,
393-
COALESCE(hem.pageviews, 0) as pageviews,
394-
COALESCE(hem.unique_visitors, 0) as visitors,
370+
formatDateTime(hem.event_hour, '%Y-%m-%d %H:00:00') as date,
371+
hem.pageviews as pageviews,
372+
hem.unique_visitors as visitors,
395373
COALESCE(hsm.sessions, 0) as sessions,
396374
ROUND(CASE
397375
WHEN COALESCE(hsm.sessions, 0) > 0
@@ -401,13 +379,12 @@ export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
401379
ROUND(COALESCE(hsm.median_session_duration, 0), 2) as avg_session_duration,
402380
ROUND(CASE
403381
WHEN COALESCE(hsm.sessions, 0) > 0
404-
THEN COALESCE(hem.pageviews, 0) / COALESCE(hsm.sessions, 0)
382+
THEN hem.pageviews / COALESCE(hsm.sessions, 0)
405383
ELSE 0
406384
END, 2) as pages_per_session
407-
FROM hour_range hr
408-
LEFT JOIN hourly_session_metrics hsm ON hr.datetime = hsm.event_hour
409-
LEFT JOIN hourly_event_metrics hem ON hr.datetime = hem.event_hour
410-
ORDER BY hr.datetime ASC
385+
FROM hourly_event_metrics hem
386+
LEFT JOIN hourly_session_metrics hsm ON hem.event_hour = hsm.event_hour
387+
ORDER BY hem.event_hour ASC
411388
`,
412389
params: {
413390
websiteId,
@@ -431,15 +408,7 @@ export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
431408
e.session_id,
432409
e.anonymous_id,
433410
e.event_name,
434-
toTimeZone(e.time, {timezone:String}) as normalized_time,
435-
sa.session_referrer as referrer,
436-
sa.session_utm_source as utm_source,
437-
sa.session_utm_medium as utm_medium,
438-
sa.session_utm_campaign as utm_campaign,
439-
sa.session_country as country,
440-
sa.session_device_type as device_type,
441-
sa.session_browser_name as browser_name,
442-
sa.session_os_name as os_name
411+
toTimeZone(e.time, {timezone:String}) as normalized_time
443412
FROM analytics.events e
444413
${helpers.sessionAttributionJoin('e')}
445414
WHERE
@@ -469,12 +438,6 @@ export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
469438
sql: `
470439
WITH ${sessionAttributionCTE}
471440
${baseEventsQuery}
472-
date_range AS (
473-
SELECT arrayJoin(arrayMap(
474-
d -> toDate({startDate:String}) + d,
475-
range(toUInt32(dateDiff('day', toDate({startDate:String}), toDate({endDate:String})) + 1))
476-
)) AS date
477-
),
478441
session_details AS (
479442
SELECT
480443
session_id,
@@ -502,9 +465,9 @@ export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
502465
GROUP BY event_date
503466
)
504467
SELECT
505-
dr.date,
506-
COALESCE(dem.pageviews, 0) as pageviews,
507-
COALESCE(dem.unique_visitors, 0) as visitors,
468+
dem.event_date as date,
469+
dem.pageviews as pageviews,
470+
dem.unique_visitors as visitors,
508471
COALESCE(dsm.sessions, 0) as sessions,
509472
ROUND(CASE
510473
WHEN COALESCE(dsm.sessions, 0) > 0
@@ -514,13 +477,12 @@ export const SummaryBuilders: Record<string, SimpleQueryConfig> = {
514477
ROUND(COALESCE(dsm.median_session_duration, 0), 2) as avg_session_duration,
515478
ROUND(CASE
516479
WHEN COALESCE(dsm.sessions, 0) > 0
517-
THEN COALESCE(dem.pageviews, 0) / COALESCE(dsm.sessions, 0)
480+
THEN dem.pageviews / COALESCE(dsm.sessions, 0)
518481
ELSE 0
519482
END, 2) as pages_per_session
520-
FROM date_range dr
521-
LEFT JOIN daily_session_metrics dsm ON dr.date = dsm.session_start_date
522-
LEFT JOIN daily_event_metrics dem ON dr.date = dem.event_date
523-
ORDER BY dr.date ASC
483+
FROM daily_event_metrics dem
484+
LEFT JOIN daily_session_metrics dsm ON dem.event_date = dsm.session_start_date
485+
ORDER BY dem.event_date ASC
524486
`,
525487
params: {
526488
websiteId,

apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -340,11 +340,82 @@ export function WebsiteOverviewTab({
340340
[dateRange.granularity]
341341
);
342342

343-
const chartData = useMemo(() => {
344-
if (!analytics.events_by_date?.length) return [];
343+
const fillMissingDates = useCallback((data: MetricPoint[]) => {
344+
const userTimezone = getUserTimezone();
345+
const isHourly = dateRange.granularity === 'hourly';
346+
347+
// Create a map of existing data for quick lookup
348+
const dataMap = new Map<string, MetricPoint>();
349+
for (const item of data) {
350+
const key = isHourly ? item.date : item.date.slice(0, 10);
351+
dataMap.set(key, item);
352+
}
353+
354+
// Generate all dates in range
355+
const startDate = dayjs(dateRange.start_date).tz(userTimezone);
356+
const endDate = dayjs(dateRange.end_date).tz(userTimezone);
357+
const now = dayjs().tz(userTimezone);
358+
359+
const filled: MetricPoint[] = [];
360+
let current = startDate;
361+
362+
while (current.isBefore(endDate) || current.isSame(endDate, 'day')) {
363+
if (isHourly) {
364+
// Hourly granularity - iterate through hours
365+
for (let hour = 0; hour < 24; hour++) {
366+
const hourDate = current.hour(hour);
367+
368+
// Don't add future hours
369+
if (hourDate.isAfter(now)) break;
370+
371+
const key = hourDate.format('YYYY-MM-DD HH:00:00');
372+
const existing = dataMap.get(key);
373+
374+
filled.push(existing || {
375+
date: key,
376+
pageviews: 0,
377+
visitors: 0,
378+
unique_visitors: 0,
379+
sessions: 0,
380+
bounce_rate: 0,
381+
avg_session_duration: 0,
382+
pages_per_session: 0,
383+
});
384+
}
385+
current = current.add(1, 'day');
386+
387+
// If we've reached today, stop after processing current hour
388+
if (current.isAfter(endDate, 'day')) break;
389+
} else {
390+
// Daily granularity
391+
const key = current.format('YYYY-MM-DD');
392+
const existing = dataMap.get(key);
393+
394+
filled.push(existing || {
395+
date: key,
396+
pageviews: 0,
397+
visitors: 0,
398+
unique_visitors: 0,
399+
sessions: 0,
400+
bounce_rate: 0,
401+
avg_session_duration: 0,
402+
pages_per_session: 0,
403+
});
404+
405+
current = current.add(1, 'day');
406+
}
407+
}
408+
409+
return filled;
410+
}, [dateRange.start_date, dateRange.end_date, dateRange.granularity]);
345411

346-
const filteredEvents = filterFutureEvents(analytics.events_by_date);
347-
return filteredEvents.map((event: MetricPoint): ChartDataPoint => ({
412+
const chartData = useMemo(() => {
413+
const filteredEvents = analytics.events_by_date?.length
414+
? filterFutureEvents(analytics.events_by_date)
415+
: [];
416+
const filledEvents = fillMissingDates(filteredEvents);
417+
418+
return filledEvents.map((event: MetricPoint): ChartDataPoint => ({
348419
date: formatDateByGranularity(event.date, dateRange.granularity),
349420
rawDate: event.date,
350421
...(visibleMetrics.pageviews && { pageviews: event.pageviews as number }),
@@ -360,19 +431,24 @@ export function WebsiteOverviewTab({
360431
}, [
361432
analytics.events_by_date,
362433
dateRange.granularity,
434+
dateRange.start_date,
435+
dateRange.end_date,
363436
visibleMetrics,
364437
filterFutureEvents,
438+
fillMissingDates,
365439
]);
366440

367441
const miniChartData = useMemo(() => {
368-
if (!analytics.events_by_date?.length) return {};
369-
370-
const filteredEvents = filterFutureEvents(analytics.events_by_date);
442+
const filteredEvents = analytics.events_by_date?.length
443+
? filterFutureEvents(analytics.events_by_date)
444+
: [];
445+
const filledEvents = fillMissingDates(filteredEvents);
446+
371447
const createChartSeries = (
372448
field: keyof MetricPoint,
373449
transform?: (value: number) => number
374450
) =>
375-
filteredEvents.map((event: MetricPoint) => ({
451+
filledEvents.map((event: MetricPoint) => ({
376452
date: dateRange.granularity === 'hourly' ? event.date : event.date.slice(0, 10),
377453
value: transform ? transform(event[field] as number) : (event[field] as number) || 0,
378454
}));
@@ -392,7 +468,7 @@ export function WebsiteOverviewTab({
392468
bounceRate: createChartSeries('bounce_rate'),
393469
sessionDuration: createChartSeries('avg_session_duration', formatSessionDuration),
394470
};
395-
}, [analytics.events_by_date, dateRange.granularity, filterFutureEvents]);
471+
}, [analytics.events_by_date, dateRange.granularity, filterFutureEvents, fillMissingDates]);
396472

397473
const createTechnologyCell = (type: 'browser' | 'os') => (info: CellInfo) => {
398474
const entry = info.row.original as TechnologyData;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

apps/dashboard/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@
1111
},
1212
"dependencies": {
1313
"@ai-sdk/react": "^2.0.68",
14+
"@databuddy/auth": "workspace:*",
15+
"@databuddy/db": "workspace:*",
1416
"@databuddy/env": "workspace:*",
17+
"@databuddy/redis": "workspace:*",
18+
"@databuddy/rpc": "workspace:*",
1519
"@databuddy/sdk": "workspace:*",
20+
"@databuddy/shared": "workspace:*",
1621
"@databuddy/validation": "workspace:*",
1722
"@hello-pangea/dnd": "^18.0.1",
1823
"@hookform/resolvers": "^5.2.2",
@@ -39,8 +44,9 @@
3944
"@types/geojson": "^7946.0.16",
4045
"@types/leaflet": "^1.9.21",
4146
"ai": "^5.0.68",
42-
"autumn-js": "^0.0.101",
47+
"autumn-js": "^0.1.40",
4348
"babel-plugin-react-compiler": "^19.1.0-rc.1-rc-af1b7da-20250421",
49+
"better-auth": "^1.3.27",
4450
"class-variance-authority": "^0.7.1",
4551
"classnames": "^2.5.1",
4652
"clsx": "^2.1.1",

0 commit comments

Comments
 (0)