mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Add plugin install suggestion outcome analytics
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(&[])?;
|
||||
|
||||
Reference in New Issue
Block a user