use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryEvent {
pub timestamp: String,
pub category: String,
pub name: String,
pub duration_ms: Option<u64>,
pub properties: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct Telemetry {
events: Arc<Mutex<Vec<TelemetryEvent>>>,
enabled: bool,
}
impl Telemetry {
pub fn new() -> Self {
Self {
events: Arc::new(Mutex::new(Vec::new())),
enabled: false,
}
}
pub fn enabled() -> Self {
Self {
events: Arc::new(Mutex::new(Vec::new())),
enabled: true,
}
}
pub fn record(&self, category: &str, name: &str) {
if !self.enabled {
return;
}
let event = TelemetryEvent {
timestamp: chrono::Utc::now().to_rfc3339(),
category: category.to_string(),
name: name.to_string(),
duration_ms: None,
properties: HashMap::new(),
};
if let Ok(mut events) = self.events.lock() {
events.push(event);
}
}
pub fn record_timed(&self, category: &str, name: &str, duration: Duration) {
if !self.enabled {
return;
}
let event = TelemetryEvent {
timestamp: chrono::Utc::now().to_rfc3339(),
category: category.to_string(),
name: name.to_string(),
duration_ms: Some(duration.as_millis() as u64),
properties: HashMap::new(),
};
if let Ok(mut events) = self.events.lock() {
events.push(event);
}
}
pub fn record_with_props(
&self,
category: &str,
name: &str,
props: HashMap<String, String>,
) {
if !self.enabled {
return;
}
let event = TelemetryEvent {
timestamp: chrono::Utc::now().to_rfc3339(),
category: category.to_string(),
name: name.to_string(),
duration_ms: None,
properties: props,
};
if let Ok(mut events) = self.events.lock() {
events.push(event);
}
}
pub fn export_to_file(&self, path: impl AsRef<Path>) -> crate::error::Result<()> {
let events = self.events.lock().map_err(|e| {
crate::error::BrowsingError::Browser(format!("Telemetry lock poisoned: {}", e))
})?;
let json = serde_json::to_string_pretty(&*events)?;
fs::write(path, json)?;
Ok(())
}
pub fn summary(&self) -> HashMap<String, usize> {
let mut counts = HashMap::new();
if let Ok(events) = self.events.lock() {
for event in events.iter() {
let key = format!("{}:{}", event.category, event.name);
*counts.entry(key).or_insert(0) += 1;
}
}
counts
}
pub fn clear(&self) {
if let Ok(mut events) = self.events.lock() {
events.clear();
}
}
}
impl Default for Telemetry {
fn default() -> Self {
Self::new()
}
}
pub struct TelemetryTimer<'a> {
telemetry: &'a Telemetry,
category: String,
name: String,
start: Instant,
}
impl<'a> TelemetryTimer<'a> {
pub fn start(telemetry: &'a Telemetry, category: &str, name: &str) -> Self {
Self {
telemetry,
category: category.to_string(),
name: name.to_string(),
start: Instant::now(),
}
}
}
impl<'a> Drop for TelemetryTimer<'a> {
fn drop(&mut self) {
self.telemetry
.record_timed(&self.category, &self.name, self.start.elapsed());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_telemetry_disabled_by_default() {
let tel = Telemetry::new();
tel.record("agent", "step");
let summary = tel.summary();
assert!(summary.is_empty());
}
#[test]
fn test_telemetry_enabled() {
let tel = Telemetry::enabled();
tel.record("agent", "step");
let summary = tel.summary();
assert_eq!(summary.get("agent:step"), Some(&1));
}
#[test]
fn test_telemetry_with_props() {
let tel = Telemetry::enabled();
let mut props = HashMap::new();
props.insert("url".to_string(), "https://example.com".to_string());
tel.record_with_props("navigate", "success", props);
assert_eq!(tel.summary().get("navigate:success"), Some(&1));
}
#[test]
fn test_telemetry_timer() {
let tel = Telemetry::enabled();
{
let _timer = TelemetryTimer::start(&tel, "dom", "serialize");
std::thread::sleep(Duration::from_millis(10));
}
let summary = tel.summary();
assert_eq!(summary.get("dom:serialize"), Some(&1));
}
#[test]
fn test_telemetry_export() {
let tel = Telemetry::enabled();
tel.record("test", "event");
let temp = tempfile::NamedTempFile::new().unwrap();
tel.export_to_file(temp.path()).unwrap();
let contents = fs::read_to_string(temp.path()).unwrap();
assert!(contents.contains("test"));
assert!(contents.contains("event"));
}
}