1use std::cell::Cell;
17use std::rc::Rc;
18use std::sync::Arc;
19
20use teksilo_core::Signal;
21use teksilo_core::telemetry::{
22 ConsentScope, Event, RemoteDataExport, TelemetryError, UsageReporter,
23};
24
25use crate::bundle::TelemetryMode;
26use crate::consent::ConsentStore;
27use crate::queue::{EventQueue, InMemoryEventQueue};
28
29pub struct DynamicReporter {
41 anonymous: Option<Rc<dyn UsageReporter>>,
42 pseudonymous: Option<Rc<dyn UsageReporter>>,
43 active: Cell<TelemetryMode>,
44 consent: ConsentStore,
45 recent_log: Arc<InMemoryEventQueue>,
46 recent_log_revision: Signal<u64>,
54}
55
56impl DynamicReporter {
57 pub fn new(
58 anonymous: Option<Rc<dyn UsageReporter>>,
59 pseudonymous: Option<Rc<dyn UsageReporter>>,
60 default: TelemetryMode,
61 consent: ConsentStore,
62 recent_log: Arc<InMemoryEventQueue>,
63 ) -> Self {
64 debug_assert!(
65 anonymous.is_some() || pseudonymous.is_some(),
66 "DynamicReporter needs at least one adapter",
67 );
68 Self {
69 anonymous,
70 pseudonymous,
71 active: Cell::new(default),
72 consent,
73 recent_log,
74 recent_log_revision: Signal::new(0),
75 }
76 }
77
78 pub fn recent_log(&self) -> &Arc<InMemoryEventQueue> {
79 &self.recent_log
80 }
81
82 pub fn recent_log_revision(&self) -> Signal<u64> {
87 self.recent_log_revision.clone()
88 }
89
90 fn bump_revision(&self) {
91 let v = self.recent_log_revision.get();
92 self.recent_log_revision.set(v.wrapping_add(1));
93 }
94
95 pub fn active_mode(&self) -> TelemetryMode {
96 self.active.get()
97 }
98
99 pub fn set_active_mode(&self, mode: TelemetryMode) {
104 self.active.set(mode);
105 }
106
107 pub fn supports_mode_switch(&self) -> bool {
110 self.anonymous.is_some() && self.pseudonymous.is_some()
111 }
112
113 pub fn has_mode(&self, mode: TelemetryMode) -> bool {
115 match mode {
116 TelemetryMode::Anonymous => self.anonymous.is_some(),
117 TelemetryMode::Pseudonymous => self.pseudonymous.is_some(),
118 }
119 }
120
121 fn active_adapter(&self) -> Option<&Rc<dyn UsageReporter>> {
122 match self.active_mode() {
123 TelemetryMode::Anonymous => self.anonymous.as_ref(),
124 TelemetryMode::Pseudonymous => self.pseudonymous.as_ref(),
125 }
126 }
127
128 pub fn consent(&self) -> &ConsentStore {
129 &self.consent
130 }
131}
132
133impl UsageReporter for DynamicReporter {
134 fn record(&self, event: &Event<'_>) {
135 if !self.consent.is_granted() {
136 return;
137 }
138 self.recent_log.push(event.to_owned());
142 self.bump_revision();
143 if let Some(adapter) = self.active_adapter() {
144 adapter.record(event);
145 }
146 }
147
148 fn flush(&self) -> Result<(), TelemetryError> {
149 if let Some(a) = &self.anonymous {
152 a.flush()?;
153 }
154 if let Some(p) = &self.pseudonymous {
155 p.flush()?;
156 }
157 Ok(())
158 }
159
160 fn discard_pending(&self) -> Result<(), TelemetryError> {
161 self.recent_log.discard_all();
165 self.bump_revision();
166 if let Some(a) = &self.anonymous {
167 a.discard_pending()?;
168 }
169 if let Some(p) = &self.pseudonymous {
170 p.discard_pending()?;
171 }
172 Ok(())
173 }
174
175 fn erase_remote_data(&self) -> Result<(), TelemetryError> {
176 match self.active_adapter() {
180 Some(a) => a.erase_remote_data(),
181 None => Err(TelemetryError::ErasureUnsupported),
182 }
183 }
184
185 fn fetch_remote_data(&self) -> Result<RemoteDataExport, TelemetryError> {
186 match self.active_adapter() {
187 Some(a) => a.fetch_remote_data(),
188 None => Err(TelemetryError::FetchUnsupported),
189 }
190 }
191
192 fn install_id(&self) -> Option<&str> {
193 match self.active_mode() {
199 TelemetryMode::Anonymous => self.anonymous.as_deref().and_then(|a| a.install_id()),
200 TelemetryMode::Pseudonymous => {
201 self.pseudonymous.as_deref().and_then(|a| a.install_id())
202 }
203 }
204 }
205
206 fn adapter_name(&self) -> &'static str {
207 match self.active_adapter() {
208 Some(a) => a.adapter_name(),
209 None => "none",
210 }
211 }
212
213 fn endpoint(&self) -> &str {
214 match self.active_mode() {
215 TelemetryMode::Anonymous => self.anonymous.as_deref().map_or("", |a| a.endpoint()),
216 TelemetryMode::Pseudonymous => {
217 self.pseudonymous.as_deref().map_or("", |a| a.endpoint())
218 }
219 }
220 }
221
222 fn supported_scopes(&self) -> ConsentScope {
223 match self.active_adapter() {
224 Some(a) => a.supported_scopes(),
225 None => ConsentScope::none(),
226 }
227 }
228}
229
230impl std::fmt::Debug for DynamicReporter {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 f.debug_struct("DynamicReporter")
233 .field("active_mode", &self.active_mode())
234 .field("has_anonymous", &self.anonymous.is_some())
235 .field("has_pseudonymous", &self.pseudonymous.is_some())
236 .finish()
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::stub::StubReporter;
244 use std::time::Duration;
245 use teksilo_settings::AppPaths;
246 use tempfile::tempdir;
247
248 fn make(
249 anon: bool,
250 pseudo: bool,
251 default: TelemetryMode,
252 ) -> (DynamicReporter, ConsentStore, tempfile::TempDir) {
253 let dir = tempdir().unwrap();
254 let paths = AppPaths::for_testing(dir.path());
255 let consent = ConsentStore::open(&paths, Duration::ZERO, 1, "stub://").unwrap();
256 let anonymous: Option<Rc<dyn UsageReporter>> =
257 anon.then(|| Rc::new(StubReporter::anonymous()) as _);
258 let pseudonymous: Option<Rc<dyn UsageReporter>> =
259 pseudo.then(|| Rc::new(StubReporter::pseudonymous("uuid-1")) as _);
260 let recent_log = Arc::new(InMemoryEventQueue::with_capacity(64));
261 let dyn_r = DynamicReporter::new(
262 anonymous,
263 pseudonymous,
264 default,
265 consent.clone(),
266 recent_log,
267 );
268 (dyn_r, consent, dir)
269 }
270
271 fn make_event(
272 name: &'static str,
273 ) -> (Event<'static>, [teksilo_core::telemetry::Prop<'static>; 0]) {
274 let props: [teksilo_core::telemetry::Prop<'static>; 0] = [];
275 (
278 Event {
279 name,
280 category: teksilo_core::telemetry::EventCategory::Intent,
281 timestamp: std::time::SystemTime::UNIX_EPOCH,
282 install_id: None,
283 session_id: "s",
284 schema_version: 1,
285 props: &[],
286 },
287 props,
288 )
289 }
290
291 #[test]
292 fn record_drops_when_consent_unknown() {
293 let (r, _consent, _dir) = make(true, false, TelemetryMode::Anonymous);
294 let (e, _) = make_event("intent.dispatched");
295 r.record(&e);
296 assert!(matches!(
298 r.fetch_remote_data(),
299 Err(TelemetryError::FetchUnsupported)
300 ));
301 }
304
305 #[test]
306 fn record_routes_to_active_adapter() {
307 let (r, consent, _dir) = make(true, true, TelemetryMode::Pseudonymous);
308 consent.grant(ConsentScope::all(), "stub://").unwrap();
309
310 let (e, _) = make_event("intent.dispatched");
311 r.record(&e);
312
313 let export = r.fetch_remote_data().unwrap();
315 assert_eq!(export.events.len(), 1);
316 }
317
318 #[test]
319 fn mode_switch_changes_active_adapter() {
320 let (r, consent, _dir) = make(true, true, TelemetryMode::Anonymous);
321 consent.grant(ConsentScope::all(), "stub://").unwrap();
322
323 let (e, _) = make_event("intent.dispatched");
324 r.record(&e);
325 assert!(matches!(
327 r.fetch_remote_data(),
328 Err(TelemetryError::FetchUnsupported)
329 ));
330
331 r.set_active_mode(TelemetryMode::Pseudonymous);
333 r.record(&e);
334 let export = r.fetch_remote_data().unwrap();
335 assert_eq!(export.events.len(), 1); }
337
338 #[test]
339 fn supports_mode_switch_only_with_both() {
340 let (r1, _, _d1) = make(true, false, TelemetryMode::Anonymous);
341 assert!(!r1.supports_mode_switch());
342 let (r2, _, _d2) = make(true, true, TelemetryMode::Anonymous);
343 assert!(r2.supports_mode_switch());
344 }
345}