Skip to content

Commit 5fad021

Browse files
feat: new metric, improved ranges
Signed-off-by: Henry Gressmann <[email protected]>
1 parent 97cdfce commit 5fad021

File tree

14 files changed

+135
-64
lines changed

14 files changed

+135
-64
lines changed

src/app/core/reports.rs

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ impl Display for DateRange {
3030
#[oai(rename_all = "snake_case")]
3131
pub enum Metric {
3232
Views,
33-
Sessions,
3433
UniqueVisitors,
35-
AvgViewsPerSession,
34+
BounceRate,
35+
AvgTimeOnSite,
3636
}
3737

3838
#[derive(Debug, Enum, Clone, Copy, PartialEq)]
@@ -78,9 +78,9 @@ pub type ReportTable = BTreeMap<String, f64>;
7878
#[oai(rename_all = "camelCase")]
7979
pub struct ReportStats {
8080
pub total_views: u64,
81-
pub total_sessions: u64,
8281
pub unique_visitors: u64,
83-
pub avg_views_per_session: f64,
82+
pub bounce_rate: f64,
83+
pub avg_time_on_site: f64,
8484
}
8585

8686
#[derive(Object, Debug)]
@@ -185,10 +185,36 @@ fn filter_sql(filters: &[DimensionFilter]) -> Result<(String, ParamVec)> {
185185
fn metric_sql(metric: Metric) -> String {
186186
match metric {
187187
Metric::Views => "count(sd.created_at)",
188-
Metric::UniqueVisitors => "count(distinct sd.visitor_id)",
189-
Metric::Sessions => "count(distinct sd.visitor_id || '-' || date_trunc('minute', timestamp 'epoch' + interval '1 second' * cast(floor(extract(epoch from created_at) / 1800) * 1800 as bigint)))",
190-
Metric::AvgViewsPerSession => "count(sd.created_at) / count(distinct sd.visitor_id)",
191-
}.to_owned()
188+
Metric::UniqueVisitors => {
189+
// Count the number of unique visitors as the number of distinct visitor IDs
190+
"--sql
191+
count(distinct sd.visitor_id)"
192+
}
193+
Metric::BounceRate => {
194+
// total sessions: no time_to_next_event / time_to_next_event is null
195+
// bounce sessions: time to next / time to prev are both null or both > 1800
196+
"--sql
197+
count(distinct sd.visitor_id)
198+
filter (
199+
where
200+
(sd.time_until_next_event is null or sd.time_until_next_event > 1800) and
201+
(sd.time_since_previous_event is null or sd.time_since_previous_event > 1800)
202+
)
203+
/
204+
count(distinct sd.visitor_id)
205+
filter (
206+
where
207+
sd.time_until_next_event is null or sd.time_until_next_event > 1800
208+
)
209+
"
210+
}
211+
Metric::AvgTimeOnSite => {
212+
// avg time_until_next_event where time_until_next_event <= 1800 and time_until_next_event is not null
213+
"--sql
214+
avg(sd.time_until_next_event) filter (where sd.time_until_next_event is not null and sd.time_until_next_event <= 1800)"
215+
}
216+
}
217+
.to_owned()
192218
}
193219

194220
pub fn online_users(conn: &DuckDBConn, entities: &[String]) -> Result<u64> {
@@ -259,7 +285,10 @@ pub fn overall_report(
259285
select
260286
visitor_id,
261287
created_at,
262-
coalesce(lead(created_at) over (partition by visitor_id order by created_at) - created_at, interval '0' second) as session_duration
288+
-- the time to the next event for the same visitor
289+
extract(epoch from (lead(created_at) over (partition by visitor_id order by created_at) - created_at)) as time_until_next_event,
290+
-- the time to the previous event for the same visitor
291+
extract(epoch from (created_at - lag(created_at) over (partition by visitor_id order by created_at))) as time_since_previous_event
263292
from events, params
264293
where
265294
event = ?::text and
@@ -281,7 +310,7 @@ pub fn overall_report(
281310
)
282311
select
283312
tb.bin_start,
284-
coalesce(eb.metric_value, 0) as metric_value
313+
coalesce(eb.metric_value, 0)
285314
from
286315
time_bins tb
287316
left join event_bins eb on tb.bin_start = eb.bin_start
@@ -292,14 +321,15 @@ pub fn overall_report(
292321
let mut stmt = conn.prepare_cached(&query)?;
293322

294323
match metric {
295-
Metric::Views | Metric::UniqueVisitors | Metric::Sessions => {
324+
Metric::Views | Metric::UniqueVisitors => {
296325
let rows = stmt.query_map(duckdb::params_from_iter(params), |row| row.get(1))?;
297326
let report_graph = rows.collect::<Result<Vec<f64>, duckdb::Error>>()?;
298327
Ok(report_graph)
299328
}
300-
Metric::AvgViewsPerSession => {
301-
let rows = stmt.query_map(duckdb::params_from_iter(params), |row| row.get(1))?;
302-
let report_graph = rows.collect::<Result<Vec<f64>, duckdb::Error>>()?;
329+
Metric::AvgTimeOnSite | Metric::BounceRate => {
330+
let rows = stmt.query_map(duckdb::params_from_iter(params), |row| row.get::<_, Option<f64>>(1))?;
331+
let report_graph =
332+
rows.map(|r| r.map(|v| v.unwrap_or(0.0))).collect::<Result<Vec<f64>, duckdb::Error>>()?;
303333
Ok(report_graph)
304334
}
305335
}
@@ -322,9 +352,9 @@ pub fn overall_stats(
322352
let (filters_sql, filters_params) = filter_sql(filters)?;
323353

324354
let metric_total = metric_sql(Metric::Views);
325-
let metric_sessions = metric_sql(Metric::Sessions);
326355
let metric_unique_visitors = metric_sql(Metric::UniqueVisitors);
327-
let metric_avg_views_per_visitor = metric_sql(Metric::AvgViewsPerSession);
356+
let metric_bounce_rate = metric_sql(Metric::BounceRate);
357+
let metric_avg_time_on_site = metric_sql(Metric::AvgTimeOnSite);
328358

329359
params.push(range.start);
330360
params.push(range.end);
@@ -343,7 +373,10 @@ pub fn overall_stats(
343373
select
344374
visitor_id,
345375
created_at,
346-
coalesce(lead(created_at) over (partition by visitor_id order by created_at) - created_at, interval '0' second) as session_duration
376+
-- the time to the next event for the same visitor
377+
extract(epoch from (lead(created_at) over (partition by visitor_id order by created_at) - created_at)) as time_until_next_event,
378+
-- the time to the previous event for the same visitor
379+
extract(epoch from (created_at - lag(created_at) over (partition by visitor_id order by created_at))) as time_since_previous_event
347380
from events, params
348381
where
349382
event = ?::text and
@@ -354,9 +387,9 @@ pub fn overall_stats(
354387
)
355388
select
356389
{metric_total} as total_views,
357-
{metric_sessions} as total_sessions,
358390
{metric_unique_visitors} as unique_visitors,
359-
{metric_avg_views_per_visitor} as avg_views_per_visitor,
391+
{metric_bounce_rate} as bounce_rate,
392+
{metric_avg_time_on_site} as avg_time_on_site
360393
from
361394
session_data sd;
362395
");
@@ -365,9 +398,9 @@ pub fn overall_stats(
365398
let result = stmt.query_row(duckdb::params_from_iter(params), |row| {
366399
Ok(ReportStats {
367400
total_views: row.get(0)?,
368-
total_sessions: row.get(1)?,
369-
unique_visitors: row.get(2)?,
370-
avg_views_per_session: row.get::<_, Option<f64>>(3)?.unwrap_or(0.0),
401+
unique_visitors: row.get(1)?,
402+
bounce_rate: row.get::<_, Option<f64>>(2)?.unwrap_or(0.0),
403+
avg_time_on_site: row.get::<_, Option<f64>>(3)?.unwrap_or(0.0),
371404
})
372405
})?;
373406

@@ -427,7 +460,10 @@ pub fn dimension_report(
427460
coalesce({dimension_column}, 'Unknown') as dimension_value,
428461
visitor_id,
429462
created_at,
430-
coalesce(lead(created_at) over (partition by visitor_id order by created_at) - created_at, interval '0' second) as session_duration
463+
-- the time to the next event for the same visitor
464+
extract(epoch from (lead(created_at) over (partition by visitor_id order by created_at) - created_at)) as time_until_next_event,
465+
-- the time to the previous event for the same visitor
466+
extract(epoch from (created_at - lag(created_at) over (partition by visitor_id order by created_at))) as time_since_previous_event
431467
from events sd, params
432468
where
433469
sd.event = ?::text and
@@ -453,15 +489,15 @@ pub fn dimension_report(
453489
let mut stmt = conn.prepare_cached(&query)?;
454490

455491
match metric {
456-
Metric::Views | Metric::UniqueVisitors | Metric::Sessions => {
492+
Metric::Views | Metric::UniqueVisitors => {
457493
let rows = stmt.query_map(params_from_iter(params), |row| {
458494
let dimension_value: String = row.get(0)?;
459495
Ok((dimension_value, row.get(1)?))
460496
})?;
461497
let report_table = rows.collect::<Result<BTreeMap<String, f64>, duckdb::Error>>()?;
462498
Ok(report_table)
463499
}
464-
Metric::AvgViewsPerSession => {
500+
Metric::AvgTimeOnSite | Metric::BounceRate => {
465501
let rows = stmt.query_map(params_from_iter(params), |row| {
466502
let dimension_value: String = row.get(0)?;
467503
Ok((dimension_value, row.get(1)?))

web/src/api/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { Dimension, DimensionFilter, Metric } from "./types";
22

33
export const metricNames: Record<Metric, string> = {
44
views: "Total Views",
5-
sessions: "Total Sessions",
65
unique_visitors: "Unique Visitors",
7-
avg_views_per_session: "Avg. Views Per Session",
6+
avg_time_on_site: "Avg Time on Site",
7+
bounce_rate: "Bounce Rate",
88
};
99

1010
export const dimensionNames: Record<Dimension, string> = {

0 commit comments

Comments
 (0)