Skip to main content

voirs_cli/commands/
telemetry_analyze.rs

1//! Telemetry data analysis and visualization
2//!
3//! Provides commands for analyzing collected telemetry data, generating reports,
4//! and visualizing usage patterns.
5
6use crate::telemetry::{EventType, TelemetryEvent, TelemetryStorage};
7use anyhow::{Context, Result};
8use chrono::Timelike;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13/// Output format for analysis reports
14#[derive(Debug, Clone, Copy)]
15pub enum OutputFormat {
16    /// Plain text format
17    Text,
18    /// JSON format
19    Json,
20    /// Markdown format
21    Markdown,
22}
23
24/// Get the default telemetry storage path
25fn get_storage_path() -> PathBuf {
26    dirs::data_local_dir()
27        .unwrap_or_else(|| PathBuf::from("."))
28        .join("voirs")
29        .join("telemetry")
30}
31
32/// Analyze telemetry data
33pub async fn analyze_telemetry(
34    output: Option<PathBuf>,
35    format: Option<OutputFormat>,
36    time_range: Option<TimeRange>,
37    verbose: bool,
38) -> Result<()> {
39    // Load telemetry storage
40    let storage_path = get_storage_path();
41    let storage =
42        TelemetryStorage::new(&storage_path).context("Failed to initialize telemetry storage")?;
43
44    // Get all events
45    let mut all_events = storage.get_all_events().await?;
46
47    // Filter by time range if specified
48    if let Some(range) = &time_range {
49        all_events.retain(|e| range.contains(e.timestamp));
50    }
51
52    if verbose {
53        eprintln!("Analyzing {} telemetry events...", all_events.len());
54    }
55
56    // Generate analysis
57    let analysis = generate_analysis(&all_events);
58
59    // Format output
60    let output_format = format.unwrap_or(OutputFormat::Text);
61    let formatted = format_analysis(&analysis, output_format)?;
62
63    // Output results
64    if let Some(path) = output {
65        std::fs::write(&path, formatted)
66            .with_context(|| format!("Failed to write output to {}", path.display()))?;
67        println!("Analysis written to {}", path.display());
68    } else {
69        println!("{}", formatted);
70    }
71
72    Ok(())
73}
74
75/// Generate insights from telemetry data
76pub async fn generate_insights(
77    min_confidence: f32,
78    max_insights: usize,
79    verbose: bool,
80) -> Result<()> {
81    let storage_path = get_storage_path();
82    let storage =
83        TelemetryStorage::new(&storage_path).context("Failed to initialize telemetry storage")?;
84
85    let all_events = storage.get_all_events().await?;
86
87    if verbose {
88        eprintln!("Generating insights from {} events...", all_events.len());
89    }
90
91    let insights = find_insights(&all_events, min_confidence);
92
93    // Display insights
94    println!("\nšŸ“Š Telemetry Insights\n");
95    println!("═══════════════════════════════════════════════════════\n");
96
97    for (i, insight) in insights.iter().take(max_insights).enumerate() {
98        println!(
99            "{}. {} (Confidence: {:.1}%)",
100            i + 1,
101            insight.message,
102            insight.confidence * 100.0
103        );
104        if !insight.recommendation.is_empty() {
105            println!("   šŸ’” {}", insight.recommendation);
106        }
107        println!();
108    }
109
110    if insights.is_empty() {
111        println!("No significant insights found. Continue using VoiRS to collect more data.\n");
112    }
113
114    Ok(())
115}
116
117/// Compare telemetry data between two time periods
118pub async fn compare_periods(period1: TimeRange, period2: TimeRange, verbose: bool) -> Result<()> {
119    let storage_path = get_storage_path();
120    let storage =
121        TelemetryStorage::new(&storage_path).context("Failed to initialize telemetry storage")?;
122
123    let all_events = storage.get_all_events().await?;
124
125    let events1: Vec<_> = all_events
126        .iter()
127        .filter(|e| period1.contains(e.timestamp))
128        .collect();
129    let events2: Vec<_> = all_events
130        .iter()
131        .filter(|e| period2.contains(e.timestamp))
132        .collect();
133
134    if verbose {
135        eprintln!(
136            "Comparing {} events (period 1) vs {} events (period 2)",
137            events1.len(),
138            events2.len()
139        );
140    }
141
142    let events1_owned: Vec<TelemetryEvent> = events1.iter().map(|e| (*e).clone()).collect();
143    let events2_owned: Vec<TelemetryEvent> = events2.iter().map(|e| (*e).clone()).collect();
144
145    let analysis1 = generate_analysis(&events1_owned);
146    let analysis2 = generate_analysis(&events2_owned);
147
148    // Display comparison
149    println!("\nšŸ“Š Telemetry Comparison\n");
150    println!("═══════════════════════════════════════════════════════\n");
151
152    println!("Period 1: {} events", events1.len());
153    println!("Period 2: {} events", events2.len());
154    println!(
155        "Change: {:+.1}%\n",
156        calculate_change(events1.len(), events2.len())
157    );
158
159    // Compare metrics
160    compare_metric(
161        "Total Commands",
162        analysis1.command_count,
163        analysis2.command_count,
164    );
165    compare_metric(
166        "Synthesis Requests",
167        analysis1.synthesis_count,
168        analysis2.synthesis_count,
169    );
170    compare_metric("Error Rate", analysis1.error_rate, analysis2.error_rate);
171    compare_metric(
172        "Avg Performance (ms)",
173        analysis1.avg_performance_ms,
174        analysis2.avg_performance_ms,
175    );
176
177    Ok(())
178}
179
180/// Time range for filtering
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct TimeRange {
183    pub start: chrono::DateTime<chrono::Utc>,
184    pub end: chrono::DateTime<chrono::Utc>,
185}
186
187impl TimeRange {
188    /// Create a new time range
189    pub fn new(start: chrono::DateTime<chrono::Utc>, end: chrono::DateTime<chrono::Utc>) -> Self {
190        Self { start, end }
191    }
192
193    /// Check if a timestamp is within the range
194    pub fn contains(&self, timestamp: chrono::DateTime<chrono::Utc>) -> bool {
195        timestamp >= self.start && timestamp <= self.end
196    }
197
198    /// Create a range for the last N days
199    pub fn last_days(days: u32) -> Self {
200        let now = chrono::Utc::now();
201        let start = now - chrono::Duration::days(days as i64);
202        Self { start, end: now }
203    }
204
205    /// Create a range for the last N hours
206    pub fn last_hours(hours: u32) -> Self {
207        let now = chrono::Utc::now();
208        let start = now - chrono::Duration::hours(hours as i64);
209        Self { start, end: now }
210    }
211}
212
213/// Analysis results
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct TelemetryAnalysis {
216    pub total_events: usize,
217    pub command_count: usize,
218    pub synthesis_count: usize,
219    pub error_count: usize,
220    pub error_rate: f32,
221    pub avg_performance_ms: f32,
222    pub command_distribution: HashMap<String, usize>,
223    pub error_distribution: HashMap<String, usize>,
224    pub peak_hours: Vec<usize>,
225}
226
227/// Generate analysis from events
228fn generate_analysis(events: &[TelemetryEvent]) -> TelemetryAnalysis {
229    let total_events = events.len();
230    let command_count = events
231        .iter()
232        .filter(|e| matches!(e.event_type, EventType::CommandExecuted))
233        .count();
234    let synthesis_count = events
235        .iter()
236        .filter(|e| matches!(e.event_type, EventType::SynthesisRequest))
237        .count();
238    let error_count = events
239        .iter()
240        .filter(|e| matches!(e.event_type, EventType::Error))
241        .count();
242
243    let error_rate = if total_events > 0 {
244        (error_count as f32 / total_events as f32) * 100.0
245    } else {
246        0.0
247    };
248
249    // Calculate average performance
250    let performance_events: Vec<_> = events
251        .iter()
252        .filter_map(|e| {
253            if matches!(e.event_type, EventType::Performance) {
254                e.metadata
255                    .get("duration_ms")
256                    .and_then(|v| v.parse::<f32>().ok())
257            } else {
258                None
259            }
260        })
261        .collect();
262
263    let avg_performance_ms = if !performance_events.is_empty() {
264        performance_events.iter().sum::<f32>() / performance_events.len() as f32
265    } else {
266        0.0
267    };
268
269    // Command distribution
270    let mut command_distribution = HashMap::new();
271    for event in events.iter() {
272        if matches!(event.event_type, EventType::CommandExecuted) {
273            if let Some(command) = event.metadata.get("command") {
274                *command_distribution.entry(command.clone()).or_insert(0) += 1;
275            }
276        }
277    }
278
279    // Error distribution
280    let mut error_distribution = HashMap::new();
281    for event in events.iter() {
282        if matches!(event.event_type, EventType::Error) {
283            if let Some(severity) = event.metadata.get("severity") {
284                *error_distribution.entry(severity.clone()).or_insert(0) += 1;
285            }
286        }
287    }
288
289    // Peak hours (0-23)
290    let mut hour_counts = vec![0usize; 24];
291    for event in events.iter() {
292        let hour = event.timestamp.hour() as usize;
293        hour_counts[hour] += 1;
294    }
295
296    let max_count = *hour_counts.iter().max().unwrap_or(&0);
297    let peak_hours: Vec<usize> = hour_counts
298        .iter()
299        .enumerate()
300        .filter(|(_, &count)| count > max_count / 2) // Hours with >50% of peak activity
301        .map(|(hour, _)| hour)
302        .collect();
303
304    TelemetryAnalysis {
305        total_events,
306        command_count,
307        synthesis_count,
308        error_count,
309        error_rate,
310        avg_performance_ms,
311        command_distribution,
312        error_distribution,
313        peak_hours,
314    }
315}
316
317/// Format analysis results
318fn format_analysis(analysis: &TelemetryAnalysis, format: OutputFormat) -> Result<String> {
319    match format {
320        OutputFormat::Json => Ok(serde_json::to_string_pretty(analysis)?),
321        OutputFormat::Text => Ok(format_text_analysis(analysis)),
322        _ => Ok(format_text_analysis(analysis)),
323    }
324}
325
326/// Format as plain text
327fn format_text_analysis(analysis: &TelemetryAnalysis) -> String {
328    let mut output = String::new();
329
330    output.push_str("\nšŸ“Š Telemetry Analysis Report\n");
331    output.push_str("═══════════════════════════════════════════════════════\n\n");
332
333    output.push_str(&format!("Total Events:        {}\n", analysis.total_events));
334    output.push_str(&format!(
335        "Commands Executed:   {}\n",
336        analysis.command_count
337    ));
338    output.push_str(&format!(
339        "Synthesis Requests:  {}\n",
340        analysis.synthesis_count
341    ));
342    output.push_str(&format!(
343        "Errors:              {} ({:.1}%)\n",
344        analysis.error_count, analysis.error_rate
345    ));
346    output.push_str(&format!(
347        "Avg Performance:     {:.1}ms\n\n",
348        analysis.avg_performance_ms
349    ));
350
351    // Top commands
352    if !analysis.command_distribution.is_empty() {
353        output.push_str("Top Commands:\n");
354        let mut commands: Vec<_> = analysis.command_distribution.iter().collect();
355        commands.sort_by(|a, b| b.1.cmp(a.1));
356        for (i, (cmd, count)) in commands.iter().take(5).enumerate() {
357            output.push_str(&format!("  {}. {} ({})\n", i + 1, cmd, count));
358        }
359        output.push('\n');
360    }
361
362    // Peak hours
363    if !analysis.peak_hours.is_empty() {
364        output.push_str("Peak Activity Hours: ");
365        output.push_str(
366            &analysis
367                .peak_hours
368                .iter()
369                .map(|h| format!("{}:00", h))
370                .collect::<Vec<_>>()
371                .join(", "),
372        );
373        output.push_str("\n\n");
374    }
375
376    output
377}
378
379/// Insight from telemetry data
380#[derive(Debug, Clone)]
381pub struct Insight {
382    pub message: String,
383    pub recommendation: String,
384    pub confidence: f32,
385}
386
387/// Find insights from telemetry data
388fn find_insights(events: &[TelemetryEvent], min_confidence: f32) -> Vec<Insight> {
389    let mut insights = Vec::new();
390    let analysis = generate_analysis(events);
391
392    // High error rate insight
393    if analysis.error_rate > 10.0 {
394        insights.push(Insight {
395            message: format!("High error rate detected ({:.1}%)", analysis.error_rate),
396            recommendation: "Review error logs and consider filing a bug report".to_string(),
397            confidence: (analysis.error_rate / 100.0).min(1.0),
398        });
399    }
400
401    // Slow performance insight
402    if analysis.avg_performance_ms > 5000.0 {
403        insights.push(Insight {
404            message: format!(
405                "Slow average performance ({:.1}ms)",
406                analysis.avg_performance_ms
407            ),
408            recommendation: "Consider optimizing models or using GPU acceleration".to_string(),
409            confidence: (analysis.avg_performance_ms / 10000.0).min(1.0),
410        });
411    }
412
413    // Underutilized features
414    if analysis.synthesis_count > 100 && analysis.command_count < 10 {
415        insights.push(Insight {
416            message: "You're mainly using synthesis - explore other VoiRS features".to_string(),
417            recommendation: "Try batch processing, voice cloning, or interactive mode".to_string(),
418            confidence: 0.8,
419        });
420    }
421
422    // Filter by confidence
423    insights.retain(|i| i.confidence >= min_confidence);
424
425    // Sort by confidence
426    insights.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
427
428    insights
429}
430
431/// Calculate percentage change
432fn calculate_change(old_value: usize, new_value: usize) -> f32 {
433    if old_value == 0 {
434        if new_value > 0 {
435            100.0
436        } else {
437            0.0
438        }
439    } else {
440        ((new_value as f32 - old_value as f32) / old_value as f32) * 100.0
441    }
442}
443
444/// Compare a metric between two periods
445fn compare_metric(name: &str, value1: impl std::fmt::Display, value2: impl std::fmt::Display) {
446    println!(
447        "{:20} {:>12} -> {:>12}",
448        name,
449        format!("{}", value1),
450        format!("{}", value2)
451    );
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_time_range_contains() {
460        let start = chrono::Utc::now() - chrono::Duration::days(1);
461        let end = chrono::Utc::now();
462        let middle = chrono::Utc::now() - chrono::Duration::hours(12);
463
464        let range = TimeRange::new(start, end);
465        assert!(range.contains(middle));
466        assert!(!range.contains(start - chrono::Duration::hours(1)));
467        assert!(!range.contains(end + chrono::Duration::hours(1)));
468    }
469
470    #[test]
471    fn test_time_range_last_days() {
472        let now = chrono::Utc::now();
473        let range = TimeRange::last_days(7);
474
475        // Test that the range includes times from the past 7 days
476        assert!(range.contains(now - chrono::Duration::hours(1)));
477        assert!(range.contains(now - chrono::Duration::days(1)));
478        assert!(range.contains(now - chrono::Duration::days(6)));
479
480        // Test that it excludes times from more than 7 days ago
481        assert!(!range.contains(now - chrono::Duration::days(8)));
482    }
483
484    #[test]
485    fn test_generate_analysis_empty() {
486        let events = vec![];
487        let analysis = generate_analysis(&events);
488        assert_eq!(analysis.total_events, 0);
489        assert_eq!(analysis.error_rate, 0.0);
490    }
491
492    #[test]
493    fn test_calculate_change() {
494        assert_eq!(calculate_change(100, 150), 50.0);
495        assert_eq!(calculate_change(100, 50), -50.0);
496        assert_eq!(calculate_change(0, 100), 100.0);
497    }
498
499    #[test]
500    fn test_find_insights_high_error_rate() {
501        // Create mock events with high error rate
502        let mut events = Vec::new();
503        let now = chrono::Utc::now();
504
505        for i in 0..20 {
506            let mut metadata = crate::telemetry::EventMetadata::new();
507            metadata.set("message", "test error");
508            metadata.set("severity", "high");
509
510            events.push(TelemetryEvent {
511                id: format!("event_{}", i),
512                event_type: EventType::Error,
513                timestamp: now,
514                metadata,
515                user_id: Some("test_user".to_string()),
516                session_id: "test_session".to_string(),
517            });
518        }
519
520        for i in 20..100 {
521            let mut metadata = crate::telemetry::EventMetadata::new();
522            metadata.set("command", "test");
523
524            events.push(TelemetryEvent {
525                id: format!("event_{}", i),
526                event_type: EventType::CommandExecuted,
527                timestamp: now,
528                metadata,
529                user_id: Some("test_user".to_string()),
530                session_id: "test_session".to_string(),
531            });
532        }
533
534        let insights = find_insights(&events, 0.1);
535        assert!(!insights.is_empty());
536    }
537
538    #[test]
539    fn test_format_text_analysis() {
540        let analysis = TelemetryAnalysis {
541            total_events: 100,
542            command_count: 80,
543            synthesis_count: 50,
544            error_count: 5,
545            error_rate: 5.0,
546            avg_performance_ms: 1500.0,
547            command_distribution: HashMap::new(),
548            error_distribution: HashMap::new(),
549            peak_hours: vec![9, 14, 20],
550        };
551
552        let text = format_text_analysis(&analysis);
553        assert!(text.contains("Total Events:"));
554        assert!(text.contains("100"));
555        assert!(text.contains("5.0%"));
556    }
557}