diff --git a/codex-rs/analytics/src/analytics_client.rs b/codex-rs/analytics/src/analytics_client.rs index 903f0b2ac4..ac06d5d6d2 100644 --- a/codex-rs/analytics/src/analytics_client.rs +++ b/codex-rs/analytics/src/analytics_client.rs @@ -44,6 +44,8 @@ pub struct TrackEventsContext { #[derive(Clone)] pub struct TurnResolvedConfigFact { pub turn_id: String, + pub thread_id: String, + pub num_input_images: usize, pub submission_type: Option, pub model: String, pub model_provider: String, @@ -786,6 +788,8 @@ impl AnalyticsReducer { out: &mut Vec, ) { let turn_id = input.turn_id.clone(); + let thread_id = input.thread_id.clone(); + let num_input_images = input.num_input_images; let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState { connection_id: None, thread_id: None, @@ -795,6 +799,8 @@ impl AnalyticsReducer { completed: None, steer_count: 0, }); + turn_state.thread_id = Some(thread_id); + turn_state.num_input_images = Some(num_input_images); turn_state.resolved_config = Some(input); self.maybe_emit_turn_event(&turn_id, out); } @@ -935,12 +941,6 @@ impl AnalyticsReducer { let Some(turn_state) = self.turns.get(turn_id) else { return; }; - let Some(connection_id) = turn_state.connection_id else { - return; - }; - let Some(connection_state) = self.connections.get(&connection_id) else { - return; - }; let Some(thread_id) = turn_state.thread_id.clone() else { return; }; @@ -958,7 +958,11 @@ impl AnalyticsReducer { CodexTurnEventRequest { event_type: "codex_turn_event", event_params: codex_turn_event_params( - connection_state.product_client_id.clone(), + turn_state + .connection_id + .and_then(|connection_id| self.connections.get(&connection_id)) + .map(|connection_state| connection_state.product_client_id.clone()) + .unwrap_or_else(|| originator().value), thread_id, turn_id.to_string(), num_input_images, @@ -1116,6 +1120,8 @@ fn codex_turn_event_params( ) -> CodexTurnEventParams { let TurnResolvedConfigFact { turn_id: _resolved_turn_id, + thread_id: _resolved_thread_id, + num_input_images: _resolved_num_input_images, submission_type, model, model_provider, diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index ab7389477b..7e6418fe69 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -195,6 +195,8 @@ fn sample_turn_completed_notification( fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact { TurnResolvedConfigFact { turn_id: turn_id.to_string(), + thread_id: "thread-2".to_string(), + num_input_images: 1, submission_type: None, model: "gpt-5".to_string(), model_provider: "openai".to_string(), @@ -453,6 +455,8 @@ fn turn_event_serializes_expected_shape() { 2, TurnResolvedConfigFact { turn_id: "turn-2".to_string(), + thread_id: "thread-2".to_string(), + num_input_images: 2, submission_type: None, model: "gpt-5".to_string(), model_provider: "openai".to_string(), @@ -658,6 +662,8 @@ async fn turn_lifecycle_emits_turn_event() { AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new( TurnResolvedConfigFact { turn_id: "turn-2".to_string(), + thread_id: "thread-2".to_string(), + num_input_images: 2, submission_type: None, model: "gpt-5".to_string(), model_provider: "openai".to_string(), diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 980e16e7b2..e4e4518d27 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -5940,6 +5940,13 @@ pub(crate) async fn run_turn( .analytics_events_client .track_turn_resolved_config(TurnResolvedConfigFact { turn_id: tracking.turn_id.clone(), + thread_id: tracking.thread_id.clone(), + num_input_images: input + .iter() + .filter(|item| { + matches!(item, UserInput::Image { .. } | UserInput::LocalImage { .. }) + }) + .count(), submission_type: None, model: turn_context.model_info.slug.clone(), model_provider: turn_context.config.model_provider_id.clone(),