diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index f957a665a0..96ace49fc9 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -548,7 +548,7 @@ impl AnalyticsEventsClient { async fn send_track_events( auth_manager: &AuthManager, destination: &AnalyticsEventsDestination, - events: Vec, + mut events: Vec, ) { if events.is_empty() { return; @@ -557,7 +557,12 @@ async fn send_track_events( let Some(auth) = auth_manager.auth().await else { return; }; - if !auth.uses_codex_backend() { + if auth.is_api_key_auth() { + events.retain(TrackEventRequest::can_send_with_api_key_auth); + } else if !auth.uses_codex_backend() { + return; + } + if events.is_empty() { return; } diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index ac50d6ebc7..62240c703a 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -4,10 +4,18 @@ use super::AnalyticsEventsQueue; #[cfg(debug_assertions)] use super::capture_track_events_request; #[cfg(debug_assertions)] +use super::send_track_events; +#[cfg(debug_assertions)] use super::send_track_events_request; use super::track_event_request_batches; use crate::events::CodexAcceptedLineFingerprintsEventParams; use crate::events::CodexAcceptedLineFingerprintsEventRequest; +#[cfg(debug_assertions)] +use crate::events::CodexPluginMetadata; +#[cfg(debug_assertions)] +use crate::events::CodexPluginUsedEventRequest; +#[cfg(debug_assertions)] +use crate::events::CodexPluginUsedMetadata; use crate::events::SkillInvocationEventParams; use crate::events::SkillInvocationEventRequest; use crate::events::TrackEventRequest; @@ -85,6 +93,29 @@ fn sample_regular_track_event(thread_id: &str) -> TrackEventRequest { }) } +#[cfg(debug_assertions)] +fn sample_plugin_used_track_event(thread_id: &str, plugin_id: Option<&str>) -> TrackEventRequest { + TrackEventRequest::PluginUsed(CodexPluginUsedEventRequest { + event_type: "codex_plugin_used", + event_params: CodexPluginUsedMetadata { + plugin: CodexPluginMetadata { + plugin_id: plugin_id.map(str::to_string), + remote_plugin_id: None, + plugin_name: Some("sample".to_string()), + marketplace_name: Some("test".to_string()), + has_skills: Some(true), + mcp_server_count: Some(1), + connector_ids: Some(vec!["calendar".to_string()]), + product_client_id: Some("codex_desktop".to_string()), + }, + mcp_server_names: Some(vec!["mcp-1".to_string()]), + thread_id: Some(thread_id.to_string()), + turn_id: Some("turn-1".to_string()), + model_slug: Some("gpt-5.1-codex".to_string()), + }, + }) +} + #[cfg(debug_assertions)] fn unique_capture_path(name: &str) -> PathBuf { let nonce = SystemTime::now() @@ -227,6 +258,68 @@ async fn capture_file_writes_final_batches_as_separate_lines() { fs::remove_file(capture_path).expect("remove capture file"); } +#[tokio::test] +#[cfg(debug_assertions)] +async fn api_key_auth_sends_only_plugin_used_events_to_codex_backend() { + let capture_path = unique_capture_path("api-key-plugin-used-events"); + let destination = AnalyticsEventsDestination::CaptureFile { + path: capture_path.clone(), + }; + let auth_manager = codex_login::AuthManager::from_auth_for_testing( + codex_login::CodexAuth::from_api_key("sk-test"), + ); + + send_track_events( + &auth_manager, + &destination, + vec![ + sample_regular_track_event("non-plugin-skill"), + sample_plugin_used_track_event("non-plugin-used", /*plugin_id*/ None), + sample_accepted_line_fingerprint_event("other-event"), + sample_plugin_used_track_event("plugin-used", Some("sample@test")), + ], + ) + .await; + + let contents = fs::read_to_string(&capture_path).expect("read capture file"); + let lines = contents.lines().collect::>(); + assert_eq!(lines.len(), 1); + let payload: serde_json::Value = + serde_json::from_str(lines[0]).expect("parse captured payload"); + let events = payload["events"].as_array().expect("events array"); + for event in events { + let event_params = event["event_params"].as_object().expect("event params"); + for server_owned_field in [ + "auth_mode", + "api_organization_id", + "api_project_id", + "api_key_tracking_id", + ] { + assert!(!event_params.contains_key(server_owned_field)); + } + } + let delivered_events = events + .iter() + .map(|event| { + serde_json::json!({ + "event_type": event["event_type"], + "plugin_id": event["event_params"]["plugin_id"], + "thread_id": event["event_params"]["thread_id"], + }) + }) + .collect::>(); + assert_eq!( + delivered_events, + vec![serde_json::json!({ + "event_type": "codex_plugin_used", + "plugin_id": "sample@test", + "thread_id": "plugin-used", + })] + ); + + fs::remove_file(capture_path).expect("remove capture file"); +} + #[test] #[cfg(debug_assertions)] fn capture_write_failure_still_consumes_delivery() { diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index ad8adf14cb..4c58b5b37e 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -95,6 +95,13 @@ impl TrackEventRequest { pub(crate) fn should_send_in_isolated_request(&self) -> bool { matches!(self, Self::AcceptedLineFingerprints(_)) } + + pub(crate) fn can_send_with_api_key_auth(&self) -> bool { + match self { + Self::PluginUsed(event) => event.event_params.plugin.plugin_id.is_some(), + _ => false, + } + } } #[derive(Serialize)]