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
+35 -4
View File
@@ -708,6 +708,37 @@ impl PluginsManager {
remote_installed_plugins_to_config(plugins, &self.store)
}
fn remote_plugin_id_for(&self, plugin_id: &PluginId) -> Option<String> {
let cached_remote_plugin_id = {
let cache = match self.remote_installed_plugins_cache.read() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
};
cache.as_ref().and_then(|plugins| {
plugins.iter().find_map(|plugin| {
(plugin.name == plugin_id.plugin_name
&& plugin.marketplace_name == plugin_id.marketplace_name)
.then(|| plugin.id.clone())
})
})
};
if cached_remote_plugin_id.is_some() {
return cached_remote_plugin_id;
}
match self.store.remote_plugin_id(plugin_id) {
Ok(remote_plugin_id) => remote_plugin_id,
Err(err) => {
tracing::warn!(
plugin_id = %plugin_id.as_key(),
error = %err,
"failed to read persisted remote plugin identity"
);
None
}
}
}
pub async fn telemetry_metadata_for_installed_plugin(
&self,
plugin_id: &PluginId,
@@ -739,8 +770,8 @@ impl PluginsManager {
plugin_id: &PluginId,
) -> PluginTelemetryMetadata {
PluginTelemetryMetadata {
plugin_id: plugin_id.clone(),
remote_plugin_id: None,
plugin_id: Some(plugin_id.clone()),
remote_plugin_id: self.remote_plugin_id_for(plugin_id),
capability_summary: None,
}
}
@@ -762,8 +793,8 @@ impl PluginsManager {
) -> Option<PluginTelemetryMetadata> {
let plugin_id = PluginId::parse(&summary.config_name).ok()?;
Some(PluginTelemetryMetadata {
plugin_id,
remote_plugin_id: None,
remote_plugin_id: self.remote_plugin_id_for(&plugin_id),
plugin_id: Some(plugin_id),
capability_summary: Some(summary.clone()),
})
}