1use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TelemetryConfig {
11 pub enabled: bool,
13
14 pub db_path: PathBuf,
16
17 pub privacy: PrivacyConfig,
19
20 pub community_contribution: bool,
22
23 pub batch_size: usize,
25
26 pub flush_interval_secs: u64,
28
29 pub max_db_size_mb: u64,
31
32 pub retention_days: u32,
34
35 pub enable_aggregation: bool,
37
38 pub aggregation_interval_hours: u32,
40}
41
42impl Default for TelemetryConfig {
43 fn default() -> Self {
44 Self {
45 enabled: false, db_path: PathBuf::from(".rk_telemetry.db"),
47 privacy: PrivacyConfig::default(),
48 community_contribution: false, batch_size: 100,
50 flush_interval_secs: 60,
51 max_db_size_mb: 100, retention_days: 90, enable_aggregation: true,
54 aggregation_interval_hours: 24,
55 }
56 }
57}
58
59impl TelemetryConfig {
60 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 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 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 pub fn from_env() -> Self {
103 let mut config = Self::default();
104
105 if let Ok(val) = std::env::var("RK_TELEMETRY_ENABLED") {
107 config.enabled = val.to_lowercase() == "true" || val == "1";
108 }
109
110 if let Ok(path) = std::env::var("RK_TELEMETRY_PATH") {
112 config.db_path = PathBuf::from(path);
113 }
114
115 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#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PrivacyConfig {
127 pub strip_pii: bool,
129
130 pub block_sensitive: bool,
132
133 pub differential_privacy: bool,
135
136 pub dp_epsilon: f64,
138
139 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 pub fn strict() -> Self {
158 Self {
159 strip_pii: true,
160 block_sensitive: true,
161 differential_privacy: true,
162 dp_epsilon: 0.1, redact_file_paths: true,
164 }
165 }
166
167 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#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ConsentRecord {
182 pub id: uuid::Uuid,
184
185 pub timestamp: chrono::DateTime<chrono::Utc>,
187
188 pub local_telemetry: bool,
190
191 pub aggregated_sharing: bool,
193
194 pub community_contribution: bool,
196
197 pub consent_version: u32,
199
200 pub ip_hash: Option<String>,
202}
203
204impl ConsentRecord {
205 pub const CURRENT_VERSION: u32 = 1;
207
208 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 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 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); }
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); }
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 let config = TelemetryConfig::from_env();
277 assert!(!config.db_path.as_os_str().is_empty());
279 }
280}