oxur_repl/metrics/
usage.rs

1//! Usage metrics for tracking REPL command patterns
2//!
3//! Provides [`UsageMetrics`] for tracking command frequency and usage patterns
4//! to understand how users interact with the REPL.
5
6use metrics::counter;
7use std::collections::VecDeque;
8
9/// Maximum command history to keep (for recent activity analysis)
10const MAX_HISTORY: usize = 100;
11
12/// Usage metrics collector.
13///
14/// Tracks command frequency and patterns to understand REPL usage.
15/// Maintains local state for analysis while also emitting to the
16/// `metrics` crate facade for external monitoring.
17///
18/// # Usage
19///
20/// ```
21/// use oxur_repl::metrics::UsageMetrics;
22///
23/// let mut metrics = UsageMetrics::new("session-1");
24///
25/// // Record commands
26/// metrics.record_eval();
27/// metrics.record_help();
28/// metrics.record_stats();
29///
30/// // Get statistics
31/// let snapshot = metrics.snapshot();
32/// println!("Total commands: {}", snapshot.total_commands);
33/// ```
34#[derive(Debug, Clone)]
35pub struct UsageMetrics {
36    /// Session identifier
37    session_id: String,
38
39    /// Eval command count
40    eval_count: u64,
41
42    /// Help command count
43    help_count: u64,
44
45    /// Stats command count
46    stats_count: u64,
47
48    /// Info command count
49    info_count: u64,
50
51    /// Sessions command count
52    sessions_count: u64,
53
54    /// Clear command count
55    clear_count: u64,
56
57    /// Banner command count
58    banner_count: u64,
59
60    /// Recent command history (for pattern analysis)
61    recent_commands: VecDeque<CommandType>,
62}
63
64/// Command type for history tracking
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum CommandType {
67    Eval,
68    Help,
69    Stats,
70    Info,
71    Sessions,
72    Clear,
73    Banner,
74}
75
76impl CommandType {
77    /// Convert to label string for metrics
78    pub fn as_label(&self) -> &'static str {
79        match self {
80            CommandType::Eval => "eval",
81            CommandType::Help => "help",
82            CommandType::Stats => "stats",
83            CommandType::Info => "info",
84            CommandType::Sessions => "sessions",
85            CommandType::Clear => "clear",
86            CommandType::Banner => "banner",
87        }
88    }
89}
90
91impl UsageMetrics {
92    /// Create a new usage metrics collector.
93    pub fn new(session_id: impl Into<String>) -> Self {
94        Self {
95            session_id: session_id.into(),
96            eval_count: 0,
97            help_count: 0,
98            stats_count: 0,
99            info_count: 0,
100            sessions_count: 0,
101            clear_count: 0,
102            banner_count: 0,
103            recent_commands: VecDeque::with_capacity(MAX_HISTORY),
104        }
105    }
106
107    /// Record a command execution.
108    fn record_command(&mut self, cmd_type: CommandType) {
109        // Update count
110        match cmd_type {
111            CommandType::Eval => self.eval_count += 1,
112            CommandType::Help => self.help_count += 1,
113            CommandType::Stats => self.stats_count += 1,
114            CommandType::Info => self.info_count += 1,
115            CommandType::Sessions => self.sessions_count += 1,
116            CommandType::Clear => self.clear_count += 1,
117            CommandType::Banner => self.banner_count += 1,
118        }
119
120        // Add to recent history
121        if self.recent_commands.len() >= MAX_HISTORY {
122            self.recent_commands.pop_front();
123        }
124        self.recent_commands.push_back(cmd_type);
125
126        // Emit metrics via facade
127        counter!("repl.commands.total", "type" => cmd_type.as_label()).increment(1);
128    }
129
130    /// Record an eval command.
131    pub fn record_eval(&mut self) {
132        self.record_command(CommandType::Eval);
133    }
134
135    /// Record a help command.
136    pub fn record_help(&mut self) {
137        self.record_command(CommandType::Help);
138    }
139
140    /// Record a stats command.
141    pub fn record_stats(&mut self) {
142        self.record_command(CommandType::Stats);
143    }
144
145    /// Record an info command.
146    pub fn record_info(&mut self) {
147        self.record_command(CommandType::Info);
148    }
149
150    /// Record a sessions command.
151    pub fn record_sessions(&mut self) {
152        self.record_command(CommandType::Sessions);
153    }
154
155    /// Record a clear command.
156    pub fn record_clear(&mut self) {
157        self.record_command(CommandType::Clear);
158    }
159
160    /// Record a banner command.
161    pub fn record_banner(&mut self) {
162        self.record_command(CommandType::Banner);
163    }
164
165    /// Get total command count.
166    pub fn total_commands(&self) -> u64 {
167        self.eval_count
168            + self.help_count
169            + self.stats_count
170            + self.info_count
171            + self.sessions_count
172            + self.clear_count
173            + self.banner_count
174    }
175
176    /// Get eval percentage.
177    pub fn eval_percentage(&self) -> f64 {
178        let total = self.total_commands();
179        if total > 0 {
180            (self.eval_count as f64 / total as f64) * 100.0
181        } else {
182            0.0
183        }
184    }
185
186    /// Get session ID.
187    pub fn session_id(&self) -> &str {
188        &self.session_id
189    }
190
191    /// Create a snapshot for display.
192    pub fn snapshot(&self) -> UsageMetricsSnapshot {
193        UsageMetricsSnapshot {
194            session_id: self.session_id.clone(),
195            eval_count: self.eval_count,
196            help_count: self.help_count,
197            stats_count: self.stats_count,
198            info_count: self.info_count,
199            sessions_count: self.sessions_count,
200            clear_count: self.clear_count,
201            banner_count: self.banner_count,
202            total_commands: self.total_commands(),
203        }
204    }
205}
206
207/// Snapshot of usage metrics for display.
208#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
209pub struct UsageMetricsSnapshot {
210    /// Session identifier
211    pub session_id: String,
212    /// Eval command count
213    pub eval_count: u64,
214    /// Help command count
215    pub help_count: u64,
216    /// Stats command count
217    pub stats_count: u64,
218    /// Info command count
219    pub info_count: u64,
220    /// Sessions command count
221    pub sessions_count: u64,
222    /// Clear command count
223    pub clear_count: u64,
224    /// Banner command count
225    pub banner_count: u64,
226    /// Total command count
227    pub total_commands: u64,
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_usage_metrics_creation() {
236        let metrics = UsageMetrics::new("test-session");
237        assert_eq!(metrics.session_id(), "test-session");
238        assert_eq!(metrics.total_commands(), 0);
239    }
240
241    #[test]
242    fn test_record_commands() {
243        let mut metrics = UsageMetrics::new("test");
244
245        metrics.record_eval();
246        metrics.record_eval();
247        metrics.record_help();
248        metrics.record_stats();
249
250        assert_eq!(metrics.eval_count, 2);
251        assert_eq!(metrics.help_count, 1);
252        assert_eq!(metrics.stats_count, 1);
253        assert_eq!(metrics.total_commands(), 4);
254    }
255
256    #[test]
257    fn test_eval_percentage() {
258        let mut metrics = UsageMetrics::new("test");
259
260        metrics.record_eval();
261        metrics.record_eval();
262        metrics.record_eval();
263        metrics.record_help();
264
265        assert_eq!(metrics.eval_percentage(), 75.0);
266    }
267
268    #[test]
269    fn test_snapshot() {
270        let mut metrics = UsageMetrics::new("test");
271
272        metrics.record_eval();
273        metrics.record_help();
274        metrics.record_stats();
275
276        let snapshot = metrics.snapshot();
277        assert_eq!(snapshot.eval_count, 1);
278        assert_eq!(snapshot.help_count, 1);
279        assert_eq!(snapshot.stats_count, 1);
280        assert_eq!(snapshot.total_commands, 3);
281    }
282
283    #[test]
284    fn test_command_history() {
285        let mut metrics = UsageMetrics::new("test");
286
287        for _ in 0..150 {
288            metrics.record_eval();
289        }
290
291        // Should cap at MAX_HISTORY
292        assert_eq!(metrics.recent_commands.len(), MAX_HISTORY);
293    }
294}