1use chrono::{DateTime, Utc};
7use std::sync::Arc;
8
9use crate::models::session::SessionMetadata;
10
11pub mod anomalies;
12pub mod discover;
13pub mod discover_llm;
14pub mod forecasting;
15pub mod insights;
16pub mod optimization;
17pub mod patterns;
18pub mod plugin_usage;
19pub mod tool_chains;
20pub mod trends;
21
22#[cfg(test)]
23mod tests;
24
25pub use anomalies::{
26 detect_anomalies, detect_daily_cost_spikes, Anomaly, AnomalyMetric, AnomalySeverity,
27 DailyCostAnomaly,
28};
29pub use discover::{
30 collect_sessions_data as discover_collect_sessions, discover_patterns, run_discover,
31 DiscoverConfig, DiscoverSuggestion, SessionData as DiscoverSessionData, SuggestionCategory,
32};
33pub use discover_llm::{call_claude_cli as discover_call_llm, LlmSuggestion};
34pub use forecasting::{forecast_usage, ForecastData, TrendDirection};
35pub use insights::{generate_budget_alerts, generate_insights, Alert};
36pub use optimization::{
37 generate_cost_suggestions, generate_model_recommendations, CostSuggestion, OptimizationCategory,
38};
39pub use patterns::{detect_patterns, UsagePatterns};
40pub use plugin_usage::{aggregate_plugin_usage, PluginAnalytics, PluginType, PluginUsage};
41pub use tool_chains::{analyze_tool_chains, ToolChain, ToolChainAnalysis};
42pub use trends::{compute_trends, SessionDurationStats, TrendsData};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum Period {
47 Days(usize),
49 Available,
51}
52
53impl Period {
54 pub fn last_7d() -> Self {
56 Self::Days(7)
57 }
58
59 pub fn last_30d() -> Self {
61 Self::Days(30)
62 }
63
64 pub fn last_90d() -> Self {
66 Self::Days(90)
67 }
68
69 pub fn available() -> Self {
71 Self::Available
72 }
73
74 pub fn days(&self) -> usize {
76 match self {
77 Period::Days(n) => *n,
78 Period::Available => 36500, }
80 }
81
82 pub fn display(&self, total_loaded: usize) -> String {
84 match self {
85 Period::Days(n) => format!("Last {} days", n),
86 Period::Available => format!("All loaded ({} sessions)", total_loaded),
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct AnalyticsData {
94 pub trends: TrendsData,
96 pub forecast: ForecastData,
98 pub patterns: UsagePatterns,
100 pub insights: Vec<String>,
102 pub tool_chains: Option<ToolChainAnalysis>,
104 pub cost_suggestions: Vec<optimization::CostSuggestion>,
106 pub anomalies: Vec<anomalies::Anomaly>,
108 pub daily_spikes: Vec<anomalies::DailyCostAnomaly>,
110 pub sessions_in_period: usize,
112 pub computed_at: DateTime<Utc>,
114 pub period: Period,
116}
117
118impl AnalyticsData {
119 pub fn compute(sessions: &[Arc<SessionMetadata>], period: Period) -> Self {
127 use chrono::Local;
128
129 let trends = compute_trends(sessions, period.days());
130 let forecast = forecast_usage(&trends);
131 let patterns = detect_patterns(sessions, period.days());
132 let insights = generate_insights(&trends, &patterns, &forecast);
133
134 let cutoff = Local::now() - chrono::Duration::days(period.days() as i64);
136 let period_sessions: Vec<Arc<SessionMetadata>> = sessions
137 .iter()
138 .filter(|s| {
139 s.first_timestamp
140 .map(|ts| ts.with_timezone(&Local) >= cutoff)
141 .unwrap_or(false)
142 })
143 .cloned()
144 .collect();
145
146 let sessions_in_period = period_sessions.len();
147 let anomalies_detected = anomalies::detect_anomalies(&period_sessions);
148 let daily_spikes_detected =
149 anomalies::detect_daily_cost_spikes(&period_sessions, period.days());
150
151 let mut aggregated_tool_tokens: std::collections::HashMap<String, u64> =
153 std::collections::HashMap::new();
154 for session in sessions {
155 for (tool, &tokens) in &session.tool_token_usage {
156 *aggregated_tool_tokens.entry(tool.clone()).or_default() += tokens;
157 }
158 }
159
160 let total_cost_estimate: f64 = trends.daily_cost.iter().sum();
162
163 let mut cost_suggestions = optimization::generate_cost_suggestions(
167 &plugin_usage::PluginAnalytics::empty(),
168 &aggregated_tool_tokens,
169 total_cost_estimate,
170 );
171
172 let model_recs =
174 optimization::generate_model_recommendations(sessions, total_cost_estimate);
175 cost_suggestions.extend(model_recs);
176 cost_suggestions.sort_by(|a, b| {
178 b.potential_savings
179 .partial_cmp(&a.potential_savings)
180 .unwrap_or(std::cmp::Ordering::Equal)
181 });
182
183 Self {
184 trends,
185 forecast,
186 patterns,
187 insights,
188 tool_chains: Some(analyze_tool_chains(sessions)),
189 cost_suggestions,
190 anomalies: anomalies_detected,
191 daily_spikes: daily_spikes_detected,
192 sessions_in_period,
193 computed_at: Utc::now(),
194 period,
195 }
196 }
197
198 pub fn from_sessions_only(sessions: &[Arc<SessionMetadata>], period: Period) -> Self {
203 tracing::warn!("Stats cache missing, computing analytics from sessions only");
204
205 Self {
206 trends: compute_trends(sessions, period.days()),
207 forecast: ForecastData::unavailable("Stats cache required for cost forecasting"),
208 patterns: detect_patterns(sessions, period.days()),
209 insights: vec!["Limited insights: stats cache unavailable".to_string()],
210 tool_chains: Some(analyze_tool_chains(sessions)),
211 cost_suggestions: Vec::new(),
212 anomalies: Vec::new(),
213 daily_spikes: Vec::new(),
214 sessions_in_period: sessions.len(),
215 computed_at: Utc::now(),
216 period,
217 }
218 }
219}