Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions codex-rs/analytics/src/analytics_client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -3680,6 +3685,125 @@ 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,
completed: 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: true,
completed: false,
},
],
response_action: PluginInstallSuggestionResponseAction::Accept,
user_confirmed: 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,
completed: false,
}],
response_action: PluginInstallSuggestionResponseAction::Decline,
user_confirmed: 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,
"completed": true,
}, {
"tool_type": "plugin",
"tool_id": "github@openai-curated-remote",
"tool_name": "GitHub",
"remote_plugin_id": null,
"connector_ids": ["connector_github"],
"selected": true,
"completed": 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,
"completed": 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();
Expand Down
14 changes: 14 additions & 0 deletions codex-rs/analytics/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
64 changes: 64 additions & 0 deletions codex-rs/analytics/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -82,6 +83,7 @@ pub(crate) enum TrackEventRequest {
ReviewEvent(CodexReviewEventRequest),
PluginUsed(CodexPluginUsedEventRequest),
PluginInstallRequested(CodexPluginInstallRequestedEventRequest),
PluginInstallSuggestionOutcome(CodexPluginInstallSuggestionOutcomeEventRequest),
PluginInstalled(CodexPluginEventRequest),
PluginUninstalled(CodexPluginEventRequest),
PluginEnabled(CodexPluginEventRequest),
Expand Down Expand Up @@ -981,6 +983,37 @@ 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,
pub(crate) completed: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we land the matching server schema before adding tools[].completed here? the current server request model has no nested completed, so Pydantic will drop this field and partial outcomes will still collapse to the top-level bit.

}

#[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,
Expand Down Expand Up @@ -1125,6 +1158,37 @@ pub(crate) fn codex_plugin_install_requested_metadata(
}
}

pub(crate) fn codex_plugin_install_suggestion_outcome_metadata(
tracking: &TrackEventsContext,
outcome: PluginInstallSuggestionOutcome,
) -> CodexPluginInstallSuggestionOutcomeMetadata {
let completed = outcome.tools.iter().any(|tool| tool.completed);
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,
completed: tool.completed,
})
.collect(),
response_action: outcome.response_action,
user_confirmed: outcome.user_confirmed,
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,
Expand Down
42 changes: 42 additions & 0 deletions codex-rs/analytics/src/facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ pub(crate) enum CustomAnalyticsFact {
HookRun(HookRunInput),
PluginUsed(PluginUsedInput),
PluginInstallRequested(PluginInstallRequestedInput),
PluginInstallSuggestionOutcome(PluginInstallSuggestionOutcomeInput),
PluginStateChanged(PluginStateChangedInput),
PluginInstallFailed(PluginInstallFailedInput),
ExternalAgentConfigImportCompleted(ExternalAgentConfigImportCompletedInput),
Expand Down Expand Up @@ -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,
}

#[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 completed: bool,
}

pub(crate) struct PluginInstallSuggestionOutcomeInput {
pub tracking: TrackEventsContext,
pub outcome: PluginInstallSuggestionOutcome,
}

pub(crate) struct PluginStateChangedInput {
pub plugin: PluginTelemetryMetadata,
pub state: PluginState,
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/analytics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions codex-rs/analytics/src/reducer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading