teksilo_telemetry/
stub.rs1use std::collections::BTreeMap;
20use std::sync::Mutex;
21use std::time::SystemTime;
22
23use teksilo_core::telemetry::{
24 ConsentScope, Event, OwnedEvent, RemoteDataExport, RemoteEvent, TelemetryError, UsageReporter,
25};
26
27pub struct StubReporter {
28 pub recorded: Mutex<Vec<OwnedEvent>>,
29 install_id: Option<String>,
30 fetch_supported: bool,
33 erase_supported: bool,
34}
35
36impl StubReporter {
37 pub fn anonymous() -> Self {
40 Self {
41 recorded: Mutex::new(Vec::new()),
42 install_id: None,
43 fetch_supported: false,
44 erase_supported: false,
45 }
46 }
47
48 pub fn pseudonymous(install_id: impl Into<String>) -> Self {
51 Self {
52 recorded: Mutex::new(Vec::new()),
53 install_id: Some(install_id.into()),
54 fetch_supported: true,
55 erase_supported: true,
56 }
57 }
58
59 pub fn recorded_count(&self) -> usize {
60 self.recorded
61 .lock()
62 .expect("StubReporter mutex poisoned")
63 .len()
64 }
65
66 pub fn last_recorded_name(&self) -> Option<String> {
67 self.recorded
68 .lock()
69 .expect("StubReporter mutex poisoned")
70 .last()
71 .map(|e| e.name.clone())
72 }
73
74 pub fn clear_recorded(&self) {
75 self.recorded
76 .lock()
77 .expect("StubReporter mutex poisoned")
78 .clear();
79 }
80}
81
82impl UsageReporter for StubReporter {
83 fn record(&self, event: &Event<'_>) {
84 self.recorded
85 .lock()
86 .expect("StubReporter mutex poisoned")
87 .push(event.to_owned());
88 }
89
90 fn discard_pending(&self) -> Result<(), TelemetryError> {
91 self.clear_recorded();
92 Ok(())
93 }
94
95 fn erase_remote_data(&self) -> Result<(), TelemetryError> {
96 if !self.erase_supported {
97 return Err(TelemetryError::ErasureUnsupported);
98 }
99 self.clear_recorded();
100 Ok(())
101 }
102
103 fn fetch_remote_data(&self) -> Result<RemoteDataExport, TelemetryError> {
104 if !self.fetch_supported {
105 return Err(TelemetryError::FetchUnsupported);
106 }
107 let events = self
108 .recorded
109 .lock()
110 .expect("StubReporter mutex poisoned")
111 .iter()
112 .map(|e| RemoteEvent {
113 name: e.name.to_string(),
114 timestamp: e.timestamp,
115 properties: BTreeMap::new(), })
117 .collect();
118 Ok(RemoteDataExport {
119 install_id: self.install_id.clone().unwrap_or_default(),
120 fetched_at: SystemTime::now(),
121 adapter: "stub",
122 endpoint: "stub://".to_string(),
123 schema_version: 1,
124 events,
125 server_metadata: BTreeMap::new(),
126 })
127 }
128
129 fn install_id(&self) -> Option<&str> {
130 self.install_id.as_deref()
131 }
132
133 fn adapter_name(&self) -> &'static str {
134 "stub"
135 }
136
137 fn endpoint(&self) -> &str {
138 "stub://"
139 }
140
141 fn supported_scopes(&self) -> ConsentScope {
142 ConsentScope::all()
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use teksilo_core::telemetry::EventCategory;
150
151 #[test]
152 fn anonymous_stub_rejects_fetch_and_erase() {
153 let r = StubReporter::anonymous();
154 assert!(matches!(
155 r.erase_remote_data(),
156 Err(TelemetryError::ErasureUnsupported)
157 ));
158 assert!(matches!(
159 r.fetch_remote_data(),
160 Err(TelemetryError::FetchUnsupported)
161 ));
162 assert!(r.install_id().is_none());
163 }
164
165 #[test]
166 fn pseudonymous_stub_round_trips() {
167 let r = StubReporter::pseudonymous("test-uuid");
168 let props = [];
169 let e = Event {
170 name: "intent.dispatched",
171 category: EventCategory::Intent,
172 timestamp: SystemTime::UNIX_EPOCH,
173 install_id: Some("test-uuid"),
174 session_id: "s",
175 schema_version: 1,
176 props: &props,
177 };
178 r.record(&e);
179 assert_eq!(r.recorded_count(), 1);
180 let export = r.fetch_remote_data().unwrap();
181 assert_eq!(export.install_id, "test-uuid");
182 assert_eq!(export.events.len(), 1);
183 r.erase_remote_data().unwrap();
184 assert_eq!(r.recorded_count(), 0);
185 }
186}