1use axum::{
6 extract::{Query, State},
7 Json,
8};
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 error::{ApiError, ApiResult},
13 middleware::AuthUser,
14 AppState,
15};
16
17#[derive(Debug, Serialize)]
18pub struct AnalyticsResponse {
19 pub users: UserAnalytics,
20 pub subscriptions: SubscriptionAnalytics,
21 pub usage: UsageAnalytics,
22 pub features: FeatureAnalytics,
23 pub growth: GrowthAnalytics,
24 pub activity: ActivityAnalytics,
25}
26
27#[derive(Debug, Serialize)]
28pub struct UserAnalytics {
29 pub total: i64,
30 pub verified: i64,
31 pub unverified: i64,
32 pub by_auth_provider: Vec<AuthProviderCount>,
33 pub new_users_last_7d: i64,
34 pub new_users_last_30d: i64,
35}
36
37#[derive(Debug, Serialize)]
38pub struct AuthProviderCount {
39 pub provider: String,
40 pub count: i64,
41}
42
43#[derive(Debug, Serialize)]
44pub struct SubscriptionAnalytics {
45 pub total_orgs: i64,
46 pub by_plan: Vec<PlanCount>,
47 pub active_subscriptions: i64,
48 pub trial_orgs: i64,
49 pub revenue_estimate: f64, }
51
52#[derive(Debug, Serialize)]
53pub struct PlanCount {
54 pub plan: String,
55 pub count: i64,
56}
57
58#[derive(Debug, Serialize)]
59pub struct UsageAnalytics {
60 pub total_requests: i64,
61 pub total_storage_gb: f64,
62 pub total_ai_tokens: i64,
63 pub avg_requests_per_org: f64,
64 pub top_orgs_by_usage: Vec<OrgUsage>,
65}
66
67#[derive(Debug, Serialize)]
68pub struct OrgUsage {
69 pub org_id: String,
70 pub org_name: String,
71 pub plan: String,
72 pub requests: i64,
73 pub storage_gb: f64,
74}
75
76#[derive(Debug, Serialize)]
77pub struct FeatureAnalytics {
78 pub hosted_mocks: FeatureUsage,
79 pub plugins_published: FeatureUsage,
80 pub templates_published: FeatureUsage,
81 pub scenarios_published: FeatureUsage,
82 pub api_tokens_created: FeatureUsage,
83}
84
85#[derive(Debug, Serialize)]
86pub struct FeatureUsage {
87 pub total: i64,
88 pub active_orgs: i64, pub last_30d: i64,
90}
91
92#[derive(Debug, Serialize)]
93pub struct GrowthAnalytics {
94 pub user_growth_7d: Vec<DailyCount>,
95 pub user_growth_30d: Vec<DailyCount>,
96 pub org_growth_7d: Vec<DailyCount>,
97 pub org_growth_30d: Vec<DailyCount>,
98}
99
100#[derive(Debug, Serialize)]
101pub struct DailyCount {
102 pub date: String,
103 pub count: i64,
104}
105
106#[derive(Debug, Serialize)]
107pub struct ActivityAnalytics {
108 pub logins_last_24h: i64,
109 pub logins_last_7d: i64,
110 pub api_requests_last_24h: i64,
111 pub api_requests_last_7d: i64,
112}
113
114#[derive(Debug, Deserialize)]
115pub struct AnalyticsQuery {
116 pub period: Option<String>, }
118
119pub async fn get_analytics(
121 State(state): State<AppState>,
122 AuthUser(user_id): AuthUser,
123 Query(_query): Query<AnalyticsQuery>,
124) -> ApiResult<Json<AnalyticsResponse>> {
125 let user = state
127 .store
128 .find_user_by_id(user_id)
129 .await?
130 .ok_or_else(|| ApiError::InvalidRequest("User not found".to_string()))?;
131
132 if !user.is_admin {
133 return Err(ApiError::PermissionDenied);
134 }
135
136 let snap = state.store.get_admin_analytics_snapshot().await?;
137
138 let revenue_estimate = snap
140 .plan_distribution
141 .iter()
142 .map(|(plan, count)| match plan.as_str() {
143 "pro" => *count as f64 * 29.0,
144 "team" => *count as f64 * 99.0,
145 _ => 0.0,
146 })
147 .sum::<f64>();
148
149 Ok(Json(AnalyticsResponse {
150 users: UserAnalytics {
151 total: snap.total_users,
152 verified: snap.verified_users,
153 unverified: snap.total_users - snap.verified_users,
154 by_auth_provider: snap
155 .auth_providers
156 .into_iter()
157 .map(|(provider, count)| AuthProviderCount {
158 provider: provider.unwrap_or_else(|| "email".to_string()),
159 count,
160 })
161 .collect(),
162 new_users_last_7d: snap.new_users_7d,
163 new_users_last_30d: snap.new_users_30d,
164 },
165 subscriptions: SubscriptionAnalytics {
166 total_orgs: snap.total_orgs,
167 by_plan: snap
168 .plan_distribution
169 .into_iter()
170 .map(|(plan, count)| PlanCount { plan, count })
171 .collect(),
172 active_subscriptions: snap.active_subs,
173 trial_orgs: snap.trial_orgs,
174 revenue_estimate,
175 },
176 usage: UsageAnalytics {
177 total_requests: snap.total_requests.unwrap_or(0),
178 total_storage_gb: (snap.total_storage.unwrap_or(0) as f64) / 1_000_000_000.0,
179 total_ai_tokens: snap.total_ai_tokens.unwrap_or(0),
180 avg_requests_per_org: if snap.total_orgs > 0 {
181 (snap.total_requests.unwrap_or(0) as f64) / (snap.total_orgs as f64)
182 } else {
183 0.0
184 },
185 top_orgs_by_usage: snap
186 .top_orgs
187 .into_iter()
188 .map(|(id, name, plan, requests, storage_bytes)| OrgUsage {
189 org_id: id.to_string(),
190 org_name: name,
191 plan,
192 requests,
193 storage_gb: (storage_bytes as f64) / 1_000_000_000.0,
194 })
195 .collect(),
196 },
197 features: FeatureAnalytics {
198 hosted_mocks: FeatureUsage {
199 total: snap.hosted_mocks_count,
200 active_orgs: snap.hosted_mocks_orgs,
201 last_30d: snap.hosted_mocks_30d,
202 },
203 plugins_published: FeatureUsage {
204 total: snap.plugins_count,
205 active_orgs: snap.plugins_orgs,
206 last_30d: snap.plugins_30d,
207 },
208 templates_published: FeatureUsage {
209 total: snap.templates_count,
210 active_orgs: snap.templates_orgs,
211 last_30d: snap.templates_30d,
212 },
213 scenarios_published: FeatureUsage {
214 total: snap.scenarios_count,
215 active_orgs: snap.scenarios_orgs,
216 last_30d: snap.scenarios_30d,
217 },
218 api_tokens_created: FeatureUsage {
219 total: snap.api_tokens_count,
220 active_orgs: snap.api_tokens_orgs,
221 last_30d: snap.api_tokens_30d,
222 },
223 },
224 growth: GrowthAnalytics {
225 user_growth_7d: {
226 let cutoff = (chrono::Utc::now() - chrono::Duration::days(7)).date_naive();
227 snap.user_growth_30d
228 .iter()
229 .filter(|(date, _)| *date >= cutoff)
230 .map(|(date, count)| DailyCount {
231 date: date.to_string(),
232 count: *count,
233 })
234 .collect()
235 },
236 user_growth_30d: snap
237 .user_growth_30d
238 .iter()
239 .map(|(date, count)| DailyCount {
240 date: date.to_string(),
241 count: *count,
242 })
243 .collect(),
244 org_growth_7d: {
245 let cutoff = (chrono::Utc::now() - chrono::Duration::days(7)).date_naive();
246 snap.org_growth_30d
247 .iter()
248 .filter(|(date, _)| *date >= cutoff)
249 .map(|(date, count)| DailyCount {
250 date: date.to_string(),
251 count: *count,
252 })
253 .collect()
254 },
255 org_growth_30d: snap
256 .org_growth_30d
257 .iter()
258 .map(|(date, count)| DailyCount {
259 date: date.to_string(),
260 count: *count,
261 })
262 .collect(),
263 },
264 activity: ActivityAnalytics {
265 logins_last_24h: snap.logins_24h,
266 logins_last_7d: snap.logins_7d,
267 api_requests_last_24h: snap.api_requests_24h,
268 api_requests_last_7d: snap.api_requests_7d,
269 },
270 }))
271}
272
273#[derive(Debug, Serialize)]
275pub struct ConversionFunnelResponse {
276 pub period: String, pub stages: Vec<FunnelStage>,
278 pub overall_conversion_rate: f64, pub time_to_convert: Option<f64>, }
281
282#[derive(Debug, Serialize)]
283pub struct FunnelStage {
284 pub stage: String,
285 pub count: i64,
286 pub conversion_rate: f64, pub drop_off: f64, }
289
290pub async fn get_conversion_funnel(
293 State(state): State<AppState>,
294 AuthUser(user_id): AuthUser,
295 Query(query): Query<AnalyticsQuery>,
296) -> ApiResult<Json<ConversionFunnelResponse>> {
297 let user = state
299 .store
300 .find_user_by_id(user_id)
301 .await?
302 .ok_or_else(|| ApiError::InvalidRequest("User not found".to_string()))?;
303
304 if !user.is_admin {
305 return Err(ApiError::PermissionDenied);
306 }
307
308 let period = query.period.as_deref().unwrap_or("30d");
310 let interval = match period {
311 "7d" => "7 days",
312 "30d" => "30 days",
313 "90d" => "90 days",
314 "all" => "1000 years", _ => "30 days",
316 };
317
318 let snap = state.store.get_conversion_funnel_snapshot(interval).await?;
319
320 let mut stages = Vec::new();
321 let signup_count = snap.signups as f64;
322
323 stages.push(FunnelStage {
324 stage: "Signup".to_string(),
325 count: snap.signups,
326 conversion_rate: 100.0,
327 drop_off: 0.0,
328 });
329
330 let verified_rate = if signup_count > 0.0 {
331 (snap.verified as f64 / signup_count) * 100.0
332 } else {
333 0.0
334 };
335 stages.push(FunnelStage {
336 stage: "Email Verified".to_string(),
337 count: snap.verified,
338 conversion_rate: verified_rate,
339 drop_off: 100.0 - verified_rate,
340 });
341
342 let login_rate = if signup_count > 0.0 {
343 (snap.logged_in as f64 / signup_count) * 100.0
344 } else {
345 0.0
346 };
347 stages.push(FunnelStage {
348 stage: "First Login".to_string(),
349 count: snap.logged_in,
350 conversion_rate: login_rate,
351 drop_off: verified_rate - login_rate,
352 });
353
354 let org_rate = if signup_count > 0.0 {
355 (snap.org_created as f64 / signup_count) * 100.0
356 } else {
357 0.0
358 };
359 stages.push(FunnelStage {
360 stage: "Organization Created".to_string(),
361 count: snap.org_created,
362 conversion_rate: org_rate,
363 drop_off: login_rate - org_rate,
364 });
365
366 let feature_rate = if signup_count > 0.0 {
367 (snap.feature_users as f64 / signup_count) * 100.0
368 } else {
369 0.0
370 };
371 stages.push(FunnelStage {
372 stage: "First Feature Use".to_string(),
373 count: snap.feature_users,
374 conversion_rate: feature_rate,
375 drop_off: org_rate - feature_rate,
376 });
377
378 let checkout_rate = if signup_count > 0.0 {
379 (snap.checkout_initiated as f64 / signup_count) * 100.0
380 } else {
381 0.0
382 };
383 stages.push(FunnelStage {
384 stage: "Checkout Initiated".to_string(),
385 count: snap.checkout_initiated,
386 conversion_rate: checkout_rate,
387 drop_off: feature_rate - checkout_rate,
388 });
389
390 let paid_rate = if signup_count > 0.0 {
391 (snap.paid_subscribers as f64 / signup_count) * 100.0
392 } else {
393 0.0
394 };
395 stages.push(FunnelStage {
396 stage: "Paid Subscription".to_string(),
397 count: snap.paid_subscribers,
398 conversion_rate: paid_rate,
399 drop_off: checkout_rate - paid_rate,
400 });
401
402 Ok(Json(ConversionFunnelResponse {
403 period: period.to_string(),
404 stages,
405 overall_conversion_rate: paid_rate,
406 time_to_convert: snap.time_to_convert_days,
407 }))
408}