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