@@ -30,9 +30,9 @@ impl Display for DateRange {
3030#[ oai( rename_all = "snake_case" ) ]
3131pub 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" ) ]
7979pub 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)> {
185185fn 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
194220pub 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 ) ?) )
0 commit comments