use chrono::{DateTime, Utc};
use std::sync::Arc;
use crate::models::session::SessionMetadata;
pub mod anomalies;
pub mod discover;
pub mod discover_llm;
pub mod forecasting;
pub mod insights;
pub mod optimization;
pub mod patterns;
pub mod plugin_usage;
pub mod tool_chains;
pub mod trends;
#[cfg(test)]
mod tests;
pub use anomalies::{
detect_anomalies, detect_daily_cost_spikes, Anomaly, AnomalyMetric, AnomalySeverity,
DailyCostAnomaly,
};
pub use discover::{
collect_sessions_data as discover_collect_sessions, discover_patterns, run_discover,
DiscoverConfig, DiscoverSuggestion, SessionData as DiscoverSessionData, SuggestionCategory,
};
pub use discover_llm::{call_claude_cli as discover_call_llm, LlmSuggestion};
pub use forecasting::{forecast_usage, ForecastData, TrendDirection};
pub use insights::{generate_budget_alerts, generate_insights, Alert};
pub use optimization::{
generate_cost_suggestions, generate_model_recommendations, CostSuggestion, OptimizationCategory,
};
pub use patterns::{detect_patterns, UsagePatterns};
pub use plugin_usage::{aggregate_plugin_usage, PluginAnalytics, PluginType, PluginUsage};
pub use tool_chains::{analyze_tool_chains, ToolChain, ToolChainAnalysis};
pub use trends::{compute_trends, SessionDurationStats, TrendsData};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Period {
Days(usize),
Available,
}
impl Period {
pub fn last_7d() -> Self {
Self::Days(7)
}
pub fn last_30d() -> Self {
Self::Days(30)
}
pub fn last_90d() -> Self {
Self::Days(90)
}
pub fn available() -> Self {
Self::Available
}
pub fn days(&self) -> usize {
match self {
Period::Days(n) => *n,
Period::Available => 36500, }
}
pub fn display(&self, total_loaded: usize) -> String {
match self {
Period::Days(n) => format!("Last {} days", n),
Period::Available => format!("All loaded ({} sessions)", total_loaded),
}
}
}
#[derive(Debug, Clone)]
pub struct AnalyticsData {
pub trends: TrendsData,
pub forecast: ForecastData,
pub patterns: UsagePatterns,
pub insights: Vec<String>,
pub tool_chains: Option<ToolChainAnalysis>,
pub cost_suggestions: Vec<optimization::CostSuggestion>,
pub anomalies: Vec<anomalies::Anomaly>,
pub daily_spikes: Vec<anomalies::DailyCostAnomaly>,
pub sessions_in_period: usize,
pub computed_at: DateTime<Utc>,
pub period: Period,
}
impl AnalyticsData {
pub fn compute(sessions: &[Arc<SessionMetadata>], period: Period) -> Self {
use chrono::Local;
let trends = compute_trends(sessions, period.days());
let forecast = forecast_usage(&trends);
let patterns = detect_patterns(sessions, period.days());
let insights = generate_insights(&trends, &patterns, &forecast);
let cutoff = Local::now() - chrono::Duration::days(period.days() as i64);
let period_sessions: Vec<Arc<SessionMetadata>> = sessions
.iter()
.filter(|s| {
s.first_timestamp
.map(|ts| ts.with_timezone(&Local) >= cutoff)
.unwrap_or(false)
})
.cloned()
.collect();
let sessions_in_period = period_sessions.len();
let anomalies_detected = anomalies::detect_anomalies(&period_sessions);
let daily_spikes_detected =
anomalies::detect_daily_cost_spikes(&period_sessions, period.days());
let mut aggregated_tool_tokens: std::collections::HashMap<String, u64> =
std::collections::HashMap::new();
for session in sessions {
for (tool, &tokens) in &session.tool_token_usage {
*aggregated_tool_tokens.entry(tool.clone()).or_default() += tokens;
}
}
let total_cost_estimate: f64 = trends.daily_cost.iter().sum();
let mut cost_suggestions = optimization::generate_cost_suggestions(
&plugin_usage::PluginAnalytics::empty(),
&aggregated_tool_tokens,
total_cost_estimate,
);
let model_recs =
optimization::generate_model_recommendations(sessions, total_cost_estimate);
cost_suggestions.extend(model_recs);
cost_suggestions.sort_by(|a, b| {
b.potential_savings
.partial_cmp(&a.potential_savings)
.unwrap_or(std::cmp::Ordering::Equal)
});
Self {
trends,
forecast,
patterns,
insights,
tool_chains: Some(analyze_tool_chains(sessions)),
cost_suggestions,
anomalies: anomalies_detected,
daily_spikes: daily_spikes_detected,
sessions_in_period,
computed_at: Utc::now(),
period,
}
}
pub fn from_sessions_only(sessions: &[Arc<SessionMetadata>], period: Period) -> Self {
tracing::warn!("Stats cache missing, computing analytics from sessions only");
Self {
trends: compute_trends(sessions, period.days()),
forecast: ForecastData::unavailable("Stats cache required for cost forecasting"),
patterns: detect_patterns(sessions, period.days()),
insights: vec!["Limited insights: stats cache unavailable".to_string()],
tool_chains: Some(analyze_tool_chains(sessions)),
cost_suggestions: Vec::new(),
anomalies: Vec::new(),
daily_spikes: Vec::new(),
sessions_in_period: sessions.len(),
computed_at: Utc::now(),
period,
}
}
}