From 2e725aa97c617aabc92d0dfdd00e681237d7b9db Mon Sep 17 00:00:00 2001 From: Roy Han Date: Fri, 27 Mar 2026 10:38:08 -0700 Subject: [PATCH 1/4] connection cleanup --- codex-rs/analytics/src/analytics_client.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client.rs b/codex-rs/analytics/src/analytics_client.rs index eadcfe876f..d9edc200d0 100644 --- a/codex-rs/analytics/src/analytics_client.rs +++ b/codex-rs/analytics/src/analytics_client.rs @@ -95,10 +95,7 @@ pub enum AnalyticsFact { connection_id: u64, response: Box, }, - Notification { - connection_id: u64, - notification: Box, - }, + Notification(Box), // Facts that do not naturally exist on the app-server protocol surface, or // would require non-trivial protocol reshaping on this branch. Custom(CustomAnalyticsFact), @@ -470,10 +467,7 @@ impl AnalyticsReducer { } => { self.ingest_response(connection_id, *response, out); } - AnalyticsFact::Notification { - connection_id: _connection_id, - notification: _notification, - } => {} + AnalyticsFact::Notification(_notification) => {} AnalyticsFact::Custom(input) => match input { CustomAnalyticsFact::SkillInvoked(input) => { self.ingest_skill_invoked(input, out).await; From a1e506f17d9f8d6147fb78b728591f656a129c30 Mon Sep 17 00:00:00 2001 From: Roy Han Date: Fri, 27 Mar 2026 10:43:05 -0700 Subject: [PATCH 2/4] rename client to connection --- codex-rs/analytics/src/analytics_client.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client.rs b/codex-rs/analytics/src/analytics_client.rs index d9edc200d0..32b91205a8 100644 --- a/codex-rs/analytics/src/analytics_client.rs +++ b/codex-rs/analytics/src/analytics_client.rs @@ -144,10 +144,10 @@ pub enum PluginState { #[derive(Default)] pub struct AnalyticsReducer { - clients: HashMap, + connections: HashMap, } -struct ClientState { +struct ConnectionState { product_client_id: String, } @@ -489,9 +489,9 @@ impl AnalyticsReducer { } fn ingest_initialize(&mut self, connection_id: u64, params: InitializeParams) { - self.clients.insert( + self.connections.insert( connection_id, - ClientState { + ConnectionState { product_client_id: params.client_info.name, }, ); @@ -502,11 +502,14 @@ impl AnalyticsReducer { input: ThreadInitializedInput, out: &mut Vec, ) { - let Some(client_state) = self.clients.get(&input.connection_id) else { + let Some(connection_state) = self.connections.get(&input.connection_id) else { return; }; out.push(TrackEventRequest::CodexThreadInitialized( - codex_thread_initialized_event_request(client_state.product_client_id.clone(), input), + codex_thread_initialized_event_request( + connection_state.product_client_id.clone(), + input, + ), )); } From 7afe91c1df05add6f86518c6f6443df167579639 Mon Sep 17 00:00:00 2001 From: Roy Han Date: Fri, 27 Mar 2026 11:00:11 -0700 Subject: [PATCH 3/4] add client metadata --- codex-rs/analytics/src/analytics_client.rs | 29 +++++++++++++------ .../analytics/src/analytics_client_tests.rs | 22 ++++++++++++-- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client.rs b/codex-rs/analytics/src/analytics_client.rs index 32b91205a8..b856118899 100644 --- a/codex-rs/analytics/src/analytics_client.rs +++ b/codex-rs/analytics/src/analytics_client.rs @@ -149,6 +149,9 @@ pub struct AnalyticsReducer { struct ConnectionState { product_client_id: String, + client_name: Option, + client_version: Option, + experimental_api_enabled: Option, } #[derive(Clone)] @@ -377,6 +380,9 @@ struct SkillInvocationEventParams { struct CodexThreadInitializedEventParams { thread_id: String, product_client_id: String, + client_name: Option, + client_version: Option, + experimental_api_enabled: Option, model: String, ephemeral: bool, session_source: Option<&'static str>, @@ -492,7 +498,12 @@ impl AnalyticsReducer { self.connections.insert( connection_id, ConnectionState { - product_client_id: params.client_info.name, + product_client_id: params.client_info.name.clone(), + client_name: Some(params.client_info.name), + client_version: Some(params.client_info.version), + experimental_api_enabled: params + .capabilities + .map(|capabilities| capabilities.experimental_api), }, ); } @@ -506,10 +517,7 @@ impl AnalyticsReducer { return; }; out.push(TrackEventRequest::CodexThreadInitialized( - codex_thread_initialized_event_request( - connection_state.product_client_id.clone(), - input, - ), + codex_thread_initialized_event_request(connection_state, input), )); } @@ -666,22 +674,25 @@ fn codex_app_metadata(tracking: &TrackEventsContext, app: AppInvocation) -> Code } fn codex_thread_initialized_event_request( - product_client_id: String, + connection_state: &ConnectionState, input: ThreadInitializedInput, ) -> CodexThreadInitializedEvent { CodexThreadInitializedEvent { event_type: "codex_thread_initialized", - event_params: codex_thread_initialized_event_params(product_client_id, input), + event_params: codex_thread_initialized_event_params(connection_state, input), } } fn codex_thread_initialized_event_params( - product_client_id: String, + connection_state: &ConnectionState, input: ThreadInitializedInput, ) -> CodexThreadInitializedEventParams { CodexThreadInitializedEventParams { thread_id: input.thread_id, - product_client_id, + product_client_id: connection_state.product_client_id.clone(), + client_name: connection_state.client_name.clone(), + client_version: connection_state.client_version.clone(), + experimental_api_enabled: connection_state.experimental_api_enabled, model: input.model, ephemeral: input.ephemeral, session_source: session_source_name(&input.session_source), diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 6156015bef..431453ca51 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -20,6 +20,7 @@ use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer; use codex_app_server_protocol::AskForApproval as AppServerAskForApproval; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::ClientResponse; +use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::InitializeParams; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy; @@ -262,7 +263,12 @@ fn app_used_dedupe_is_keyed_by_turn_and_connector() { #[test] fn thread_initialized_event_serializes_expected_shape() { let event = TrackEventRequest::CodexThreadInitialized(codex_thread_initialized_event_request( - "codex-tui".to_string(), + &super::ConnectionState { + product_client_id: "codex-tui".to_string(), + client_name: Some("codex-tui".to_string()), + client_version: Some("1.0.0".to_string()), + experimental_api_enabled: Some(true), + }, ThreadInitializedInput { connection_id: 1, thread_id: "thread-0".to_string(), @@ -282,6 +288,9 @@ fn thread_initialized_event_serializes_expected_shape() { "event_params": { "thread_id": "thread-0", "product_client_id": "codex-tui", + "client_name": "codex-tui", + "client_version": "1.0.0", + "experimental_api_enabled": true, "model": "gpt-5", "ephemeral": true, "session_source": "user", @@ -325,7 +334,10 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize title: None, version: "1.0.0".to_string(), }, - capabilities: None, + capabilities: Some(InitializeCapabilities { + experimental_api: false, + opt_out_notification_methods: None, + }), }, }, &mut events, @@ -347,6 +359,12 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize assert_eq!(payload.as_array().expect("events array").len(), 1); assert_eq!(payload[0]["event_type"], "codex_thread_initialized"); assert_eq!(payload[0]["event_params"]["product_client_id"], "codex-tui"); + assert_eq!(payload[0]["event_params"]["client_name"], "codex-tui"); + assert_eq!(payload[0]["event_params"]["client_version"], "1.0.0"); + assert_eq!( + payload[0]["event_params"]["experimental_api_enabled"], + false + ); assert_eq!(payload[0]["event_params"]["initialization_mode"], "resumed"); assert_eq!(payload[0]["event_params"]["session_source"], "user"); assert_eq!(payload[0]["event_params"]["subagent_source"], json!(null)); From ed327248a25fd2175f7b48a63d3f46c758c221bb Mon Sep 17 00:00:00 2001 From: Roy Han Date: Fri, 27 Mar 2026 11:15:51 -0700 Subject: [PATCH 4/4] various fixes --- codex-rs/analytics/src/analytics_client.rs | 84 ++++++++----------- .../analytics/src/analytics_client_tests.rs | 8 +- 2 files changed, 41 insertions(+), 51 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client.rs b/codex-rs/analytics/src/analytics_client.rs index b856118899..275073c92f 100644 --- a/codex-rs/analytics/src/analytics_client.rs +++ b/codex-rs/analytics/src/analytics_client.rs @@ -38,6 +38,7 @@ struct ThreadInitializedInput { pub ephemeral: bool, pub session_source: SessionSource, pub initialization_mode: InitializationMode, + pub created_at: u64, } #[derive(Clone, Copy, Debug, Serialize)] @@ -348,7 +349,7 @@ struct TrackEventsRequest { #[serde(untagged)] enum TrackEventRequest { SkillInvocation(SkillInvocationEventRequest), - CodexThreadInitialized(CodexThreadInitializedEvent), + ThreadInitialized(ThreadInitializedEvent), AppMentioned(CodexAppMentionedEventRequest), AppUsed(CodexAppUsedEventRequest), PluginUsed(CodexPluginUsedEventRequest), @@ -377,7 +378,7 @@ struct SkillInvocationEventParams { } #[derive(Serialize)] -struct CodexThreadInitializedEventParams { +struct ThreadInitializedEventParams { thread_id: String, product_client_id: String, client_name: Option, @@ -393,9 +394,9 @@ struct CodexThreadInitializedEventParams { } #[derive(Serialize)] -struct CodexThreadInitializedEvent { +struct ThreadInitializedEvent { event_type: &'static str, - event_params: CodexThreadInitializedEventParams, + event_params: ThreadInitializedEventParams, } #[derive(Serialize)] @@ -516,8 +517,8 @@ impl AnalyticsReducer { let Some(connection_state) = self.connections.get(&input.connection_id) else { return; }; - out.push(TrackEventRequest::CodexThreadInitialized( - codex_thread_initialized_event_request(connection_state, input), + out.push(TrackEventRequest::ThreadInitialized( + thread_initialized_event_request(connection_state, input), )); } @@ -527,34 +528,30 @@ impl AnalyticsReducer { response: ClientResponse, out: &mut Vec, ) { - let input = match response { - ClientResponse::ThreadStart { response, .. } => ThreadInitializedInput { - connection_id, - thread_id: response.thread.id, - model: response.model, - ephemeral: response.thread.ephemeral, - session_source: response.thread.source.into(), - initialization_mode: InitializationMode::New, - }, - ClientResponse::ThreadResume { response, .. } => ThreadInitializedInput { - connection_id, - thread_id: response.thread.id, - model: response.model, - ephemeral: response.thread.ephemeral, - session_source: response.thread.source.into(), - initialization_mode: InitializationMode::Resumed, - }, - ClientResponse::ThreadFork { response, .. } => ThreadInitializedInput { - connection_id, - thread_id: response.thread.id, - model: response.model, - ephemeral: response.thread.ephemeral, - session_source: response.thread.source.into(), - initialization_mode: InitializationMode::Forked, - }, + let (thread, model, initialization_mode) = match response { + ClientResponse::ThreadStart { response, .. } => { + (response.thread, response.model, InitializationMode::New) + } + ClientResponse::ThreadResume { response, .. } => { + (response.thread, response.model, InitializationMode::Resumed) + } + ClientResponse::ThreadFork { response, .. } => { + (response.thread, response.model, InitializationMode::Forked) + } _ => return, }; - self.ingest_thread_initialized(input, out); + self.ingest_thread_initialized( + ThreadInitializedInput { + connection_id, + thread_id: thread.id, + model, + ephemeral: thread.ephemeral, + session_source: thread.source.into(), + initialization_mode, + created_at: u64::try_from(thread.created_at).unwrap_or_default(), + }, + out, + ); } async fn ingest_skill_invoked( @@ -673,21 +670,21 @@ fn codex_app_metadata(tracking: &TrackEventsContext, app: AppInvocation) -> Code } } -fn codex_thread_initialized_event_request( +fn thread_initialized_event_request( connection_state: &ConnectionState, input: ThreadInitializedInput, -) -> CodexThreadInitializedEvent { - CodexThreadInitializedEvent { +) -> ThreadInitializedEvent { + ThreadInitializedEvent { event_type: "codex_thread_initialized", - event_params: codex_thread_initialized_event_params(connection_state, input), + event_params: thread_initialized_event_params(connection_state, input), } } -fn codex_thread_initialized_event_params( +fn thread_initialized_event_params( connection_state: &ConnectionState, input: ThreadInitializedInput, -) -> CodexThreadInitializedEventParams { - CodexThreadInitializedEventParams { +) -> ThreadInitializedEventParams { + ThreadInitializedEventParams { thread_id: input.thread_id, product_client_id: connection_state.product_client_id.clone(), client_name: connection_state.client_name.clone(), @@ -699,7 +696,7 @@ fn codex_thread_initialized_event_params( initialization_mode: input.initialization_mode, subagent_source: None, parent_thread_id: None, - created_at: now_unix_timestamp_secs(), + created_at: input.created_at, } } @@ -748,13 +745,6 @@ fn session_source_name(session_source: &SessionSource) -> Option<&'static str> { } } -fn now_unix_timestamp_secs() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() -} - async fn send_track_events( auth_manager: &AuthManager, base_url: &str, diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 431453ca51..6689ba5e79 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -14,8 +14,8 @@ use super::TrackEventsContext; use super::codex_app_metadata; use super::codex_plugin_metadata; use super::codex_plugin_used_metadata; -use super::codex_thread_initialized_event_request; use super::normalize_path_for_skill_id; +use super::thread_initialized_event_request; use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer; use codex_app_server_protocol::AskForApproval as AppServerAskForApproval; use codex_app_server_protocol::ClientInfo; @@ -262,7 +262,7 @@ fn app_used_dedupe_is_keyed_by_turn_and_connector() { #[test] fn thread_initialized_event_serializes_expected_shape() { - let event = TrackEventRequest::CodexThreadInitialized(codex_thread_initialized_event_request( + let event = TrackEventRequest::ThreadInitialized(thread_initialized_event_request( &super::ConnectionState { product_client_id: "codex-tui".to_string(), client_name: Some("codex-tui".to_string()), @@ -276,6 +276,7 @@ fn thread_initialized_event_serializes_expected_shape() { ephemeral: true, session_source: SessionSource::Exec, initialization_mode: InitializationMode::New, + created_at: 1, }, )); @@ -297,11 +298,10 @@ fn thread_initialized_event_serializes_expected_shape() { "initialization_mode": "new", "subagent_source": null, "parent_thread_id": null, - "created_at": payload["event_params"]["created_at"] + "created_at": 1 } }) ); - assert!(payload["event_params"]["created_at"].as_u64().is_some()); } #[tokio::test]