mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Track plugin install and import telemetry failures (#28731)
## Summary - Track plugin install failures through the unified `codex_plugin_install_failed` event for local installs, remote install preflight failures, bundle failures, and remote catalog/backend failures. - Send classified `error_type` values in plugin install failure analytics instead of raw error strings. - Stop sending raw external-agent import errors in analytics while preserving raw failure details in app-facing import notifications/history. - Keep raw plugin/migration diagnostics in `tracing::warn!` logs. - Keep remote failure plugin names as the existing local placeholder (`unknown`) and remove the extra telemetry plugin-name override. - Change `ExternalAgentConfigImportParams.source` from a generated enum to `string | null`, with legacy `claudeCode` / `claudeCowork` inputs normalized to existing analytics values. ## Testing
This commit is contained in:
@@ -9,7 +9,11 @@ use crate::events::CodexCommandExecutionEventParams;
|
||||
use crate::events::CodexCommandExecutionEventRequest;
|
||||
use crate::events::CodexCompactionEventRequest;
|
||||
use crate::events::CodexHookRunEventRequest;
|
||||
use crate::events::CodexOnboardingExternalAgentImportFailureEventRequest;
|
||||
use crate::events::CodexOnboardingExternalAgentImportFailureMetadata;
|
||||
use crate::events::CodexPluginEventRequest;
|
||||
use crate::events::CodexPluginInstallFailedEventRequest;
|
||||
use crate::events::CodexPluginInstallFailedMetadata;
|
||||
use crate::events::CodexPluginUsedEventRequest;
|
||||
use crate::events::CodexReviewEventParams;
|
||||
use crate::events::CodexReviewEventRequest;
|
||||
@@ -51,10 +55,13 @@ use crate::facts::CompactionStatus;
|
||||
use crate::facts::CompactionStrategy;
|
||||
use crate::facts::CompactionTrigger;
|
||||
use crate::facts::CustomAnalyticsFact;
|
||||
use crate::facts::ExternalAgentConfigImportCompletedInput;
|
||||
use crate::facts::ExternalAgentConfigImportFailureInput;
|
||||
use crate::facts::HookRunFact;
|
||||
use crate::facts::HookRunInput;
|
||||
use crate::facts::InputError;
|
||||
use crate::facts::InvocationType;
|
||||
use crate::facts::PluginInstallFailedInput;
|
||||
use crate::facts::PluginState;
|
||||
use crate::facts::PluginStateChangedInput;
|
||||
use crate::facts::PluginUsedInput;
|
||||
@@ -3054,6 +3061,36 @@ fn plugin_management_event_serializes_expected_shape() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_install_failed_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::PluginInstallFailed(CodexPluginInstallFailedEventRequest {
|
||||
event_type: "codex_plugin_install_failed",
|
||||
event_params: CodexPluginInstallFailedMetadata {
|
||||
plugin: codex_plugin_metadata(sample_plugin_metadata()),
|
||||
error_type: "store_io".to_string(),
|
||||
},
|
||||
});
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize plugin install failed event");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 2,
|
||||
"connector_ids": ["calendar", "drive"],
|
||||
"product_client_id": originator().value,
|
||||
"error_type": "store_io"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_management_event_can_use_remote_plugin_id_override() {
|
||||
let mut plugin = sample_plugin_metadata();
|
||||
@@ -3421,6 +3458,190 @@ async fn reducer_ingests_plugin_state_changed_fact() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reducer_ingests_plugin_install_failed_fact() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::PluginInstallFailed(
|
||||
PluginInstallFailedInput {
|
||||
plugin: sample_plugin_metadata(),
|
||||
error_type: "invalid_plugin".to_string(),
|
||||
},
|
||||
)),
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
|
||||
let payload = serde_json::to_value(&events).expect("serialize events");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!([{
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 2,
|
||||
"connector_ids": ["calendar", "drive"],
|
||||
"product_client_id": originator().value,
|
||||
"error_type": "invalid_plugin"
|
||||
}
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reducer_ingests_plugin_install_failed_fact_without_detail() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
let mut plugin = PluginTelemetryMetadata::from_plugin_id(
|
||||
&PluginId::parse("unknown@openai-curated-remote").expect("valid plugin id"),
|
||||
);
|
||||
plugin.remote_plugin_id = Some("plugins~Plugin_00000000000000000000000000000000".to_string());
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::PluginInstallFailed(
|
||||
PluginInstallFailedInput {
|
||||
plugin,
|
||||
error_type: "remote_catalog_unexpected_status".to_string(),
|
||||
},
|
||||
)),
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
|
||||
let payload = serde_json::to_value(&events).expect("serialize events");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!([{
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"plugin_name": "unknown",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"has_skills": null,
|
||||
"mcp_server_count": null,
|
||||
"connector_ids": null,
|
||||
"product_client_id": originator().value,
|
||||
"error_type": "remote_catalog_unexpected_status"
|
||||
}
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reducer_ingests_external_agent_config_import_completed_fact() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::ExternalAgentConfigImportCompleted(
|
||||
ExternalAgentConfigImportCompletedInput {
|
||||
import_id: "import-1".to_string(),
|
||||
source: "app_server".to_string(),
|
||||
item_type: "PLUGINS".to_string(),
|
||||
success_count: 2,
|
||||
failed_count: 1,
|
||||
},
|
||||
)),
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
|
||||
let payload = serde_json::to_value(&events).expect("serialize events");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!([{
|
||||
"event_type": "codex_onboarding_external_agent_import_complete",
|
||||
"event_params": {
|
||||
"import_id": "import-1",
|
||||
"source": "app_server",
|
||||
"type": "PLUGINS",
|
||||
"success_count": 2,
|
||||
"failed_count": 1,
|
||||
"product_client_id": originator().value,
|
||||
}
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_agent_config_import_failure_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::ExternalAgentConfigImportFailure(
|
||||
CodexOnboardingExternalAgentImportFailureEventRequest {
|
||||
event_type: "codex_onboarding_external_agent_import_failure",
|
||||
event_params: CodexOnboardingExternalAgentImportFailureMetadata {
|
||||
import_id: "import-1".to_string(),
|
||||
source: "app_server".to_string(),
|
||||
item_type: "SESSIONS".to_string(),
|
||||
failure_stage: "session_missing".to_string(),
|
||||
error_type: "session_missing".to_string(),
|
||||
product_client_id: Some(originator().value),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize import failure event");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_onboarding_external_agent_import_failure",
|
||||
"event_params": {
|
||||
"import_id": "import-1",
|
||||
"source": "app_server",
|
||||
"type": "SESSIONS",
|
||||
"failure_stage": "session_missing",
|
||||
"error_type": "session_missing",
|
||||
"product_client_id": originator().value,
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reducer_ingests_external_agent_config_import_failure_fact() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::ExternalAgentConfigImportFailure(
|
||||
ExternalAgentConfigImportFailureInput {
|
||||
import_id: "import-1".to_string(),
|
||||
source: "app_server".to_string(),
|
||||
item_type: "SESSIONS".to_string(),
|
||||
failure_stage: "session_missing".to_string(),
|
||||
error_type: "session_missing".to_string(),
|
||||
},
|
||||
)),
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
|
||||
let payload = serde_json::to_value(&events).expect("serialize events");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!([{
|
||||
"event_type": "codex_onboarding_external_agent_import_failure",
|
||||
"event_params": {
|
||||
"import_id": "import-1",
|
||||
"source": "app_server",
|
||||
"type": "SESSIONS",
|
||||
"failure_stage": "session_missing",
|
||||
"error_type": "session_missing",
|
||||
"product_client_id": originator().value,
|
||||
}
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::TurnEvent(Box::new(CodexTurnEventRequest {
|
||||
|
||||
@@ -11,8 +11,11 @@ use crate::facts::AppMentionedInput;
|
||||
use crate::facts::AppUsedInput;
|
||||
use crate::facts::CodexGoalEvent;
|
||||
use crate::facts::CustomAnalyticsFact;
|
||||
use crate::facts::ExternalAgentConfigImportCompletedInput;
|
||||
use crate::facts::ExternalAgentConfigImportFailureInput;
|
||||
use crate::facts::HookRunFact;
|
||||
use crate::facts::HookRunInput;
|
||||
use crate::facts::PluginInstallFailedInput;
|
||||
use crate::facts::PluginState;
|
||||
use crate::facts::PluginStateChangedInput;
|
||||
use crate::facts::SkillInvocation;
|
||||
@@ -343,6 +346,33 @@ impl AnalyticsEventsClient {
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_plugin_install_failed(&self, plugin: PluginTelemetryMetadata, error_type: String) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::PluginInstallFailed(PluginInstallFailedInput {
|
||||
plugin,
|
||||
error_type,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_external_agent_config_import_completed(
|
||||
&self,
|
||||
input: ExternalAgentConfigImportCompletedInput,
|
||||
) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::ExternalAgentConfigImportCompleted(input),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_external_agent_config_import_failure(
|
||||
&self,
|
||||
input: ExternalAgentConfigImportFailureInput,
|
||||
) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::ExternalAgentConfigImportFailure(input),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_plugin_uninstalled(&self, plugin: PluginTelemetryMetadata) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput {
|
||||
|
||||
@@ -83,6 +83,9 @@ pub(crate) enum TrackEventRequest {
|
||||
PluginUninstalled(CodexPluginEventRequest),
|
||||
PluginEnabled(CodexPluginEventRequest),
|
||||
PluginDisabled(CodexPluginEventRequest),
|
||||
PluginInstallFailed(CodexPluginInstallFailedEventRequest),
|
||||
ExternalAgentConfigImportCompleted(CodexOnboardingExternalAgentImportCompleteEventRequest),
|
||||
ExternalAgentConfigImportFailure(CodexOnboardingExternalAgentImportFailureEventRequest),
|
||||
}
|
||||
|
||||
impl TrackEventRequest {
|
||||
@@ -954,6 +957,53 @@ pub(crate) struct CodexPluginEventRequest {
|
||||
pub(crate) event_params: CodexPluginMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginInstallFailedMetadata {
|
||||
#[serde(flatten)]
|
||||
pub(crate) plugin: CodexPluginMetadata,
|
||||
pub(crate) error_type: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginInstallFailedEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexPluginInstallFailedMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexOnboardingExternalAgentImportCompleteMetadata {
|
||||
pub(crate) import_id: String,
|
||||
pub(crate) source: String,
|
||||
#[serde(rename = "type")]
|
||||
pub(crate) item_type: String,
|
||||
pub(crate) success_count: usize,
|
||||
pub(crate) failed_count: usize,
|
||||
pub(crate) product_client_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexOnboardingExternalAgentImportCompleteEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexOnboardingExternalAgentImportCompleteMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexOnboardingExternalAgentImportFailureMetadata {
|
||||
pub(crate) import_id: String,
|
||||
pub(crate) source: String,
|
||||
#[serde(rename = "type")]
|
||||
pub(crate) item_type: String,
|
||||
pub(crate) failure_stage: String,
|
||||
pub(crate) error_type: String,
|
||||
pub(crate) product_client_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexOnboardingExternalAgentImportFailureEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexOnboardingExternalAgentImportFailureMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginUsedEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
|
||||
@@ -504,6 +504,9 @@ pub(crate) enum CustomAnalyticsFact {
|
||||
HookRun(HookRunInput),
|
||||
PluginUsed(PluginUsedInput),
|
||||
PluginStateChanged(PluginStateChangedInput),
|
||||
PluginInstallFailed(PluginInstallFailedInput),
|
||||
ExternalAgentConfigImportCompleted(ExternalAgentConfigImportCompletedInput),
|
||||
ExternalAgentConfigImportFailure(ExternalAgentConfigImportFailureInput),
|
||||
}
|
||||
|
||||
pub(crate) struct SkillInvokedInput {
|
||||
@@ -542,6 +545,27 @@ pub(crate) struct PluginStateChangedInput {
|
||||
pub state: PluginState,
|
||||
}
|
||||
|
||||
pub(crate) struct PluginInstallFailedInput {
|
||||
pub plugin: PluginTelemetryMetadata,
|
||||
pub error_type: String,
|
||||
}
|
||||
|
||||
pub struct ExternalAgentConfigImportCompletedInput {
|
||||
pub import_id: String,
|
||||
pub source: String,
|
||||
pub item_type: String,
|
||||
pub success_count: usize,
|
||||
pub failed_count: usize,
|
||||
}
|
||||
|
||||
pub struct ExternalAgentConfigImportFailureInput {
|
||||
pub import_id: String,
|
||||
pub source: String,
|
||||
pub item_type: String,
|
||||
pub failure_stage: String,
|
||||
pub error_type: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum PluginState {
|
||||
Installed,
|
||||
|
||||
@@ -36,6 +36,8 @@ pub use facts::CompactionReason;
|
||||
pub use facts::CompactionStatus;
|
||||
pub use facts::CompactionStrategy;
|
||||
pub use facts::CompactionTrigger;
|
||||
pub use facts::ExternalAgentConfigImportCompletedInput;
|
||||
pub use facts::ExternalAgentConfigImportFailureInput;
|
||||
pub use facts::GoalEventKind;
|
||||
pub use facts::HookRunFact;
|
||||
pub use facts::InputError;
|
||||
|
||||
@@ -21,7 +21,13 @@ use crate::events::CodexImageGenerationEventParams;
|
||||
use crate::events::CodexImageGenerationEventRequest;
|
||||
use crate::events::CodexMcpToolCallEventParams;
|
||||
use crate::events::CodexMcpToolCallEventRequest;
|
||||
use crate::events::CodexOnboardingExternalAgentImportCompleteEventRequest;
|
||||
use crate::events::CodexOnboardingExternalAgentImportCompleteMetadata;
|
||||
use crate::events::CodexOnboardingExternalAgentImportFailureEventRequest;
|
||||
use crate::events::CodexOnboardingExternalAgentImportFailureMetadata;
|
||||
use crate::events::CodexPluginEventRequest;
|
||||
use crate::events::CodexPluginInstallFailedEventRequest;
|
||||
use crate::events::CodexPluginInstallFailedMetadata;
|
||||
use crate::events::CodexPluginUsedEventRequest;
|
||||
use crate::events::CodexReviewEventParams;
|
||||
use crate::events::CodexReviewEventRequest;
|
||||
@@ -66,7 +72,10 @@ use crate::facts::AppUsedInput;
|
||||
use crate::facts::CodexCompactionEvent;
|
||||
use crate::facts::CodexGoalEvent;
|
||||
use crate::facts::CustomAnalyticsFact;
|
||||
use crate::facts::ExternalAgentConfigImportCompletedInput;
|
||||
use crate::facts::ExternalAgentConfigImportFailureInput;
|
||||
use crate::facts::HookRunInput;
|
||||
use crate::facts::PluginInstallFailedInput;
|
||||
use crate::facts::PluginState;
|
||||
use crate::facts::PluginStateChangedInput;
|
||||
use crate::facts::PluginUsedInput;
|
||||
@@ -511,6 +520,15 @@ impl AnalyticsReducer {
|
||||
CustomAnalyticsFact::PluginStateChanged(input) => {
|
||||
self.ingest_plugin_state_changed(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::PluginInstallFailed(input) => {
|
||||
self.ingest_plugin_install_failed(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::ExternalAgentConfigImportCompleted(input) => {
|
||||
self.ingest_external_agent_config_import_completed(input, out);
|
||||
}
|
||||
CustomAnalyticsFact::ExternalAgentConfigImportFailure(input) => {
|
||||
self.ingest_external_agent_config_import_failure(input, out);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -775,6 +793,63 @@ impl AnalyticsReducer {
|
||||
});
|
||||
}
|
||||
|
||||
fn ingest_plugin_install_failed(
|
||||
&mut self,
|
||||
input: PluginInstallFailedInput,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let PluginInstallFailedInput { plugin, error_type } = input;
|
||||
out.push(TrackEventRequest::PluginInstallFailed(
|
||||
CodexPluginInstallFailedEventRequest {
|
||||
event_type: "codex_plugin_install_failed",
|
||||
event_params: CodexPluginInstallFailedMetadata {
|
||||
plugin: codex_plugin_metadata(plugin),
|
||||
error_type,
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn ingest_external_agent_config_import_completed(
|
||||
&mut self,
|
||||
input: ExternalAgentConfigImportCompletedInput,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
out.push(TrackEventRequest::ExternalAgentConfigImportCompleted(
|
||||
CodexOnboardingExternalAgentImportCompleteEventRequest {
|
||||
event_type: "codex_onboarding_external_agent_import_complete",
|
||||
event_params: CodexOnboardingExternalAgentImportCompleteMetadata {
|
||||
import_id: input.import_id,
|
||||
source: input.source,
|
||||
item_type: input.item_type,
|
||||
success_count: input.success_count,
|
||||
failed_count: input.failed_count,
|
||||
product_client_id: Some(originator().value),
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn ingest_external_agent_config_import_failure(
|
||||
&mut self,
|
||||
input: ExternalAgentConfigImportFailureInput,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
out.push(TrackEventRequest::ExternalAgentConfigImportFailure(
|
||||
CodexOnboardingExternalAgentImportFailureEventRequest {
|
||||
event_type: "codex_onboarding_external_agent_import_failure",
|
||||
event_params: CodexOnboardingExternalAgentImportFailureMetadata {
|
||||
import_id: input.import_id,
|
||||
source: input.source,
|
||||
item_type: input.item_type,
|
||||
failure_stage: input.failure_stage,
|
||||
error_type: input.error_type,
|
||||
product_client_id: Some(originator().value),
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
async fn ingest_response(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
|
||||
Reference in New Issue
Block a user