1use std::rc::Rc;
13use std::sync::Arc;
14use std::time::Duration;
15
16use teksilo_core::telemetry::UsageReporter;
17use teksilo_settings::{AppPaths, SettingsFileError, SettingsStore};
18
19use crate::consent::ConsentStore;
20use crate::dynamic_reporter::DynamicReporter;
21use crate::install_id::InstallId;
22use crate::queue::{EventQueue, InMemoryEventQueue};
23use crate::scopes::{TELEMETRY_ENDPOINT_OVERRIDE, TELEMETRY_REGION_OVERRIDE};
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq)]
27pub enum TelemetryMode {
28 Anonymous,
32 Pseudonymous,
36}
37
38#[derive(Copy, Clone, Debug, PartialEq, Eq)]
39pub enum DataResidencyRegion {
40 EU,
41 US,
42 Other,
43}
44
45impl DataResidencyRegion {
46 pub fn as_str(self) -> &'static str {
47 match self {
48 Self::EU => "EU",
49 Self::US => "US",
50 Self::Other => "other",
51 }
52 }
53}
54
55#[derive(Clone, Debug)]
56pub struct PrivacyPolicy {
57 pub data_processor_name: String,
58 pub privacy_policy_url: Option<String>,
59 pub data_residency_region: DataResidencyRegion,
60 pub retention_days: u32,
62}
63
64impl Default for PrivacyPolicy {
65 fn default() -> Self {
66 Self {
67 data_processor_name: String::new(),
68 privacy_policy_url: None,
69 data_residency_region: DataResidencyRegion::EU,
70 retention_days: 395, }
72 }
73}
74
75#[derive(Debug, thiserror::Error)]
76pub enum TelemetryBundleError {
77 #[error(
78 "TelemetryBundle: no adapter configured (call .with_anonymous(...) or .with_pseudonymous(...))"
79 )]
80 NoAdapter,
81 #[error("TelemetryBundle: {0}")]
82 Settings(#[from] SettingsFileError),
83}
84
85#[derive(Clone)]
88pub struct TelemetryBundle {
89 anonymous: Option<Rc<dyn UsageReporter>>,
90 pseudonymous: Option<Rc<dyn UsageReporter>>,
91 default_mode: TelemetryMode,
92 event_schema_version: u32,
93 debounce: Duration,
94 policy: PrivacyPolicy,
95 recent_log_capacity: usize,
96}
97
98impl TelemetryBundle {
99 pub fn new(event_schema_version: u32) -> Self {
102 Self {
103 anonymous: None,
104 pseudonymous: None,
105 default_mode: TelemetryMode::Anonymous,
106 event_schema_version,
107 debounce: Duration::from_millis(500),
108 policy: PrivacyPolicy::default(),
109 recent_log_capacity: 200,
110 }
111 }
112
113 pub fn with_recent_log_capacity(mut self, n: usize) -> Self {
118 self.recent_log_capacity = n.max(1);
119 self
120 }
121
122 pub fn with_anonymous(mut self, reporter: Rc<dyn UsageReporter>) -> Self {
126 self.anonymous = Some(reporter);
127 self
128 }
129
130 pub fn with_pseudonymous(mut self, reporter: Rc<dyn UsageReporter>) -> Self {
134 self.pseudonymous = Some(reporter);
135 self
136 }
137
138 pub fn with_default_mode(mut self, mode: TelemetryMode) -> Self {
140 self.default_mode = mode;
141 self
142 }
143
144 pub fn with_debounce(mut self, debounce: Duration) -> Self {
145 self.debounce = debounce;
146 self
147 }
148
149 pub fn with_data_processor_name(mut self, name: impl Into<String>) -> Self {
150 self.policy.data_processor_name = name.into();
151 self
152 }
153
154 pub fn with_privacy_policy_url(mut self, url: impl Into<String>) -> Self {
155 self.policy.privacy_policy_url = Some(url.into());
156 self
157 }
158
159 pub fn with_data_residency_region(mut self, region: DataResidencyRegion) -> Self {
160 self.policy.data_residency_region = region;
161 self
162 }
163
164 pub fn with_retention_days(mut self, days: u32) -> Self {
165 self.policy.retention_days = days;
166 self
167 }
168
169 pub fn open(
174 self,
175 paths: &AppPaths,
176 settings: &SettingsStore,
177 ) -> Result<OpenedTelemetry, TelemetryBundleError> {
178 if self.anonymous.is_none() && self.pseudonymous.is_none() {
179 return Err(TelemetryBundleError::NoAdapter);
180 }
181
182 let default_mode = match self.default_mode {
184 TelemetryMode::Anonymous if self.anonymous.is_some() => TelemetryMode::Anonymous,
185 TelemetryMode::Pseudonymous if self.pseudonymous.is_some() => {
186 TelemetryMode::Pseudonymous
187 }
188 _ => {
190 if self.anonymous.is_some() {
191 TelemetryMode::Anonymous
192 } else {
193 TelemetryMode::Pseudonymous
194 }
195 }
196 };
197
198 let endpoint_override = settings.signal_for(&TELEMETRY_ENDPOINT_OVERRIDE).get();
204 let _region_override = settings.signal_for(&TELEMETRY_REGION_OVERRIDE).get();
205
206 let endpoint_for_consent = match (&self.anonymous, &self.pseudonymous) {
207 _ if !endpoint_override.is_empty() => endpoint_override.clone(),
210 (_, Some(p)) if matches!(default_mode, TelemetryMode::Pseudonymous) => {
211 p.endpoint().to_string()
212 }
213 (Some(a), _) => a.endpoint().to_string(),
214 (None, Some(p)) => p.endpoint().to_string(),
215 _ => unreachable!("validated above"),
216 };
217
218 let consent = ConsentStore::open(
219 paths,
220 self.debounce,
221 self.event_schema_version,
222 &endpoint_for_consent,
223 )?
224 .with_settings_mirror(settings.clone());
225
226 let install_id = if self.pseudonymous.is_some() {
227 Some(InstallId::open_or_create(paths, self.debounce)?)
228 } else {
229 None
230 };
231
232 let recent_log = Arc::new(InMemoryEventQueue::with_capacity(self.recent_log_capacity));
233 let reporter = Rc::new(DynamicReporter::new(
234 self.anonymous,
235 self.pseudonymous,
236 default_mode,
237 consent.clone(),
238 recent_log.clone(),
239 ));
240
241 Ok(OpenedTelemetry {
242 consent,
243 install_id,
244 reporter,
245 recent_log,
246 policy: self.policy,
247 event_schema_version: self.event_schema_version,
248 })
249 }
250}
251
252#[derive(Clone)]
256pub struct OpenedTelemetry {
257 pub consent: ConsentStore,
258 pub install_id: Option<InstallId>,
259 pub reporter: Rc<DynamicReporter>,
260 pub recent_log: Arc<InMemoryEventQueue>,
268 pub policy: PrivacyPolicy,
269 pub event_schema_version: u32,
270}
271
272impl OpenedTelemetry {
273 pub fn flush_all(&self) -> Result<(), TelemetryBundleError> {
277 self.consent.flush_now()?;
278 if let Some(id) = &self.install_id {
279 id.flush_now()?;
280 }
281 Ok(())
282 }
283
284 pub fn discard_pending(&self) -> Result<(), teksilo_core::telemetry::TelemetryError> {
292 self.reporter.discard_pending()
293 }
294}
295
296impl std::fmt::Debug for OpenedTelemetry {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 f.debug_struct("OpenedTelemetry")
299 .field("consent", &self.consent)
300 .field("install_id", &self.install_id)
301 .field("policy", &self.policy)
302 .field("recent_log_len", &self.recent_log.len())
303 .field("event_schema_version", &self.event_schema_version)
304 .finish()
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::stub::StubReporter;
312 use std::time::Duration;
313 use tempfile::tempdir;
314
315 #[test]
316 fn empty_bundle_errors() {
317 let dir = tempdir().unwrap();
318 let paths = AppPaths::for_testing(dir.path());
319 let store =
320 SettingsStore::open_with_delay(paths.config_file("general"), Duration::ZERO).unwrap();
321 let bundle = TelemetryBundle::new(1);
322 let err = bundle.open(&paths, &store).unwrap_err();
323 assert!(matches!(err, TelemetryBundleError::NoAdapter));
324 }
325
326 #[test]
327 fn anonymous_only_bundle_opens() {
328 let dir = tempdir().unwrap();
329 let paths = AppPaths::for_testing(dir.path());
330 let store =
331 SettingsStore::open_with_delay(paths.config_file("general"), Duration::ZERO).unwrap();
332 let opened = TelemetryBundle::new(1)
333 .with_anonymous(Rc::new(StubReporter::anonymous()))
334 .with_default_mode(TelemetryMode::Anonymous)
335 .with_debounce(Duration::ZERO)
336 .open(&paths, &store)
337 .unwrap();
338 assert!(opened.install_id.is_none());
339 assert_eq!(opened.reporter.active_mode(), TelemetryMode::Anonymous);
340 }
341
342 #[test]
343 fn pseudonymous_bundle_creates_install_id() {
344 let dir = tempdir().unwrap();
345 let paths = AppPaths::for_testing(dir.path());
346 let store =
347 SettingsStore::open_with_delay(paths.config_file("general"), Duration::ZERO).unwrap();
348 let opened = TelemetryBundle::new(1)
349 .with_pseudonymous(Rc::new(StubReporter::pseudonymous("u")))
350 .with_default_mode(TelemetryMode::Pseudonymous)
351 .with_debounce(Duration::ZERO)
352 .open(&paths, &store)
353 .unwrap();
354 assert!(opened.install_id.is_some());
355 let id = opened.install_id.as_ref().unwrap().get();
356 assert!(!id.is_empty());
357 }
358
359 #[test]
360 fn default_mode_falls_back_when_unsupported() {
361 let dir = tempdir().unwrap();
362 let paths = AppPaths::for_testing(dir.path());
363 let store =
364 SettingsStore::open_with_delay(paths.config_file("general"), Duration::ZERO).unwrap();
365 let opened = TelemetryBundle::new(1)
367 .with_anonymous(Rc::new(StubReporter::anonymous()))
368 .with_default_mode(TelemetryMode::Pseudonymous)
369 .with_debounce(Duration::ZERO)
370 .open(&paths, &store)
371 .unwrap();
372 assert_eq!(opened.reporter.active_mode(), TelemetryMode::Anonymous);
373 }
374}