From ee60c494da77ee8470ee0700a18f6ee61740d652 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Fri, 22 May 2026 16:54:46 -0700 Subject: [PATCH] Track app-server start in ChatGPT telemetry --- .../analytics/src/analytics_client_tests.rs | 75 +++++++++++++++++++ codex-rs/analytics/src/client.rs | 36 +++++++++ codex-rs/analytics/src/events.rs | 15 ++++ codex-rs/analytics/src/facts.rs | 8 ++ codex-rs/analytics/src/lib.rs | 1 + codex-rs/analytics/src/reducer.rs | 24 ++++++ codex-rs/app-server/src/lib.rs | 12 ++- 7 files changed, 168 insertions(+), 3 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index cabea80886..b8e3950005 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -4,6 +4,8 @@ use crate::events::CodexAcceptedLineFingerprintsEventParams; use crate::events::CodexAcceptedLineFingerprintsEventRequest; use crate::events::CodexAppMentionedEventRequest; use crate::events::CodexAppServerClientMetadata; +use crate::events::CodexAppServerStartedEventParams; +use crate::events::CodexAppServerStartedEventRequest; use crate::events::CodexAppUsedEventRequest; use crate::events::CodexCommandExecutionEventParams; use crate::events::CodexCommandExecutionEventRequest; @@ -41,6 +43,7 @@ use crate::facts::AnalyticsFact; use crate::facts::AnalyticsJsonRpcError; use crate::facts::AppInvocation; use crate::facts::AppMentionedInput; +use crate::facts::AppServerStartedInput; use crate::facts::AppUsedInput; use crate::facts::CodexCompactionEvent; use crate::facts::CompactionImplementation; @@ -1362,6 +1365,39 @@ fn thread_initialized_event_serializes_expected_shape() { ); } +#[test] +fn app_server_started_event_serializes_expected_shape() { + let event = TrackEventRequest::AppServerStarted(CodexAppServerStartedEventRequest { + event_type: "codex_app_server_started", + event_params: CodexAppServerStartedEventParams { + runtime: sample_runtime_metadata(), + remote_control_enabled: true, + startup_duration_ms: 987, + completed_at: 12, + }, + }); + + let payload = serde_json::to_value(&event).expect("serialize app-server started event"); + + assert_eq!( + payload, + json!({ + "event_type": "codex_app_server_started", + "event_params": { + "runtime": { + "codex_rs_version": "0.1.0", + "runtime_os": "macos", + "runtime_os_version": "15.3.1", + "runtime_arch": "aarch64" + }, + "remote_control_enabled": true, + "startup_duration_ms": 987, + "completed_at": 12 + } + }) + ); +} + #[test] fn command_execution_event_serializes_expected_shape() { let event = TrackEventRequest::CommandExecution(CodexCommandExecutionEventRequest { @@ -1639,6 +1675,45 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize ); } +#[tokio::test] +async fn app_server_started_fact_emits_event() { + let mut reducer = AnalyticsReducer::default(); + let mut events = Vec::new(); + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::AppServerStarted( + AppServerStartedInput { + runtime: sample_runtime_metadata(), + remote_control_enabled: true, + startup_duration_ms: 456, + completed_at: 12, + }, + )), + &mut events, + ) + .await; + + let payload = serde_json::to_value(&events).expect("serialize events"); + assert_eq!( + payload, + json!([{ + "event_type": "codex_app_server_started", + "event_params": { + "runtime": { + "codex_rs_version": "0.1.0", + "runtime_os": "macos", + "runtime_os_version": "15.3.1", + "runtime_arch": "aarch64" + }, + "remote_control_enabled": true, + "startup_duration_ms": 456, + "completed_at": 12 + } + }]) + ); +} + #[tokio::test] async fn unrelated_client_requests_are_ignored_by_reducer() { let mut reducer = AnalyticsReducer::default(); diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index fbcfa32dc5..fac6644bca 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -8,6 +8,7 @@ use crate::facts::AnalyticsFact; use crate::facts::AnalyticsJsonRpcError; use crate::facts::AppInvocation; use crate::facts::AppMentionedInput; +use crate::facts::AppServerStartedInput; use crate::facts::AppUsedInput; use crate::facts::CustomAnalyticsFact; use crate::facts::HookRunFact; @@ -20,6 +21,7 @@ use crate::facts::SubAgentThreadStartedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnTokenUsageFact; +use crate::now_unix_seconds; use crate::reducer::AnalyticsReducer; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientResponsePayload; @@ -38,12 +40,35 @@ use std::collections::HashSet; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; +use std::time::Instant; use tokio::sync::mpsc; const ANALYTICS_EVENTS_QUEUE_SIZE: usize = 256; const ANALYTICS_EVENTS_TIMEOUT: Duration = Duration::from_secs(10); const ANALYTICS_EVENT_DEDUPE_MAX_KEYS: usize = 4096; +#[derive(Clone, Copy, Debug)] +pub struct StartedTimer { + started_at: Instant, +} + +impl StartedTimer { + #[must_use] + pub fn start() -> Self { + Self { + started_at: Instant::now(), + } + } + + fn elapsed_ms(self) -> u64 { + self.started_at + .elapsed() + .as_millis() + .try_into() + .unwrap_or(u64::MAX) + } +} + #[derive(Clone)] pub(crate) struct AnalyticsEventsQueue { pub(crate) sender: mpsc::Sender, @@ -163,6 +188,17 @@ impl AnalyticsEventsClient { }); } + pub fn track_app_server_started(&self, timer: StartedTimer, remote_control_enabled: bool) { + self.record_fact(AnalyticsFact::Custom( + CustomAnalyticsFact::AppServerStarted(AppServerStartedInput { + runtime: current_runtime_metadata(), + remote_control_enabled, + startup_duration_ms: timer.elapsed_ms(), + completed_at: now_unix_seconds(), + }), + )); + } + pub fn track_subagent_thread_started(&self, input: SubAgentThreadStartedInput) { self.record_fact(AnalyticsFact::Custom( CustomAnalyticsFact::SubAgentThreadStarted(input), diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 87cb165ac2..48d668573c 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -56,6 +56,7 @@ pub(crate) struct TrackEventsRequest { #[serde(untagged)] pub(crate) enum TrackEventRequest { SkillInvocation(SkillInvocationEventRequest), + AppServerStarted(CodexAppServerStartedEventRequest), ThreadInitialized(ThreadInitializedEvent), GuardianReview(Box), AppMentioned(CodexAppMentionedEventRequest), @@ -144,6 +145,20 @@ pub(crate) struct CodexRuntimeMetadata { pub(crate) runtime_arch: String, } +#[derive(Serialize)] +pub(crate) struct CodexAppServerStartedEventParams { + pub(crate) runtime: CodexRuntimeMetadata, + pub(crate) remote_control_enabled: bool, + pub(crate) startup_duration_ms: u64, + pub(crate) completed_at: u64, +} + +#[derive(Serialize)] +pub(crate) struct CodexAppServerStartedEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexAppServerStartedEventParams, +} + #[derive(Serialize)] pub(crate) struct ThreadInitializedEventParams { pub(crate) thread_id: String, diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 56bd0a5d2c..7ca0736c00 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -274,6 +274,13 @@ pub struct CodexCompactionEvent { pub duration_ms: Option, } +pub(crate) struct AppServerStartedInput { + pub runtime: CodexRuntimeMetadata, + pub remote_control_enabled: bool, + pub startup_duration_ms: u64, + pub completed_at: u64, +} + #[allow(dead_code)] pub(crate) enum AnalyticsFact { Initialize { @@ -323,6 +330,7 @@ pub(crate) enum AnalyticsFact { } pub(crate) enum CustomAnalyticsFact { + AppServerStarted(AppServerStartedInput), SubAgentThreadStarted(SubAgentThreadStartedInput), Compaction(Box), GuardianReview(Box), diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index a33ca7b9e3..a1ebd25c6c 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -10,6 +10,7 @@ use std::time::UNIX_EPOCH; pub use accepted_lines::accepted_line_fingerprints_from_unified_diff; pub use accepted_lines::fingerprint_hash; pub use client::AnalyticsEventsClient; +pub use client::StartedTimer; pub use events::AppServerRpcTransport; pub use events::GuardianApprovalRequestSource; pub use events::GuardianReviewAnalyticsResult; diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index d072720d10..f68e42c1d1 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -5,6 +5,8 @@ use crate::accepted_lines::accepted_line_repo_hash_for_cwd; use crate::events::AppServerRpcTransport; use crate::events::CodexAppMentionedEventRequest; use crate::events::CodexAppServerClientMetadata; +use crate::events::CodexAppServerStartedEventParams; +use crate::events::CodexAppServerStartedEventRequest; use crate::events::CodexAppUsedEventRequest; use crate::events::CodexCollabAgentToolCallEventParams; use crate::events::CodexCollabAgentToolCallEventRequest; @@ -61,6 +63,7 @@ use crate::events::subagent_thread_started_event_request; use crate::facts::AnalyticsFact; use crate::facts::AnalyticsJsonRpcError; use crate::facts::AppMentionedInput; +use crate::facts::AppServerStartedInput; use crate::facts::AppUsedInput; use crate::facts::CodexCompactionEvent; use crate::facts::CustomAnalyticsFact; @@ -446,6 +449,9 @@ impl AnalyticsReducer { self.ingest_server_request_aborted(completed_at_ms, request_id, out); } AnalyticsFact::Custom(input) => match input { + CustomAnalyticsFact::AppServerStarted(input) => { + self.ingest_app_server_started(input, out); + } CustomAnalyticsFact::SubAgentThreadStarted(input) => { self.ingest_subagent_thread_started(input, out); } @@ -508,6 +514,24 @@ impl AnalyticsReducer { ); } + fn ingest_app_server_started( + &mut self, + input: AppServerStartedInput, + out: &mut Vec, + ) { + out.push(TrackEventRequest::AppServerStarted( + CodexAppServerStartedEventRequest { + event_type: "codex_app_server_started", + event_params: CodexAppServerStartedEventParams { + runtime: input.runtime, + remote_control_enabled: input.remote_control_enabled, + startup_duration_ms: input.startup_duration_ms, + completed_at: input.completed_at, + }, + }, + )); + } + fn ingest_subagent_thread_started( &mut self, input: SubAgentThreadStartedInput, diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 80305d2d9f..16d79e5741 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -41,6 +41,7 @@ use crate::transport::start_remote_control; use crate::transport::start_stdio_connection; use crate::transport::start_websocket_acceptor; use codex_analytics::AppServerRpcTransport; +use codex_analytics::StartedTimer; use codex_app_server_protocol::ConfigLayerSource; use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::JSONRPCMessage; @@ -428,6 +429,7 @@ pub async fn run_main_with_transport_options( auth: AppServerWebsocketAuthSettings, runtime_options: AppServerRuntimeOptions, ) -> IoResult<()> { + let app_server_start_timer = StartedTimer::start(); let (transport_event_tx, mut transport_event_rx) = mpsc::channel::(CHANNEL_CAPACITY); let (outgoing_tx, mut outgoing_rx) = mpsc::channel::(CHANNEL_CAPACITY); @@ -698,6 +700,9 @@ pub async fn run_main_with_transport_options( let auth_manager = AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await; + let analytics_events_client = + analytics_events_client_from_config(Arc::clone(&auth_manager), &config); + let analytics_transport = analytics_rpc_transport(&transport); let remote_control_requested = runtime_options.remote_control_enabled; let remote_control_enabled = remote_control_requested && state_db.is_some(); @@ -787,8 +792,7 @@ pub async fn run_main_with_transport_options( let processor_handle = tokio::spawn({ let auth_manager = Arc::clone(&auth_manager); - let analytics_events_client = - analytics_events_client_from_config(Arc::clone(&auth_manager), &config); + let analytics_events_client = analytics_events_client.clone(); let outgoing_message_sender = Arc::new(OutgoingMessageSender::new( outgoing_tx, analytics_events_client.clone(), @@ -809,7 +813,7 @@ pub async fn run_main_with_transport_options( session_source, auth_manager, installation_id, - rpc_transport: analytics_rpc_transport(&transport), + rpc_transport: analytics_transport, remote_control_handle: Some(remote_control_handle.clone()), plugin_startup_tasks: runtime_options.plugin_startup_tasks, })); @@ -1065,6 +1069,8 @@ pub async fn run_main_with_transport_options( info!("processor task exited (channel closed)"); } }); + analytics_events_client + .track_app_server_started(app_server_start_timer, remote_control_enabled); drop(transport_event_tx);