systemprompt_analytics/repository/
cli_sessions.rs1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use sqlx::PgPool;
4use std::sync::Arc;
5use systemprompt_database::DbPool;
6use systemprompt_identifiers::UserId;
7
8use crate::models::cli::{LiveSessionRow, SessionStatsRow, SessionTrendRow};
9
10#[derive(Debug)]
11pub struct CliSessionAnalyticsRepository {
12 pool: Arc<PgPool>,
13}
14
15impl CliSessionAnalyticsRepository {
16 pub fn new(db: &DbPool) -> Result<Self> {
17 let pool = db.pool_arc()?;
18 Ok(Self { pool })
19 }
20
21 pub async fn get_stats(
22 &self,
23 start: DateTime<Utc>,
24 end: DateTime<Utc>,
25 ) -> Result<SessionStatsRow> {
26 sqlx::query_as!(
27 SessionStatsRow,
28 r#"
29 SELECT
30 COUNT(*)::bigint as "total_sessions!",
31 COUNT(DISTINCT user_id)::bigint as "unique_users!",
32 AVG(COALESCE(
33 duration_seconds,
34 EXTRACT(EPOCH FROM (COALESCE(ended_at, last_activity_at) - started_at))::INTEGER
35 ))::float8 as "avg_duration",
36 AVG(request_count)::float8 as "avg_requests",
37 COUNT(*) FILTER (WHERE converted_at IS NOT NULL)::bigint as "conversions!"
38 FROM user_sessions
39 WHERE started_at >= $1 AND started_at < $2
40 AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false
41 "#,
42 start,
43 end
44 )
45 .fetch_one(&*self.pool)
46 .await
47 .map_err(Into::into)
48 }
49
50 pub async fn get_active_session_count(&self, since: DateTime<Utc>) -> Result<i64> {
51 let count = sqlx::query_scalar!(
52 r#"SELECT COUNT(*)::bigint as "count!" FROM user_sessions WHERE ended_at IS NULL AND last_activity_at >= $1 AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false"#,
53 since
54 )
55 .fetch_one(&*self.pool)
56 .await?;
57 Ok(count)
58 }
59
60 pub async fn get_live_sessions(
61 &self,
62 cutoff: DateTime<Utc>,
63 limit: i64,
64 ) -> Result<Vec<LiveSessionRow>> {
65 sqlx::query_as!(
66 LiveSessionRow,
67 r#"
68 SELECT
69 session_id as "session_id!",
70 COALESCE(user_type, 'unknown') as "user_type",
71 started_at as "started_at!",
72 duration_seconds,
73 request_count,
74 last_activity_at as "last_activity_at!"
75 FROM user_sessions
76 WHERE ended_at IS NULL
77 AND last_activity_at >= $1
78 AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false
79 ORDER BY last_activity_at DESC
80 LIMIT $2
81 "#,
82 cutoff,
83 limit
84 )
85 .fetch_all(&*self.pool)
86 .await
87 .map_err(Into::into)
88 }
89
90 pub async fn get_active_count(&self, cutoff: DateTime<Utc>) -> Result<i64> {
91 let count = sqlx::query_scalar!(
92 r#"SELECT COUNT(*)::bigint as "count!" FROM user_sessions WHERE ended_at IS NULL AND last_activity_at >= $1 AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false"#,
93 cutoff
94 )
95 .fetch_one(&*self.pool)
96 .await?;
97 Ok(count)
98 }
99
100 pub async fn get_sessions_for_trends(
101 &self,
102 start: DateTime<Utc>,
103 end: DateTime<Utc>,
104 ) -> Result<Vec<SessionTrendRow>> {
105 sqlx::query_as!(
106 SessionTrendRow,
107 r#"
108 SELECT
109 started_at as "started_at!",
110 user_id as "user_id: UserId",
111 duration_seconds
112 FROM user_sessions
113 WHERE started_at >= $1 AND started_at < $2
114 AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false
115 ORDER BY started_at
116 "#,
117 start,
118 end
119 )
120 .fetch_all(&*self.pool)
121 .await
122 .map_err(Into::into)
123 }
124
125 pub async fn get_active_count_since(&self, start: DateTime<Utc>) -> Result<i64> {
126 let count = sqlx::query_scalar!(
127 r#"
128 SELECT COUNT(*)::bigint as "count!"
129 FROM user_sessions
130 WHERE ended_at IS NULL
131 AND last_activity_at >= $1
132 AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false
133 "#,
134 start
135 )
136 .fetch_one(&*self.pool)
137 .await?;
138 Ok(count)
139 }
140
141 pub async fn get_total_count(&self, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<i64> {
142 let count = sqlx::query_scalar!(
143 r#"SELECT COUNT(*)::bigint as "count!" FROM user_sessions WHERE started_at >= $1 AND started_at < $2 AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false"#,
144 start,
145 end
146 )
147 .fetch_one(&*self.pool)
148 .await?;
149 Ok(count)
150 }
151}