systemprompt_analytics/repository/costs/
per_user.rs1use super::CostAnalyticsRepository;
15use crate::Result;
16use crate::models::RecentContextDbRow;
17use chrono::{DateTime, Utc};
18use systemprompt_identifiers::{ContextId, UserId};
19
20use crate::models::reporting::{
21 ContextGroupRow, ContextSummaryRow, CostBreakdownRow, CostSummaryRow, PreviousCostRow,
22 RecentContextRow,
23};
24
25impl CostAnalyticsRepository {
26 pub async fn get_summary_for_user(
27 &self,
28 user_id: &UserId,
29 start: DateTime<Utc>,
30 end: DateTime<Utc>,
31 ) -> Result<CostSummaryRow> {
32 sqlx::query_as!(
33 CostSummaryRow,
34 r#"
35 SELECT
36 COALESCE(SUM(request_count), 0)::bigint as "requests!",
37 SUM(cost_microdollars)::bigint as "cost",
38 SUM(tokens)::bigint as "tokens",
39 SUM(reasoning_tokens)::bigint as "reasoning_tokens",
40 SUM(cache_read_tokens)::bigint as "cache_read_tokens",
41 SUM(cache_creation_tokens)::bigint as "cache_creation_tokens"
42 FROM report_ai_usage_hourly
43 WHERE bucket >= date_trunc('hour', $1::timestamptz, 'UTC') AND bucket < $2
44 AND NOT synthetic AND user_id = $3
45 "#,
46 start,
47 end,
48 user_id.as_str()
49 )
50 .fetch_one(&*self.pool)
51 .await
52 .map_err(Into::into)
53 }
54
55 pub async fn get_previous_cost_for_user(
56 &self,
57 user_id: &UserId,
58 start: DateTime<Utc>,
59 end: DateTime<Utc>,
60 ) -> Result<PreviousCostRow> {
61 sqlx::query_as!(
62 PreviousCostRow,
63 r#"
64 SELECT SUM(cost_microdollars)::bigint as "cost"
65 FROM report_ai_usage_hourly
66 WHERE bucket >= date_trunc('hour', $1::timestamptz, 'UTC') AND bucket < $2
67 AND NOT synthetic AND user_id = $3
68 "#,
69 start,
70 end,
71 user_id.as_str()
72 )
73 .fetch_one(&*self.pool)
74 .await
75 .map_err(Into::into)
76 }
77
78 pub async fn get_breakdown_by_model_for_user(
79 &self,
80 user_id: &UserId,
81 start: DateTime<Utc>,
82 end: DateTime<Utc>,
83 limit: i64,
84 ) -> Result<Vec<CostBreakdownRow>> {
85 sqlx::query_as!(
86 CostBreakdownRow,
87 r#"
88 SELECT
89 model as "name!",
90 COALESCE(SUM(cost_microdollars), 0)::bigint as "cost!",
91 SUM(request_count)::bigint as "requests!",
92 COALESCE(SUM(tokens), 0)::bigint as "tokens!"
93 FROM report_ai_usage_hourly
94 WHERE bucket >= date_trunc('hour', $1::timestamptz, 'UTC') AND bucket < $2
95 AND NOT synthetic AND user_id = $4
96 AND model <> ''
97 GROUP BY model
98 ORDER BY SUM(cost_microdollars) DESC NULLS LAST
99 LIMIT $3
100 "#,
101 start,
102 end,
103 limit,
104 user_id.as_str()
105 )
106 .fetch_all(&*self.pool)
107 .await
108 .map_err(Into::into)
109 }
110
111 pub async fn get_context_summary_for_user(
112 &self,
113 user_id: &UserId,
114 start: DateTime<Utc>,
115 end: DateTime<Utc>,
116 ) -> Result<ContextSummaryRow> {
117 sqlx::query_as!(
118 ContextSummaryRow,
119 r#"
120 SELECT
121 COUNT(DISTINCT context_id)::bigint as "conversations!",
122 COUNT(*)::bigint as "ai_requests!"
123 FROM report_ai_requests
124 WHERE created_at >= $1 AND created_at < $2
125 AND NOT synthetic
126 AND user_id = $3
127 AND context_id IS NOT NULL
128 "#,
129 start,
130 end,
131 user_id.as_str()
132 )
133 .fetch_one(&*self.pool)
134 .await
135 .map_err(Into::into)
136 }
137
138 pub async fn get_contexts_by_model_for_user(
139 &self,
140 user_id: &UserId,
141 start: DateTime<Utc>,
142 end: DateTime<Utc>,
143 limit: i64,
144 ) -> Result<Vec<ContextGroupRow>> {
145 sqlx::query_as!(
146 ContextGroupRow,
147 r#"
148 SELECT
149 model as "name!",
150 COUNT(DISTINCT context_id)::bigint as "conversations!",
151 COUNT(*)::bigint as "ai_requests!"
152 FROM report_ai_requests
153 WHERE created_at >= $1 AND created_at < $2
154 AND NOT synthetic
155 AND user_id = $3
156 AND context_id IS NOT NULL
157 AND model IS NOT NULL
158 GROUP BY model
159 ORDER BY COUNT(DISTINCT context_id) DESC
160 LIMIT $4
161 "#,
162 start,
163 end,
164 user_id.as_str(),
165 limit
166 )
167 .fetch_all(&*self.pool)
168 .await
169 .map_err(Into::into)
170 }
171
172 pub async fn get_contexts_by_agent_for_user(
173 &self,
174 user_id: &UserId,
175 start: DateTime<Utc>,
176 end: DateTime<Utc>,
177 limit: i64,
178 ) -> Result<Vec<ContextGroupRow>> {
179 sqlx::query_as!(
180 ContextGroupRow,
181 r#"
182 SELECT
183 COALESCE(at.agent_name, 'unattributed') as "name!",
184 COUNT(DISTINCT r.context_id)::bigint as "conversations!",
185 COUNT(*)::bigint as "ai_requests!"
186 FROM report_ai_requests r
187 LEFT JOIN report_agent_tasks at ON at.task_id = r.task_id
188 WHERE r.created_at >= $1 AND r.created_at < $2
189 AND NOT r.synthetic
190 AND r.user_id = $3
191 AND r.context_id IS NOT NULL
192 GROUP BY COALESCE(at.agent_name, 'unattributed')
193 ORDER BY COUNT(DISTINCT r.context_id) DESC
194 LIMIT $4
195 "#,
196 start,
197 end,
198 user_id.as_str(),
199 limit
200 )
201 .fetch_all(&*self.pool)
202 .await
203 .map_err(Into::into)
204 }
205
206 pub async fn get_recent_contexts_for_user(
207 &self,
208 user_id: &UserId,
209 end: DateTime<Utc>,
210 limit: i64,
211 ) -> Result<Vec<RecentContextRow>> {
212 let rows = sqlx::query_as!(
213 RecentContextDbRow,
214 r#"
215 SELECT
216 ctx.context_id as "context_id!: ContextId",
217 ctx.last_activity as "last_activity!",
218 ctx.ai_requests as "ai_requests!",
219 last_req.model,
220 last_task.agent_name,
221 uc.name as "context_name?"
222 FROM (
223 SELECT
224 r.context_id,
225 MAX(r.created_at) AS last_activity,
226 COUNT(*) AS ai_requests
227 FROM report_ai_requests r
228 WHERE r.user_id = $1
229 AND r.created_at < $2
230 AND NOT r.synthetic
231 AND r.context_id IS NOT NULL
232 GROUP BY r.context_id
233 ORDER BY MAX(r.created_at) DESC
234 LIMIT $3
235 ) ctx
236 LEFT JOIN LATERAL (
237 SELECT model, task_id FROM report_ai_requests
238 WHERE context_id = ctx.context_id
239 ORDER BY created_at DESC
240 LIMIT 1
241 ) last_req ON TRUE
242 LEFT JOIN report_agent_tasks last_task ON last_task.task_id = last_req.task_id
243 LEFT JOIN report_user_contexts uc ON uc.context_id = ctx.context_id
244 ORDER BY ctx.last_activity DESC
245 "#,
246 user_id.as_str(),
247 end,
248 limit
249 )
250 .fetch_all(&*self.pool)
251 .await?;
252 Ok(rows.into_iter().map(RecentContextRow::from).collect())
253 }
254}