systemprompt_analytics/repository/
queries.rs1use anyhow::{anyhow, Result};
2use serde::Serialize;
3use systemprompt_database::{DatabaseProvider, DbPool, JsonRow, ToDbValue};
4use systemprompt_traits::{Repository as RepositoryTrait, RepositoryError};
5
6#[derive(Debug, Clone)]
7pub struct AnalyticsQueryRepository {
8 db_pool: DbPool,
9}
10
11impl RepositoryTrait for AnalyticsQueryRepository {
12 type Pool = DbPool;
13 type Error = RepositoryError;
14
15 fn pool(&self) -> &Self::Pool {
16 &self.db_pool
17 }
18}
19
20impl AnalyticsQueryRepository {
21 pub const fn new(db_pool: DbPool) -> Self {
22 Self { db_pool }
23 }
24
25 pub async fn get_ai_provider_usage(
26 &self,
27 days: i32,
28 user_id: Option<&str>,
29 ) -> Result<Vec<ProviderUsage>> {
30 let base_query = r"
31 SELECT
32 provider,
33 model,
34 COUNT(*) as request_count,
35 SUM(tokens_used) as total_tokens,
36 SUM(cost_cents) as total_cost_cents,
37 AVG(latency_ms) as avg_latency_ms,
38 COUNT(DISTINCT user_id) as unique_users,
39 COUNT(DISTINCT session_id) as unique_sessions
40 FROM ai_requests
41 WHERE created_at >= NOW() - INTERVAL '1 day' * $1
42 ";
43
44 let mut query = base_query.to_string();
45 let mut params: Vec<Box<dyn ToDbValue>> = vec![Box::new(days)];
46 let mut param_index = 2;
47
48 let placeholder = |idx: &mut i32| {
49 let placeholder = format!("${idx}");
50 *idx += 1;
51 placeholder
52 };
53
54 if let Some(uid) = user_id {
55 query.push_str(&format!(" AND user_id = {}", placeholder(&mut param_index)));
56 params.push(Box::new(uid.to_string()));
57 }
58
59 query.push_str(" GROUP BY provider, model ORDER BY request_count DESC");
60
61 let param_refs: Vec<&dyn ToDbValue> = params.iter().map(|p| &**p).collect();
62
63 let rows = self.db_pool.as_ref().fetch_all(&query, ¶m_refs).await?;
64
65 rows.iter()
66 .map(ProviderUsage::from_json_row)
67 .collect::<Result<Vec<_>>>()
68 }
69}
70
71#[derive(Debug, Serialize)]
72pub struct ProviderUsage {
73 pub provider: String,
74 pub model: String,
75 pub request_count: i32,
76 pub total_tokens: Option<i32>,
77 pub total_cost_cents: Option<i32>,
78 pub avg_latency_ms: Option<f64>,
79 pub unique_users: i32,
80 pub unique_sessions: i32,
81}
82
83impl ProviderUsage {
84 pub fn from_json_row(row: &JsonRow) -> Result<Self> {
85 let provider = row
86 .get("provider")
87 .and_then(|v| v.as_str())
88 .ok_or_else(|| anyhow!("Missing provider"))?
89 .to_string();
90
91 let model = row
92 .get("model")
93 .and_then(|v| v.as_str())
94 .ok_or_else(|| anyhow!("Missing model"))?
95 .to_string();
96
97 let request_count = row
98 .get("request_count")
99 .and_then(serde_json::Value::as_i64)
100 .ok_or_else(|| anyhow!("Missing request_count"))? as i32;
101
102 let total_tokens = row
103 .get("total_tokens")
104 .and_then(serde_json::Value::as_i64)
105 .map(|i| i as i32);
106
107 let total_cost_cents = row
108 .get("total_cost_cents")
109 .and_then(serde_json::Value::as_i64)
110 .map(|i| i as i32);
111
112 let avg_latency_ms = row
113 .get("avg_latency_ms")
114 .and_then(serde_json::Value::as_f64);
115
116 let unique_users = row
117 .get("unique_users")
118 .and_then(serde_json::Value::as_i64)
119 .ok_or_else(|| anyhow!("Missing unique_users"))? as i32;
120
121 let unique_sessions =
122 row.get("unique_sessions")
123 .and_then(serde_json::Value::as_i64)
124 .ok_or_else(|| anyhow!("Missing unique_sessions"))? as i32;
125
126 Ok(Self {
127 provider,
128 model,
129 request_count,
130 total_tokens,
131 total_cost_cents,
132 avg_latency_ms,
133 unique_users,
134 unique_sessions,
135 })
136 }
137}