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:
@@ -538,12 +538,10 @@ async fn plugin_install_tracks_analytics_when_remote_detail_fetch_fails() -> Res
|
||||
payload["events"][0]["event_type"],
|
||||
"codex_plugin_install_failed"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["plugin_name"], "unknown");
|
||||
assert_eq!(
|
||||
event_params["marketplace_name"],
|
||||
"caller-marketplace-is-ignored"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], json!(null));
|
||||
assert_eq!(event_params["remote_plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["plugin_name"], json!(null));
|
||||
assert_eq!(event_params["marketplace_name"], json!(null));
|
||||
assert_eq!(
|
||||
event_params["error_type"],
|
||||
"remote_catalog_unexpected_status"
|
||||
@@ -883,6 +881,7 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> {
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample-plugin@debug",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample-plugin",
|
||||
"marketplace_name": "debug",
|
||||
"has_skills": false,
|
||||
@@ -946,6 +945,7 @@ async fn plugin_install_failure_tracks_analytics_event() -> Result<()> {
|
||||
"codex_plugin_install_failed"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], "sample-plugin@debug");
|
||||
assert_eq!(event_params["remote_plugin_id"], json!(null));
|
||||
assert_eq!(event_params["plugin_name"], "sample-plugin");
|
||||
assert_eq!(event_params["marketplace_name"], "debug");
|
||||
assert_eq!(event_params["has_skills"], json!(null));
|
||||
@@ -995,7 +995,8 @@ async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()> {
|
||||
"events": [{
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_id": "linear@openai-curated-remote",
|
||||
"remote_plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_name": "linear",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"has_skills": true,
|
||||
@@ -1072,7 +1073,8 @@ async fn plugin_install_preserves_status_when_remote_bundle_error_body_is_too_la
|
||||
payload["events"][0]["event_type"],
|
||||
"codex_plugin_install_failed"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["plugin_id"], "linear@openai-curated-remote");
|
||||
assert_eq!(event_params["remote_plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["marketplace_name"], "openai-curated-remote");
|
||||
assert_eq!(event_params["error_type"], "remote_bundle_download_status");
|
||||
assert!(
|
||||
|
||||
@@ -139,6 +139,7 @@ async fn plugin_uninstall_tracks_analytics_event() -> Result<()> {
|
||||
"event_type": "codex_plugin_uninstalled",
|
||||
"event_params": {
|
||||
"plugin_id": "sample-plugin@debug",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample-plugin",
|
||||
"marketplace_name": "debug",
|
||||
"has_skills": false,
|
||||
@@ -216,6 +217,11 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/backend-api/codex/analytics-events/events"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(r#"{"status":"ok"}"#))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let remote_plugin_cache_root = codex_home
|
||||
.path()
|
||||
@@ -225,6 +231,11 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
remote_plugin_cache_root.join("1.0.0/.codex-plugin/plugin.json"),
|
||||
r#"{"name":"linear","version":"1.0.0"}"#,
|
||||
)?;
|
||||
std::fs::create_dir_all(remote_plugin_cache_root.join("1.0.0/skills/plan-work"))?;
|
||||
std::fs::write(
|
||||
remote_plugin_cache_root.join("1.0.0/skills/plan-work/SKILL.md"),
|
||||
"---\nname: plan-work\ndescription: Plan work\n---\n",
|
||||
)?;
|
||||
let legacy_remote_plugin_cache_root = codex_home.path().join(format!(
|
||||
"plugins/cache/openai-curated-remote/{REMOTE_PLUGIN_ID}"
|
||||
));
|
||||
@@ -233,6 +244,10 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
// Simulate a background remote-cache refresh removing the local bundle
|
||||
// before the uninstall request captures its telemetry metadata.
|
||||
std::fs::remove_dir_all(remote_plugin_cache_root.join("1.0.0"))?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_uninstall_request(PluginUninstallParams {
|
||||
plugin_id: REMOTE_PLUGIN_ID.to_string(),
|
||||
@@ -255,6 +270,25 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
.await?;
|
||||
assert!(!remote_plugin_cache_root.exists());
|
||||
assert!(!legacy_remote_plugin_cache_root.exists());
|
||||
let payload = wait_for_plugin_analytics_payload(&server).await?;
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"events": [{
|
||||
"event_type": "codex_plugin_uninstalled",
|
||||
"event_params": {
|
||||
"plugin_id": "linear@openai-curated-remote",
|
||||
"remote_plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_name": "linear",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 0,
|
||||
"connector_ids": [],
|
||||
"product_client_id": DEFAULT_CLIENT_NAME,
|
||||
}
|
||||
}]
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -638,7 +672,11 @@ async fn mount_remote_plugin_detail_with_name(
|
||||
"interface": {{
|
||||
"short_description": "Plan and track work"
|
||||
}},
|
||||
"skills": []
|
||||
"skills": [{{
|
||||
"name": "plan-work",
|
||||
"description": "Plan work",
|
||||
"interface": null
|
||||
}}]
|
||||
}}
|
||||
}}"#
|
||||
);
|
||||
@@ -652,6 +690,29 @@ async fn mount_remote_plugin_detail_with_name(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_for_plugin_analytics_payload(server: &MockServer) -> Result<serde_json::Value> {
|
||||
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| {
|
||||
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}"));
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn wait_for_remote_plugin_request_count(
|
||||
server: &MockServer,
|
||||
method_name: &str,
|
||||
|
||||
Reference in New Issue
Block a user