use std::io;
use std::rc::Rc;
use super::event::{Event, RemoteDataExport};
pub trait UsageReporter: 'static {
fn record(&self, event: &Event<'_>);
fn flush(&self) -> Result<(), TelemetryError> {
Ok(())
}
fn discard_pending(&self) -> Result<(), TelemetryError> {
Ok(())
}
fn erase_remote_data(&self) -> Result<(), TelemetryError> {
Err(TelemetryError::ErasureUnsupported)
}
fn fetch_remote_data(&self) -> Result<RemoteDataExport, TelemetryError> {
Err(TelemetryError::FetchUnsupported)
}
fn install_id(&self) -> Option<&str> {
None
}
fn adapter_name(&self) -> &'static str;
fn endpoint(&self) -> &str;
fn supported_scopes(&self) -> ConsentScope {
ConsentScope::all()
}
}
pub struct TelemetryContext {
pub reporter: Rc<dyn UsageReporter>,
pub session_id: String,
pub schema_version: u32,
}
impl std::fmt::Debug for TelemetryContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TelemetryContext")
.field("session_id", &self.session_id)
.field("schema_version", &self.schema_version)
.field("adapter", &self.reporter.adapter_name())
.finish()
}
}
#[derive(Debug, thiserror::Error)]
pub enum TelemetryError {
#[error("erasure unsupported (anonymous mode)")]
ErasureUnsupported,
#[error("fetch unsupported (anonymous mode)")]
FetchUnsupported,
#[error("erasure unsupported by configured backend")]
ErasureUnsupportedByBackend,
#[error("fetch unsupported by configured backend")]
FetchUnsupportedByBackend,
#[error("network error: {0}")]
Network(#[from] io::Error),
#[error("server returned {status}: {body}")]
Server { status: u16, body: String },
#[error("rate-limited; retry later")]
QuotaExceeded,
#[error("consent not granted")]
NotConsented,
#[error("{0}")]
Other(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub enum ConsentState {
#[default]
Unknown,
Granted(ConsentScope),
Denied,
}
impl ConsentState {
pub fn is_granted(&self) -> bool {
matches!(self, ConsentState::Granted(_))
}
pub fn scope(&self) -> Option<&ConsentScope> {
match self {
ConsentState::Granted(s) => Some(s),
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct ConsentScope {
pub anonymous_metrics: bool,
pub crash_reports: bool,
pub feature_flags: bool,
pub session_recording: bool,
}
impl ConsentScope {
pub fn none() -> Self {
Self::default()
}
pub fn all() -> Self {
Self {
anonymous_metrics: true,
crash_reports: true,
feature_flags: true,
session_recording: false, }
}
pub fn anonymous_metrics_only() -> Self {
Self {
anonymous_metrics: true,
..Self::default()
}
}
pub fn any(&self) -> bool {
self.anonymous_metrics || self.crash_reports || self.feature_flags || self.session_recording
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn consent_state_defaults_to_unknown() {
let s = ConsentState::default();
assert!(matches!(s, ConsentState::Unknown));
assert!(!s.is_granted());
}
#[test]
fn scope_all_excludes_session_recording() {
let s = ConsentScope::all();
assert!(s.anonymous_metrics);
assert!(s.crash_reports);
assert!(s.feature_flags);
assert!(!s.session_recording);
}
#[test]
fn telemetry_error_display() {
let e = TelemetryError::ErasureUnsupported;
assert!(e.to_string().contains("anonymous"));
}
}