Skip to main content

voirs_cli/telemetry/
config.rs

1//! Telemetry configuration
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6use super::privacy::AnonymizationLevel;
7
8/// Telemetry configuration
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TelemetryConfig {
11    /// Enable or disable telemetry
12    pub enabled: bool,
13
14    /// Telemetry collection level
15    pub level: TelemetryLevel,
16
17    /// Local storage path for telemetry data
18    pub storage_path: PathBuf,
19
20    /// Anonymization level for privacy
21    pub anonymization: AnonymizationLevel,
22
23    /// Optional remote endpoint for telemetry submission
24    pub remote_endpoint: Option<String>,
25
26    /// Batch size for remote submission
27    pub batch_size: usize,
28
29    /// Flush interval in seconds
30    pub flush_interval_secs: u64,
31}
32
33impl Default for TelemetryConfig {
34    fn default() -> Self {
35        Self {
36            enabled: false, // Opt-in by default
37            level: TelemetryLevel::Standard,
38            storage_path: Self::default_storage_path(),
39            anonymization: AnonymizationLevel::Medium,
40            remote_endpoint: None,
41            batch_size: 100,
42            flush_interval_secs: 300, // 5 minutes
43        }
44    }
45}
46
47impl TelemetryConfig {
48    /// Get default storage path
49    fn default_storage_path() -> PathBuf {
50        dirs::data_local_dir()
51            .unwrap_or_else(|| PathBuf::from("."))
52            .join("voirs")
53            .join("telemetry")
54    }
55
56    /// Create config with telemetry enabled
57    pub fn enabled() -> Self {
58        Self {
59            enabled: true,
60            ..Default::default()
61        }
62    }
63
64    /// Create config with telemetry disabled
65    pub fn disabled() -> Self {
66        Self {
67            enabled: false,
68            ..Default::default()
69        }
70    }
71
72    /// Set telemetry level
73    pub fn with_level(mut self, level: TelemetryLevel) -> Self {
74        self.level = level;
75        self
76    }
77
78    /// Set storage path
79    pub fn with_storage_path(mut self, path: PathBuf) -> Self {
80        self.storage_path = path;
81        self
82    }
83
84    /// Set anonymization level
85    pub fn with_anonymization(mut self, level: AnonymizationLevel) -> Self {
86        self.anonymization = level;
87        self
88    }
89
90    /// Set remote endpoint
91    pub fn with_remote_endpoint(mut self, endpoint: String) -> Self {
92        self.remote_endpoint = Some(endpoint);
93        self
94    }
95
96    /// Validate configuration
97    pub fn validate(&self) -> Result<(), String> {
98        if self.batch_size == 0 {
99            return Err("Batch size must be greater than 0".to_string());
100        }
101
102        if self.flush_interval_secs == 0 {
103            return Err("Flush interval must be greater than 0".to_string());
104        }
105
106        if let Some(ref endpoint) = self.remote_endpoint {
107            if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
108                return Err("Remote endpoint must be a valid HTTP(S) URL".to_string());
109            }
110        }
111
112        Ok(())
113    }
114}
115
116/// Telemetry collection level
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum TelemetryLevel {
119    /// Minimal telemetry - only critical errors and basic usage
120    Minimal,
121
122    /// Standard telemetry - errors, performance, and command usage
123    Standard,
124
125    /// Detailed telemetry - full diagnostic information
126    Detailed,
127
128    /// Debug telemetry - includes all events and debug information
129    Debug,
130}
131
132impl TelemetryLevel {
133    /// Check if this level includes command execution events
134    pub fn includes_commands(&self) -> bool {
135        matches!(
136            self,
137            TelemetryLevel::Standard | TelemetryLevel::Detailed | TelemetryLevel::Debug
138        )
139    }
140
141    /// Check if this level includes performance metrics
142    pub fn includes_performance(&self) -> bool {
143        matches!(
144            self,
145            TelemetryLevel::Standard | TelemetryLevel::Detailed | TelemetryLevel::Debug
146        )
147    }
148
149    /// Check if this level includes debug information
150    pub fn includes_debug(&self) -> bool {
151        matches!(self, TelemetryLevel::Debug)
152    }
153
154    /// Check if this level includes errors
155    pub fn includes_errors(&self) -> bool {
156        true // All levels include errors
157    }
158}
159
160impl std::fmt::Display for TelemetryLevel {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        match self {
163            TelemetryLevel::Minimal => write!(f, "minimal"),
164            TelemetryLevel::Standard => write!(f, "standard"),
165            TelemetryLevel::Detailed => write!(f, "detailed"),
166            TelemetryLevel::Debug => write!(f, "debug"),
167        }
168    }
169}
170
171impl std::str::FromStr for TelemetryLevel {
172    type Err = String;
173
174    fn from_str(s: &str) -> Result<Self, Self::Err> {
175        match s.to_lowercase().as_str() {
176            "minimal" | "min" => Ok(TelemetryLevel::Minimal),
177            "standard" | "std" => Ok(TelemetryLevel::Standard),
178            "detailed" | "full" => Ok(TelemetryLevel::Detailed),
179            "debug" | "dbg" => Ok(TelemetryLevel::Debug),
180            _ => Err(format!("Invalid telemetry level: {}", s)),
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_default_config() {
191        let config = TelemetryConfig::default();
192        assert!(!config.enabled);
193        assert_eq!(config.level, TelemetryLevel::Standard);
194        assert!(config.remote_endpoint.is_none());
195    }
196
197    #[test]
198    fn test_enabled_config() {
199        let config = TelemetryConfig::enabled();
200        assert!(config.enabled);
201    }
202
203    #[test]
204    fn test_disabled_config() {
205        let config = TelemetryConfig::disabled();
206        assert!(!config.enabled);
207    }
208
209    #[test]
210    fn test_builder_pattern() {
211        let config = TelemetryConfig::default()
212            .with_level(TelemetryLevel::Detailed)
213            .with_remote_endpoint("https://telemetry.example.com".to_string());
214
215        assert_eq!(config.level, TelemetryLevel::Detailed);
216        assert_eq!(
217            config.remote_endpoint,
218            Some("https://telemetry.example.com".to_string())
219        );
220    }
221
222    #[test]
223    fn test_config_validation() {
224        let config = TelemetryConfig::default();
225        assert!(config.validate().is_ok());
226
227        let invalid_config = TelemetryConfig {
228            batch_size: 0,
229            ..Default::default()
230        };
231        assert!(invalid_config.validate().is_err());
232    }
233
234    #[test]
235    fn test_telemetry_level_includes() {
236        assert!(TelemetryLevel::Minimal.includes_errors());
237        assert!(!TelemetryLevel::Minimal.includes_commands());
238        assert!(!TelemetryLevel::Minimal.includes_debug());
239
240        assert!(TelemetryLevel::Standard.includes_commands());
241        assert!(TelemetryLevel::Standard.includes_performance());
242        assert!(!TelemetryLevel::Standard.includes_debug());
243
244        assert!(TelemetryLevel::Debug.includes_debug());
245    }
246
247    #[test]
248    fn test_telemetry_level_from_str() {
249        assert_eq!(
250            "minimal".parse::<TelemetryLevel>().unwrap(),
251            TelemetryLevel::Minimal
252        );
253        assert_eq!(
254            "standard".parse::<TelemetryLevel>().unwrap(),
255            TelemetryLevel::Standard
256        );
257        assert_eq!(
258            "detailed".parse::<TelemetryLevel>().unwrap(),
259            TelemetryLevel::Detailed
260        );
261        assert_eq!(
262            "debug".parse::<TelemetryLevel>().unwrap(),
263            TelemetryLevel::Debug
264        );
265        assert!("invalid".parse::<TelemetryLevel>().is_err());
266    }
267
268    #[test]
269    fn test_telemetry_level_display() {
270        assert_eq!(TelemetryLevel::Minimal.to_string(), "minimal");
271        assert_eq!(TelemetryLevel::Standard.to_string(), "standard");
272        assert_eq!(TelemetryLevel::Detailed.to_string(), "detailed");
273        assert_eq!(TelemetryLevel::Debug.to_string(), "debug");
274    }
275}