Skip to main content

voirs_cli/commands/
telemetry.rs

1//! Telemetry management commands
2
3use clap::Subcommand;
4use std::path::{Path, PathBuf};
5
6use crate::telemetry::{
7    config::{TelemetryConfig, TelemetryLevel},
8    export::ExportFormat,
9    privacy::AnonymizationLevel,
10    TelemetrySystem,
11};
12
13/// Telemetry subcommands
14#[derive(Clone, Subcommand)]
15pub enum TelemetryCommands {
16    /// Enable telemetry collection
17    Enable {
18        /// Telemetry level (minimal, standard, detailed, debug)
19        #[arg(short, long, default_value = "standard")]
20        level: String,
21
22        /// Anonymization level (none, low, medium, high)
23        #[arg(short, long, default_value = "medium")]
24        anonymization: String,
25    },
26
27    /// Disable telemetry collection
28    Disable,
29
30    /// Show telemetry status and statistics
31    Status {
32        /// Show detailed statistics
33        #[arg(short, long)]
34        detailed: bool,
35    },
36
37    /// Export telemetry data
38    Export {
39        /// Output file path
40        #[arg(short, long)]
41        output: PathBuf,
42
43        /// Export format (json, jsonl, csv, markdown, html)
44        #[arg(short, long, default_value = "json")]
45        format: String,
46    },
47
48    /// Clear all telemetry data
49    Clear {
50        /// Skip confirmation prompt
51        #[arg(short, long)]
52        yes: bool,
53    },
54
55    /// Show telemetry configuration
56    Config,
57
58    /// Set telemetry configuration
59    SetConfig {
60        /// Enable/disable telemetry
61        #[arg(long)]
62        enabled: Option<bool>,
63
64        /// Telemetry level
65        #[arg(long)]
66        level: Option<String>,
67
68        /// Anonymization level
69        #[arg(long)]
70        anonymization: Option<String>,
71
72        /// Remote endpoint URL
73        #[arg(long)]
74        remote_endpoint: Option<String>,
75    },
76}
77
78/// Execute telemetry command
79pub async fn execute(cmd: TelemetryCommands) -> Result<(), Box<dyn std::error::Error>> {
80    match cmd {
81        TelemetryCommands::Enable {
82            level,
83            anonymization,
84        } => enable_telemetry(&level, &anonymization).await,
85        TelemetryCommands::Disable => disable_telemetry().await,
86        TelemetryCommands::Status { detailed } => show_status(detailed).await,
87        TelemetryCommands::Export { output, format } => export_data(&output, &format).await,
88        TelemetryCommands::Clear { yes } => clear_data(yes).await,
89        TelemetryCommands::Config => show_config().await,
90        TelemetryCommands::SetConfig {
91            enabled,
92            level,
93            anonymization,
94            remote_endpoint,
95        } => set_config(enabled, level, anonymization, remote_endpoint).await,
96    }
97}
98
99/// Enable telemetry
100async fn enable_telemetry(
101    level_str: &str,
102    anonymization_str: &str,
103) -> Result<(), Box<dyn std::error::Error>> {
104    let level: TelemetryLevel = level_str.parse()?;
105    let anonymization: AnonymizationLevel = match anonymization_str.to_lowercase().as_str() {
106        "none" => AnonymizationLevel::None,
107        "low" => AnonymizationLevel::Low,
108        "medium" => AnonymizationLevel::Medium,
109        "high" => AnonymizationLevel::High,
110        _ => return Err(format!("Invalid anonymization level: {}", anonymization_str).into()),
111    };
112
113    let config = TelemetryConfig::enabled()
114        .with_level(level)
115        .with_anonymization(anonymization);
116
117    config.validate()?;
118    save_config(&config).await?;
119
120    println!("✅ Telemetry enabled");
121    println!("   Level: {}", level);
122    println!("   Anonymization: {}", anonymization);
123    println!("   Storage: {}", config.storage_path.display());
124
125    Ok(())
126}
127
128/// Disable telemetry
129async fn disable_telemetry() -> Result<(), Box<dyn std::error::Error>> {
130    let config = TelemetryConfig::disabled();
131    save_config(&config).await?;
132
133    println!("✅ Telemetry disabled");
134
135    Ok(())
136}
137
138/// Show telemetry status
139async fn show_status(detailed: bool) -> Result<(), Box<dyn std::error::Error>> {
140    let config = load_config().await?;
141
142    println!("Telemetry Status");
143    println!("================");
144    println!(
145        "Enabled: {}",
146        if config.enabled { "✅ Yes" } else { "❌ No" }
147    );
148
149    if !config.enabled {
150        return Ok(());
151    }
152
153    println!("Level: {}", config.level);
154    println!("Anonymization: {}", config.anonymization);
155    println!("Storage: {}", config.storage_path.display());
156
157    if let Some(ref endpoint) = config.remote_endpoint {
158        println!("Remote endpoint: {}", endpoint);
159    }
160
161    // Show statistics
162    let system = TelemetrySystem::new(config).await?;
163    let stats = system.get_statistics().await?;
164
165    println!("\nStatistics");
166    println!("----------");
167    println!("Total events: {}", stats.total_events);
168    println!("Synthesis requests: {}", stats.synthesis_requests);
169    println!(
170        "Average synthesis duration: {:.2}ms",
171        stats.avg_synthesis_duration
172    );
173    println!("Total errors: {}", stats.total_errors);
174    println!("Storage size: {}", format_bytes(stats.storage_size_bytes));
175
176    if detailed {
177        println!("\nEvents by type:");
178        for (event_type, count) in &stats.events_by_type {
179            println!("  {}: {}", event_type, count);
180        }
181
182        if !stats.most_used_commands.is_empty() {
183            println!("\nMost used commands:");
184            for (command, count) in stats.most_used_commands.iter().take(5) {
185                println!("  {}: {}", command, count);
186            }
187        }
188
189        if !stats.most_used_voices.is_empty() {
190            println!("\nMost used voices:");
191            for (voice, count) in stats.most_used_voices.iter().take(5) {
192                println!("  {}: {}", voice, count);
193            }
194        }
195
196        if let (Some(start), Some(end)) = (stats.start_time, stats.end_time) {
197            println!("\nTime range:");
198            println!("  From: {}", start.format("%Y-%m-%d %H:%M:%S"));
199            println!("  To: {}", end.format("%Y-%m-%d %H:%M:%S"));
200        }
201    }
202
203    Ok(())
204}
205
206/// Export telemetry data
207async fn export_data(output: &Path, format_str: &str) -> Result<(), Box<dyn std::error::Error>> {
208    let config = load_config().await?;
209
210    if !config.enabled {
211        println!("⚠️  Telemetry is disabled. No data to export.");
212        return Ok(());
213    }
214
215    let format: ExportFormat = format_str.parse()?;
216    let system = TelemetrySystem::new(config).await?;
217
218    println!("Exporting telemetry data...");
219    system.export(format, output).await?;
220
221    println!("✅ Telemetry data exported to: {}", output.display());
222    println!("   Format: {}", format_str);
223
224    Ok(())
225}
226
227/// Clear telemetry data
228async fn clear_data(skip_confirmation: bool) -> Result<(), Box<dyn std::error::Error>> {
229    let config = load_config().await?;
230
231    if !skip_confirmation {
232        println!("⚠️  This will permanently delete all telemetry data.");
233        print!("Are you sure? (y/N): ");
234        use std::io::{self, Write};
235        io::stdout().flush()?;
236
237        let mut input = String::new();
238        io::stdin().read_line(&mut input)?;
239
240        if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
241            println!("Cancelled.");
242            return Ok(());
243        }
244    }
245
246    let system = TelemetrySystem::new(config).await?;
247    system.clear_data().await?;
248
249    println!("✅ Telemetry data cleared");
250
251    Ok(())
252}
253
254/// Show telemetry configuration
255async fn show_config() -> Result<(), Box<dyn std::error::Error>> {
256    let config = load_config().await?;
257
258    println!("Telemetry Configuration");
259    println!("=======================");
260    println!("Enabled: {}", config.enabled);
261    println!("Level: {}", config.level);
262    println!("Anonymization: {}", config.anonymization);
263    println!("Storage path: {}", config.storage_path.display());
264    println!("Batch size: {}", config.batch_size);
265    println!("Flush interval: {}s", config.flush_interval_secs);
266
267    if let Some(ref endpoint) = config.remote_endpoint {
268        println!("Remote endpoint: {}", endpoint);
269    } else {
270        println!("Remote endpoint: (not configured)");
271    }
272
273    Ok(())
274}
275
276/// Set telemetry configuration
277async fn set_config(
278    enabled: Option<bool>,
279    level: Option<String>,
280    anonymization: Option<String>,
281    remote_endpoint: Option<String>,
282) -> Result<(), Box<dyn std::error::Error>> {
283    let mut config = load_config().await?;
284
285    if let Some(enabled) = enabled {
286        config.enabled = enabled;
287    }
288
289    if let Some(level_str) = level {
290        config.level = level_str.parse()?;
291    }
292
293    if let Some(anon_str) = anonymization {
294        config.anonymization = match anon_str.to_lowercase().as_str() {
295            "none" => AnonymizationLevel::None,
296            "low" => AnonymizationLevel::Low,
297            "medium" => AnonymizationLevel::Medium,
298            "high" => AnonymizationLevel::High,
299            _ => return Err(format!("Invalid anonymization level: {}", anon_str).into()),
300        };
301    }
302
303    if let Some(endpoint) = remote_endpoint {
304        if endpoint.is_empty() {
305            config.remote_endpoint = None;
306        } else {
307            config.remote_endpoint = Some(endpoint);
308        }
309    }
310
311    config.validate()?;
312    save_config(&config).await?;
313
314    println!("✅ Telemetry configuration updated");
315
316    Ok(())
317}
318
319/// Load telemetry configuration
320async fn load_config() -> Result<TelemetryConfig, Box<dyn std::error::Error>> {
321    let config_path = get_config_path()?;
322
323    if !config_path.exists() {
324        return Ok(TelemetryConfig::default());
325    }
326
327    let content = tokio::fs::read_to_string(&config_path).await?;
328    let config: TelemetryConfig = serde_json::from_str(&content)?;
329
330    Ok(config)
331}
332
333/// Save telemetry configuration
334async fn save_config(config: &TelemetryConfig) -> Result<(), Box<dyn std::error::Error>> {
335    let config_path = get_config_path()?;
336
337    if let Some(parent) = config_path.parent() {
338        tokio::fs::create_dir_all(parent).await?;
339    }
340
341    let content = serde_json::to_string_pretty(config)?;
342    tokio::fs::write(&config_path, content).await?;
343
344    Ok(())
345}
346
347/// Get telemetry configuration file path
348fn get_config_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
349    let config_dir = dirs::config_dir()
350        .ok_or("Could not determine config directory")?
351        .join("voirs");
352
353    Ok(config_dir.join("telemetry.json"))
354}
355
356/// Format bytes for display
357fn format_bytes(bytes: u64) -> String {
358    const KB: u64 = 1024;
359    const MB: u64 = KB * 1024;
360    const GB: u64 = MB * 1024;
361
362    if bytes >= GB {
363        format!("{:.2} GB", bytes as f64 / GB as f64)
364    } else if bytes >= MB {
365        format!("{:.2} MB", bytes as f64 / MB as f64)
366    } else if bytes >= KB {
367        format!("{:.2} KB", bytes as f64 / KB as f64)
368    } else {
369        format!("{} bytes", bytes)
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_format_bytes() {
379        assert_eq!(format_bytes(0), "0 bytes");
380        assert_eq!(format_bytes(500), "500 bytes");
381        assert_eq!(format_bytes(1024), "1.00 KB");
382        assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
383        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
384    }
385
386    #[tokio::test]
387    async fn test_get_config_path() {
388        let path = get_config_path();
389        assert!(path.is_ok());
390        assert!(path.unwrap().to_str().unwrap().contains("voirs"));
391    }
392
393    #[tokio::test]
394    async fn test_save_and_load_config() {
395        let config = TelemetryConfig::enabled();
396        let result = save_config(&config).await;
397        assert!(result.is_ok());
398
399        let loaded = load_config().await;
400        assert!(loaded.is_ok());
401        assert!(loaded.unwrap().enabled);
402    }
403}