reasonkit/telemetry/
config.rs

1//! Telemetry Configuration
2//!
3//! Configuration structures for the telemetry system.
4
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8/// Main telemetry configuration
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TelemetryConfig {
11    /// Enable telemetry collection
12    pub enabled: bool,
13
14    /// Path to telemetry database
15    pub db_path: PathBuf,
16
17    /// Privacy settings
18    pub privacy: PrivacyConfig,
19
20    /// Contribute anonymized data to community model
21    pub community_contribution: bool,
22
23    /// Batch size for writing events
24    pub batch_size: usize,
25
26    /// Flush interval in seconds
27    pub flush_interval_secs: u64,
28
29    /// Maximum database size in MB (0 = unlimited)
30    pub max_db_size_mb: u64,
31
32    /// Retention period in days (0 = forever)
33    pub retention_days: u32,
34
35    /// Enable aggregation for ML training
36    pub enable_aggregation: bool,
37
38    /// Aggregation interval in hours
39    pub aggregation_interval_hours: u32,
40}
41
42impl Default for TelemetryConfig {
43    fn default() -> Self {
44        Self {
45            enabled: false, // Opt-in by default
46            db_path: PathBuf::from(".rk_telemetry.db"),
47            privacy: PrivacyConfig::default(),
48            community_contribution: false, // Opt-in
49            batch_size: 100,
50            flush_interval_secs: 60,
51            max_db_size_mb: 100, // 100MB default limit
52            retention_days: 90,  // 3 months default
53            enable_aggregation: true,
54            aggregation_interval_hours: 24,
55        }
56    }
57}
58
59impl TelemetryConfig {
60    /// Create a minimal config for testing
61    pub fn minimal() -> Self {
62        Self {
63            enabled: true,
64            db_path: PathBuf::from(":memory:"),
65            privacy: PrivacyConfig::strict(),
66            community_contribution: false,
67            batch_size: 10,
68            flush_interval_secs: 5,
69            max_db_size_mb: 0,
70            retention_days: 0,
71            enable_aggregation: false,
72            aggregation_interval_hours: 24,
73        }
74    }
75
76    /// Create a production config
77    pub fn production() -> Self {
78        Self {
79            enabled: true,
80            db_path: Self::default_db_path(),
81            privacy: PrivacyConfig::default(),
82            community_contribution: false,
83            batch_size: 100,
84            flush_interval_secs: 60,
85            max_db_size_mb: 500,
86            retention_days: 365,
87            enable_aggregation: true,
88            aggregation_interval_hours: 24,
89        }
90    }
91
92    /// Get the default database path in user's data directory
93    pub fn default_db_path() -> PathBuf {
94        if let Some(data_dir) = dirs::data_local_dir() {
95            data_dir.join("reasonkit").join(".rk_telemetry.db")
96        } else {
97            PathBuf::from(".rk_telemetry.db")
98        }
99    }
100
101    /// Load config from environment and/or file
102    pub fn from_env() -> Self {
103        let mut config = Self::default();
104
105        // Check RK_TELEMETRY_ENABLED env var
106        if let Ok(val) = std::env::var("RK_TELEMETRY_ENABLED") {
107            config.enabled = val.to_lowercase() == "true" || val == "1";
108        }
109
110        // Check RK_TELEMETRY_PATH env var
111        if let Ok(path) = std::env::var("RK_TELEMETRY_PATH") {
112            config.db_path = PathBuf::from(path);
113        }
114
115        // Check RK_TELEMETRY_COMMUNITY env var
116        if let Ok(val) = std::env::var("RK_TELEMETRY_COMMUNITY") {
117            config.community_contribution = val.to_lowercase() == "true" || val == "1";
118        }
119
120        config
121    }
122}
123
124/// Privacy configuration
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PrivacyConfig {
127    /// Strip PII from all stored data
128    pub strip_pii: bool,
129
130    /// Block events containing sensitive keywords
131    pub block_sensitive: bool,
132
133    /// Apply differential privacy to aggregates
134    pub differential_privacy: bool,
135
136    /// Differential privacy epsilon (lower = more private)
137    pub dp_epsilon: f64,
138
139    /// Redact file paths
140    pub redact_file_paths: bool,
141}
142
143impl Default for PrivacyConfig {
144    fn default() -> Self {
145        Self {
146            strip_pii: true,
147            block_sensitive: false,
148            differential_privacy: false,
149            dp_epsilon: 1.0,
150            redact_file_paths: true,
151        }
152    }
153}
154
155impl PrivacyConfig {
156    /// Strict privacy settings (maximum protection)
157    pub fn strict() -> Self {
158        Self {
159            strip_pii: true,
160            block_sensitive: true,
161            differential_privacy: true,
162            dp_epsilon: 0.1, // Very private
163            redact_file_paths: true,
164        }
165    }
166
167    /// Relaxed privacy settings (more utility, less privacy)
168    pub fn relaxed() -> Self {
169        Self {
170            strip_pii: true,
171            block_sensitive: false,
172            differential_privacy: false,
173            dp_epsilon: 1.0,
174            redact_file_paths: false,
175        }
176    }
177}
178
179/// Consent record for GDPR/CCPA compliance
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ConsentRecord {
182    /// Unique consent ID
183    pub id: uuid::Uuid,
184
185    /// Timestamp of consent
186    pub timestamp: chrono::DateTime<chrono::Utc>,
187
188    /// Allow local telemetry storage
189    pub local_telemetry: bool,
190
191    /// Allow aggregated sharing
192    pub aggregated_sharing: bool,
193
194    /// Contribute to community model
195    pub community_contribution: bool,
196
197    /// Consent form version (for re-consent when updated)
198    pub consent_version: u32,
199
200    /// Hashed IP for legal compliance
201    pub ip_hash: Option<String>,
202}
203
204impl ConsentRecord {
205    /// Current consent form version
206    pub const CURRENT_VERSION: u32 = 1;
207
208    /// Create a new consent record with all permissions
209    pub fn allow_all() -> Self {
210        Self {
211            id: uuid::Uuid::new_v4(),
212            timestamp: chrono::Utc::now(),
213            local_telemetry: true,
214            aggregated_sharing: true,
215            community_contribution: true,
216            consent_version: Self::CURRENT_VERSION,
217            ip_hash: None,
218        }
219    }
220
221    /// Create a new consent record with minimal permissions
222    pub fn minimal() -> Self {
223        Self {
224            id: uuid::Uuid::new_v4(),
225            timestamp: chrono::Utc::now(),
226            local_telemetry: true,
227            aggregated_sharing: false,
228            community_contribution: false,
229            consent_version: Self::CURRENT_VERSION,
230            ip_hash: None,
231        }
232    }
233
234    /// Create a new consent record denying all telemetry
235    pub fn deny_all() -> Self {
236        Self {
237            id: uuid::Uuid::new_v4(),
238            timestamp: chrono::Utc::now(),
239            local_telemetry: false,
240            aggregated_sharing: false,
241            community_contribution: false,
242            consent_version: Self::CURRENT_VERSION,
243            ip_hash: None,
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_default_config_disabled() {
254        let config = TelemetryConfig::default();
255        assert!(!config.enabled); // Opt-in by default
256    }
257
258    #[test]
259    fn test_strict_privacy() {
260        let privacy = PrivacyConfig::strict();
261        assert!(privacy.strip_pii);
262        assert!(privacy.block_sensitive);
263        assert!(privacy.differential_privacy);
264        assert!(privacy.dp_epsilon < 1.0); // More private
265    }
266
267    #[test]
268    fn test_consent_versions() {
269        let consent = ConsentRecord::allow_all();
270        assert_eq!(consent.consent_version, ConsentRecord::CURRENT_VERSION);
271    }
272
273    #[test]
274    fn test_from_env() {
275        // This test just verifies the method runs without panicking
276        let config = TelemetryConfig::from_env();
277        // Verify the config was created (db_path is always set)
278        assert!(!config.db_path.as_os_str().is_empty());
279    }
280}