diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index e7cc07b2f8..bdb8b368e0 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -3,13 +3,20 @@ 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::CodexPluginEventRequest; use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; +use crate::events::CodexToolItemEventBase; use crate::events::CodexTurnEventRequest; +use crate::events::CommandExecutionFamily; +use crate::events::CommandExecutionSourceKind; 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_plugin_metadata; @@ -830,6 +837,152 @@ 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: 123, + completed_at: Some(125), + duration_ms: Some(2000), + execution_started: true, + 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, + retry_count: 0, + }, + command_execution_source: CommandExecutionSourceKind::Agent, + command_execution_family: CommandExecutionFamily::Shell, + exit_code: Some(0), + command_action_count: Some(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": 123, + "completed_at": 125, + "duration_ms": 2000, + "execution_started": true, + "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, + "retry_count": 0, + "command_execution_source": "agent", + "command_execution_family": "shell", + "exit_code": 0, + "command_action_count": 1 + } + }) + ); +} + +#[test] +fn command_execution_event_allows_null_thread_denormalization() { + 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: None, + subagent_source: None, + parent_thread_id: None, + tool_name: "shell".to_string(), + started_at: 123, + completed_at: Some(125), + duration_ms: Some(2000), + execution_started: true, + 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, + retry_count: 0, + }, + command_execution_source: CommandExecutionSourceKind::Agent, + command_execution_family: CommandExecutionFamily::Shell, + exit_code: Some(0), + command_action_count: Some(0), + }, + }); + + let payload = serde_json::to_value(&event).expect("serialize command execution event"); + assert_eq!(payload["event_params"]["thread_source"], json!(null)); +} + #[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 eb312da140..13c071c604 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -42,6 +42,14 @@ pub(crate) enum TrackEventRequest { Compaction(Box), TurnEvent(Box), TurnSteer(CodexTurnSteerEventRequest), + #[allow(dead_code)] + CommandExecution(CodexCommandExecutionEventRequest), + FileChange(CodexFileChangeEventRequest), + McpToolCall(CodexMcpToolCallEventRequest), + DynamicToolCall(CodexDynamicToolCallEventRequest), + CollabAgentToolCall(CodexCollabAgentToolCallEventRequest), + WebSearch(CodexWebSearchEventRequest), + ImageGeneration(CodexImageGenerationEventRequest), PluginUsed(CodexPluginUsedEventRequest), PluginInstalled(CodexPluginEventRequest), PluginUninstalled(CodexPluginEventRequest), @@ -277,6 +285,226 @@ 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 { + NotNeeded, + ConfigAllowed, + PolicyForbidden, + GuardianApproved, + GuardianDenied, + GuardianAborted, + UserApproved, + UserApprovedForSession, + UserDenied, + UserAborted, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +#[allow(dead_code)] +pub(crate) enum ToolItemTerminalStatus { + Completed, + Failed, + Rejected, + Interrupted, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +#[allow(dead_code)] +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: u64, + pub(crate) completed_at: Option, + pub(crate) duration_ms: Option, + pub(crate) execution_started: bool, + 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, + pub(crate) retry_count: u64, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum CommandExecutionFamily { + Shell, + UserShell, + UnifiedExec, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum CommandExecutionSourceKind { + Agent, + UserShell, + UnifiedExecStartup, + UnifiedExecInteraction, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum CollabAgentToolKind { + SpawnAgent, + SendInput, + ResumeAgent, + Wait, + CloseAgent, +} + +#[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: CommandExecutionSourceKind, + pub(crate) command_execution_family: CommandExecutionFamily, + pub(crate) exit_code: Option, + pub(crate) command_action_count: Option, +} + +#[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, + pub(crate) mcp_error_code: Option, +} + +#[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) collab_agent_tool: CollabAgentToolKind, + 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,