Skip to main content

mockforge_registry_server/handlers/
analytics.rs

1//! Admin analytics handlers
2//!
3//! Provides comprehensive analytics for admin users
4
5use 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, // Monthly recurring revenue estimate
50}
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, // Orgs that have used this feature
89    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>, // "7d", "30d", "90d", "all"
117}
118
119/// Get comprehensive analytics (admin only)
120pub async fn get_analytics(
121    State(state): State<AppState>,
122    AuthUser(user_id): AuthUser,
123    Query(_query): Query<AnalyticsQuery>,
124) -> ApiResult<Json<AnalyticsResponse>> {
125    // Check if user is admin
126    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    // Revenue estimate (Pro: $29, Team: $99)
139    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/// Conversion funnel stages
274#[derive(Debug, Serialize)]
275pub struct ConversionFunnelResponse {
276    pub period: String, // "7d", "30d", "90d", "all"
277    pub stages: Vec<FunnelStage>,
278    pub overall_conversion_rate: f64, // Signup to paid conversion
279    pub time_to_convert: Option<f64>, // Average days from signup to paid (if available)
280}
281
282#[derive(Debug, Serialize)]
283pub struct FunnelStage {
284    pub stage: String,
285    pub count: i64,
286    pub conversion_rate: f64, // Percentage of previous stage
287    pub drop_off: f64,        // Percentage lost from previous stage
288}
289
290/// Get conversion funnel analysis (admin only)
291/// Tracks user journey from signup to paid subscription
292pub 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    // Check if user is admin
298    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    // Determine time period
309    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", // Effectively all time
315        _ => "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}