use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
static GLOBAL_TRACKER: OnceLock<Arc<RtkTracker>> = OnceLock::new();
pub fn global_tracker() -> Arc<RtkTracker> {
GLOBAL_TRACKER
.get_or_init(|| Arc::new(RtkTracker::new()))
.clone()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenSavings {
pub command: String,
pub rewritten_command: String,
pub original_tokens: usize,
pub filtered_tokens: usize,
pub tokens_saved: usize,
pub savings_percent: f64,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RtkMetrics {
pub total_commands: usize,
pub total_tokens_saved: usize,
pub average_savings_percent: f64,
pub savings_by_command: HashMap<String, CommandSavings>,
pub recent_savings: Vec<TokenSavings>,
pub tracking_since: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandSavings {
pub execution_count: usize,
pub total_tokens_saved: usize,
pub average_savings_percent: f64,
}
#[derive(Debug, Clone)]
pub struct RtkTracker {
metrics: Arc<Mutex<RtkMetrics>>,
}
impl RtkTracker {
pub fn new() -> Self {
Self {
metrics: Arc::new(Mutex::new(RtkMetrics {
total_commands: 0,
total_tokens_saved: 0,
average_savings_percent: 0.0,
savings_by_command: HashMap::new(),
recent_savings: Vec::new(),
tracking_since: Utc::now(),
})),
}
}
pub fn record_savings(&self, savings: TokenSavings) {
let mut metrics = self.metrics.lock().unwrap();
metrics.total_commands += 1;
metrics.total_tokens_saved += savings.tokens_saved;
let total_percent: f64 = metrics
.recent_savings
.iter()
.map(|s| s.savings_percent)
.sum::<f64>()
+ savings.savings_percent;
let count = metrics.recent_savings.len() + 1;
metrics.average_savings_percent = total_percent / count as f64;
let command_type = savings
.command
.split_whitespace()
.next()
.unwrap_or("unknown")
.to_string();
let entry = metrics
.savings_by_command
.entry(command_type)
.or_insert_with(|| CommandSavings {
execution_count: 0,
total_tokens_saved: 0,
average_savings_percent: 0.0,
});
entry.execution_count += 1;
entry.total_tokens_saved += savings.tokens_saved;
let cmd_total_percent: f64 = entry.average_savings_percent
* (entry.execution_count - 1) as f64
+ savings.savings_percent;
entry.average_savings_percent = cmd_total_percent / entry.execution_count as f64;
metrics.recent_savings.push(savings);
if metrics.recent_savings.len() > 100 {
metrics.recent_savings.remove(0);
}
}
pub fn get_metrics(&self) -> RtkMetrics {
self.metrics.lock().unwrap().clone()
}
pub fn total_tokens_saved(&self) -> usize {
self.metrics.lock().unwrap().total_tokens_saved
}
pub fn total_commands(&self) -> usize {
self.metrics.lock().unwrap().total_commands
}
pub fn average_savings_percent(&self) -> f64 {
self.metrics.lock().unwrap().average_savings_percent
}
pub fn format_report(&self) -> String {
let metrics = self.get_metrics();
let mut report = String::new();
report.push_str("═══ RTK Token Savings Report ═══\n\n");
report.push_str(&format!("Total Commands: {}\n", metrics.total_commands));
report.push_str(&format!(
"Total Tokens Saved: {}\n",
metrics.total_tokens_saved
));
report.push_str(&format!(
"Average Savings: {:.1}%\n",
metrics.average_savings_percent
));
report.push_str(&format!(
"Tracking Since: {}\n\n",
metrics.tracking_since.format("%Y-%m-%d %H:%M:%S UTC")
));
report.push_str("Savings by Command Type:\n");
let mut sorted_commands: Vec<_> = metrics.savings_by_command.iter().collect();
sorted_commands.sort_by_key(|b| std::cmp::Reverse(b.1.total_tokens_saved));
for (cmd, savings) in sorted_commands.iter().take(10) {
report.push_str(&format!(
" {}: {} cmds, {} tokens saved, {:.1}% avg\n",
cmd,
savings.execution_count,
savings.total_tokens_saved,
savings.average_savings_percent
));
}
report
}
}
impl Default for RtkTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tracker_creation() {
let tracker = RtkTracker::new();
assert_eq!(tracker.total_commands(), 0);
assert_eq!(tracker.total_tokens_saved(), 0);
}
#[test]
fn test_record_savings() {
let tracker = RtkTracker::new();
let savings = TokenSavings {
command: "git status".to_string(),
rewritten_command: "rtk git status".to_string(),
original_tokens: 100,
filtered_tokens: 20,
tokens_saved: 80,
savings_percent: 80.0,
timestamp: Utc::now(),
};
tracker.record_savings(savings);
assert_eq!(tracker.total_commands(), 1);
assert_eq!(tracker.total_tokens_saved(), 80);
assert!((tracker.average_savings_percent() - 80.0).abs() < 0.01);
}
#[test]
fn test_multiple_commands() {
let tracker = RtkTracker::new();
tracker.record_savings(TokenSavings {
command: "git status".to_string(),
rewritten_command: "rtk git status".to_string(),
original_tokens: 100,
filtered_tokens: 20,
tokens_saved: 80,
savings_percent: 80.0,
timestamp: Utc::now(),
});
tracker.record_savings(TokenSavings {
command: "cargo build".to_string(),
rewritten_command: "rtk cargo build".to_string(),
original_tokens: 200,
filtered_tokens: 40,
tokens_saved: 160,
savings_percent: 80.0,
timestamp: Utc::now(),
});
assert_eq!(tracker.total_commands(), 2);
assert_eq!(tracker.total_tokens_saved(), 240);
}
#[test]
fn test_format_report() {
let tracker = RtkTracker::new();
tracker.record_savings(TokenSavings {
command: "git status".to_string(),
rewritten_command: "rtk git status".to_string(),
original_tokens: 100,
filtered_tokens: 20,
tokens_saved: 80,
savings_percent: 80.0,
timestamp: Utc::now(),
});
let report = tracker.format_report();
assert!(report.contains("RTK Token Savings Report"));
assert!(report.contains("Total Commands: 1"));
assert!(report.contains("git"));
}
}