use std::collections::BTreeMap;
use std::sync::Mutex;
use std::time::SystemTime;
use teksilo_core::telemetry::{
ConsentScope, Event, OwnedEvent, RemoteDataExport, RemoteEvent, TelemetryError, UsageReporter,
};
pub struct StubReporter {
pub recorded: Mutex<Vec<OwnedEvent>>,
install_id: Option<String>,
fetch_supported: bool,
erase_supported: bool,
}
impl StubReporter {
pub fn anonymous() -> Self {
Self {
recorded: Mutex::new(Vec::new()),
install_id: None,
fetch_supported: false,
erase_supported: false,
}
}
pub fn pseudonymous(install_id: impl Into<String>) -> Self {
Self {
recorded: Mutex::new(Vec::new()),
install_id: Some(install_id.into()),
fetch_supported: true,
erase_supported: true,
}
}
pub fn recorded_count(&self) -> usize {
self.recorded
.lock()
.expect("StubReporter mutex poisoned")
.len()
}
pub fn last_recorded_name(&self) -> Option<String> {
self.recorded
.lock()
.expect("StubReporter mutex poisoned")
.last()
.map(|e| e.name.clone())
}
pub fn clear_recorded(&self) {
self.recorded
.lock()
.expect("StubReporter mutex poisoned")
.clear();
}
}
impl UsageReporter for StubReporter {
fn record(&self, event: &Event<'_>) {
self.recorded
.lock()
.expect("StubReporter mutex poisoned")
.push(event.to_owned());
}
fn discard_pending(&self) -> Result<(), TelemetryError> {
self.clear_recorded();
Ok(())
}
fn erase_remote_data(&self) -> Result<(), TelemetryError> {
if !self.erase_supported {
return Err(TelemetryError::ErasureUnsupported);
}
self.clear_recorded();
Ok(())
}
fn fetch_remote_data(&self) -> Result<RemoteDataExport, TelemetryError> {
if !self.fetch_supported {
return Err(TelemetryError::FetchUnsupported);
}
let events = self
.recorded
.lock()
.expect("StubReporter mutex poisoned")
.iter()
.map(|e| RemoteEvent {
name: e.name.to_string(),
timestamp: e.timestamp,
properties: BTreeMap::new(), })
.collect();
Ok(RemoteDataExport {
install_id: self.install_id.clone().unwrap_or_default(),
fetched_at: SystemTime::now(),
adapter: "stub",
endpoint: "stub://".to_string(),
schema_version: 1,
events,
server_metadata: BTreeMap::new(),
})
}
fn install_id(&self) -> Option<&str> {
self.install_id.as_deref()
}
fn adapter_name(&self) -> &'static str {
"stub"
}
fn endpoint(&self) -> &str {
"stub://"
}
fn supported_scopes(&self) -> ConsentScope {
ConsentScope::all()
}
}
#[cfg(test)]
mod tests {
use super::*;
use teksilo_core::telemetry::EventCategory;
#[test]
fn anonymous_stub_rejects_fetch_and_erase() {
let r = StubReporter::anonymous();
assert!(matches!(
r.erase_remote_data(),
Err(TelemetryError::ErasureUnsupported)
));
assert!(matches!(
r.fetch_remote_data(),
Err(TelemetryError::FetchUnsupported)
));
assert!(r.install_id().is_none());
}
#[test]
fn pseudonymous_stub_round_trips() {
let r = StubReporter::pseudonymous("test-uuid");
let props = [];
let e = Event {
name: "intent.dispatched",
category: EventCategory::Intent,
timestamp: SystemTime::UNIX_EPOCH,
install_id: Some("test-uuid"),
session_id: "s",
schema_version: 1,
props: &props,
};
r.record(&e);
assert_eq!(r.recorded_count(), 1);
let export = r.fetch_remote_data().unwrap();
assert_eq!(export.install_id, "test-uuid");
assert_eq!(export.events.len(), 1);
r.erase_remote_data().unwrap();
assert_eq!(r.recorded_count(), 0);
}
}