diff --git a/server/packages/sandbox-agent/src/router.rs b/server/packages/sandbox-agent/src/router.rs index 92460d5..06434a9 100644 --- a/server/packages/sandbox-agent/src/router.rs +++ b/server/packages/sandbox-agent/src/router.rs @@ -40,6 +40,7 @@ use utoipa::{Modify, OpenApi, ToSchema}; use crate::agent_server_logs::AgentServerLogs; use crate::opencode_compat::{build_opencode_router, OpenCodeAppState}; +use crate::telemetry; use crate::ui; use sandbox_agent_agent_management::agents::{ AgentError as ManagerError, AgentId, AgentManager, InstallOptions, SpawnOptions, StreamingSpawn, @@ -1622,6 +1623,9 @@ impl SessionManager { session.native_session_id = Some(format!("mock-{session_id}")); } + let telemetry_agent = request.agent.clone(); + let telemetry_model = request.model.clone(); + let telemetry_variant = request.variant.clone(); let metadata = json!({ "agent": request.agent, "agentMode": session.agent_mode, @@ -1651,6 +1655,8 @@ impl SessionManager { } let native_session_id = session.native_session_id.clone(); + let telemetry_agent_mode = session.agent_mode.clone(); + let telemetry_permission_mode = session.permission_mode.clone(); let mut sessions = self.sessions.lock().await; sessions.push(session); drop(sessions); @@ -1664,6 +1670,14 @@ impl SessionManager { self.ensure_opencode_stream(session_id).await?; } + telemetry::log_session_created(telemetry::SessionConfig { + agent: telemetry_agent, + agent_mode: Some(telemetry_agent_mode), + permission_mode: Some(telemetry_permission_mode), + model: telemetry_model, + variant: telemetry_variant, + }); + Ok(CreateSessionResponse { healthy: true, error: None, diff --git a/server/packages/sandbox-agent/src/telemetry.rs b/server/packages/sandbox-agent/src/telemetry.rs index 6ff221b..6bed31e 100644 --- a/server/packages/sandbox-agent/src/telemetry.rs +++ b/server/packages/sandbox-agent/src/telemetry.rs @@ -3,6 +3,7 @@ use std::env; use std::fs; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use reqwest::Client; @@ -10,6 +11,8 @@ use serde::Serialize; use time::OffsetDateTime; use tokio::time::Instant; +static TELEMETRY_ENABLED: AtomicBool = AtomicBool::new(false); + const TELEMETRY_URL: &str = "https://tc.rivet.dev"; const TELEMETRY_ENV_DEBUG: &str = "SANDBOX_AGENT_TELEMETRY_DEBUG"; const TELEMETRY_ID_FILE: &str = "telemetry_id"; @@ -19,7 +22,7 @@ const TELEMETRY_INTERVAL_SECS: u64 = 300; const TELEMETRY_MIN_GAP_SECS: i64 = 300; #[derive(Debug, Serialize)] -struct TelemetryEvent { +struct TelemetryEvent { // p = project identifier p: String, // dt = unix timestamp (seconds) @@ -31,13 +34,13 @@ struct TelemetryEvent { // ev = event name ev: String, // d = data payload - d: TelemetryData, + d: D, // v = schema version v: u8, } #[derive(Debug, Serialize)] -struct TelemetryData { +struct BeaconData { version: String, os: OsInfo, provider: ProviderInfo, @@ -60,15 +63,17 @@ struct ProviderInfo { } pub fn telemetry_enabled(no_telemetry: bool) -> bool { - if no_telemetry { - return false; - } - if cfg!(debug_assertions) { - return env::var(TELEMETRY_ENV_DEBUG) + let enabled = if no_telemetry { + false + } else if cfg!(debug_assertions) { + env::var(TELEMETRY_ENV_DEBUG) .map(|value| matches!(value.as_str(), "1" | "true" | "TRUE")) - .unwrap_or(false); - } - true + .unwrap_or(false) + } else { + true + }; + TELEMETRY_ENABLED.store(enabled, Ordering::Relaxed); + enabled } pub fn log_enabled_message() { @@ -105,7 +110,7 @@ async fn attempt_send(client: &Client) { return; } - let event = build_event(dt); + let event = build_beacon_event(dt); if let Err(err) = client.post(TELEMETRY_URL).json(&event).send().await { tracing::debug!(error = %err, "telemetry request failed"); return; @@ -113,15 +118,12 @@ async fn attempt_send(client: &Client) { write_last_sent(dt); } -fn build_event(dt: i64) -> TelemetryEvent { - let eid = load_or_create_id(); - TelemetryEvent { - p: "sandbox-agent".to_string(), +fn build_beacon_event(dt: i64) -> TelemetryEvent { + new_event( dt, - et: "sandbox".to_string(), - eid, - ev: "entity_beacon".to_string(), - d: TelemetryData { + "sandbox", + "entity_beacon", + BeaconData { version: env!("CARGO_PKG_VERSION").to_string(), os: OsInfo { name: std::env::consts::OS.to_string(), @@ -130,6 +132,18 @@ fn build_event(dt: i64) -> TelemetryEvent { }, provider: detect_provider(), }, + ) +} + +fn new_event(dt: i64, entity_type: &str, event_name: &str, data: D) -> TelemetryEvent { + let eid = load_or_create_id(); + TelemetryEvent { + p: "sandbox-agent".to_string(), + dt, + et: entity_type.to_string(), + eid, + ev: event_name.to_string(), + d: data, v: 1, } } @@ -431,3 +445,66 @@ fn metadata_or_none( Some(map) } } + +#[derive(Debug, Serialize)] +struct SessionCreatedData { + version: String, + agent: String, + #[serde(skip_serializing_if = "Option::is_none")] + agent_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + permission_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + model: Option, + #[serde(skip_serializing_if = "Option::is_none")] + variant: Option, +} + +pub struct SessionConfig { + pub agent: String, + pub agent_mode: Option, + pub permission_mode: Option, + pub model: Option, + pub variant: Option, +} + +pub fn log_session_created(config: SessionConfig) { + if !TELEMETRY_ENABLED.load(Ordering::Relaxed) { + return; + } + + let event = new_event( + OffsetDateTime::now_utc().unix_timestamp(), + "session", + "session_created", + SessionCreatedData { + version: env!("CARGO_PKG_VERSION").to_string(), + agent: config.agent, + agent_mode: config.agent_mode, + permission_mode: config.permission_mode, + model: config.model, + variant: config.variant, + }, + ); + + spawn_send(event); +} + +fn spawn_send(event: TelemetryEvent) { + tokio::spawn(async move { + let client = match Client::builder() + .timeout(Duration::from_millis(TELEMETRY_TIMEOUT_MS)) + .build() + { + Ok(client) => client, + Err(err) => { + tracing::debug!(error = %err, "failed to build telemetry client"); + return; + } + }; + + if let Err(err) = client.post(TELEMETRY_URL).json(&event).send().await { + tracing::debug!(error = %err, "telemetry send failed"); + } + }); +}