diff --git a/codex-rs/core/src/analytics_client.rs b/codex-rs/core/src/analytics_client.rs index 3c24989001..93510dfe93 100644 --- a/codex-rs/core/src/analytics_client.rs +++ b/codex-rs/core/src/analytics_client.rs @@ -55,6 +55,13 @@ pub(crate) enum TurnType { PromptQueued, } +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum TurnEventType { + Start, + Steer, +} + #[derive(Clone, Copy)] pub(crate) struct UserMessageMetadata { pub(crate) user_message_type: UserMessageType, @@ -121,11 +128,11 @@ impl AnalyticsEventsQueue { TrackEventsJob::AppUsed(job) => { send_track_app_used(&auth_manager, job).await; } - TrackEventsJob::TurnMetadata(job) => { - send_track_turn_metadata(&auth_manager, job).await; + TrackEventsJob::TurnStart(job) => { + send_track_turn_start(&auth_manager, job).await; } - TrackEventsJob::InputMessageMetadata(job) => { - send_track_input_message_metadata(&auth_manager, job).await; + TrackEventsJob::TurnSteer(job) => { + send_track_turn_steer(&auth_manager, job).await; } TrackEventsJob::PluginUsed(job) => { send_track_plugin_used(&auth_manager, job).await; @@ -240,29 +247,25 @@ impl AnalyticsEventsClient { ); } - pub(crate) fn track_turn_metadata( - &self, - tracking: TrackEventsContext, - turn_metadata: TurnMetadata, - ) { - track_turn_metadata( + pub(crate) fn track_turn_start(&self, tracking: TrackEventsContext, turn_start: TurnMetadata) { + track_turn_start( &self.queue, Arc::clone(&self.config), Some(tracking), - turn_metadata, + turn_start, ); } - pub(crate) fn track_input_message_metadata( + pub(crate) fn track_turn_steer( &self, tracking: TrackEventsContext, - input_message_metadata: UserMessageMetadata, + turn_steer: UserMessageMetadata, ) { - track_input_message_metadata( + track_turn_steer( &self.queue, Arc::clone(&self.config), Some(tracking), - input_message_metadata, + turn_steer, ); } @@ -307,8 +310,8 @@ enum TrackEventsJob { SkillInvocations(TrackSkillInvocationsJob), AppMentioned(TrackAppMentionedJob), AppUsed(TrackAppUsedJob), - TurnMetadata(TrackTurnMetadataJob), - InputMessageMetadata(TrackInputMessageMetadataJob), + TurnStart(TrackTurnStartJob), + TurnSteer(TrackTurnSteerJob), PluginUsed(TrackPluginUsedJob), PluginInstalled(TrackPluginManagementJob), PluginUninstalled(TrackPluginManagementJob), @@ -334,16 +337,16 @@ struct TrackAppUsedJob { app: AppInvocation, } -struct TrackTurnMetadataJob { +struct TrackTurnStartJob { config: Arc, tracking: TrackEventsContext, - turn_metadata: TurnMetadata, + turn_start: TurnMetadata, } -struct TrackInputMessageMetadataJob { +struct TrackTurnSteerJob { config: Arc, tracking: TrackEventsContext, - input_message_metadata: UserMessageMetadata, + turn_steer: UserMessageMetadata, } struct TrackPluginUsedJob { @@ -380,8 +383,7 @@ enum TrackEventRequest { SkillInvocation(SkillInvocationEventRequest), AppMentioned(CodexAppMentionedEventRequest), AppUsed(CodexAppUsedEventRequest), - TurnMetadata(CodexTurnMetadataEventRequest), - InputMessageMetadata(CodexInputMessageMetadataEventRequest), + TurnEvent(CodexTurnEventRequest), PluginUsed(CodexPluginUsedEventRequest), PluginInstalled(CodexPluginEventRequest), PluginUninstalled(CodexPluginEventRequest), @@ -431,44 +433,31 @@ struct CodexAppUsedEventRequest { } #[derive(Serialize)] -struct CodexTurnMetadata { - thread_id: String, - turn_id: String, - product_client_id: Option, - model: Option, - turn_type: Option, - model_provider: String, - sandbox_policy: Option<&'static str>, - reasoning_effort: Option, - reasoning_summary: Option, - service_tier: String, - approval_policy: String, - approvals_reviewer: String, - sandbox_network_access: bool, - collaboration_mode: Option<&'static str>, - personality: Option, - num_input_images: usize, -} - -#[derive(Serialize)] -struct CodexTurnMetadataEventRequest { - event_type: &'static str, - event_params: CodexTurnMetadata, -} - -#[derive(Serialize)] -struct CodexInputMessageMetadata { +struct CodexTurnEvent { thread_id: Option, turn_id: Option, product_client_id: Option, model_slug: Option, + turn_event_type: TurnEventType, + turn_type: Option, user_message_type: Option, + model_provider: Option, + sandbox_policy: Option<&'static str>, + reasoning_effort: Option, + reasoning_summary: Option, + service_tier: Option, + approval_policy: Option, + approvals_reviewer: Option, + sandbox_network_access: Option, + collaboration_mode: Option<&'static str>, + personality: Option, + num_input_images: Option, } #[derive(Serialize)] -struct CodexInputMessageMetadataEventRequest { +struct CodexTurnEventRequest { event_type: &'static str, - event_params: CodexInputMessageMetadata, + event_params: CodexTurnEvent, } #[derive(Serialize)] @@ -572,11 +561,11 @@ pub(crate) fn track_app_used( queue.try_send(job); } -pub(crate) fn track_turn_metadata( +pub(crate) fn track_turn_start( queue: &AnalyticsEventsQueue, config: Arc, tracking: Option, - turn_metadata: TurnMetadata, + turn_start: TurnMetadata, ) { if config.analytics_enabled == Some(false) { return; @@ -584,19 +573,19 @@ pub(crate) fn track_turn_metadata( let Some(tracking) = tracking else { return; }; - let job = TrackEventsJob::TurnMetadata(TrackTurnMetadataJob { + let job = TrackEventsJob::TurnStart(TrackTurnStartJob { config, tracking, - turn_metadata, + turn_start, }); queue.try_send(job); } -pub(crate) fn track_input_message_metadata( +pub(crate) fn track_turn_steer( queue: &AnalyticsEventsQueue, config: Arc, tracking: Option, - input_message_metadata: UserMessageMetadata, + turn_steer: UserMessageMetadata, ) { if config.analytics_enabled == Some(false) { return; @@ -604,10 +593,10 @@ pub(crate) fn track_input_message_metadata( let Some(tracking) = tracking else { return; }; - let job = TrackEventsJob::InputMessageMetadata(TrackInputMessageMetadataJob { + let job = TrackEventsJob::TurnSteer(TrackTurnSteerJob { config, tracking, - input_message_metadata, + turn_steer, }); queue.try_send(job); } @@ -737,37 +726,30 @@ async fn send_track_app_used(auth_manager: &AuthManager, job: TrackAppUsedJob) { send_track_events(auth_manager, config, events).await; } -async fn send_track_turn_metadata(auth_manager: &AuthManager, job: TrackTurnMetadataJob) { - let TrackTurnMetadataJob { +async fn send_track_turn_start(auth_manager: &AuthManager, job: TrackTurnStartJob) { + let TrackTurnStartJob { config, tracking, - turn_metadata, + turn_start, } = job; - let events = vec![TrackEventRequest::TurnMetadata( - CodexTurnMetadataEventRequest { - event_type: "codex_turn_metadata", - event_params: codex_turn_metadata(&tracking, turn_metadata), - }, - )]; + let events = vec![TrackEventRequest::TurnEvent(CodexTurnEventRequest { + event_type: "codex_turn_event", + event_params: codex_turn_start_event(&tracking, turn_start), + })]; send_track_events(auth_manager, config, events).await; } -async fn send_track_input_message_metadata( - auth_manager: &AuthManager, - job: TrackInputMessageMetadataJob, -) { - let TrackInputMessageMetadataJob { +async fn send_track_turn_steer(auth_manager: &AuthManager, job: TrackTurnSteerJob) { + let TrackTurnSteerJob { config, tracking, - input_message_metadata, + turn_steer, } = job; - let events = vec![TrackEventRequest::InputMessageMetadata( - CodexInputMessageMetadataEventRequest { - event_type: "codex_input_message_metadata", - event_params: codex_input_message_metadata(&tracking, input_message_metadata), - }, - )]; + let events = vec![TrackEventRequest::TurnEvent(CodexTurnEventRequest { + event_type: "codex_turn_event", + event_params: codex_turn_steer_event(&tracking, turn_steer), + })]; send_track_events(auth_manager, config, events).await; } @@ -836,45 +818,60 @@ fn codex_app_metadata(tracking: &TrackEventsContext, app: AppInvocation) -> Code } } -fn codex_turn_metadata( +fn codex_turn_start_event( tracking: &TrackEventsContext, - turn_metadata: TurnMetadata, -) -> CodexTurnMetadata { - CodexTurnMetadata { - thread_id: tracking.thread_id.clone(), - turn_id: tracking.turn_id.clone(), - product_client_id: Some(crate::default_client::originator().value), - model: Some(tracking.model_slug.clone()), - turn_type: turn_metadata.turn_type, - model_provider: turn_metadata.model_provider, - sandbox_policy: Some(sandbox_policy_mode(&turn_metadata.sandbox_policy)), - reasoning_effort: turn_metadata - .reasoning_effort - .map(|value| value.to_string()), - reasoning_summary: reasoning_summary_mode(turn_metadata.reasoning_summary), - service_tier: turn_metadata - .service_tier - .map(|value| value.to_string()) - .unwrap_or_else(|| "default".to_string()), - approval_policy: turn_metadata.approval_policy.to_string(), - approvals_reviewer: turn_metadata.approvals_reviewer.to_string(), - sandbox_network_access: turn_metadata.sandbox_network_access, - collaboration_mode: Some(collaboration_mode_mode(turn_metadata.collaboration_mode)), - personality: personality_mode(turn_metadata.personality), - num_input_images: turn_metadata.num_input_images, - } -} - -fn codex_input_message_metadata( - tracking: &TrackEventsContext, - input_message_metadata: UserMessageMetadata, -) -> CodexInputMessageMetadata { - CodexInputMessageMetadata { + turn_start: TurnMetadata, +) -> CodexTurnEvent { + CodexTurnEvent { thread_id: Some(tracking.thread_id.clone()), turn_id: Some(tracking.turn_id.clone()), product_client_id: Some(crate::default_client::originator().value), model_slug: Some(tracking.model_slug.clone()), - user_message_type: Some(input_message_metadata.user_message_type), + turn_event_type: TurnEventType::Start, + turn_type: turn_start.turn_type, + user_message_type: None, + model_provider: Some(turn_start.model_provider), + sandbox_policy: Some(sandbox_policy_mode(&turn_start.sandbox_policy)), + reasoning_effort: turn_start.reasoning_effort.map(|value| value.to_string()), + reasoning_summary: reasoning_summary_mode(turn_start.reasoning_summary), + service_tier: Some( + turn_start + .service_tier + .map(|value| value.to_string()) + .unwrap_or_else(|| "default".to_string()), + ), + approval_policy: Some(turn_start.approval_policy.to_string()), + approvals_reviewer: Some(turn_start.approvals_reviewer.to_string()), + sandbox_network_access: Some(turn_start.sandbox_network_access), + collaboration_mode: Some(collaboration_mode_mode(turn_start.collaboration_mode)), + personality: personality_mode(turn_start.personality), + num_input_images: Some(turn_start.num_input_images), + } +} + +fn codex_turn_steer_event( + tracking: &TrackEventsContext, + turn_steer: UserMessageMetadata, +) -> CodexTurnEvent { + CodexTurnEvent { + thread_id: Some(tracking.thread_id.clone()), + turn_id: Some(tracking.turn_id.clone()), + product_client_id: Some(crate::default_client::originator().value), + model_slug: Some(tracking.model_slug.clone()), + turn_event_type: TurnEventType::Steer, + turn_type: None, + user_message_type: Some(turn_steer.user_message_type), + model_provider: None, + sandbox_policy: None, + reasoning_effort: None, + reasoning_summary: None, + service_tier: None, + approval_policy: None, + approvals_reviewer: None, + sandbox_network_access: None, + collaboration_mode: None, + personality: None, + num_input_images: None, } } diff --git a/codex-rs/core/src/analytics_client_tests.rs b/codex-rs/core/src/analytics_client_tests.rs index 49fabe04a5..f31fcd21cd 100644 --- a/codex-rs/core/src/analytics_client_tests.rs +++ b/codex-rs/core/src/analytics_client_tests.rs @@ -2,22 +2,22 @@ use super::AnalyticsEventsQueue; use super::AppInvocation; use super::CodexAppMentionedEventRequest; use super::CodexAppUsedEventRequest; -use super::CodexInputMessageMetadataEventRequest; use super::CodexPluginEventRequest; use super::CodexPluginUsedEventRequest; -use super::CodexTurnMetadataEventRequest; +use super::CodexTurnEventRequest; use super::InvocationType; use super::TrackEventRequest; use super::TrackEventsContext; +use super::TurnEventType; use super::TurnMetadata; use super::TurnType; use super::UserMessageMetadata; use super::UserMessageType; use super::codex_app_metadata; -use super::codex_input_message_metadata; use super::codex_plugin_metadata; use super::codex_plugin_used_metadata; -use super::codex_turn_metadata; +use super::codex_turn_start_event; +use super::codex_turn_steer_event; use super::normalize_path_for_skill_id; use crate::plugins::AppConnectorId; use crate::plugins::PluginCapabilitySummary; @@ -202,15 +202,15 @@ fn app_used_dedupe_is_keyed_by_turn_and_connector() { } #[test] -fn turn_metadata_event_serializes_expected_shape() { +fn turn_start_event_serializes_expected_shape() { let tracking = TrackEventsContext { model_slug: "gpt-5".to_string(), thread_id: "thread-2".to_string(), turn_id: "turn-2".to_string(), }; - let event = TrackEventRequest::TurnMetadata(CodexTurnMetadataEventRequest { - event_type: "codex_turn_metadata", - event_params: codex_turn_metadata( + let event = TrackEventRequest::TurnEvent(CodexTurnEventRequest { + event_type: "codex_turn_event", + event_params: codex_turn_start_event( &tracking, TurnMetadata { model_provider: "openai".to_string(), @@ -234,13 +234,15 @@ fn turn_metadata_event_serializes_expected_shape() { assert_eq!( payload, json!({ - "event_type": "codex_turn_metadata", + "event_type": "codex_turn_event", "event_params": { "thread_id": "thread-2", "turn_id": "turn-2", "product_client_id": crate::default_client::originator().value, - "model": "gpt-5", + "model_slug": "gpt-5", + "turn_event_type": "start", "turn_type": "prompt", + "user_message_type": null, "model_provider": "openai", "sandbox_policy": "read_only", "reasoning_effort": "high", @@ -258,15 +260,15 @@ fn turn_metadata_event_serializes_expected_shape() { } #[test] -fn input_message_metadata_event_serializes_expected_shape() { +fn turn_steer_event_serializes_expected_shape() { let tracking = TrackEventsContext { model_slug: "gpt-5".to_string(), thread_id: "thread-2".to_string(), turn_id: "turn-2".to_string(), }; - let event = TrackEventRequest::InputMessageMetadata(CodexInputMessageMetadataEventRequest { - event_type: "codex_input_message_metadata", - event_params: codex_input_message_metadata( + let event = TrackEventRequest::TurnEvent(CodexTurnEventRequest { + event_type: "codex_turn_event", + event_params: codex_turn_steer_event( &tracking, UserMessageMetadata { user_message_type: UserMessageType::PromptSteering, @@ -279,13 +281,25 @@ fn input_message_metadata_event_serializes_expected_shape() { assert_eq!( payload, json!({ - "event_type": "codex_input_message_metadata", + "event_type": "codex_turn_event", "event_params": { "thread_id": "thread-2", "turn_id": "turn-2", "product_client_id": crate::default_client::originator().value, "model_slug": "gpt-5", - "user_message_type": "prompt_steering" + "turn_event_type": "steer", + "turn_type": null, + "user_message_type": "prompt_steering", + "sandbox_policy": null, + "reasoning_effort": null, + "reasoning_summary": null, + "service_tier": null, + "approval_policy": null, + "approvals_reviewer": null, + "sandbox_network_access": null, + "collaboration_mode": null, + "personality": null, + "num_input_images": null } }) ); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 41d90e1fac..9f107b4e9a 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3938,14 +3938,12 @@ impl Session { drop(turn_state); drop(active); if let Some(tracking) = self.active_turn_tracking().await { - self.services - .analytics_events_client - .track_input_message_metadata( - tracking, - UserMessageMetadata { - user_message_type: SubmittedUserMessageType::PromptSteering, - }, - ); + self.services.analytics_events_client.track_turn_steer( + tracking, + UserMessageMetadata { + user_message_type: SubmittedUserMessageType::PromptSteering, + }, + ); } Ok(active_turn_id) } @@ -5715,7 +5713,7 @@ pub(crate) async fn run_turn( let user_message_type = turn_context .user_message_type .unwrap_or(SubmittedUserMessageType::Prompt); - sess.services.analytics_events_client.track_turn_metadata( + sess.services.analytics_events_client.track_turn_start( tracking.clone(), TurnMetadata { model_provider: turn_context.config.model_provider_id.clone(), diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index 5b7ba35773..aba5926a8e 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -163,7 +163,7 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn user_turn_tracks_turn_metadata_analytics() -> anyhow::Result<()> { +async fn user_turn_tracks_turn_start_analytics() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -234,11 +234,12 @@ async fn user_turn_tracks_turn_metadata_analytics() -> anyhow::Result<()> { let event = payload["events"] .as_array() .and_then(|events| { - events - .iter() - .find(|event| event["event_type"] == "codex_turn_event") + events.iter().find(|event| { + event["event_type"] == "codex_turn_event" + && event["event_params"]["turn_event_type"] == "start" + }) }) - .expect("codex_turn_event should be present"); + .expect("codex_turn_event start event should be present"); let event_params = &event["event_params"]; @@ -272,7 +273,7 @@ async fn user_turn_tracks_turn_metadata_analytics() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn user_turn_tracks_turn_type_prompt_metadata_analytics() -> anyhow::Result<()> { +async fn user_turn_tracks_turn_start_prompt_type_analytics() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -324,8 +325,9 @@ async fn user_turn_tracks_turn_type_prompt_metadata_analytics() -> anyhow::Resul wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; - let event = wait_for_analytics_event(&server, "codex_turn_metadata", |event| { - event["event_params"]["turn_type"] == "prompt" + let event = wait_for_analytics_event(&server, "codex_turn_event", |event| { + event["event_params"]["turn_event_type"] == "start" + && event["event_params"]["turn_type"] == "prompt" }) .await; let event_params = &event["event_params"]; @@ -343,7 +345,7 @@ async fn user_turn_tracks_turn_type_prompt_metadata_analytics() -> anyhow::Resul } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn user_turn_tracks_input_message_prompt_steering_metadata_analytics() -> anyhow::Result<()> { +async fn user_turn_tracks_turn_steer_analytics() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -438,8 +440,9 @@ async fn user_turn_tracks_input_message_prompt_steering_metadata_analytics() -> wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; - let event = wait_for_analytics_event(&server, "codex_input_message_metadata", |event| { - event["event_params"]["user_message_type"] == "prompt_steering" + let event = wait_for_analytics_event(&server, "codex_turn_event", |event| { + event["event_params"]["turn_event_type"] == "steer" + && event["event_params"]["user_message_type"] == "prompt_steering" }) .await; let event_params = &event["event_params"];