Skip to content

Commit 47738a2

Browse files
committed
feat: better stats
1 parent e99bc0d commit 47738a2

File tree

5 files changed

+189
-66
lines changed

5 files changed

+189
-66
lines changed

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

Lines changed: 90 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,57 +6,75 @@ export const SummaryBuilders: Record<
66
SimpleQueryConfig<typeof Analytics.events>
77
> = {
88
summary_metrics: {
9-
customSql: (websiteId: string, startDate: string, endDate: string) => {
9+
customSql: (
10+
websiteId: string,
11+
startDate: string,
12+
endDate: string,
13+
_filters?: Filter[],
14+
_granularity?: TimeUnit,
15+
_limit?: number,
16+
_offset?: number,
17+
timezone?: string
18+
) => {
19+
const tz = timezone || 'UTC';
1020
return {
1121
sql: `
12-
WITH session_metrics AS (
22+
WITH base_events AS (
1323
SELECT
1424
session_id,
15-
countIf(event_name = 'screen_view') as page_count
25+
anonymous_id,
26+
event_name,
27+
toTimeZone(time, {timezone:String}) as normalized_time
1628
FROM analytics.events
1729
WHERE
1830
client_id = {websiteId:String}
1931
AND time >= parseDateTimeBestEffort({startDate:String})
2032
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
33+
AND session_id != ''
34+
),
35+
session_metrics AS (
36+
SELECT
37+
session_id,
38+
countIf(event_name = 'screen_view') as page_count
39+
FROM base_events
2140
GROUP BY session_id
2241
),
2342
session_durations AS (
2443
SELECT
2544
session_id,
26-
dateDiff('second', MIN(time), MAX(time)) as duration
27-
FROM analytics.events
28-
WHERE
29-
client_id = {websiteId:String}
30-
AND time >= parseDateTimeBestEffort({startDate:String})
31-
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
45+
dateDiff('second', MIN(normalized_time), MAX(normalized_time)) as duration
46+
FROM base_events
3247
GROUP BY session_id
33-
HAVING duration > 0
48+
HAVING duration >= 0
3449
),
3550
unique_visitors AS (
3651
SELECT
3752
countDistinct(anonymous_id) as unique_visitors
38-
FROM analytics.events
39-
WHERE
40-
client_id = {websiteId:String}
41-
AND time >= parseDateTimeBestEffort({startDate:String})
42-
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
43-
AND event_name = 'screen_view'
53+
FROM base_events
54+
WHERE event_name = 'screen_view'
4455
),
4556
all_events AS (
4657
SELECT
47-
count() as total_events
48-
FROM analytics.events
49-
WHERE
50-
client_id = {websiteId:String}
51-
AND time >= parseDateTimeBestEffort({startDate:String})
52-
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
58+
count() as total_events,
59+
countIf(event_name = 'screen_view') as total_screen_views
60+
FROM base_events
61+
),
62+
bounce_sessions AS (
63+
SELECT
64+
countIf(page_count = 1) as bounced_sessions,
65+
count() as total_sessions
66+
FROM session_metrics
5367
)
5468
SELECT
5569
sum(page_count) as pageviews,
5670
(SELECT unique_visitors FROM unique_visitors) as unique_visitors,
57-
count(session_metrics.session_id) as sessions,
58-
ROUND((COALESCE(countIf(page_count = 1), 0) / COALESCE(COUNT(*), 0)) * 100, 2) as bounce_rate,
59-
ROUND(AVG(sd.duration), 2) as avg_session_duration,
71+
(SELECT total_sessions FROM bounce_sessions) as sessions,
72+
ROUND(CASE
73+
WHEN (SELECT total_sessions FROM bounce_sessions) > 0
74+
THEN ((SELECT bounced_sessions FROM bounce_sessions) / (SELECT total_sessions FROM bounce_sessions)) * 100
75+
ELSE 0
76+
END, 2) as bounce_rate,
77+
ROUND(median(sd.duration), 2) as avg_session_duration,
6078
(SELECT total_events FROM all_events) as total_events
6179
FROM session_metrics
6280
LEFT JOIN session_durations as sd ON session_metrics.session_id = sd.session_id
@@ -65,6 +83,7 @@ export const SummaryBuilders: Record<
6583
websiteId,
6684
startDate,
6785
endDate,
86+
timezone: tz,
6887
},
6988
};
7089
},
@@ -98,10 +117,10 @@ export const SummaryBuilders: Record<
98117
websiteId: string,
99118
startDate: string,
100119
endDate: string,
101-
filters?: Filter[],
120+
_filters?: Filter[],
102121
granularity?: TimeUnit,
103-
limit?: number,
104-
offset?: number,
122+
_limit?: number,
123+
_offset?: number,
105124
timezone?: string
106125
) => {
107126
const tz = timezone || 'UTC';
@@ -110,7 +129,20 @@ export const SummaryBuilders: Record<
110129
if (isHourly) {
111130
return {
112131
sql: `
113-
WITH hour_range AS (
132+
WITH base_events AS (
133+
SELECT
134+
session_id,
135+
anonymous_id,
136+
event_name,
137+
toTimeZone(time, {timezone:String}) as normalized_time
138+
FROM analytics.events
139+
WHERE
140+
client_id = {websiteId:String}
141+
AND time >= parseDateTimeBestEffort({startDate:String})
142+
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
143+
AND session_id != ''
144+
),
145+
hour_range AS (
114146
SELECT arrayJoin(arrayMap(
115147
h -> toDateTime(concat({startDate:String}, ' 00:00:00')) + (h * 3600),
116148
range(toUInt32(dateDiff('hour', toDateTime(concat({startDate:String}, ' 00:00:00')), toDateTime(concat({endDate:String}, ' 23:59:59'))) + 1))
@@ -119,35 +151,27 @@ export const SummaryBuilders: Record<
119151
session_details AS (
120152
SELECT
121153
session_id,
122-
toStartOfHour(toTimeZone(MIN(time), {timezone:String})) as session_start_hour,
154+
toStartOfHour(MIN(normalized_time)) as session_start_hour,
123155
countIf(event_name = 'screen_view') as page_count,
124-
dateDiff('second', MIN(time), MAX(time)) as duration
125-
FROM analytics.events
126-
WHERE
127-
client_id = {websiteId:String}
128-
AND time >= parseDateTimeBestEffort({startDate:String})
129-
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
156+
dateDiff('second', MIN(normalized_time), MAX(normalized_time)) as duration
157+
FROM base_events
130158
GROUP BY session_id
131159
),
132160
hourly_session_metrics AS (
133161
SELECT
134162
session_start_hour as event_hour,
135163
count(session_id) as sessions,
136164
countIf(page_count = 1) as bounced_sessions,
137-
avgIf(duration, duration > 0) as avg_session_duration
165+
medianIf(duration, duration >= 0) as median_session_duration
138166
FROM session_details
139167
GROUP BY session_start_hour
140168
),
141169
hourly_event_metrics AS (
142170
SELECT
143-
toStartOfHour(toTimeZone(time, {timezone:String})) as event_hour,
171+
toStartOfHour(normalized_time) as event_hour,
144172
countIf(event_name = 'screen_view') as pageviews,
145173
count(distinct anonymous_id) as unique_visitors
146-
FROM analytics.events
147-
WHERE
148-
client_id = {websiteId:String}
149-
AND time >= parseDateTimeBestEffort({startDate:String})
150-
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
174+
FROM base_events
151175
GROUP BY event_hour
152176
)
153177
SELECT
@@ -160,7 +184,7 @@ export const SummaryBuilders: Record<
160184
THEN (COALESCE(hsm.bounced_sessions, 0) / hsm.sessions) * 100
161185
ELSE 0
162186
END, 2) as bounce_rate,
163-
ROUND(COALESCE(hsm.avg_session_duration, 0), 2) as avg_session_duration,
187+
ROUND(COALESCE(hsm.median_session_duration, 0), 2) as avg_session_duration,
164188
ROUND(CASE
165189
WHEN COALESCE(hsm.sessions, 0) > 0
166190
THEN COALESCE(hem.pageviews, 0) / COALESCE(hsm.sessions, 0)
@@ -182,7 +206,20 @@ export const SummaryBuilders: Record<
182206

183207
return {
184208
sql: `
185-
WITH date_range AS (
209+
WITH base_events AS (
210+
SELECT
211+
session_id,
212+
anonymous_id,
213+
event_name,
214+
toTimeZone(time, {timezone:String}) as normalized_time
215+
FROM analytics.events
216+
WHERE
217+
client_id = {websiteId:String}
218+
AND time >= parseDateTimeBestEffort({startDate:String})
219+
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
220+
AND session_id != ''
221+
),
222+
date_range AS (
186223
SELECT arrayJoin(arrayMap(
187224
d -> toDate({startDate:String}) + d,
188225
range(toUInt32(dateDiff('day', toDate({startDate:String}), toDate({endDate:String})) + 1))
@@ -191,35 +228,27 @@ export const SummaryBuilders: Record<
191228
session_details AS (
192229
SELECT
193230
session_id,
194-
toDate(toTimeZone(MIN(time), {timezone:String})) as session_start_date,
231+
toDate(MIN(normalized_time)) as session_start_date,
195232
countIf(event_name = 'screen_view') as page_count,
196-
dateDiff('second', MIN(time), MAX(time)) as duration
197-
FROM analytics.events
198-
WHERE
199-
client_id = {websiteId:String}
200-
AND time >= parseDateTimeBestEffort({startDate:String})
201-
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
233+
dateDiff('second', MIN(normalized_time), MAX(normalized_time)) as duration
234+
FROM base_events
202235
GROUP BY session_id
203236
),
204237
daily_session_metrics AS (
205238
SELECT
206239
session_start_date,
207240
count(session_id) as sessions,
208241
countIf(page_count = 1) as bounced_sessions,
209-
avgIf(duration, duration > 0) as avg_session_duration
242+
medianIf(duration, duration >= 0) as median_session_duration
210243
FROM session_details
211244
GROUP BY session_start_date
212245
),
213246
daily_event_metrics AS (
214247
SELECT
215-
toDate(toTimeZone(time, {timezone:String})) as event_date,
248+
toDate(normalized_time) as event_date,
216249
countIf(event_name = 'screen_view') as pageviews,
217250
count(distinct anonymous_id) as unique_visitors
218-
FROM analytics.events
219-
WHERE
220-
client_id = {websiteId:String}
221-
AND time >= parseDateTimeBestEffort({startDate:String})
222-
AND time <= parseDateTimeBestEffort(concat({endDate:String}, ' 23:59:59'))
251+
FROM base_events
223252
GROUP BY event_date
224253
)
225254
SELECT
@@ -232,7 +261,7 @@ export const SummaryBuilders: Record<
232261
THEN (COALESCE(dsm.bounced_sessions, 0) / dsm.sessions) * 100
233262
ELSE 0
234263
END, 2) as bounce_rate,
235-
ROUND(COALESCE(dsm.avg_session_duration, 0), 2) as avg_session_duration,
264+
ROUND(COALESCE(dsm.median_session_duration, 0), 2) as avg_session_duration,
236265
ROUND(CASE
237266
WHEN COALESCE(dsm.sessions, 0) > 0
238267
THEN COALESCE(dem.pageviews, 0) / COALESCE(dsm.sessions, 0)
@@ -273,6 +302,7 @@ export const SummaryBuilders: Record<
273302
FROM analytics.events
274303
WHERE event_name = 'screen_view'
275304
AND client_id = {websiteId:String}
305+
AND session_id != ''
276306
AND ${timeCondition}
277307
`,
278308
params: {

apps/dashboard/app/(main)/layout.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export default async function MainLayout({
99
children: React.ReactNode;
1010
}) {
1111
const session = await auth.api.getSession({ headers: await headers() });
12-
1312
if (!session) {
1413
redirect('/login');
1514
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,12 @@ export function WebsiteOverviewTab({
478478
pageviews: createChartSeries('pageviews'),
479479
pagesPerSession: createChartSeries('pages_per_session'),
480480
bounceRate: createChartSeries('bounce_rate'),
481-
sessionDuration: createChartSeries('avg_session_duration'),
481+
sessionDuration: createChartSeries('avg_session_duration', (value) => {
482+
if (value < 60) return Math.round(value);
483+
const minutes = Math.floor(value / 60);
484+
const seconds = Math.round(value % 60);
485+
return minutes * 60 + seconds;
486+
}),
482487
};
483488
})();
484489

@@ -925,7 +930,13 @@ export function WebsiteOverviewTab({
925930
chartData: miniChartData.sessionDuration,
926931
trend: calculateTrends.session_duration,
927932
formatValue: (value: number) => {
928-
if (value < 60) return `${value.toFixed(1)}s`;
933+
if (value < 60) return `${Math.round(value)}s`;
934+
const minutes = Math.floor(value / 60);
935+
const seconds = Math.round(value % 60);
936+
return `${minutes}m ${seconds}s`;
937+
},
938+
formatChartValue: (value: number) => {
939+
if (value < 60) return `${Math.round(value)}s`;
929940
const minutes = Math.floor(value / 60);
930941
const seconds = Math.round(value % 60);
931942
return `${minutes}m ${seconds}s`;
@@ -944,6 +955,7 @@ export function WebsiteOverviewTab({
944955
' today'
945956
: metric.description
946957
}
958+
formatChartValue={metric.formatChartValue}
947959
formatValue={metric.formatValue}
948960
icon={metric.icon}
949961
id={metric.id}

apps/dashboard/components/analytics/stat-card.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ interface StatCardProps {
5050
chartData?: MiniChartDataPoint[];
5151
showChart?: boolean;
5252
formatValue?: (value: number) => string;
53+
formatChartValue?: (value: number) => string;
5354
}
5455

5556
const formatTrendValue = (
@@ -68,7 +69,15 @@ const formatTrendValue = (
6869
};
6970

7071
const MiniChart = memo(
71-
({ data, id }: { data: MiniChartDataPoint[]; id: string }) => {
72+
({
73+
data,
74+
id,
75+
formatChartValue,
76+
}: {
77+
data: MiniChartDataPoint[];
78+
id: string;
79+
formatChartValue?: (value: number) => string;
80+
}) => {
7281
const hasData = data && data.length > 0;
7382
const hasVariation = hasData && data.some((d) => d.value !== data[0].value);
7483

@@ -137,7 +146,9 @@ const MiniChart = memo(
137146
})}
138147
</p>
139148
<p className="font-semibold text-primary">
140-
{formatMetricNumber(payload[0].value)}
149+
{formatChartValue
150+
? formatChartValue(payload[0].value)
151+
: formatMetricNumber(payload[0].value)}
141152
</p>
142153
</div>
143154
) : null
@@ -188,6 +199,7 @@ export function StatCard({
188199
chartData,
189200
showChart = false,
190201
formatValue,
202+
formatChartValue,
191203
}: StatCardProps) {
192204
const getVariantClasses = () => {
193205
switch (variant) {
@@ -334,7 +346,11 @@ export function StatCard({
334346

335347
{hasValidChartData && (
336348
<div className="-mb-0.5 sm:-mb-1 [--chart-color:theme(colors.primary.DEFAULT)] group-hover:[--chart-color:theme(colors.primary.500)]">
337-
<MiniChart data={chartData} id={id || `chart-${Math.random()}`} />
349+
<MiniChart
350+
data={chartData}
351+
formatChartValue={formatChartValue}
352+
id={id || `chart-${Math.random()}`}
353+
/>
338354
</div>
339355
)}
340356
</div>

0 commit comments

Comments
 (0)