use anon_telemetry::TelemetryClient;
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::sync::Arc;
const TELEMETRY_SINK_URL: &str = "https://anon-telemetry-sink.seemueller.workers.dev/v1/events";
const TELEMETRY_APP_ID: &str = "caretta";
static TELEMETRY_CLIENT: OnceCell<Arc<TelemetryClient>> = OnceCell::new();
pub struct EventNames;
impl EventNames {
pub const APP_START: &str = "app_start";
pub const APP_EXIT: &str = "app_exit";
pub const COMMAND_EXECUTION: &str = "command_execution";
pub const WORKFLOW_START: &str = "workflow_start";
pub const WORKFLOW_COMPLETE: &str = "workflow_complete";
pub const AGENT_INVOCATION: &str = "agent_invocation";
pub const ERROR: &str = "error";
pub const UI_LAUNCH: &str = "ui_launch";
pub const CONFIG_LOAD: &str = "config_load";
}
pub async fn initialize_telemetry(config: &crate::cli_common::TelemetryConfig) {
if !config.enabled {
return;
}
let client = TelemetryClient::new(TELEMETRY_APP_ID, TELEMETRY_SINK_URL).await;
TELEMETRY_CLIENT.set(client).ok();
}
pub fn get_telemetry_client() -> Option<&'static Arc<TelemetryClient>> {
TELEMETRY_CLIENT.get()
}
pub fn track_event(event_name: &str, properties: Option<HashMap<String, serde_json::Value>>) {
if let Some(client) = get_telemetry_client() {
client.track(event_name, properties);
}
}
pub fn track_simple_event(event_name: &str) {
track_event(event_name, None);
}
pub fn track_event_with_property(event_name: &str, key: &str, value: serde_json::Value) {
let mut properties = HashMap::new();
properties.insert(key.to_string(), value);
track_event(event_name, Some(properties));
}
pub struct EventBuilder {
event_name: String,
properties: HashMap<String, serde_json::Value>,
}
impl EventBuilder {
pub fn new(event_name: &str) -> Self {
Self {
event_name: event_name.to_string(),
properties: HashMap::new(),
}
}
pub fn with_string(mut self, key: &str, value: &str) -> Self {
self.properties.insert(
key.to_string(),
serde_json::Value::String(value.to_string()),
);
self
}
pub fn with_bool(mut self, key: &str, value: bool) -> Self {
self.properties
.insert(key.to_string(), serde_json::Value::Bool(value));
self
}
pub fn with_number(mut self, key: &str, value: impl Into<serde_json::Value>) -> Self {
self.properties.insert(key.to_string(), value.into());
self
}
pub fn with_property(mut self, key: &str, value: serde_json::Value) -> Self {
self.properties.insert(key.to_string(), value);
self
}
pub fn send(self) {
track_event(&self.event_name, Some(self.properties));
}
}
#[macro_export]
macro_rules! telemetry {
($event:expr) => {
$crate::agent::telemetry::track_simple_event($event)
};
($event:expr, $key:expr => $value:expr) => {
$crate::agent::telemetry::track_event_with_property($event, $key, serde_json::Value::String($value.to_string()))
};
($event:expr, { $($key:expr => $value:expr),* $(,)? }) => {
{
let mut properties = std::collections::HashMap::new();
$(properties.insert($key.to_string(), serde_json::Value::String($value.to_string()));)*
$crate::agent::telemetry::track_event($event, Some(properties));
}
};
}
pub fn record_app_start(version: &str, platform: &str) {
let mut properties = HashMap::new();
properties.insert(
"version".to_string(),
serde_json::Value::String(version.to_string()),
);
properties.insert(
"platform".to_string(),
serde_json::Value::String(platform.to_string()),
);
track_event(EventNames::APP_START, Some(properties));
}
pub fn record_app_exit() {
track_simple_event(EventNames::APP_EXIT);
}
pub fn record_command_execution(command: &str, success: bool) {
let mut properties = HashMap::new();
properties.insert(
"command".to_string(),
serde_json::Value::String(command.to_string()),
);
properties.insert("success".to_string(), serde_json::Value::Bool(success));
track_event(EventNames::COMMAND_EXECUTION, Some(properties));
}
pub fn record_workflow_start(workflow_name: &str, agent: &str) {
let mut properties = HashMap::new();
properties.insert(
"workflow_name".to_string(),
serde_json::Value::String(workflow_name.to_string()),
);
properties.insert(
"agent".to_string(),
serde_json::Value::String(agent.to_string()),
);
track_event(EventNames::WORKFLOW_START, Some(properties));
}
pub fn record_workflow_complete(workflow_name: &str, success: bool, duration_ms: u64) {
let mut properties = HashMap::new();
properties.insert(
"workflow_name".to_string(),
serde_json::Value::String(workflow_name.to_string()),
);
properties.insert("success".to_string(), serde_json::Value::Bool(success));
properties.insert(
"duration_ms".to_string(),
serde_json::Value::Number(duration_ms.into()),
);
track_event(EventNames::WORKFLOW_COMPLETE, Some(properties));
}
pub fn record_agent_invocation(agent: &str, model: &str, action: &str) {
let mut properties = HashMap::new();
properties.insert(
"agent".to_string(),
serde_json::Value::String(agent.to_string()),
);
properties.insert(
"model".to_string(),
serde_json::Value::String(model.to_string()),
);
properties.insert(
"action".to_string(),
serde_json::Value::String(action.to_string()),
);
track_event(EventNames::AGENT_INVOCATION, Some(properties));
}
pub fn record_error(error_type: &str, message: &str) {
let mut properties = HashMap::new();
properties.insert(
"error_type".to_string(),
serde_json::Value::String(error_type.to_string()),
);
properties.insert(
"message".to_string(),
serde_json::Value::String(message.to_string()),
);
track_event(EventNames::ERROR, Some(properties));
}
pub fn record_ui_launch() {
track_simple_event(EventNames::UI_LAUNCH);
}
pub fn record_config_load(project_name: &str, workspace: Option<&str>) {
let mut properties = HashMap::new();
properties.insert(
"project_name".to_string(),
serde_json::Value::String(project_name.to_string()),
);
if let Some(ws) = workspace {
properties.insert(
"workspace".to_string(),
serde_json::Value::String(ws.to_string()),
);
}
track_event(EventNames::CONFIG_LOAD, Some(properties));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_builder() {
EventBuilder::new("test_event")
.with_string("key1", "value1")
.with_bool("key2", true)
.with_number("key3", 42)
.send();
}
#[test]
fn test_telemetry_config_defaults() {
use crate::cli_common::TelemetryConfig;
let config = TelemetryConfig::default();
assert!(config.enabled);
}
}