From fb7e1eb6fc23426f570b7ee6890e75456dab4a8c Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Tue, 5 May 2026 11:49:30 -0700 Subject: [PATCH] [codex-analytics] add tool item event schemas (#17089) ## Why Tool analytics need stable, typed payloads before the later lifecycle reducer starts emitting them. Keeping the event schema definitions isolated in their own PR makes the emitted surface reviewable separately from the reducer logic that produces those events. ## What changed - Adds the common tool-item analytics event base plus event payload types for command execution, file changes, MCP calls, dynamic tools, collaboration tools, web search, and image generation. - Extends `TrackEventRequest` with the corresponding tool-item variants. - Adds serialization coverage for the command-execution event shape. ## Verification - `cargo test -p codex-analytics` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/17089). * #18748 * #18747 * #17090 * __->__ #17089 * #20514 --- .../analytics/src/analytics_client_tests.rs | 103 +++++++++ codex-rs/analytics/src/events.rs | 216 ++++++++++++++++++ 2 files changed, 319 insertions(+) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 9e0ff9764..baaaafd75 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -3,12 +3,16 @@ use crate::events::AppServerRpcTransport; use crate::events::CodexAppMentionedEventRequest; use crate::events::CodexAppServerClientMetadata; use crate::events::CodexAppUsedEventRequest; +use crate::events::CodexCommandExecutionEventParams; +use crate::events::CodexCommandExecutionEventRequest; use crate::events::CodexCompactionEventRequest; use crate::events::CodexHookRunEventRequest; use crate::events::CodexPluginEventRequest; use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; +use crate::events::CodexToolItemEventBase; use crate::events::CodexTurnEventRequest; +use crate::events::CommandExecutionSource; use crate::events::GuardianApprovalRequestSource; use crate::events::GuardianReviewDecision; use crate::events::GuardianReviewEventParams; @@ -17,6 +21,8 @@ use crate::events::GuardianReviewTerminalStatus; use crate::events::GuardianReviewedAction; use crate::events::ThreadInitializedEvent; use crate::events::ThreadInitializedEventParams; +use crate::events::ToolItemFinalApprovalOutcome; +use crate::events::ToolItemTerminalStatus; use crate::events::TrackEventRequest; use crate::events::codex_app_metadata; use crate::events::codex_hook_run_metadata; @@ -884,6 +890,103 @@ fn thread_initialized_event_serializes_expected_shape() { ); } +#[test] +fn command_execution_event_serializes_expected_shape() { + let event = TrackEventRequest::CommandExecution(CodexCommandExecutionEventRequest { + event_type: "codex_command_execution_event", + event_params: CodexCommandExecutionEventParams { + base: CodexToolItemEventBase { + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + item_id: "item-1".to_string(), + app_server_client: CodexAppServerClientMetadata { + product_client_id: "codex_tui".to_string(), + client_name: Some("codex-tui".to_string()), + client_version: Some("1.2.3".to_string()), + rpc_transport: AppServerRpcTransport::Websocket, + experimental_api_enabled: Some(true), + }, + runtime: CodexRuntimeMetadata { + codex_rs_version: "0.99.0".to_string(), + runtime_os: "macos".to_string(), + runtime_os_version: "15.3.1".to_string(), + runtime_arch: "aarch64".to_string(), + }, + thread_source: Some("user"), + subagent_source: None, + parent_thread_id: None, + tool_name: "shell".to_string(), + started_at_ms: 123_000, + completed_at_ms: 125_000, + duration_ms: Some(2000), + review_count: 0, + guardian_review_count: 0, + user_review_count: 0, + final_approval_outcome: ToolItemFinalApprovalOutcome::NotNeeded, + terminal_status: ToolItemTerminalStatus::Completed, + failure_kind: None, + requested_additional_permissions: false, + requested_network_access: false, + }, + command_execution_source: CommandExecutionSource::Agent, + exit_code: Some(0), + command_total_action_count: 4, + command_read_action_count: 1, + command_list_files_action_count: 1, + command_search_action_count: 1, + command_unknown_action_count: 1, + }, + }); + + let payload = serde_json::to_value(&event).expect("serialize command execution event"); + assert_eq!( + payload, + json!({ + "event_type": "codex_command_execution_event", + "event_params": { + "thread_id": "thread-1", + "turn_id": "turn-1", + "item_id": "item-1", + "app_server_client": { + "product_client_id": "codex_tui", + "client_name": "codex-tui", + "client_version": "1.2.3", + "rpc_transport": "websocket", + "experimental_api_enabled": true + }, + "runtime": { + "codex_rs_version": "0.99.0", + "runtime_os": "macos", + "runtime_os_version": "15.3.1", + "runtime_arch": "aarch64" + }, + "thread_source": "user", + "subagent_source": null, + "parent_thread_id": null, + "tool_name": "shell", + "started_at_ms": 123000, + "completed_at_ms": 125000, + "duration_ms": 2000, + "review_count": 0, + "guardian_review_count": 0, + "user_review_count": 0, + "final_approval_outcome": "not_needed", + "terminal_status": "completed", + "failure_kind": null, + "requested_additional_permissions": false, + "requested_network_access": false, + "command_execution_source": "agent", + "exit_code": 0, + "command_total_action_count": 4, + "command_read_action_count": 1, + "command_list_files_action_count": 1, + "command_search_action_count": 1, + "command_unknown_action_count": 1 + } + }) + ); +} + #[tokio::test] async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialized() { let mut reducer = AnalyticsReducer::default(); diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 5b606d411..ca8febd1b 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -61,6 +61,20 @@ pub(crate) enum TrackEventRequest { Compaction(Box), TurnEvent(Box), TurnSteer(CodexTurnSteerEventRequest), + #[allow(dead_code)] + CommandExecution(CodexCommandExecutionEventRequest), + #[allow(dead_code)] + FileChange(CodexFileChangeEventRequest), + #[allow(dead_code)] + McpToolCall(CodexMcpToolCallEventRequest), + #[allow(dead_code)] + DynamicToolCall(CodexDynamicToolCallEventRequest), + #[allow(dead_code)] + CollabAgentToolCall(CodexCollabAgentToolCallEventRequest), + #[allow(dead_code)] + WebSearch(CodexWebSearchEventRequest), + #[allow(dead_code)] + ImageGeneration(CodexImageGenerationEventRequest), PluginUsed(CodexPluginUsedEventRequest), PluginInstalled(CodexPluginEventRequest), PluginUninstalled(CodexPluginEventRequest), @@ -386,6 +400,208 @@ pub(crate) struct GuardianReviewEventPayload { pub(crate) guardian_review: GuardianReviewEventParams, } +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ToolItemFinalApprovalOutcome { + Unknown, + NotNeeded, + ConfigAllowed, + PolicyForbidden, + GuardianApproved, + GuardianDenied, + GuardianAborted, + UserApproved, + UserApprovedForSession, + UserDenied, + UserAborted, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ToolItemTerminalStatus { + Completed, + Failed, + Rejected, + Interrupted, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ToolItemFailureKind { + ToolError, + ApprovalDenied, + ApprovalAborted, + SandboxDenied, + PolicyForbidden, +} + +#[derive(Serialize)] +pub(crate) struct CodexToolItemEventBase { + pub(crate) thread_id: String, + pub(crate) turn_id: String, + /// App-server ThreadItem.id. For tool-originated items this generally + /// corresponds to the originating core call_id. + pub(crate) item_id: String, + pub(crate) app_server_client: CodexAppServerClientMetadata, + pub(crate) runtime: CodexRuntimeMetadata, + pub(crate) thread_source: Option<&'static str>, + pub(crate) subagent_source: Option, + pub(crate) parent_thread_id: Option, + pub(crate) tool_name: String, + pub(crate) started_at_ms: u64, + pub(crate) completed_at_ms: u64, + pub(crate) duration_ms: Option, + pub(crate) review_count: u64, + pub(crate) guardian_review_count: u64, + pub(crate) user_review_count: u64, + pub(crate) final_approval_outcome: ToolItemFinalApprovalOutcome, + pub(crate) terminal_status: ToolItemTerminalStatus, + pub(crate) failure_kind: Option, + pub(crate) requested_additional_permissions: bool, + pub(crate) requested_network_access: bool, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum CommandExecutionSource { + Agent, + UserShell, + UnifiedExecStartup, + UnifiedExecInteraction, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum WebSearchActionKind { + Search, + OpenPage, + FindInPage, + Other, +} + +#[derive(Serialize)] +pub(crate) struct CodexCommandExecutionEventParams { + #[serde(flatten)] + pub(crate) base: CodexToolItemEventBase, + pub(crate) command_execution_source: CommandExecutionSource, + pub(crate) exit_code: Option, + pub(crate) command_total_action_count: u64, + pub(crate) command_read_action_count: u64, + pub(crate) command_list_files_action_count: u64, + pub(crate) command_search_action_count: u64, + pub(crate) command_unknown_action_count: u64, +} + +#[derive(Serialize)] +pub(crate) struct CodexCommandExecutionEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexCommandExecutionEventParams, +} + +#[derive(Serialize)] +pub(crate) struct CodexFileChangeEventParams { + #[serde(flatten)] + pub(crate) base: CodexToolItemEventBase, + pub(crate) file_change_count: u64, + pub(crate) file_add_count: u64, + pub(crate) file_update_count: u64, + pub(crate) file_delete_count: u64, + pub(crate) file_move_count: u64, +} + +#[derive(Serialize)] +pub(crate) struct CodexFileChangeEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexFileChangeEventParams, +} + +#[derive(Serialize)] +pub(crate) struct CodexMcpToolCallEventParams { + #[serde(flatten)] + pub(crate) base: CodexToolItemEventBase, + pub(crate) mcp_server_name: String, + pub(crate) mcp_tool_name: String, + pub(crate) mcp_error_present: bool, +} + +#[derive(Serialize)] +pub(crate) struct CodexMcpToolCallEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexMcpToolCallEventParams, +} + +#[derive(Serialize)] +pub(crate) struct CodexDynamicToolCallEventParams { + #[serde(flatten)] + pub(crate) base: CodexToolItemEventBase, + pub(crate) dynamic_tool_name: String, + pub(crate) success: Option, + pub(crate) output_content_item_count: Option, + pub(crate) output_text_item_count: Option, + pub(crate) output_image_item_count: Option, +} + +#[derive(Serialize)] +pub(crate) struct CodexDynamicToolCallEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexDynamicToolCallEventParams, +} + +#[derive(Serialize)] +pub(crate) struct CodexCollabAgentToolCallEventParams { + #[serde(flatten)] + pub(crate) base: CodexToolItemEventBase, + pub(crate) sender_thread_id: String, + pub(crate) receiver_thread_count: u64, + pub(crate) receiver_thread_ids: Option>, + pub(crate) requested_model: Option, + pub(crate) requested_reasoning_effort: Option, + pub(crate) agent_state_count: Option, + pub(crate) completed_agent_count: Option, + pub(crate) failed_agent_count: Option, +} + +#[derive(Serialize)] +pub(crate) struct CodexCollabAgentToolCallEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexCollabAgentToolCallEventParams, +} + +#[derive(Serialize)] +pub(crate) struct CodexWebSearchEventParams { + #[serde(flatten)] + pub(crate) base: CodexToolItemEventBase, + pub(crate) web_search_action: Option, + pub(crate) query_present: bool, + pub(crate) query_count: Option, +} + +#[derive(Serialize)] +pub(crate) struct CodexWebSearchEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexWebSearchEventParams, +} + +#[derive(Serialize)] +pub(crate) struct CodexImageGenerationEventParams { + #[serde(flatten)] + pub(crate) base: CodexToolItemEventBase, + pub(crate) image_generation_status: String, + pub(crate) revised_prompt_present: bool, + pub(crate) saved_path_present: bool, +} + +#[derive(Serialize)] +pub(crate) struct CodexImageGenerationEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexImageGenerationEventParams, +} + #[derive(Serialize)] pub(crate) struct CodexAppMetadata { pub(crate) connector_id: Option,