Skip to main content

ccboard_types/analytics/
mod.rs

1//! Advanced analytics module for Claude Code usage analysis
2//!
3//! Provides time series trends, forecasting, usage pattern detection,
4//! and actionable insights to optimize costs and productivity.
5
6use chrono::{DateTime, Utc};
7use std::sync::Arc;
8
9use crate::models::session::SessionMetadata;
10
11pub mod anomalies;
12pub mod forecasting;
13pub mod insights;
14pub mod patterns;
15pub mod trends;
16
17// #[cfg(test)]
18// mod tests;
19
20pub use anomalies::{detect_anomalies, Anomaly, AnomalyMetric, AnomalySeverity};
21pub use forecasting::{forecast_usage, ForecastData, TrendDirection};
22pub use insights::{generate_budget_alerts, generate_insights, Alert};
23pub use patterns::{detect_patterns, UsagePatterns};
24pub use trends::{compute_trends, SessionDurationStats, TrendsData};
25
26/// Period selection for analytics computation
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Period {
29    /// Last N days from now
30    Days(usize),
31    /// All loaded sessions (honest: not "all time", limited by DataStore)
32    Available,
33}
34
35impl Period {
36    /// Last 7 days
37    pub fn last_7d() -> Self {
38        Self::Days(7)
39    }
40
41    /// Last 30 days
42    pub fn last_30d() -> Self {
43        Self::Days(30)
44    }
45
46    /// Last 90 days
47    pub fn last_90d() -> Self {
48        Self::Days(90)
49    }
50
51    /// All available sessions
52    pub fn available() -> Self {
53        Self::Available
54    }
55
56    /// Convert to days (for filtering)
57    pub fn days(&self) -> usize {
58        match self {
59            Period::Days(n) => *n,
60            Period::Available => 36500, // 100 years (effectively all)
61        }
62    }
63
64    /// Display label (shows loaded count for Available)
65    pub fn display(&self, total_loaded: usize) -> String {
66        match self {
67            Period::Days(n) => format!("Last {} days", n),
68            Period::Available => format!("All loaded ({} sessions)", total_loaded),
69        }
70    }
71}
72
73/// Complete analytics data for a period
74#[derive(Debug, Clone)]
75pub struct AnalyticsData {
76    /// Time series trends
77    pub trends: TrendsData,
78    /// Usage forecasting
79    pub forecast: ForecastData,
80    /// Behavioral patterns
81    pub patterns: UsagePatterns,
82    /// Actionable insights
83    pub insights: Vec<String>,
84    /// Timestamp of computation
85    pub computed_at: DateTime<Utc>,
86    /// Period analyzed
87    pub period: Period,
88}
89
90impl AnalyticsData {
91    /// Compute analytics from sessions (sync function)
92    ///
93    /// This is a sync function for simplicity. If computation exceeds 16ms
94    /// (render deadline), caller should offload to `tokio::task::spawn_blocking`.
95    ///
96    /// # Performance
97    /// Target: <100ms for 1000 sessions over 30 days
98    pub fn compute(sessions: &[Arc<SessionMetadata>], period: Period) -> Self {
99        let trends = compute_trends(sessions, period.days());
100        let forecast = forecast_usage(&trends);
101        let patterns = detect_patterns(sessions, period.days());
102        let insights = generate_insights(&trends, &patterns, &forecast);
103
104        Self {
105            trends,
106            forecast,
107            patterns,
108            insights,
109            computed_at: Utc::now(),
110            period,
111        }
112    }
113
114    /// Graceful fallback if stats-cache.json missing
115    ///
116    /// Cost forecasting requires pricing data from StatsCache.
117    /// If unavailable, returns limited analytics.
118    pub fn from_sessions_only(sessions: &[Arc<SessionMetadata>], period: Period) -> Self {
119        // Stats cache missing - computing limited analytics from sessions only
120
121        Self {
122            trends: compute_trends(sessions, period.days()),
123            forecast: ForecastData::unavailable("Stats cache required for cost forecasting"),
124            patterns: detect_patterns(sessions, period.days()),
125            insights: vec!["Limited insights: stats cache unavailable".to_string()],
126            computed_at: Utc::now(),
127            period,
128        }
129    }
130}