Add plugin install suggestion outcome analytics

This commit is contained in:
Alex Daley
2026-07-07 18:03:25 -04:00
parent 9deb4f9c86
commit 0d107eade9
9 changed files with 261 additions and 67 deletions
@@ -66,6 +66,11 @@ use crate::facts::PluginInstallRequestSource;
use crate::facts::PluginInstallRequested;
use crate::facts::PluginInstallRequestedInput;
use crate::facts::PluginInstallRequestedPlugin;
use crate::facts::PluginInstallSuggestionOutcome;
use crate::facts::PluginInstallSuggestionOutcomeInput;
use crate::facts::PluginInstallSuggestionOutcomeTool;
use crate::facts::PluginInstallSuggestionResponseAction;
use crate::facts::PluginInstallSuggestionToolType;
use crate::facts::PluginState;
use crate::facts::PluginStateChangedInput;
use crate::facts::PluginUsedInput;
@@ -3680,6 +3685,121 @@ async fn reducer_ingests_plugin_install_requested_fact() {
);
}
#[tokio::test]
async fn reducer_ingests_plugin_install_suggestion_outcome_facts() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
for outcome in [
PluginInstallSuggestionOutcome {
suggestion_id: "request_plugin_install_plugin-1".to_string(),
source: PluginInstallRequestSource::EndpointRecommendation,
tools: vec![
PluginInstallSuggestionOutcomeTool {
tool_type: PluginInstallSuggestionToolType::Plugin,
tool_id: "calendar@openai-curated-remote".to_string(),
tool_name: "Calendar".to_string(),
remote_plugin_id: Some("plugin_calendar".to_string()),
connector_ids: vec!["connector_calendar".to_string()],
selected: true,
},
PluginInstallSuggestionOutcomeTool {
tool_type: PluginInstallSuggestionToolType::Plugin,
tool_id: "github@openai-curated-remote".to_string(),
tool_name: "GitHub".to_string(),
remote_plugin_id: None,
connector_ids: vec!["connector_github".to_string()],
selected: false,
},
],
response_action: PluginInstallSuggestionResponseAction::Accept,
user_confirmed: true,
completed: true,
},
PluginInstallSuggestionOutcome {
suggestion_id: "request_plugin_install_connector-1".to_string(),
source: PluginInstallRequestSource::LegacyDiscovery,
tools: vec![PluginInstallSuggestionOutcomeTool {
tool_type: PluginInstallSuggestionToolType::Connector,
tool_id: "connector_drive".to_string(),
tool_name: "Drive".to_string(),
remote_plugin_id: None,
connector_ids: vec!["connector_drive".to_string()],
selected: false,
}],
response_action: PluginInstallSuggestionResponseAction::Decline,
user_confirmed: false,
completed: false,
},
] {
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::PluginInstallSuggestionOutcome(
PluginInstallSuggestionOutcomeInput {
tracking: test_tracking_context("thread-1", "turn-1"),
outcome,
},
)),
&mut events,
)
.await;
}
assert_eq!(
serde_json::to_value(&events).expect("serialize events"),
json!([{
"event_type": "codex_plugin_install_suggestion_outcome",
"event_params": {
"suggestion_id": "request_plugin_install_plugin-1",
"source": "endpoint_recommendation",
"tools": [{
"tool_type": "plugin",
"tool_id": "calendar@openai-curated-remote",
"tool_name": "Calendar",
"remote_plugin_id": "plugin_calendar",
"connector_ids": ["connector_calendar"],
"selected": true,
}, {
"tool_type": "plugin",
"tool_id": "github@openai-curated-remote",
"tool_name": "GitHub",
"remote_plugin_id": null,
"connector_ids": ["connector_github"],
"selected": false,
}],
"response_action": "accept",
"user_confirmed": true,
"completed": true,
"thread_id": "thread-1",
"turn_id": "turn-1",
"model_slug": "gpt-5",
"product_client_id": TEST_PRODUCT_CLIENT_ID,
}
}, {
"event_type": "codex_plugin_install_suggestion_outcome",
"event_params": {
"suggestion_id": "request_plugin_install_connector-1",
"source": "legacy_discovery",
"tools": [{
"tool_type": "connector",
"tool_id": "connector_drive",
"tool_name": "Drive",
"remote_plugin_id": null,
"connector_ids": ["connector_drive"],
"selected": false,
}],
"response_action": "decline",
"user_confirmed": false,
"completed": false,
"thread_id": "thread-1",
"turn_id": "turn-1",
"model_slug": "gpt-5",
"product_client_id": TEST_PRODUCT_CLIENT_ID,
}
}])
);
}
#[tokio::test]
async fn reducer_ingests_plugin_install_failed_fact() {
let mut reducer = AnalyticsReducer::default();
+14
View File
@@ -18,6 +18,8 @@ use crate::facts::HookRunInput;
use crate::facts::PluginInstallFailedInput;
use crate::facts::PluginInstallRequested;
use crate::facts::PluginInstallRequestedInput;
use crate::facts::PluginInstallSuggestionOutcome;
use crate::facts::PluginInstallSuggestionOutcomeInput;
use crate::facts::PluginState;
use crate::facts::PluginStateChangedInput;
use crate::facts::SkillInvocation;
@@ -325,6 +327,18 @@ impl AnalyticsEventsClient {
));
}
pub fn track_plugin_install_suggestion_outcome(
&self,
tracking: TrackEventsContext,
outcome: PluginInstallSuggestionOutcome,
) {
self.record_fact(AnalyticsFact::Custom(
CustomAnalyticsFact::PluginInstallSuggestionOutcome(
PluginInstallSuggestionOutcomeInput { tracking, outcome },
),
));
}
pub fn track_compaction(&self, event: crate::facts::CodexCompactionEvent) {
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::Compaction(
Box::new(event),
+61
View File
@@ -15,6 +15,7 @@ use crate::facts::GoalEventKind;
use crate::facts::HookRunFact;
use crate::facts::InvocationType;
use crate::facts::PluginInstallRequested;
use crate::facts::PluginInstallSuggestionOutcome;
use crate::facts::PluginState;
use crate::facts::SubAgentThreadStartedInput;
use crate::facts::ThreadInitializationMode;
@@ -82,6 +83,7 @@ pub(crate) enum TrackEventRequest {
ReviewEvent(CodexReviewEventRequest),
PluginUsed(CodexPluginUsedEventRequest),
PluginInstallRequested(CodexPluginInstallRequestedEventRequest),
PluginInstallSuggestionOutcome(CodexPluginInstallSuggestionOutcomeEventRequest),
PluginInstalled(CodexPluginEventRequest),
PluginUninstalled(CodexPluginEventRequest),
PluginEnabled(CodexPluginEventRequest),
@@ -981,6 +983,36 @@ pub(crate) struct CodexPluginInstallRequestedEventRequest {
pub(crate) event_params: CodexPluginInstallRequestedMetadata,
}
#[derive(Serialize)]
pub(crate) struct CodexPluginInstallSuggestionOutcomeToolMetadata {
pub(crate) tool_type: crate::facts::PluginInstallSuggestionToolType,
pub(crate) tool_id: String,
pub(crate) tool_name: String,
pub(crate) remote_plugin_id: Option<String>,
pub(crate) connector_ids: Vec<String>,
pub(crate) selected: bool,
}
#[derive(Serialize)]
pub(crate) struct CodexPluginInstallSuggestionOutcomeMetadata {
pub(crate) suggestion_id: String,
pub(crate) source: crate::facts::PluginInstallRequestSource,
pub(crate) tools: Vec<CodexPluginInstallSuggestionOutcomeToolMetadata>,
pub(crate) response_action: crate::facts::PluginInstallSuggestionResponseAction,
pub(crate) user_confirmed: bool,
pub(crate) completed: bool,
pub(crate) thread_id: String,
pub(crate) turn_id: String,
pub(crate) model_slug: String,
pub(crate) product_client_id: Option<String>,
}
#[derive(Serialize)]
pub(crate) struct CodexPluginInstallSuggestionOutcomeEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexPluginInstallSuggestionOutcomeMetadata,
}
#[derive(Serialize)]
pub(crate) struct CodexPluginEventRequest {
pub(crate) event_type: &'static str,
@@ -1125,6 +1157,35 @@ pub(crate) fn codex_plugin_install_requested_metadata(
}
}
pub(crate) fn codex_plugin_install_suggestion_outcome_metadata(
tracking: &TrackEventsContext,
outcome: PluginInstallSuggestionOutcome,
) -> CodexPluginInstallSuggestionOutcomeMetadata {
CodexPluginInstallSuggestionOutcomeMetadata {
suggestion_id: outcome.suggestion_id,
source: outcome.source,
tools: outcome
.tools
.into_iter()
.map(|tool| CodexPluginInstallSuggestionOutcomeToolMetadata {
tool_type: tool.tool_type,
tool_id: tool.tool_id,
tool_name: tool.tool_name,
remote_plugin_id: tool.remote_plugin_id,
connector_ids: tool.connector_ids,
selected: tool.selected,
})
.collect(),
response_action: outcome.response_action,
user_confirmed: outcome.user_confirmed,
completed: outcome.completed,
thread_id: tracking.thread_id.clone(),
turn_id: tracking.turn_id.clone(),
model_slug: tracking.model_slug.clone(),
product_client_id: Some(tracking.product_client_id.clone()),
}
}
pub(crate) fn codex_compaction_event_params(
input: CodexCompactionEvent,
session_id: String,
+42
View File
@@ -510,6 +510,7 @@ pub(crate) enum CustomAnalyticsFact {
HookRun(HookRunInput),
PluginUsed(PluginUsedInput),
PluginInstallRequested(PluginInstallRequestedInput),
PluginInstallSuggestionOutcome(PluginInstallSuggestionOutcomeInput),
PluginStateChanged(PluginStateChangedInput),
PluginInstallFailed(PluginInstallFailedInput),
ExternalAgentConfigImportCompleted(ExternalAgentConfigImportCompletedInput),
@@ -574,6 +575,47 @@ pub(crate) struct PluginInstallRequestedInput {
pub request: PluginInstallRequested,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PluginInstallSuggestionToolType {
Connector,
Plugin,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PluginInstallSuggestionResponseAction {
Accept,
Decline,
Cancel,
Unavailable,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginInstallSuggestionOutcome {
pub suggestion_id: String,
pub source: PluginInstallRequestSource,
pub tools: Vec<PluginInstallSuggestionOutcomeTool>,
pub response_action: PluginInstallSuggestionResponseAction,
pub user_confirmed: bool,
pub completed: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginInstallSuggestionOutcomeTool {
pub tool_type: PluginInstallSuggestionToolType,
pub tool_id: String,
pub tool_name: String,
pub remote_plugin_id: Option<String>,
pub connector_ids: Vec<String>,
pub selected: bool,
}
pub(crate) struct PluginInstallSuggestionOutcomeInput {
pub tracking: TrackEventsContext,
pub outcome: PluginInstallSuggestionOutcome,
}
pub(crate) struct PluginStateChangedInput {
pub plugin: PluginTelemetryMetadata,
pub state: PluginState,
+4
View File
@@ -45,6 +45,10 @@ pub use facts::InvocationType;
pub use facts::PluginInstallRequestSource;
pub use facts::PluginInstallRequested;
pub use facts::PluginInstallRequestedPlugin;
pub use facts::PluginInstallSuggestionOutcome;
pub use facts::PluginInstallSuggestionOutcomeTool;
pub use facts::PluginInstallSuggestionResponseAction;
pub use facts::PluginInstallSuggestionToolType;
pub use facts::SkillInvocation;
pub use facts::SubAgentThreadStartedInput;
pub use facts::ThreadInitializationMode;
+20
View File
@@ -29,6 +29,7 @@ use crate::events::CodexPluginEventRequest;
use crate::events::CodexPluginInstallFailedEventRequest;
use crate::events::CodexPluginInstallFailedMetadata;
use crate::events::CodexPluginInstallRequestedEventRequest;
use crate::events::CodexPluginInstallSuggestionOutcomeEventRequest;
use crate::events::CodexPluginUsedEventRequest;
use crate::events::CodexReviewEventParams;
use crate::events::CodexReviewEventRequest;
@@ -62,6 +63,7 @@ use crate::events::codex_compaction_event_params;
use crate::events::codex_goal_event_params;
use crate::events::codex_hook_run_metadata;
use crate::events::codex_plugin_install_requested_metadata;
use crate::events::codex_plugin_install_suggestion_outcome_metadata;
use crate::events::codex_plugin_metadata;
use crate::events::codex_plugin_used_metadata;
use crate::events::plugin_state_event_type;
@@ -79,6 +81,7 @@ use crate::facts::ExternalAgentConfigImportFailureInput;
use crate::facts::HookRunInput;
use crate::facts::PluginInstallFailedInput;
use crate::facts::PluginInstallRequestedInput;
use crate::facts::PluginInstallSuggestionOutcomeInput;
use crate::facts::PluginState;
use crate::facts::PluginStateChangedInput;
use crate::facts::PluginUsedInput;
@@ -539,6 +542,9 @@ impl AnalyticsReducer {
CustomAnalyticsFact::PluginInstallRequested(input) => {
self.ingest_plugin_install_requested(input, out);
}
CustomAnalyticsFact::PluginInstallSuggestionOutcome(input) => {
self.ingest_plugin_install_suggestion_outcome(input, out);
}
CustomAnalyticsFact::PluginStateChanged(input) => {
self.ingest_plugin_state_changed(input, out);
}
@@ -814,6 +820,20 @@ impl AnalyticsReducer {
));
}
fn ingest_plugin_install_suggestion_outcome(
&mut self,
input: PluginInstallSuggestionOutcomeInput,
out: &mut Vec<TrackEventRequest>,
) {
let PluginInstallSuggestionOutcomeInput { tracking, outcome } = input;
out.push(TrackEventRequest::PluginInstallSuggestionOutcome(
CodexPluginInstallSuggestionOutcomeEventRequest {
event_type: "codex_plugin_install_suggestion_outcome",
event_params: codex_plugin_install_suggestion_outcome_metadata(&tracking, outcome),
},
));
}
fn ingest_plugin_state_changed(
&mut self,
input: PluginStateChangedInput,
@@ -9,7 +9,6 @@ use crate::metrics::MetricsClient;
use crate::metrics::MetricsConfig;
use crate::metrics::MetricsError;
use crate::metrics::PLUGIN_INSTALL_ELICITATION_SENT_METRIC;
use crate::metrics::PLUGIN_INSTALL_SUGGESTION_METRIC;
use crate::metrics::RESPONSES_API_ENGINE_IAPI_TBT_DURATION_METRIC;
use crate::metrics::RESPONSES_API_ENGINE_IAPI_TTFT_DURATION_METRIC;
use crate::metrics::RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC;
@@ -279,16 +278,6 @@ impl SessionTelemetry {
user_confirmed: bool,
completed: bool,
) {
let completed_tag = if completed { "true" } else { "false" };
self.counter(
PLUGIN_INSTALL_SUGGESTION_METRIC,
/*inc*/ 1,
&[
("tool_type", tool_type),
("response_action", response_action),
("completed", completed_tag),
],
);
log_and_trace_event!(
self,
common: {
-1
View File
@@ -41,7 +41,6 @@ pub const GOAL_BLOCKED_METRIC: &str = "codex.goal.blocked";
pub const GOAL_TOKEN_COUNT_METRIC: &str = "codex.goal.token_count";
pub const GOAL_DURATION_SECONDS_METRIC: &str = "codex.goal.duration_s";
pub const PLUGIN_INSTALL_ELICITATION_SENT_METRIC: &str = "codex.plugins.install_elicitation.sent";
pub const PLUGIN_INSTALL_SUGGESTION_METRIC: &str = "codex.plugins.install_suggestion";
pub const CURATED_PLUGINS_STARTUP_SYNC_METRIC: &str = "codex.plugins.startup_sync";
pub const CURATED_PLUGINS_STARTUP_SYNC_FINAL_METRIC: &str = "codex.plugins.startup_sync.final";
pub const HOOK_RUN_METRIC: &str = "codex.hooks.run";
@@ -3,7 +3,6 @@ use crate::harness::build_metrics_with_defaults;
use crate::harness::find_metric;
use crate::harness::latest_metrics;
use codex_otel::PLUGIN_INSTALL_ELICITATION_SENT_METRIC;
use codex_otel::PLUGIN_INSTALL_SUGGESTION_METRIC;
use codex_otel::Result;
use codex_otel::SessionTelemetry;
use codex_otel::TelemetryAuthMode;
@@ -164,60 +163,6 @@ fn manager_attaches_optional_service_name_tag() -> Result<()> {
Ok(())
}
#[test]
fn manager_records_plugin_install_suggestion_metric() -> Result<()> {
let (metrics, exporter) = build_metrics_with_defaults(&[])?;
let manager = SessionTelemetry::new(
ThreadId::new(),
"gpt-5.1",
"gpt-5.1",
Some("account-id".to_string()),
/*account_email*/ None,
Some(TelemetryAuthMode::ApiKey),
"test_originator".to_string(),
/*log_user_prompts*/ false,
"tty".to_string(),
SessionSource::Cli,
)
.with_metrics_without_metadata_tags(metrics);
manager.record_plugin_install_suggestion(
"connector",
"connector_calendar",
"Google Calendar",
"accept",
/*user_confirmed*/ true,
/*completed*/ false,
);
manager.shutdown_metrics()?;
let resource_metrics = latest_metrics(&exporter);
let metric = find_metric(&resource_metrics, PLUGIN_INSTALL_SUGGESTION_METRIC)
.expect("plugin install suggestion metric missing");
let attrs = match metric.data() {
AggregatedMetrics::U64(data) => match data {
MetricData::Sum(sum) => {
let points: Vec<_> = sum.data_points().collect();
assert_eq!(points.len(), 1);
attributes_to_map(points[0].attributes())
}
_ => panic!("unexpected counter aggregation"),
},
_ => panic!("unexpected counter data type"),
};
assert_eq!(
attrs,
BTreeMap::from([
("completed".to_string(), "false".to_string()),
("response_action".to_string(), "accept".to_string()),
("tool_type".to_string(), "connector".to_string()),
])
);
Ok(())
}
#[test]
fn manager_records_plugin_install_elicitation_sent_metric() -> Result<()> {
let (metrics, exporter) = build_metrics_with_defaults(&[])?;