1use 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#[derive(Clone, Subcommand)]
15pub enum TelemetryCommands {
16 Enable {
18 #[arg(short, long, default_value = "standard")]
20 level: String,
21
22 #[arg(short, long, default_value = "medium")]
24 anonymization: String,
25 },
26
27 Disable,
29
30 Status {
32 #[arg(short, long)]
34 detailed: bool,
35 },
36
37 Export {
39 #[arg(short, long)]
41 output: PathBuf,
42
43 #[arg(short, long, default_value = "json")]
45 format: String,
46 },
47
48 Clear {
50 #[arg(short, long)]
52 yes: bool,
53 },
54
55 Config,
57
58 SetConfig {
60 #[arg(long)]
62 enabled: Option<bool>,
63
64 #[arg(long)]
66 level: Option<String>,
67
68 #[arg(long)]
70 anonymization: Option<String>,
71
72 #[arg(long)]
74 remote_endpoint: Option<String>,
75 },
76}
77
78pub 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
99async 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
128async 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
138async 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 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
206async 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
227async 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
254async 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
276async 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
319async 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
333async 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
347fn 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
356fn 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}