Skip to main content

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