diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index 84a62aa9cb..827d228623 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -587,7 +587,7 @@ async fn plugin_install_tracks_analytics_when_remote_detail_fetch_fails() -> Res assert_eq!(err.error.code, -32600); assert!(err.error.message.contains("failed with status 404")); - let payload = wait_for_plugin_analytics_payload(&server).await?; + let payload = wait_for_plugin_analytics_payload(&server, "codex_plugin_install_failed").await?; let event_params = &payload["events"][0]["event_params"]; assert_eq!( payload["events"][0]["event_type"], @@ -659,7 +659,7 @@ async fn plugin_install_tracks_analytics_when_remote_install_is_rate_limited() - /*expected_count*/ 1, ) .await?; - let payload = wait_for_plugin_analytics_payload(&server).await?; + let payload = wait_for_plugin_analytics_payload(&server, "codex_plugin_install_failed").await?; let event_params = &payload["events"][0]["event_params"]; assert_eq!( payload["events"][0]["event_type"], @@ -734,7 +734,7 @@ async fn plugin_install_rejects_remote_plugin_disabled_by_admin_before_download( .join("plugins/cache/openai-curated-remote/linear") .exists() ); - let payload = wait_for_plugin_analytics_payload(&server).await?; + let payload = wait_for_plugin_analytics_payload(&server, "codex_plugin_install_failed").await?; let event_params = &payload["events"][0]["event_params"]; assert_eq!( payload["events"][0]["event_type"], @@ -783,7 +783,7 @@ async fn plugin_install_rejects_remote_plugin_not_available() -> Result<()> { /*expected_count*/ 0, ) .await?; - let payload = wait_for_plugin_analytics_payload(&server).await?; + let payload = wait_for_plugin_analytics_payload(&server, "codex_plugin_install_failed").await?; let event_params = &payload["events"][0]["event_params"]; assert_eq!( payload["events"][0]["event_type"], @@ -872,7 +872,8 @@ async fn plugin_install_tracks_analytics_when_marketplace_file_cannot_be_read() .contains("failed to read marketplace file") ); - let payload = wait_for_plugin_analytics_payload(&analytics_server).await?; + let payload = + wait_for_plugin_analytics_payload(&analytics_server, "codex_plugin_install_failed").await?; let event_params = &payload["events"][0]["event_params"]; assert_eq!( payload["events"][0]["event_type"], @@ -1026,7 +1027,8 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> { timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; assert_eq!(response.apps_needing_auth, Vec::::new()); - let payload = wait_for_plugin_analytics_payload(&analytics_server).await?; + let payload = + wait_for_plugin_analytics_payload(&analytics_server, "codex_plugin_installed").await?; assert_eq!( payload, json!({ @@ -1094,7 +1096,8 @@ async fn plugin_install_failure_tracks_analytics_event() -> Result<()> { .await??; assert_eq!(err.error.code, -32600); - let payload = wait_for_plugin_analytics_payload(&analytics_server).await?; + let payload = + wait_for_plugin_analytics_payload(&analytics_server, "codex_plugin_install_failed").await?; let event_params = &payload["events"][0]["event_params"]; assert_eq!( payload["events"][0]["event_type"], @@ -1151,7 +1154,7 @@ async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()> { json!({"install_attempt_id": INSTALL_ATTEMPT_ID}) ); - let payload = wait_for_plugin_analytics_payload(&server).await?; + let payload = wait_for_plugin_analytics_payload(&server, "codex_plugin_installed").await?; assert_eq!( payload, json!({ @@ -1229,7 +1232,7 @@ async fn plugin_install_preserves_status_when_remote_bundle_error_body_is_too_la /*expected_count*/ 0, ) .await?; - let payload = wait_for_plugin_analytics_payload(&server).await?; + let payload = wait_for_plugin_analytics_payload(&server, "codex_plugin_install_failed").await?; let event_params = &payload["events"][0]["event_params"]; assert_eq!( payload["events"][0]["event_type"], @@ -2762,22 +2765,32 @@ async fn mount_backend_analytics_events(server: &MockServer) { .await; } -async fn wait_for_plugin_analytics_payload(server: &MockServer) -> Result { +async fn wait_for_plugin_analytics_payload( + server: &MockServer, + event_type: &str, +) -> Result { timeout(DEFAULT_TIMEOUT, async { loop { let Some(requests) = server.received_requests().await else { tokio::time::sleep(Duration::from_millis(25)).await; continue; }; - if let Some(request) = requests.iter().find(|request| { + for request in requests.iter().filter(|request| { request.method == "POST" && request .url .path() .ends_with("/codex/analytics-events/events") }) { - return serde_json::from_slice(&request.body) - .map_err(|err| anyhow::anyhow!("invalid analytics payload: {err}")); + let mut payload: serde_json::Value = serde_json::from_slice(&request.body) + .map_err(|err| anyhow::anyhow!("invalid analytics payload: {err}"))?; + let events = payload["events"] + .as_array_mut() + .ok_or_else(|| anyhow::anyhow!("analytics payload missing events array"))?; + events.retain(|event| event["event_type"] == event_type); + if !events.is_empty() { + return Ok(payload); + } } tokio::time::sleep(Duration::from_millis(25)).await; } @@ -2785,6 +2798,36 @@ async fn wait_for_plugin_analytics_payload(server: &MockServer) -> Result Result<()> { + let server = MockServer::start().await; + mount_backend_analytics_events(&server).await; + let client = HttpClientBuilder::new().build_direct()?; + let expected = json!({ + "event_type": "codex_plugin_install_failed", + "event_params": {"plugin_id": "sample-plugin@debug"} + }); + for events in [ + json!([{"event_type": "codex_turn_event"}]), + json!([{"event_type": "codex_thread_initialized"}, expected.clone()]), + ] { + client + .post(format!( + "{}/backend-api/codex/analytics-events/events", + server.uri() + )) + .json(&json!({"events": events})) + .send() + .await? + .error_for_status()?; + } + assert_eq!( + wait_for_plugin_analytics_payload(&server, "codex_plugin_install_failed").await?, + json!({"events": [expected]}) + ); + Ok(()) +} + async fn oauth_discovery_request_count(server: &MockServer) -> usize { server .received_requests()