Skip to main content

voirs_cli/telemetry/
mod.rs

1//! Telemetry system for VoiRS CLI
2//!
3//! Provides privacy-focused usage tracking, performance monitoring, and error reporting.
4//! All telemetry is opt-in and respects user privacy with anonymization and local-first storage.
5
6pub mod collector;
7pub mod config;
8pub mod events;
9pub mod export;
10pub mod privacy;
11pub mod storage;
12
13use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15use tokio::sync::RwLock;
16
17pub use collector::TelemetryCollector;
18pub use config::{TelemetryConfig, TelemetryLevel};
19pub use events::{EventMetadata, EventType, TelemetryEvent};
20pub use export::{ExportFormat, TelemetryExporter};
21pub use privacy::{AnonymizationLevel, PrivacyControl};
22pub use storage::TelemetryStorage;
23
24/// Telemetry system coordinator
25pub struct TelemetrySystem {
26    config: Arc<RwLock<TelemetryConfig>>,
27    collector: Arc<TelemetryCollector>,
28    storage: Arc<RwLock<TelemetryStorage>>,
29    exporter: Arc<TelemetryExporter>,
30}
31
32impl TelemetrySystem {
33    /// Create a new telemetry system
34    pub async fn new(config: TelemetryConfig) -> Result<Self, TelemetryError> {
35        let config = Arc::new(RwLock::new(config));
36        let storage = Arc::new(RwLock::new(TelemetryStorage::new(
37            &config.read().await.storage_path,
38        )?));
39        let collector = Arc::new(TelemetryCollector::new(Arc::clone(&config)).await);
40        let exporter = Arc::new(TelemetryExporter::new(Arc::clone(&config)));
41
42        Ok(Self {
43            config,
44            collector,
45            storage,
46            exporter,
47        })
48    }
49
50    /// Record a telemetry event
51    pub async fn record_event(&self, event: TelemetryEvent) -> Result<(), TelemetryError> {
52        if !self.config.read().await.enabled {
53            return Ok(());
54        }
55
56        // Apply privacy controls
57        let event = self.collector.apply_privacy(event).await?;
58
59        // Store event
60        self.storage.write().await.store_event(&event).await?;
61
62        Ok(())
63    }
64
65    /// Get telemetry statistics
66    pub async fn get_statistics(&self) -> Result<TelemetryStatistics, TelemetryError> {
67        self.storage.read().await.get_statistics().await
68    }
69
70    /// Export telemetry data
71    pub async fn export(
72        &self,
73        format: ExportFormat,
74        output: &std::path::Path,
75    ) -> Result<(), TelemetryError> {
76        let events = self.storage.read().await.get_all_events().await?;
77        self.exporter.export(&events, format, output).await
78    }
79
80    /// Clear telemetry data
81    pub async fn clear_data(&self) -> Result<(), TelemetryError> {
82        self.storage.write().await.clear().await
83    }
84
85    /// Update telemetry configuration
86    pub async fn update_config(&self, new_config: TelemetryConfig) -> Result<(), TelemetryError> {
87        *self.config.write().await = new_config;
88        Ok(())
89    }
90
91    /// Check if telemetry is enabled
92    pub async fn is_enabled(&self) -> bool {
93        self.config.read().await.enabled
94    }
95}
96
97/// Telemetry error types
98#[derive(Debug, thiserror::Error)]
99pub enum TelemetryError {
100    #[error("I/O error: {0}")]
101    Io(#[from] std::io::Error),
102
103    #[error("Serialization error: {0}")]
104    Serialization(#[from] serde_json::Error),
105
106    #[error("Privacy violation: {0}")]
107    PrivacyViolation(String),
108
109    #[error("Configuration error: {0}")]
110    Configuration(String),
111
112    #[error("Storage error: {0}")]
113    Storage(String),
114
115    #[error("Export error: {0}")]
116    Export(String),
117}
118
119/// Telemetry statistics
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct TelemetryStatistics {
122    /// Total number of events
123    pub total_events: u64,
124
125    /// Events by type
126    pub events_by_type: std::collections::HashMap<String, u64>,
127
128    /// Total synthesis requests
129    pub synthesis_requests: u64,
130
131    /// Average synthesis duration (ms)
132    pub avg_synthesis_duration: f64,
133
134    /// Total errors
135    pub total_errors: u64,
136
137    /// Most used commands
138    pub most_used_commands: Vec<(String, u64)>,
139
140    /// Most used voices
141    pub most_used_voices: Vec<(String, u64)>,
142
143    /// Time range
144    pub start_time: Option<chrono::DateTime<chrono::Utc>>,
145    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
146
147    /// Storage size (bytes)
148    pub storage_size_bytes: u64,
149}
150
151impl Default for TelemetryStatistics {
152    fn default() -> Self {
153        Self {
154            total_events: 0,
155            events_by_type: std::collections::HashMap::new(),
156            synthesis_requests: 0,
157            avg_synthesis_duration: 0.0,
158            total_errors: 0,
159            most_used_commands: Vec::new(),
160            most_used_voices: Vec::new(),
161            start_time: None,
162            end_time: None,
163            storage_size_bytes: 0,
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use std::path::PathBuf;
172
173    #[tokio::test]
174    async fn test_telemetry_system_creation() {
175        let temp_dir = std::env::temp_dir().join("voirs_telemetry_test");
176        let config = TelemetryConfig {
177            enabled: true,
178            level: TelemetryLevel::Standard,
179            storage_path: temp_dir.clone(),
180            anonymization: AnonymizationLevel::Medium,
181            remote_endpoint: None,
182            batch_size: 100,
183            flush_interval_secs: 60,
184        };
185
186        let system = TelemetrySystem::new(config).await;
187        assert!(system.is_ok());
188
189        // Cleanup
190        let _ = std::fs::remove_dir_all(temp_dir);
191    }
192
193    #[tokio::test]
194    async fn test_telemetry_disabled() {
195        let temp_dir = std::env::temp_dir().join("voirs_telemetry_test_disabled");
196        let config = TelemetryConfig {
197            enabled: false,
198            level: TelemetryLevel::Minimal,
199            storage_path: temp_dir.clone(),
200            anonymization: AnonymizationLevel::High,
201            remote_endpoint: None,
202            batch_size: 100,
203            flush_interval_secs: 60,
204        };
205
206        let system = TelemetrySystem::new(config).await.unwrap();
207        assert!(!system.is_enabled().await);
208
209        // Cleanup
210        let _ = std::fs::remove_dir_all(temp_dir);
211    }
212
213    #[tokio::test]
214    async fn test_record_event() {
215        let temp_dir = std::env::temp_dir().join("voirs_telemetry_test_record");
216        let config = TelemetryConfig {
217            enabled: true,
218            level: TelemetryLevel::Standard,
219            storage_path: temp_dir.clone(),
220            anonymization: AnonymizationLevel::Low,
221            remote_endpoint: None,
222            batch_size: 100,
223            flush_interval_secs: 60,
224        };
225
226        let system = TelemetrySystem::new(config).await.unwrap();
227
228        let event = TelemetryEvent {
229            id: uuid::Uuid::new_v4().to_string(),
230            event_type: EventType::CommandExecuted,
231            timestamp: chrono::Utc::now(),
232            metadata: EventMetadata::default(),
233            user_id: Some("test_user".to_string()),
234            session_id: uuid::Uuid::new_v4().to_string(),
235        };
236
237        let result = system.record_event(event).await;
238        assert!(result.is_ok());
239
240        // Cleanup
241        let _ = std::fs::remove_dir_all(temp_dir);
242    }
243
244    #[tokio::test]
245    async fn test_get_statistics() {
246        let temp_dir = std::env::temp_dir().join("voirs_telemetry_test_stats");
247        let config = TelemetryConfig {
248            enabled: true,
249            level: TelemetryLevel::Standard,
250            storage_path: temp_dir.clone(),
251            anonymization: AnonymizationLevel::Low,
252            remote_endpoint: None,
253            batch_size: 100,
254            flush_interval_secs: 60,
255        };
256
257        let system = TelemetrySystem::new(config).await.unwrap();
258        let stats = system.get_statistics().await;
259        assert!(stats.is_ok());
260
261        // Cleanup
262        let _ = std::fs::remove_dir_all(temp_dir);
263    }
264
265    #[tokio::test]
266    async fn test_clear_data() {
267        let temp_dir = std::env::temp_dir().join("voirs_telemetry_test_clear");
268        let config = TelemetryConfig {
269            enabled: true,
270            level: TelemetryLevel::Standard,
271            storage_path: temp_dir.clone(),
272            anonymization: AnonymizationLevel::Low,
273            remote_endpoint: None,
274            batch_size: 100,
275            flush_interval_secs: 60,
276        };
277
278        let system = TelemetrySystem::new(config).await.unwrap();
279        let result = system.clear_data().await;
280        assert!(result.is_ok());
281
282        // Cleanup
283        let _ = std::fs::remove_dir_all(temp_dir);
284    }
285}