diff --git a/codex-rs/analytics/src/analytics_client.rs b/codex-rs/analytics/src/analytics_client.rs index 8431ffba5f..f75b455197 100644 --- a/codex-rs/analytics/src/analytics_client.rs +++ b/codex-rs/analytics/src/analytics_client.rs @@ -62,8 +62,34 @@ pub struct CodexThreadInitializedEvent { pub created_at: u64, } -#[derive(Clone, Copy)] -pub struct CodexTurnSteerEvent; +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TurnSteerResult { + Accepted, + Rejected, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TurnSteerRejectionReason { + NoActiveTurn, + ExpectedTurnMismatch, + NonSteerableReview, + NonSteerableCompact, + EmptyInput, + InputTooLarge, + InternalError, +} + +#[derive(Clone)] +pub struct CodexTurnSteerEvent { + pub expected_turn_id: String, + pub accepted_turn_id: Option, + pub num_input_images: usize, + pub result: TurnSteerResult, + pub rejection_reason: Option, + pub created_at: u64, +} #[derive(Clone)] pub struct CodexThreadInitializedInput { @@ -484,9 +510,13 @@ struct CodexTurnEventRequest { #[derive(Serialize)] struct CodexTurnSteerEventParams { thread_id: String, - turn_id: String, + expected_turn_id: String, + accepted_turn_id: Option, product_client_id: Option, - model: Option, + num_input_images: usize, + result: TurnSteerResult, + rejection_reason: Option, + created_at: u64, } #[derive(Serialize)] @@ -590,7 +620,7 @@ impl AnalyticsReducer { turn_event, } = input; out.push(TrackEventRequest::TurnEvent(CodexTurnEventRequest { - event_type: "codex_turn_event", + event_type: "codex_turn_steer_event", event_params: codex_turn_event_params(&tracking, turn_event), })); } @@ -750,13 +780,17 @@ fn codex_turn_event_params( fn codex_turn_steer_event_params( tracking: &TrackEventsContext, - _turn_steer: CodexTurnSteerEvent, + turn_steer: CodexTurnSteerEvent, ) -> CodexTurnSteerEventParams { CodexTurnSteerEventParams { thread_id: tracking.thread_id.clone(), - turn_id: tracking.turn_id.clone(), + expected_turn_id: turn_steer.expected_turn_id, + accepted_turn_id: turn_steer.accepted_turn_id, product_client_id: Some(originator().value), - model: Some(tracking.model_slug.clone()), + num_input_images: turn_steer.num_input_images, + result: turn_steer.result, + rejection_reason: turn_steer.rejection_reason, + created_at: turn_steer.created_at, } } diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index c0d45c840c..111f34ee2e 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -13,6 +13,7 @@ use super::InitializationMode; use super::InvocationType; use super::TrackEventRequest; use super::TrackEventsContext; +use super::TurnSteerResult; use super::codex_app_metadata; use super::codex_plugin_metadata; use super::codex_plugin_used_metadata; @@ -269,8 +270,18 @@ fn turn_steer_event_serializes_expected_shape() { turn_id: "turn-2".to_string(), }; let event = TrackEventRequest::TurnSteer(CodexTurnSteerEventRequest { - event_type: "codex_turn_event", - event_params: codex_turn_steer_event_params(&tracking, CodexTurnSteerEvent), + event_type: "codex_turn_steer_event", + event_params: codex_turn_steer_event_params( + &tracking, + CodexTurnSteerEvent { + expected_turn_id: "turn-2".to_string(), + accepted_turn_id: Some("turn-2".to_string()), + num_input_images: 2, + result: TurnSteerResult::Accepted, + rejection_reason: None, + created_at: 1_716_000_123, + }, + ), }); let payload = serde_json::to_value(&event).expect("serialize turn steer event"); @@ -278,12 +289,16 @@ fn turn_steer_event_serializes_expected_shape() { assert_eq!( payload, json!({ - "event_type": "codex_turn_event", + "event_type": "codex_turn_steer_event", "event_params": { "thread_id": "thread-2", - "turn_id": "turn-2", + "expected_turn_id": "turn-2", + "accepted_turn_id": "turn-2", "product_client_id": originator().value, - "model": "gpt-5" + "num_input_images": 2, + "result": "accepted", + "rejection_reason": null, + "created_at": 1_716_000_123 } }) ); diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index cfbe7a258b..0e2b23d795 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -19,4 +19,6 @@ pub use analytics_client::PluginUsedInput; pub use analytics_client::SkillInvocation; pub use analytics_client::SkillInvokedInput; pub use analytics_client::TrackEventsContext; +pub use analytics_client::TurnSteerRejectionReason; +pub use analytics_client::TurnSteerResult; pub use analytics_client::build_track_events_context; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index b059b50166..631f4c741f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -357,6 +357,7 @@ use codex_analytics::CodexTurnEvent; use codex_analytics::CodexTurnSteerEvent; use codex_analytics::InitializationMode; use codex_analytics::InvocationType; +use codex_analytics::TurnSteerResult; use codex_analytics::build_track_events_context; use codex_async_utils::OrCancelExt; use codex_git_utils::get_git_repo_root; @@ -3970,6 +3971,11 @@ impl Session { return Err(SteerInputError::EmptyInput); } + let num_input_images = input + .iter() + .filter(|item| matches!(item, UserInput::Image { .. })) + .count(); + let mut active = self.active_turn.lock().await; let Some(active_turn) = active.as_mut() else { return Err(SteerInputError::NoActiveTurn(input)); @@ -4013,9 +4019,23 @@ impl Session { turn_state.push_pending_input(input.into()); drop(turn_state); drop(active); - self.services - .analytics_events_client - .track_turn_steer(tracking, CodexTurnSteerEvent); + let expected_turn_id = expected_turn_id + .map(str::to_string) + .unwrap_or_else(|| active_turn_id.clone()); + self.services.analytics_events_client.track_turn_steer( + tracking, + CodexTurnSteerEvent { + expected_turn_id, + accepted_turn_id: Some(active_turn_id.clone()), + num_input_images, + result: TurnSteerResult::Accepted, + rejection_reason: None, + created_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + }, + ); Ok(active_turn_id) }