Skip to main content

systemprompt_analytics/repository/
cli_sessions.rs

1use 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(duration_seconds)::float8 as "avg_duration",
33                AVG(request_count)::float8 as "avg_requests",
34                COUNT(*) FILTER (WHERE converted_at IS NOT NULL)::bigint as "conversions!"
35            FROM user_sessions
36            WHERE started_at >= $1 AND started_at < $2
37              AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false
38            "#,
39            start,
40            end
41        )
42        .fetch_one(&*self.pool)
43        .await
44        .map_err(Into::into)
45    }
46
47    pub async fn get_active_session_count(&self, since: DateTime<Utc>) -> Result<i64> {
48        let count = sqlx::query_scalar!(
49            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"#,
50            since
51        )
52        .fetch_one(&*self.pool)
53        .await?;
54        Ok(count)
55    }
56
57    pub async fn get_live_sessions(
58        &self,
59        cutoff: DateTime<Utc>,
60        limit: i64,
61    ) -> Result<Vec<LiveSessionRow>> {
62        sqlx::query_as!(
63            LiveSessionRow,
64            r#"
65            SELECT
66                session_id as "session_id!",
67                COALESCE(user_type, 'unknown') as "user_type",
68                started_at as "started_at!",
69                duration_seconds,
70                request_count,
71                last_activity_at as "last_activity_at!"
72            FROM user_sessions
73            WHERE ended_at IS NULL
74              AND last_activity_at >= $1
75              AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false
76            ORDER BY last_activity_at DESC
77            LIMIT $2
78            "#,
79            cutoff,
80            limit
81        )
82        .fetch_all(&*self.pool)
83        .await
84        .map_err(Into::into)
85    }
86
87    pub async fn get_active_count(&self, cutoff: DateTime<Utc>) -> Result<i64> {
88        let count = sqlx::query_scalar!(
89            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"#,
90            cutoff
91        )
92        .fetch_one(&*self.pool)
93        .await?;
94        Ok(count)
95    }
96
97    pub async fn get_sessions_for_trends(
98        &self,
99        start: DateTime<Utc>,
100        end: DateTime<Utc>,
101    ) -> Result<Vec<SessionTrendRow>> {
102        sqlx::query_as!(
103            SessionTrendRow,
104            r#"
105            SELECT
106                started_at as "started_at!",
107                user_id as "user_id: UserId",
108                duration_seconds
109            FROM user_sessions
110            WHERE started_at >= $1 AND started_at < $2
111              AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false
112            ORDER BY started_at
113            "#,
114            start,
115            end
116        )
117        .fetch_all(&*self.pool)
118        .await
119        .map_err(Into::into)
120    }
121
122    pub async fn get_active_count_since(&self, start: DateTime<Utc>) -> Result<i64> {
123        let count = sqlx::query_scalar!(
124            r#"
125            SELECT COUNT(*)::bigint as "count!"
126            FROM user_sessions
127            WHERE ended_at IS NULL
128              AND last_activity_at >= $1
129              AND is_bot = false AND is_behavioral_bot = false AND is_scanner = false
130            "#,
131            start
132        )
133        .fetch_one(&*self.pool)
134        .await?;
135        Ok(count)
136    }
137
138    pub async fn get_total_count(&self, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<i64> {
139        let count = sqlx::query_scalar!(
140            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"#,
141            start,
142            end
143        )
144        .fetch_one(&*self.pool)
145        .await?;
146        Ok(count)
147    }
148}