diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index eb511483a4..39b5e0fa88 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -6,9 +6,14 @@ use crate::events::CodexAppUsedEventRequest; use crate::events::CodexPluginEventRequest; use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; +use crate::events::CodexToolCallEventParams; +use crate::events::CodexToolCallEventRequest; use crate::events::ThreadInitializationMode; use crate::events::ThreadInitializedEvent; use crate::events::ThreadInitializedEventParams; +use crate::events::ToolCallFinalReviewOutcome; +use crate::events::ToolCallTerminalStatus; +use crate::events::ToolKind; use crate::events::TrackEventRequest; use crate::events::codex_app_metadata; use crate::events::codex_plugin_metadata; @@ -346,6 +351,89 @@ fn thread_initialized_event_serializes_expected_shape() { ); } +#[test] +fn tool_call_event_serializes_expected_shape() { + let event = TrackEventRequest::ToolCall(CodexToolCallEventRequest { + event_type: "codex_tool_call_event", + event_params: CodexToolCallEventParams { + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + tool_call_id: "tool-call-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(), + }, + tool_name: "shell".to_string(), + tool_kind: ToolKind::Shell, + 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_review_outcome: ToolCallFinalReviewOutcome::NotNeeded, + terminal_status: ToolCallTerminalStatus::Completed, + failure_kind: None, + exit_code: Some(0), + requested_additional_permissions: false, + requested_network_access: false, + retry_count: 0, + }, + }); + + let payload = serde_json::to_value(&event).expect("serialize tool call event"); + assert_eq!( + payload, + json!({ + "event_type": "codex_tool_call_event", + "event_params": { + "thread_id": "thread-1", + "turn_id": "turn-1", + "tool_call_id": "tool-call-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" + }, + "tool_name": "shell", + "tool_kind": "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_review_outcome": "not_needed", + "terminal_status": "completed", + "failure_kind": null, + "exit_code": 0, + "requested_additional_permissions": false, + "requested_network_access": false, + "retry_count": 0 + } + }) + ); +} + #[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 885e93bbb9..f82ebe6f0a 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -37,6 +37,7 @@ pub(crate) enum TrackEventRequest { ThreadInitialized(ThreadInitializedEvent), AppMentioned(CodexAppMentionedEventRequest), AppUsed(CodexAppUsedEventRequest), + ToolCall(CodexToolCallEventRequest), PluginUsed(CodexPluginUsedEventRequest), PluginInstalled(CodexPluginEventRequest), PluginUninstalled(CodexPluginEventRequest), @@ -99,6 +100,81 @@ pub(crate) struct ThreadInitializedEvent { pub(crate) event_params: ThreadInitializedEventParams, } +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ToolKind { + Shell, + UnifiedExec, + ApplyPatch, + Mcp, + Dynamic, + Other, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ToolCallFinalReviewOutcome { + NotNeeded, + GuardianApproved, + GuardianDenied, + GuardianAborted, + UserApproved, + UserApprovedForSession, + UserDenied, + UserAborted, + ConfigAllowed, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ToolCallTerminalStatus { + Completed, + Failed, + Rejected, + Interrupted, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum ToolCallFailureKind { + ToolError, + ApprovalDenied, + ApprovalAborted, + SandboxDenied, + PolicyForbidden, +} + +#[derive(Serialize)] +pub(crate) struct CodexToolCallEventParams { + pub(crate) thread_id: String, + pub(crate) turn_id: String, + pub(crate) tool_call_id: String, + pub(crate) app_server_client: CodexAppServerClientMetadata, + pub(crate) runtime: CodexRuntimeMetadata, + pub(crate) tool_name: String, + pub(crate) tool_kind: ToolKind, + 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_review_outcome: ToolCallFinalReviewOutcome, + pub(crate) terminal_status: ToolCallTerminalStatus, + pub(crate) failure_kind: Option, + pub(crate) exit_code: Option, + pub(crate) requested_additional_permissions: bool, + pub(crate) requested_network_access: bool, + pub(crate) retry_count: u64, +} + +#[derive(Serialize)] +pub(crate) struct CodexToolCallEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexToolCallEventParams, +} + #[derive(Serialize)] pub(crate) struct CodexAppMetadata { pub(crate) connector_id: Option,