mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Propagate turn triggers to usage analytics events (#60018)
## What changed Include `turn_trigger` from turn metadata in tool, artifact, skill, app, plugin usage, and hook analytics events. Omit the field when no trigger is available. ## Testing Extend app serialization and reducer tests to check trigger propagation, and verify that MCP tool events retain the trigger after turn completion. GitOrigin-RevId: 975780983d0761eb34fecf5e8b4abe98a432d0c3
This commit is contained in:
@@ -155,6 +155,7 @@ fn sample_skill_track_event(thread_id: &str, plugin_id: Option<&str>) -> TrackEv
|
||||
skill_id: format!("skill-{thread_id}"),
|
||||
skill_name: "doc".to_string(),
|
||||
event_params: SkillInvocationEventParams {
|
||||
turn_trigger: None,
|
||||
product_client_id: None,
|
||||
skill_scope: None,
|
||||
plugin_id: plugin_id.map(str::to_string),
|
||||
@@ -205,6 +206,7 @@ fn sample_mcp_tool_call_event(thread_id: &str, plugin_id: Option<&str>) -> Track
|
||||
event_type: "codex_mcp_tool_call_event",
|
||||
event_params: CodexMcpToolCallEventParams {
|
||||
base: CodexToolItemEventBase {
|
||||
turn_trigger: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
session_id: format!("session-{thread_id}"),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -261,6 +263,7 @@ fn sample_plugin_used_track_event(thread_id: &str, plugin_id: Option<&str>) -> T
|
||||
TrackEventRequest::PluginUsed(CodexPluginUsedEventRequest {
|
||||
event_type: "codex_plugin_used",
|
||||
event_params: CodexPluginUsedMetadata {
|
||||
turn_trigger: None,
|
||||
plugin: CodexPluginMetadata {
|
||||
plugin_id: plugin_id.map(str::to_string),
|
||||
remote_plugin_id: None,
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::facts::PluginState;
|
||||
use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::ThreadInitializationMode;
|
||||
use crate::facts::TrackEventsContext;
|
||||
use crate::facts::TurnAnalyticsMetadata;
|
||||
use crate::facts::TurnStatus;
|
||||
use crate::facts::TurnSteerRejectionReason;
|
||||
use crate::facts::TurnSteerResult;
|
||||
@@ -108,6 +109,8 @@ pub(crate) enum TrackEventRequest {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexArtifactOperationEventParams {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) turn_trigger: Option<String>,
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) item_id: String,
|
||||
@@ -136,9 +139,11 @@ pub(crate) fn codex_artifact_operation_event_request(
|
||||
tracking: TrackEventsContext,
|
||||
operation: ArtifactOperation,
|
||||
) -> CodexArtifactOperationEventRequest {
|
||||
let turn = tracking.turn_metadata.as_deref();
|
||||
CodexArtifactOperationEventRequest {
|
||||
event_type: "codex_artifact_operation",
|
||||
event_params: CodexArtifactOperationEventParams {
|
||||
turn_trigger: turn.and_then(TurnAnalyticsMetadata::turn_trigger),
|
||||
thread_id: tracking.thread_id,
|
||||
turn_id: tracking.turn_id,
|
||||
item_id: operation.item_id,
|
||||
@@ -209,6 +214,8 @@ pub(crate) struct SkillInvocationEventRequest {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct SkillInvocationEventParams {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) turn_trigger: Option<String>,
|
||||
pub(crate) product_client_id: Option<String>,
|
||||
pub(crate) skill_scope: Option<String>,
|
||||
pub(crate) plugin_id: Option<String>,
|
||||
@@ -682,6 +689,8 @@ pub(crate) enum ToolEventType {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexToolItemEventBase {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) turn_trigger: Option<String>,
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) turn_id: String,
|
||||
@@ -719,6 +728,8 @@ pub(crate) struct CodexToolItemEventBase {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexCuaToolCallStartedEventParams {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) turn_trigger: Option<String>,
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) turn_id: String,
|
||||
@@ -1000,6 +1011,8 @@ pub(crate) struct CodexImageGenerationEventRequest {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexAppMetadata {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) turn_trigger: Option<String>,
|
||||
pub(crate) connector_id: Option<String>,
|
||||
pub(crate) thread_id: Option<String>,
|
||||
pub(crate) turn_id: Option<String>,
|
||||
@@ -1031,6 +1044,8 @@ pub(crate) struct CodexAppUsedEventRequest {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexHookRunMetadata {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) turn_trigger: Option<String>,
|
||||
pub(crate) thread_id: Option<String>,
|
||||
pub(crate) turn_id: Option<String>,
|
||||
pub(crate) product_client_id: Option<String>,
|
||||
@@ -1230,6 +1245,8 @@ pub(crate) struct CodexPluginMetadata {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginUsedMetadata {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) turn_trigger: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub(crate) plugin: CodexPluginMetadata,
|
||||
pub(crate) mcp_server_names: Option<Vec<String>>,
|
||||
@@ -1340,7 +1357,9 @@ pub(crate) fn codex_app_metadata(
|
||||
tracking: &TrackEventsContext,
|
||||
app: AppInvocation,
|
||||
) -> CodexAppMetadata {
|
||||
let turn = tracking.turn_metadata.as_deref();
|
||||
CodexAppMetadata {
|
||||
turn_trigger: turn.and_then(TurnAnalyticsMetadata::turn_trigger),
|
||||
connector_id: app.connector_id,
|
||||
thread_id: Some(tracking.thread_id.clone()),
|
||||
turn_id: Some(tracking.turn_id.clone()),
|
||||
@@ -1487,7 +1506,9 @@ pub(crate) fn codex_plugin_used_metadata(
|
||||
.capability_summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.mcp_server_names.clone());
|
||||
let turn = tracking.turn_metadata.as_deref();
|
||||
CodexPluginUsedMetadata {
|
||||
turn_trigger: turn.and_then(TurnAnalyticsMetadata::turn_trigger),
|
||||
plugin: codex_plugin_metadata_with_product_client_id(
|
||||
plugin,
|
||||
tracking.product_client_id.clone(),
|
||||
@@ -1503,7 +1524,9 @@ pub(crate) fn codex_hook_run_metadata(
|
||||
tracking: &TrackEventsContext,
|
||||
hook: HookRunFact,
|
||||
) -> CodexHookRunMetadata {
|
||||
let turn = tracking.turn_metadata.as_deref();
|
||||
CodexHookRunMetadata {
|
||||
turn_trigger: turn.and_then(TurnAnalyticsMetadata::turn_trigger),
|
||||
thread_id: Some(tracking.thread_id.clone()),
|
||||
turn_id: Some(tracking.turn_id.clone()),
|
||||
product_client_id: Some(tracking.product_client_id.clone()),
|
||||
|
||||
@@ -110,6 +110,7 @@ use crate::facts::SkillInvocationLocation;
|
||||
use crate::facts::SkillInvokedInput;
|
||||
use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::ThreadInitializationMode;
|
||||
use crate::facts::TurnAnalyticsMetadata;
|
||||
use crate::facts::TurnCodexError;
|
||||
use crate::facts::TurnCodexErrorFact;
|
||||
use crate::facts::TurnProfile;
|
||||
@@ -182,6 +183,7 @@ use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
const MAX_TOOL_RESPONSE_ENTRIES: usize = 256;
|
||||
const MAX_RECENT_VOICE_SESSION_WINDOWS: usize = 16;
|
||||
|
||||
@@ -1056,7 +1058,7 @@ impl AnalyticsReducer {
|
||||
self.record_tool_event(
|
||||
&thread_id,
|
||||
&turn_id,
|
||||
turn_metadata.root_turn_id(),
|
||||
Some(turn_metadata.as_ref()),
|
||||
event,
|
||||
ToolEventEmission::AwaitResponse,
|
||||
out,
|
||||
@@ -1133,7 +1135,7 @@ impl AnalyticsReducer {
|
||||
self.record_tool_event(
|
||||
&thread_id,
|
||||
&turn_id,
|
||||
turn_metadata.root_turn_id(),
|
||||
Some(turn_metadata.as_ref()),
|
||||
event,
|
||||
ToolEventEmission::AwaitResponse,
|
||||
out,
|
||||
@@ -1192,7 +1194,7 @@ impl AnalyticsReducer {
|
||||
&mut self,
|
||||
thread_id: &str,
|
||||
turn_id: &str,
|
||||
root_turn_id: Option<String>,
|
||||
turn_metadata: Option<&dyn TurnAnalyticsMetadata>,
|
||||
mut event: TrackEventRequest,
|
||||
emission: ToolEventEmission,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
@@ -1201,7 +1203,8 @@ impl AnalyticsReducer {
|
||||
out.push(event);
|
||||
return;
|
||||
};
|
||||
base.root_turn_id = root_turn_id;
|
||||
base.root_turn_id = turn_metadata.and_then(TurnAnalyticsMetadata::root_turn_id);
|
||||
base.turn_trigger = turn_metadata.and_then(TurnAnalyticsMetadata::turn_trigger);
|
||||
let state = self
|
||||
.tool_response_states
|
||||
.entry((thread_id.to_string(), turn_id.to_string()))
|
||||
@@ -1489,12 +1492,14 @@ impl AnalyticsReducer {
|
||||
(skill_id, skill_scope)
|
||||
}
|
||||
};
|
||||
let turn = tracking.turn_metadata.as_deref();
|
||||
out.push(TrackEventRequest::SkillInvocation(
|
||||
SkillInvocationEventRequest {
|
||||
event_type: "skill_invocation",
|
||||
skill_id,
|
||||
skill_name: invocation.skill_name.clone(),
|
||||
event_params: SkillInvocationEventParams {
|
||||
turn_trigger: turn.and_then(TurnAnalyticsMetadata::turn_trigger),
|
||||
thread_id: Some(tracking.thread_id.clone()),
|
||||
turn_id: Some(tracking.turn_id.clone()),
|
||||
voice_session_id: self
|
||||
@@ -2120,11 +2125,11 @@ impl AnalyticsReducer {
|
||||
.get(&(notification.thread_id.clone(), notification.turn_id.clone())),
|
||||
self.code_mode_cells.get(¬ification.thread_id),
|
||||
);
|
||||
let root_turn_id = self
|
||||
let turn_metadata = self
|
||||
.turns
|
||||
.get(¬ification.turn_id)
|
||||
.and_then(|turn| turn.resolved_config.as_ref())
|
||||
.and_then(|config| config.turn_metadata.root_turn_id());
|
||||
.map(|config| config.turn_metadata.as_ref());
|
||||
out.push(TrackEventRequest::CuaToolCallStarted(
|
||||
CodexCuaToolCallStartedEventRequest {
|
||||
event_type: "codex_cua_tool_call_started",
|
||||
@@ -2132,7 +2137,10 @@ impl AnalyticsReducer {
|
||||
thread_id: notification.thread_id,
|
||||
session_id: thread_metadata.session_id.clone(),
|
||||
turn_id: notification.turn_id,
|
||||
root_turn_id,
|
||||
root_turn_id: turn_metadata
|
||||
.and_then(TurnAnalyticsMetadata::root_turn_id),
|
||||
turn_trigger: turn_metadata
|
||||
.and_then(TurnAnalyticsMetadata::turn_trigger),
|
||||
item_id: item_id.to_string(),
|
||||
cell_id: attribution.cell_id,
|
||||
parent_call_id: attribution.parent_call_id,
|
||||
@@ -2248,11 +2256,11 @@ impl AnalyticsReducer {
|
||||
.get(¬ification.turn_id)
|
||||
.and_then(|turn| turn.voice_session_id.clone());
|
||||
}
|
||||
let root_turn_id = self
|
||||
let turn_metadata = self
|
||||
.turns
|
||||
.get(¬ification.turn_id)
|
||||
.and_then(|turn| turn.resolved_config.as_ref())
|
||||
.and_then(|config| config.turn_metadata.root_turn_id());
|
||||
.map(|config| Arc::clone(&config.turn_metadata));
|
||||
// Fast collaborator tools can complete before their sampling response.
|
||||
// Keep the event until that response can supply its originating ID.
|
||||
let emission = if matches!(
|
||||
@@ -2266,7 +2274,7 @@ impl AnalyticsReducer {
|
||||
self.record_tool_event(
|
||||
¬ification.thread_id,
|
||||
¬ification.turn_id,
|
||||
root_turn_id,
|
||||
turn_metadata.as_deref(),
|
||||
event,
|
||||
emission,
|
||||
out,
|
||||
@@ -3469,6 +3477,7 @@ fn tool_item_base(
|
||||
thread_id: thread_id.to_string(),
|
||||
session_id: thread_metadata.session_id.clone(),
|
||||
turn_id: turn_id.to_string(),
|
||||
turn_trigger: None,
|
||||
root_turn_id: None,
|
||||
item_id,
|
||||
cell_id: None,
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::facts::InvocationType;
|
||||
use crate::reducer::AnalyticsReducer;
|
||||
use crate::tests::support::TEST_PRODUCT_CLIENT_ID;
|
||||
use crate::tests::support::test_tracking_context;
|
||||
use crate::tests::support::test_turn_metadata;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -54,7 +55,12 @@ fn app_mentioned_event_serializes_expected_shape() {
|
||||
|
||||
#[test]
|
||||
fn app_used_event_serializes_expected_shape() {
|
||||
let tracking = test_tracking_context("thread-2", "turn-2");
|
||||
let mut tracking = test_tracking_context("thread-2", "turn-2");
|
||||
let mut metadata = test_turn_metadata(/*root_turn_id*/ None);
|
||||
std::sync::Arc::get_mut(&mut metadata)
|
||||
.expect("unique metadata")
|
||||
.turn_trigger = Some("appServer-goal".to_string());
|
||||
tracking.turn_metadata = Some(metadata);
|
||||
let event = TrackEventRequest::AppUsed(CodexAppUsedEventRequest {
|
||||
event_type: "codex_app_used",
|
||||
event_params: CodexAppUsedMetadata {
|
||||
@@ -78,6 +84,7 @@ fn app_used_event_serializes_expected_shape() {
|
||||
json!({
|
||||
"event_type": "codex_app_used",
|
||||
"event_params": {
|
||||
"turn_trigger": "appServer-goal",
|
||||
"connector_id": "drive",
|
||||
"thread_id": "thread-2",
|
||||
"turn_id": "turn-2",
|
||||
@@ -97,6 +104,14 @@ async fn reducer_ingests_app_facts() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
let tracking = test_tracking_context("thread-1", "turn-1");
|
||||
let mut metadata = test_turn_metadata(/*root_turn_id*/ None);
|
||||
std::sync::Arc::get_mut(&mut metadata)
|
||||
.expect("unique metadata")
|
||||
.turn_trigger = Some("automation_cron_run_now".to_string());
|
||||
let tracking = crate::facts::TrackEventsContext {
|
||||
turn_metadata: Some(metadata),
|
||||
..tracking
|
||||
};
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
@@ -149,6 +164,16 @@ async fn reducer_ingests_app_facts() {
|
||||
("codex_app_used", Some(TEST_PRODUCT_CLIENT_ID)),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
mention.event_params.turn_trigger.as_deref(),
|
||||
usage.event_params.app.turn_trigger.as_deref()
|
||||
),
|
||||
(
|
||||
Some("automation_cron_run_now"),
|
||||
Some("automation_cron_run_now")
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
usage.event_params.elicitation_type,
|
||||
Some(ElicitationType::AuthOrLink)
|
||||
|
||||
@@ -105,6 +105,7 @@ fn command_execution_event_serializes_expected_shape() {
|
||||
model_slug: None,
|
||||
reasoning_effort: None,
|
||||
base: CodexToolItemEventBase {
|
||||
turn_trigger: None,
|
||||
thread_id: "thread-1".to_string(),
|
||||
session_id: "session-thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -771,7 +772,10 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c
|
||||
)
|
||||
.await;
|
||||
|
||||
let turn_metadata = test_turn_metadata(/*root_turn_id*/ None);
|
||||
let mut turn_metadata = test_turn_metadata(/*root_turn_id*/ None);
|
||||
std::sync::Arc::get_mut(&mut turn_metadata)
|
||||
.expect("unique metadata")
|
||||
.turn_trigger = Some("automation_cron_scheduled".to_string());
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new(
|
||||
@@ -870,6 +874,12 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c
|
||||
}
|
||||
|
||||
let payload = serde_json::to_value(&events).expect("serialize analytics events");
|
||||
for event in payload.as_array().expect("analytics events array") {
|
||||
assert_eq!(
|
||||
event["event_params"]["turn_trigger"],
|
||||
"automation_cron_scheduled"
|
||||
);
|
||||
}
|
||||
let classifications = payload
|
||||
.as_array()
|
||||
.expect("analytics events array")
|
||||
|
||||
@@ -64,6 +64,7 @@ pub(super) const TEST_PRODUCT_CLIENT_ID: &str = "codex_work_desktop";
|
||||
|
||||
pub(super) struct TestTurnMetadata {
|
||||
pub(super) root_turn_id: Mutex<Option<String>>,
|
||||
pub(super) turn_trigger: Option<String>,
|
||||
}
|
||||
|
||||
impl TurnAnalyticsMetadata for TestTurnMetadata {
|
||||
@@ -72,7 +73,7 @@ impl TurnAnalyticsMetadata for TestTurnMetadata {
|
||||
}
|
||||
|
||||
fn turn_trigger(&self) -> Option<String> {
|
||||
None
|
||||
self.turn_trigger.clone()
|
||||
}
|
||||
|
||||
fn codex_turn_source(&self) -> Option<String> {
|
||||
@@ -83,6 +84,7 @@ impl TurnAnalyticsMetadata for TestTurnMetadata {
|
||||
pub(super) fn test_turn_metadata(root_turn_id: Option<&str>) -> Arc<TestTurnMetadata> {
|
||||
Arc::new(TestTurnMetadata {
|
||||
root_turn_id: Mutex::new(root_turn_id.map(str::to_string)),
|
||||
turn_trigger: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user