This commit is contained in:
Roy Han
2026-03-25 23:02:02 -07:00
6 changed files with 218 additions and 94 deletions
+110 -60
View File
@@ -11,11 +11,11 @@ use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SkillScope;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::SubmissionType;
use serde::Serialize;
use sha1::Digest;
use sha1::Sha1;
@@ -37,7 +37,7 @@ pub struct TrackEventsContext {
#[derive(Clone)]
pub struct CodexTurnEvent {
pub submission_type: Option<SubmissionType>,
pub submission_type: Option<TurnSubmissionType>,
pub model_provider: String,
pub sandbox_policy: SandboxPolicy,
pub reasoning_effort: Option<ReasoningEffort>,
@@ -50,18 +50,40 @@ pub struct CodexTurnEvent {
pub personality: Option<Personality>,
pub num_input_images: usize,
pub is_first_turn: bool,
pub status: Option<TurnStatus>,
pub turn_error: Option<CodexErrorInfo>,
pub steer_count: Option<usize>,
pub total_tool_call_count: Option<usize>,
pub shell_command_count: Option<usize>,
pub file_change_count: Option<usize>,
pub mcp_tool_call_count: Option<usize>,
pub dynamic_tool_call_count: Option<usize>,
pub subagent_tool_call_count: Option<usize>,
pub web_search_count: Option<usize>,
pub image_generation_count: Option<usize>,
pub input_tokens: Option<i64>,
pub cached_input_tokens: Option<i64>,
pub output_tokens: Option<i64>,
pub reasoning_output_tokens: Option<i64>,
pub total_tokens: Option<i64>,
pub duration_ms: Option<u64>,
pub started_at: Option<u64>,
pub completed_at: Option<u64>,
}
#[derive(Clone)]
pub struct CodexThreadInitializedEvent {
pub thread_id: String,
pub model: String,
pub ephemeral: bool,
pub session_source: SessionSource,
pub initialization_mode: InitializationMode,
pub subagent_source: Option<SubAgentSource>,
pub parent_thread_id: Option<String>,
pub created_at: u64,
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TurnSubmissionType {
Default,
Queued,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TurnStatus {
Completed,
Failed,
Interrupted,
}
#[derive(Clone)]
@@ -124,7 +146,7 @@ pub struct AppInvocation {
}
pub enum AnalyticsInput {
ThreadInitialized(CodexThreadInitializedInput),
CodexThreadInitialized(CodexThreadInitializedInput),
TurnEvent(TurnEventInput),
SkillInvoked(SkillInvokedInput),
AppMentioned(AppMentionedInput),
@@ -176,7 +198,7 @@ pub struct AnalyticsReducer {
}
struct ThreadState {
_initialized_event: CodexThreadInitializedEvent,
_initialized_input: CodexThreadInitializedInput,
}
#[derive(Clone)]
@@ -274,7 +296,7 @@ impl AnalyticsEventsClient {
}
pub fn track_thread_initialized(&self, input: CodexThreadInitializedInput) {
self.record(AnalyticsInput::ThreadInitialized(input));
self.record(AnalyticsInput::CodexThreadInitialized(input));
}
pub fn track_app_mentioned(&self, tracking: TrackEventsContext, mentions: Vec<AppInvocation>) {
@@ -368,10 +390,10 @@ struct TrackEventsRequest {
#[serde(untagged)]
enum TrackEventRequest {
SkillInvocation(SkillInvocationEventRequest),
ThreadInitialized(CodexThreadInitializedEventRequest),
CodexThreadInitialized(CodexThreadInitializedEvent),
AppMentioned(CodexAppMentionedEventRequest),
AppUsed(CodexAppUsedEventRequest),
TurnEvent(CodexTurnEventRequest),
TurnEvent(Box<CodexTurnEventRequest>),
PluginUsed(CodexPluginUsedEventRequest),
PluginInstalled(CodexPluginEventRequest),
PluginUninstalled(CodexPluginEventRequest),
@@ -411,7 +433,7 @@ struct CodexThreadInitializedEventParams {
}
#[derive(Serialize)]
struct CodexThreadInitializedEventRequest {
struct CodexThreadInitializedEvent {
event_type: &'static str,
event_params: CodexThreadInitializedEventParams,
}
@@ -444,8 +466,8 @@ struct CodexTurnEventParams {
thread_id: String,
turn_id: String,
product_client_id: Option<String>,
submission_type: Option<TurnSubmissionType>,
model: Option<String>,
submission_type: Option<SubmissionType>,
model_provider: String,
sandbox_policy: Option<&'static str>,
reasoning_effort: Option<String>,
@@ -458,6 +480,25 @@ struct CodexTurnEventParams {
personality: Option<String>,
num_input_images: usize,
is_first_turn: bool,
status: Option<TurnStatus>,
turn_error: Option<CodexErrorInfo>,
steer_count: Option<usize>,
total_tool_call_count: Option<usize>,
shell_command_count: Option<usize>,
file_change_count: Option<usize>,
mcp_tool_call_count: Option<usize>,
dynamic_tool_call_count: Option<usize>,
subagent_tool_call_count: Option<usize>,
web_search_count: Option<usize>,
image_generation_count: Option<usize>,
input_tokens: Option<i64>,
cached_input_tokens: Option<i64>,
output_tokens: Option<i64>,
reasoning_output_tokens: Option<i64>,
total_tokens: Option<i64>,
duration_ms: Option<u64>,
started_at: Option<u64>,
completed_at: Option<u64>,
}
#[derive(Serialize)]
@@ -501,7 +542,7 @@ struct CodexPluginUsedEventRequest {
impl AnalyticsReducer {
async fn ingest(&mut self, input: AnalyticsInput, out: &mut Vec<TrackEventRequest>) {
match input {
AnalyticsInput::ThreadInitialized(input) => {
AnalyticsInput::CodexThreadInitialized(input) => {
self.ingest_thread_initialized(input, out);
}
AnalyticsInput::TurnEvent(input) => {
@@ -530,25 +571,14 @@ impl AnalyticsReducer {
input: CodexThreadInitializedInput,
out: &mut Vec<TrackEventRequest>,
) {
let product_client_id = input.product_client_id.clone();
let event = CodexThreadInitializedEvent {
thread_id: input.thread_id,
model: input.model,
ephemeral: input.thread_context.ephemeral,
session_source: input.thread_context.session_source,
initialization_mode: input.thread_context.initialization_mode,
subagent_source: input.thread_context.subagent_source,
parent_thread_id: input.thread_context.parent_thread_id,
created_at: input.created_at,
};
self.threads.insert(
event.thread_id.clone(),
input.thread_id.clone(),
ThreadState {
_initialized_event: event.clone(),
_initialized_input: input.clone(),
},
);
out.push(TrackEventRequest::ThreadInitialized(
codex_thread_initialized_event_request(product_client_id, event),
out.push(TrackEventRequest::CodexThreadInitialized(
codex_thread_initialized_event_request(input),
));
}
@@ -557,10 +587,12 @@ impl AnalyticsReducer {
tracking,
turn_event,
} = input;
out.push(TrackEventRequest::TurnEvent(CodexTurnEventRequest {
event_type: "codex_turn_event",
event_params: codex_turn_event_params(&tracking, turn_event),
}));
out.push(TrackEventRequest::TurnEvent(Box::new(
CodexTurnEventRequest {
event_type: "codex_turn_event",
event_params: codex_turn_event_params(&tracking, turn_event),
},
)));
}
async fn ingest_skill_invoked(
@@ -686,8 +718,8 @@ fn codex_turn_event_params(
thread_id: tracking.thread_id.clone(),
turn_id: tracking.turn_id.clone(),
product_client_id: Some(originator().value),
model: Some(tracking.model_slug.clone()),
submission_type: turn_event.submission_type,
model: Some(tracking.model_slug.clone()),
model_provider: turn_event.model_provider,
sandbox_policy: Some(sandbox_policy_mode(&turn_event.sandbox_policy)),
reasoning_effort: turn_event.reasoning_effort.map(|value| value.to_string()),
@@ -703,6 +735,25 @@ fn codex_turn_event_params(
personality: personality_mode(turn_event.personality),
num_input_images: turn_event.num_input_images,
is_first_turn: turn_event.is_first_turn,
status: turn_event.status,
turn_error: turn_event.turn_error,
steer_count: turn_event.steer_count,
total_tool_call_count: turn_event.total_tool_call_count,
shell_command_count: turn_event.shell_command_count,
file_change_count: turn_event.file_change_count,
mcp_tool_call_count: turn_event.mcp_tool_call_count,
dynamic_tool_call_count: turn_event.dynamic_tool_call_count,
subagent_tool_call_count: turn_event.subagent_tool_call_count,
web_search_count: turn_event.web_search_count,
image_generation_count: turn_event.image_generation_count,
input_tokens: turn_event.input_tokens,
cached_input_tokens: turn_event.cached_input_tokens,
output_tokens: turn_event.output_tokens,
reasoning_output_tokens: turn_event.reasoning_output_tokens,
total_tokens: turn_event.total_tokens,
duration_ms: turn_event.duration_ms,
started_at: turn_event.started_at,
completed_at: turn_event.completed_at,
}
}
@@ -735,33 +786,32 @@ fn personality_mode(personality: Option<Personality>) -> Option<String> {
Some(personality) => Some(personality.to_string()),
}
}
fn codex_thread_initialized_event_request(
product_client_id: String,
thread_event: CodexThreadInitializedEvent,
) -> CodexThreadInitializedEventRequest {
CodexThreadInitializedEventRequest {
input: CodexThreadInitializedInput,
) -> CodexThreadInitializedEvent {
CodexThreadInitializedEvent {
event_type: "codex_thread_initialized",
event_params: codex_thread_initialized_event_params_with_product_client_id(
product_client_id,
thread_event,
),
event_params: codex_thread_initialized_event_params(input),
}
}
fn codex_thread_initialized_event_params_with_product_client_id(
product_client_id: String,
thread_event: CodexThreadInitializedEvent,
fn codex_thread_initialized_event_params(
input: CodexThreadInitializedInput,
) -> CodexThreadInitializedEventParams {
CodexThreadInitializedEventParams {
thread_id: thread_event.thread_id,
product_client_id,
model: thread_event.model,
ephemeral: thread_event.ephemeral,
session_source: session_source_name(&thread_event.session_source),
initialization_mode: thread_event.initialization_mode,
subagent_source: thread_event.subagent_source.map(subagent_source_name),
parent_thread_id: thread_event.parent_thread_id,
created_at: thread_event.created_at,
thread_id: input.thread_id,
product_client_id: input.product_client_id,
model: input.model,
ephemeral: input.thread_context.ephemeral,
session_source: session_source_name(&input.thread_context.session_source),
initialization_mode: input.thread_context.initialization_mode,
subagent_source: input
.thread_context
.subagent_source
.map(subagent_source_name),
parent_thread_id: input.thread_context.parent_thread_id,
created_at: input.created_at,
}
}
@@ -4,14 +4,15 @@ use super::CodexAppMentionedEventRequest;
use super::CodexAppUsedEventRequest;
use super::CodexPluginEventRequest;
use super::CodexPluginUsedEventRequest;
use super::CodexThreadInitializedEvent;
use super::CodexThreadContext;
use super::CodexThreadInitializedInput;
use super::CodexTurnEvent;
use super::CodexTurnEventRequest;
use super::InitializationMode;
use super::InvocationType;
use super::SubmissionType;
use super::TrackEventRequest;
use super::TrackEventsContext;
use super::TurnSubmissionType;
use super::codex_app_metadata;
use super::codex_plugin_metadata;
use super::codex_plugin_used_metadata;
@@ -210,12 +211,12 @@ fn turn_event_serializes_expected_shape() {
thread_id: "thread-2".to_string(),
turn_id: "turn-2".to_string(),
};
let event = TrackEventRequest::TurnEvent(CodexTurnEventRequest {
let event = TrackEventRequest::TurnEvent(Box::new(CodexTurnEventRequest {
event_type: "codex_turn_event",
event_params: codex_turn_event_params(
&tracking,
CodexTurnEvent {
submission_type: Some(SubmissionType::Prompt),
submission_type: Some(TurnSubmissionType::Default),
model_provider: "openai".to_string(),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
reasoning_effort: Some(ReasoningEffort::High),
@@ -228,9 +229,28 @@ fn turn_event_serializes_expected_shape() {
personality: Some(Personality::Pragmatic),
num_input_images: 2,
is_first_turn: true,
status: None,
turn_error: None,
steer_count: None,
total_tool_call_count: None,
shell_command_count: None,
file_change_count: None,
mcp_tool_call_count: None,
dynamic_tool_call_count: None,
subagent_tool_call_count: None,
web_search_count: None,
image_generation_count: None,
input_tokens: None,
cached_input_tokens: None,
output_tokens: None,
reasoning_output_tokens: None,
total_tokens: None,
duration_ms: None,
started_at: None,
completed_at: None,
},
),
});
}));
let payload = serde_json::to_value(&event).expect("serialize turn event");
@@ -243,7 +263,7 @@ fn turn_event_serializes_expected_shape() {
"turn_id": "turn-2",
"product_client_id": originator().value,
"model": "gpt-5",
"submission_type": "prompt",
"submission_type": "default",
"model_provider": "openai",
"sandbox_policy": "read_only",
"reasoning_effort": "high",
@@ -255,7 +275,26 @@ fn turn_event_serializes_expected_shape() {
"collaboration_mode": "plan",
"personality": "pragmatic",
"num_input_images": 2,
"is_first_turn": true
"is_first_turn": true,
"status": null,
"turn_error": null,
"steer_count": null,
"total_tool_call_count": null,
"shell_command_count": null,
"file_change_count": null,
"mcp_tool_call_count": null,
"dynamic_tool_call_count": null,
"subagent_tool_call_count": null,
"web_search_count": null,
"image_generation_count": null,
"input_tokens": null,
"cached_input_tokens": null,
"output_tokens": null,
"reasoning_output_tokens": null,
"total_tokens": null,
"duration_ms": null,
"started_at": null,
"completed_at": null
}
})
);
@@ -263,17 +302,19 @@ fn turn_event_serializes_expected_shape() {
#[test]
fn thread_initialized_event_serializes_expected_shape() {
let event = TrackEventRequest::ThreadInitialized(codex_thread_initialized_event_request(
originator().value,
CodexThreadInitializedEvent {
let event = TrackEventRequest::CodexThreadInitialized(codex_thread_initialized_event_request(
CodexThreadInitializedInput {
thread_id: "thread-0".to_string(),
model: "gpt-5".to_string(),
ephemeral: true,
session_source: SessionSource::Exec,
initialization_mode: InitializationMode::New,
subagent_source: None,
parent_thread_id: None,
product_client_id: originator().value,
created_at: 1_716_000_000,
thread_context: CodexThreadContext {
ephemeral: true,
session_source: SessionSource::Exec,
initialization_mode: InitializationMode::New,
subagent_source: None,
parent_thread_id: None,
},
},
));
@@ -300,17 +341,19 @@ fn thread_initialized_event_serializes_expected_shape() {
#[test]
fn thread_initialized_event_serializes_subagent_source() {
let event = TrackEventRequest::ThreadInitialized(codex_thread_initialized_event_request(
originator().value,
CodexThreadInitializedEvent {
let event = TrackEventRequest::CodexThreadInitialized(codex_thread_initialized_event_request(
CodexThreadInitializedInput {
thread_id: "thread-1".to_string(),
model: "gpt-5".to_string(),
ephemeral: false,
session_source: SessionSource::SubAgent(SubAgentSource::Review),
initialization_mode: InitializationMode::New,
subagent_source: Some(SubAgentSource::Review),
parent_thread_id: None,
product_client_id: originator().value,
created_at: 1,
thread_context: CodexThreadContext {
ephemeral: false,
session_source: SessionSource::SubAgent(SubAgentSource::Review),
initialization_mode: InitializationMode::New,
subagent_source: Some(SubAgentSource::Review),
parent_thread_id: None,
},
},
));
@@ -322,17 +365,19 @@ fn thread_initialized_event_serializes_subagent_source() {
#[test]
fn thread_initialized_event_omits_non_user_non_subagent_session_source() {
let event = TrackEventRequest::ThreadInitialized(codex_thread_initialized_event_request(
originator().value,
CodexThreadInitializedEvent {
let event = TrackEventRequest::CodexThreadInitialized(codex_thread_initialized_event_request(
CodexThreadInitializedInput {
thread_id: "thread-2".to_string(),
model: "gpt-5".to_string(),
ephemeral: false,
session_source: SessionSource::Mcp,
initialization_mode: InitializationMode::New,
subagent_source: None,
parent_thread_id: None,
product_client_id: originator().value,
created_at: 1,
thread_context: CodexThreadContext {
ephemeral: false,
session_source: SessionSource::Mcp,
initialization_mode: InitializationMode::New,
subagent_source: None,
parent_thread_id: None,
},
},
));
+2 -1
View File
@@ -7,7 +7,6 @@ pub use analytics_client::AppInvocation;
pub use analytics_client::AppMentionedInput;
pub use analytics_client::AppUsedInput;
pub use analytics_client::CodexThreadContext;
pub use analytics_client::CodexThreadInitializedEvent;
pub use analytics_client::CodexThreadInitializedInput;
pub use analytics_client::CodexTurnEvent;
pub use analytics_client::InitializationMode;
@@ -18,4 +17,6 @@ pub use analytics_client::PluginUsedInput;
pub use analytics_client::SkillInvocation;
pub use analytics_client::SkillInvokedInput;
pub use analytics_client::TrackEventsContext;
pub use analytics_client::TurnStatus;
pub use analytics_client::TurnSubmissionType;
pub use analytics_client::build_track_events_context;
+29 -1
View File
@@ -357,6 +357,7 @@ use codex_analytics::CodexThreadInitializedInput;
use codex_analytics::CodexTurnEvent;
use codex_analytics::InitializationMode;
use codex_analytics::InvocationType;
use codex_analytics::TurnSubmissionType;
use codex_analytics::build_track_events_context;
use codex_async_utils::OrCancelExt;
use codex_git_utils::get_git_repo_root;
@@ -836,6 +837,13 @@ fn session_source_parent_thread_id(session_source: &SessionSource) -> Option<Str
}
}
fn turn_submission_type(submission_type: SubmissionType) -> TurnSubmissionType {
match submission_type {
SubmissionType::Prompt => TurnSubmissionType::Default,
SubmissionType::QueuedPrompt => TurnSubmissionType::Queued,
}
}
#[cfg(test)]
pub(crate) fn completed_session_loop_termination() -> SessionLoopTermination {
futures::future::ready(()).boxed().shared()
@@ -6169,7 +6177,8 @@ pub(crate) async fn run_turn(
if !input.is_empty() {
let submission_type = turn_context
.submission_type
.unwrap_or(SubmissionType::Prompt);
.map(turn_submission_type)
.unwrap_or(TurnSubmissionType::Default);
let is_first_turn = {
let mut state = sess.state.lock().await;
state.take_next_turn_is_first()
@@ -6195,6 +6204,25 @@ pub(crate) async fn run_turn(
})
.count(),
is_first_turn,
status: None,
turn_error: None,
steer_count: None,
total_tool_call_count: None,
shell_command_count: None,
file_change_count: None,
mcp_tool_call_count: None,
dynamic_tool_call_count: None,
subagent_tool_call_count: None,
web_search_count: None,
image_generation_count: None,
input_tokens: None,
cached_input_tokens: None,
output_tokens: None,
reasoning_output_tokens: None,
total_tokens: None,
duration_ms: None,
started_at: None,
completed_at: None,
},
);
}
+1 -1
View File
@@ -58,6 +58,7 @@ pub static CODEX_ALIASES_TEMP_DIR: TestCodexAliasesGuard = unsafe {
mod abort_tasks;
mod agent_jobs;
mod agent_websocket;
mod analytics;
mod apply_patch_cli;
#[cfg(not(target_os = "windows"))]
mod approvals;
@@ -127,7 +128,6 @@ mod stream_error_allows_next_turn;
mod stream_no_completed;
mod subagent_notifications;
mod text_encoding_fix;
mod thread_metadata;
mod tool_harness;
mod tool_parallelism;
mod tool_suggest;