Send plugin-used analytics with API key auth

This commit is contained in:
Felix Xia
2026-07-08 18:52:40 +01:00
parent 0bbea86a6a
commit f0c63fbf24
3 changed files with 107 additions and 2 deletions

View File

@@ -548,7 +548,7 @@ impl AnalyticsEventsClient {
async fn send_track_events(
auth_manager: &AuthManager,
destination: &AnalyticsEventsDestination,
events: Vec<TrackEventRequest>,
mut events: Vec<TrackEventRequest>,
) {
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;
}

View File

@@ -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::<Vec<_>>();
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::<Vec<_>>();
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() {

View File

@@ -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)]