mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Separate local and remote plugin analytics IDs (#29495)
## Why Plugin analytics overloaded `plugin_id`: most events used the Codex `<plugin>@<marketplace>` identity, while remote install events used the backend plugin ID. That makes the same field change meaning across event types and complicates downstream identity resolution. This change makes the contract unambiguous: - `plugin_id`: the local Codex `<plugin>@<marketplace>` identity, when resolved - `remote_plugin_id`: the backend plugin identity, when available For a remote install failure that happens before plugin details resolve, `plugin_id` is `null` and `remote_plugin_id` remains populated. ## What changed All six plugin analytics events use the same identity contract: - `codex_plugin_installed` - `codex_plugin_install_failed` - `codex_plugin_uninstalled` - `codex_plugin_enabled` - `codex_plugin_disabled` - `codex_plugin_used` Remote identity is resolved from the current installed-plugin snapshot first, with persisted install metadata as fallback. The telemetry metadata type keeps local identity optional for failures that occur before remote details are available. The app-server test client's manual analytics smokes now find remote mutation events through `remote_plugin_id` and validate that `plugin_id` remains local. ## Remote uninstall Resolve and capture telemetry metadata before removing the local plugin cache, then emit `codex_plugin_uninstalled` after the backend confirms success. The event is also emitted when backend uninstall succeeds but local cache cleanup reports `CacheRemove`. If a concurrent remote-cache refresh removes the local bundle before telemetry capture, the already-fetched remote plugin detail supplies fallback capability metadata. ## Validation - `just test -p codex-analytics` — 82 passed - `just test -p codex-core-plugins` — 271 passed - `just test -p codex-app-server-test-client` — 5 passed - `just test -p codex-plugin` — 3 passed - `just test -p codex-app-server plugin_install` — 37 passed - `just test -p codex-app-server plugin_uninstall` — 10 passed The production app-server install/uninstall flow was also exercised against `plugins~Plugin_f1b845ac33888191ac156169c58733c2` (`build-ios-apps@openai-curated-remote`), and the plugin's original uninstalled state was restored.
This commit is contained in:
committed by
GitHub
Unverified
parent
c5a9a95ab6
commit
ff50b47dce
@@ -3027,6 +3027,7 @@ fn plugin_used_event_serializes_expected_shape() {
|
||||
"event_type": "codex_plugin_used",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3057,6 +3058,7 @@ fn plugin_management_event_serializes_expected_shape() {
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3086,6 +3088,7 @@ fn plugin_install_failed_event_serializes_expected_shape() {
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3099,7 +3102,7 @@ fn plugin_install_failed_event_serializes_expected_shape() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_management_event_can_use_remote_plugin_id_override() {
|
||||
fn plugin_management_event_keeps_plugin_id_local_when_remote_id_exists() {
|
||||
let mut plugin = sample_plugin_metadata();
|
||||
plugin.remote_plugin_id = Some("plugins~Plugin_remote".to_string());
|
||||
let event = TrackEventRequest::PluginInstalled(CodexPluginEventRequest {
|
||||
@@ -3110,11 +3113,21 @@ fn plugin_management_event_can_use_remote_plugin_id_override() {
|
||||
let payload = serde_json::to_value(&event).expect("serialize plugin installed event");
|
||||
|
||||
assert_eq!(
|
||||
payload["event_params"]["plugin_id"],
|
||||
"plugins~Plugin_remote"
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": "plugins~Plugin_remote",
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 2,
|
||||
"connector_ids": ["calendar", "drive"],
|
||||
"product_client_id": originator().value
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(payload["event_params"]["plugin_name"], "sample");
|
||||
assert_eq!(payload["event_params"]["marketplace_name"], "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3454,6 +3467,7 @@ async fn reducer_ingests_plugin_state_changed_fact() {
|
||||
"event_type": "codex_plugin_disabled",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3489,6 +3503,7 @@ async fn reducer_ingests_plugin_install_failed_fact() {
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3506,7 +3521,7 @@ async fn reducer_ingests_plugin_install_failed_fact_without_detail() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
let plugin = PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("unknown@openai-curated-remote").expect("valid plugin id"),
|
||||
plugin_id: None,
|
||||
remote_plugin_id: Some("plugins~Plugin_00000000000000000000000000000000".to_string()),
|
||||
capability_summary: None,
|
||||
};
|
||||
@@ -3529,9 +3544,10 @@ async fn reducer_ingests_plugin_install_failed_fact_without_detail() {
|
||||
json!([{
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"plugin_name": "unknown",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"plugin_id": null,
|
||||
"remote_plugin_id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"plugin_name": null,
|
||||
"marketplace_name": null,
|
||||
"has_skills": null,
|
||||
"mcp_server_count": null,
|
||||
"connector_ids": null,
|
||||
@@ -4570,7 +4586,7 @@ async fn turn_completed_without_started_notification_emits_null_started_at() {
|
||||
|
||||
fn sample_plugin_metadata() -> PluginTelemetryMetadata {
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("sample@test").expect("valid plugin id"),
|
||||
plugin_id: Some(PluginId::parse("sample@test").expect("valid plugin id")),
|
||||
remote_plugin_id: None,
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "sample@test".to_string(),
|
||||
|
||||
@@ -38,6 +38,7 @@ use codex_app_server_protocol::ServerResponse;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::create_client;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use std::collections::HashSet;
|
||||
@@ -173,7 +174,15 @@ impl AnalyticsEventsQueue {
|
||||
if emitted.len() >= ANALYTICS_EVENT_DEDUPE_MAX_KEYS {
|
||||
emitted.clear();
|
||||
}
|
||||
emitted.insert((tracking.turn_id.clone(), plugin.plugin_id.as_key()))
|
||||
let Some(plugin_id) = plugin
|
||||
.plugin_id
|
||||
.as_ref()
|
||||
.map(PluginId::as_key)
|
||||
.or_else(|| plugin.remote_plugin_id.clone())
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
emitted.insert((tracking.turn_id.clone(), plugin_id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::now_unix_millis;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
@@ -933,6 +934,7 @@ pub(crate) struct CodexTurnSteerEventRequest {
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginMetadata {
|
||||
pub(crate) plugin_id: Option<String>,
|
||||
pub(crate) remote_plugin_id: Option<String>,
|
||||
pub(crate) plugin_name: Option<String>,
|
||||
pub(crate) marketplace_name: Option<String>,
|
||||
pub(crate) has_skills: Option<bool>,
|
||||
@@ -1040,11 +1042,13 @@ pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPlu
|
||||
remote_plugin_id,
|
||||
capability_summary,
|
||||
} = plugin;
|
||||
let event_plugin_id = remote_plugin_id.unwrap_or_else(|| plugin_id.as_key());
|
||||
CodexPluginMetadata {
|
||||
plugin_id: Some(event_plugin_id),
|
||||
plugin_name: Some(plugin_id.plugin_name),
|
||||
marketplace_name: Some(plugin_id.marketplace_name),
|
||||
plugin_id: plugin_id.as_ref().map(PluginId::as_key),
|
||||
remote_plugin_id,
|
||||
plugin_name: plugin_id
|
||||
.as_ref()
|
||||
.map(|plugin_id| plugin_id.plugin_name.clone()),
|
||||
marketplace_name: plugin_id.map(|plugin_id| plugin_id.marketplace_name),
|
||||
has_skills: capability_summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.has_skills),
|
||||
|
||||
Reference in New Issue
Block a user