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:
jameswt-oai
2026-06-23 12:27:14 -07:00
committed by GitHub
Unverified
parent c5a9a95ab6
commit ff50b47dce
15 changed files with 427 additions and 91 deletions
+101 -4
View File
@@ -859,7 +859,7 @@ async fn installed_plugin_telemetry_metadata_collects_capabilities() {
assert_eq!(
metadata,
PluginTelemetryMetadata {
plugin_id,
plugin_id: Some(plugin_id),
remote_plugin_id: None,
capability_summary: Some(PluginCapabilitySummary {
config_name: "sample@test".to_string(),
@@ -873,6 +873,71 @@ async fn installed_plugin_telemetry_metadata_collects_capabilities() {
);
}
#[tokio::test]
async fn installed_plugin_telemetry_metadata_resolves_persisted_remote_identity() {
let codex_home = TempDir::new().unwrap();
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
let plugin_id =
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
PluginStore::new(codex_home.path().to_path_buf())
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_linear")
.expect("persist remote plugin id");
let manager = PluginsManager::new(codex_home.path().to_path_buf());
let metadata = manager
.telemetry_metadata_for_installed_plugin(&plugin_id)
.await;
assert_eq!(
metadata,
PluginTelemetryMetadata {
plugin_id: Some(plugin_id),
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
capability_summary: Some(PluginCapabilitySummary {
config_name: "linear@openai-curated-remote".to_string(),
display_name: "linear".to_string(),
description: None,
has_skills: true,
mcp_server_names: Vec::new(),
app_connector_ids: Vec::new(),
}),
}
);
}
#[tokio::test]
async fn installed_plugin_telemetry_metadata_prefers_remote_snapshot_identity() {
let codex_home = TempDir::new().unwrap();
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
let plugin_id =
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
PluginStore::new(codex_home.path().to_path_buf())
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_stale")
.expect("persist remote plugin id");
let manager = PluginsManager::new(codex_home.path().to_path_buf());
manager.write_remote_installed_plugins_cache(vec![remote_installed_linear_plugin()]);
let metadata = manager
.telemetry_metadata_for_installed_plugin(&plugin_id)
.await;
assert_eq!(
metadata,
PluginTelemetryMetadata {
plugin_id: Some(plugin_id),
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
capability_summary: Some(PluginCapabilitySummary {
config_name: "linear@openai-curated-remote".to_string(),
display_name: "linear".to_string(),
description: None,
has_skills: true,
mcp_server_names: Vec::new(),
app_connector_ids: Vec::new(),
}),
}
);
}
#[tokio::test]
async fn installed_plugin_telemetry_metadata_accepts_authoritative_remote_identity() {
let codex_home = TempDir::new().unwrap();
@@ -887,7 +952,7 @@ async fn installed_plugin_telemetry_metadata_accepts_authoritative_remote_identi
assert_eq!(
metadata,
PluginTelemetryMetadata {
plugin_id,
plugin_id: Some(plugin_id),
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
capability_summary: None,
}
@@ -912,14 +977,46 @@ fn capability_summary_telemetry_metadata_uses_local_identity() {
assert_eq!(
metadata,
Some(PluginTelemetryMetadata {
plugin_id: PluginId::parse("linear@openai-curated-remote")
.expect("plugin id should parse"),
plugin_id: Some(
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse"),
),
remote_plugin_id: None,
capability_summary: Some(summary),
})
);
}
#[test]
fn capability_summary_telemetry_metadata_resolves_persisted_remote_identity() {
let codex_home = TempDir::new().unwrap();
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
let plugin_id =
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
PluginStore::new(codex_home.path().to_path_buf())
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_linear")
.expect("persist remote plugin id");
let manager = PluginsManager::new(codex_home.path().to_path_buf());
let summary = PluginCapabilitySummary {
config_name: "linear@openai-curated-remote".to_string(),
display_name: "Linear".to_string(),
description: Some("Track work".to_string()),
has_skills: true,
mcp_server_names: vec!["linear".to_string()],
app_connector_ids: vec![AppConnectorId("linear-app".to_string())],
};
let metadata = manager.telemetry_metadata_for_capability_summary(&summary);
assert_eq!(
metadata,
Some(PluginTelemetryMetadata {
plugin_id: Some(plugin_id),
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
capability_summary: Some(summary),
})
);
}
#[tokio::test]
async fn remote_installed_cache_prefers_local_curated_conflicts_when_remote_plugin_disabled() {
let codex_home = TempDir::new().unwrap();