This commit is contained in:
Roy Han
2026-03-25 14:22:21 -07:00
parent f10cf5c9fa
commit 3a2d6eac2f
5 changed files with 382 additions and 0 deletions
+150
View File
@@ -12,6 +12,7 @@ use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SkillScope;
use codex_protocol::protocol::SubmissionType;
use serde::Serialize;
@@ -32,6 +33,27 @@ pub(crate) struct TrackEventsContext {
pub(crate) turn_id: String,
}
#[derive(Clone)]
pub(crate) struct CodexThreadStartedEvent {
pub(crate) thread_id: String,
pub(crate) model: String,
pub(crate) model_provider: String,
pub(crate) reasoning_effort: Option<ReasoningEffort>,
pub(crate) reasoning_summary: Option<ReasoningSummary>,
pub(crate) service_tier: Option<ServiceTier>,
pub(crate) approval_policy: AskForApproval,
pub(crate) approvals_reviewer: ApprovalsReviewer,
pub(crate) sandbox_policy: SandboxPolicy,
pub(crate) sandbox_network_access: bool,
pub(crate) collaboration_mode: ModeKind,
pub(crate) personality: Option<Personality>,
pub(crate) ephemeral: bool,
pub(crate) session_source: SessionSource,
pub(crate) parent_thread_id: Option<String>,
pub(crate) create_source: ThreadCreateSource,
pub(crate) created_at: u64,
}
#[derive(Clone)]
pub(crate) struct CodexTurnEvent {
pub(crate) submission_type: Option<SubmissionType>,
@@ -49,6 +71,14 @@ pub(crate) struct CodexTurnEvent {
pub(crate) is_first_turn: bool,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ThreadCreateSource {
ThreadStart,
ThreadFork,
DetachedReview,
}
pub(crate) fn build_track_events_context(
model_slug: String,
thread_id: String,
@@ -104,6 +134,9 @@ impl AnalyticsEventsQueue {
TrackEventsJob::SkillInvocations(job) => {
send_track_skill_invocations(&auth_manager, job).await;
}
TrackEventsJob::ThreadStarted(job) => {
send_track_thread_started(&auth_manager, job).await;
}
TrackEventsJob::AppMentioned(job) => {
send_track_app_mentioned(&auth_manager, job).await;
}
@@ -196,6 +229,10 @@ impl AnalyticsEventsClient {
);
}
pub(crate) fn track_thread_started(&self, thread_event: CodexThreadStartedEvent) {
track_thread_started(&self.queue, Arc::clone(&self.config), thread_event);
}
pub(crate) fn track_app_mentioned(
&self,
tracking: TrackEventsContext,
@@ -278,6 +315,7 @@ impl AnalyticsEventsClient {
enum TrackEventsJob {
SkillInvocations(TrackSkillInvocationsJob),
ThreadStarted(TrackThreadStartedJob),
AppMentioned(TrackAppMentionedJob),
AppUsed(TrackAppUsedJob),
TurnEvent(TrackTurnEventJob),
@@ -294,6 +332,11 @@ struct TrackSkillInvocationsJob {
invocations: Vec<SkillInvocation>,
}
struct TrackThreadStartedJob {
config: Arc<Config>,
thread_event: CodexThreadStartedEvent,
}
struct TrackAppMentionedJob {
config: Arc<Config>,
tracking: TrackEventsContext,
@@ -344,6 +387,7 @@ struct TrackEventsRequest {
#[serde(untagged)]
enum TrackEventRequest {
SkillInvocation(SkillInvocationEventRequest),
ThreadStarted(CodexThreadStartedEventRequest),
AppMentioned(CodexAppMentionedEventRequest),
AppUsed(CodexAppUsedEventRequest),
TurnEvent(CodexTurnEventRequest),
@@ -372,6 +416,34 @@ struct SkillInvocationEventParams {
model_slug: Option<String>,
}
#[derive(Serialize)]
struct CodexThreadStartedEventParams {
thread_id: String,
product_client_id: String,
model: String,
model_provider: String,
reasoning_effort: Option<String>,
reasoning_summary: Option<String>,
service_tier: String,
approval_policy: String,
approvals_reviewer: String,
sandbox_policy: &'static str,
sandbox_network_access: bool,
collaboration_mode: &'static str,
personality: Option<String>,
ephemeral: bool,
session_source: SessionSource,
parent_thread_id: Option<String>,
create_source: ThreadCreateSource,
created_at: u64,
}
#[derive(Serialize)]
struct CodexThreadStartedEventRequest {
event_type: &'static str,
event_params: CodexThreadStartedEventParams,
}
#[derive(Serialize)]
struct CodexAppMetadata {
connector_id: Option<String>,
@@ -477,6 +549,21 @@ pub(crate) fn track_skill_invocations(
queue.try_send(job);
}
pub(crate) fn track_thread_started(
queue: &AnalyticsEventsQueue,
config: Arc<Config>,
thread_event: CodexThreadStartedEvent,
) {
if config.analytics_enabled == Some(false) {
return;
}
let job = TrackEventsJob::ThreadStarted(TrackThreadStartedJob {
config,
thread_event,
});
queue.try_send(job);
}
pub(crate) fn track_app_mentioned(
queue: &AnalyticsEventsQueue,
config: Arc<Config>,
@@ -633,6 +720,21 @@ async fn send_track_skill_invocations(auth_manager: &AuthManager, job: TrackSkil
send_track_events(auth_manager, config, events).await;
}
async fn send_track_thread_started(auth_manager: &AuthManager, job: TrackThreadStartedJob) {
let TrackThreadStartedJob {
config,
thread_event,
} = job;
let events = vec![TrackEventRequest::ThreadStarted(
CodexThreadStartedEventRequest {
event_type: "codex_thread_started",
event_params: codex_thread_started_event_params(thread_event),
},
)];
send_track_events(auth_manager, config, events).await;
}
async fn send_track_app_mentioned(auth_manager: &AuthManager, job: TrackAppMentionedJob) {
let TrackAppMentionedJob {
config,
@@ -804,6 +906,36 @@ fn personality_mode(personality: Option<Personality>) -> Option<String> {
}
}
fn codex_thread_started_event_params(
thread_event: CodexThreadStartedEvent,
) -> CodexThreadStartedEventParams {
CodexThreadStartedEventParams {
thread_id: thread_event.thread_id,
product_client_id: crate::default_client::originator().value,
model: thread_event.model,
model_provider: thread_event.model_provider,
reasoning_effort: thread_event.reasoning_effort.map(|value| value.to_string()),
reasoning_summary: thread_event
.reasoning_summary
.map(|value| value.to_string()),
service_tier: thread_event
.service_tier
.map(|value| value.to_string())
.unwrap_or_else(|| "default".to_string()),
approval_policy: thread_event.approval_policy.to_string(),
approvals_reviewer: thread_event.approvals_reviewer.to_string(),
sandbox_policy: sandbox_policy_mode(&thread_event.sandbox_policy),
sandbox_network_access: thread_event.sandbox_network_access,
collaboration_mode: collaboration_mode_mode(thread_event.collaboration_mode),
personality: thread_event.personality.map(|value| value.to_string()),
ephemeral: thread_event.ephemeral,
session_source: thread_event.session_source,
parent_thread_id: thread_event.parent_thread_id,
create_source: thread_event.create_source,
created_at: thread_event.created_at,
}
}
fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPluginMetadata {
let capability_summary = plugin.capability_summary;
CodexPluginMetadata {
@@ -839,6 +971,24 @@ fn codex_plugin_used_metadata(
}
}
fn sandbox_policy_mode(sandbox_policy: &SandboxPolicy) -> &'static str {
match sandbox_policy {
SandboxPolicy::DangerFullAccess => "full_access",
SandboxPolicy::ReadOnly { .. } => "read_only",
SandboxPolicy::WorkspaceWrite { .. } => "workspace_write",
SandboxPolicy::ExternalSandbox { .. } => "external_sandbox",
}
}
fn collaboration_mode_mode(mode: ModeKind) -> &'static str {
match mode {
ModeKind::Plan => "plan",
ModeKind::Default => "default",
ModeKind::PairProgramming => "pair_programming",
ModeKind::Execute => "execute",
}
}
async fn send_track_events(
auth_manager: &AuthManager,
config: Arc<Config>,
@@ -4,15 +4,19 @@ use super::CodexAppMentionedEventRequest;
use super::CodexAppUsedEventRequest;
use super::CodexPluginEventRequest;
use super::CodexPluginUsedEventRequest;
use super::CodexThreadStartedEvent;
use super::CodexThreadStartedEventRequest;
use super::CodexTurnEvent;
use super::CodexTurnEventRequest;
use super::InvocationType;
use super::SubmissionType;
use super::ThreadCreateSource;
use super::TrackEventRequest;
use super::TrackEventsContext;
use super::codex_app_metadata;
use super::codex_plugin_metadata;
use super::codex_plugin_used_metadata;
use super::codex_thread_started_event_params;
use super::codex_turn_event_params;
use super::normalize_path_for_skill_id;
use crate::plugins::AppConnectorId;
@@ -27,6 +31,7 @@ use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::HashSet;
@@ -255,6 +260,61 @@ fn turn_event_serializes_expected_shape() {
);
}
#[test]
fn thread_started_event_serializes_expected_shape() {
let event = TrackEventRequest::ThreadStarted(CodexThreadStartedEventRequest {
event_type: "codex_thread_started",
event_params: codex_thread_started_event_params(CodexThreadStartedEvent {
thread_id: "thread-0".to_string(),
model: "gpt-5".to_string(),
model_provider: "openai".to_string(),
reasoning_effort: Some(ReasoningEffort::High),
reasoning_summary: Some(ReasoningSummary::Detailed),
service_tier: Some(ServiceTier::Flex),
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::GuardianSubagent,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
sandbox_network_access: false,
collaboration_mode: ModeKind::Plan,
personality: Some(Personality::Friendly),
ephemeral: true,
session_source: SessionSource::Exec,
parent_thread_id: None,
create_source: ThreadCreateSource::ThreadStart,
created_at: 1_716_000_000,
}),
});
let payload = serde_json::to_value(&event).expect("serialize thread started event");
assert_eq!(
payload,
json!({
"event_type": "codex_thread_started",
"event_params": {
"thread_id": "thread-0",
"product_client_id": crate::default_client::originator().value,
"model": "gpt-5",
"model_provider": "openai",
"reasoning_effort": "high",
"reasoning_summary": "detailed",
"service_tier": "flex",
"approval_policy": "on-request",
"approvals_reviewer": "guardian_subagent",
"sandbox_policy": "read_only",
"sandbox_network_access": false,
"collaboration_mode": "plan",
"personality": "friendly",
"ephemeral": true,
"session_source": "exec",
"parent_thread_id": null,
"create_source": "thread_start",
"created_at": 1716000000
}
})
);
}
#[test]
fn plugin_used_event_serializes_expected_shape() {
let tracking = TrackEventsContext {
+78
View File
@@ -5,6 +5,8 @@ use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use crate::AuthManager;
use crate::CodexAuth;
@@ -14,8 +16,10 @@ use crate::agent::AgentStatus;
use crate::agent::agent_status_from_event;
use crate::analytics_client::AnalyticsEventsClient;
use crate::analytics_client::AppInvocation;
use crate::analytics_client::CodexThreadStartedEvent;
use crate::analytics_client::CodexTurnEvent;
use crate::analytics_client::InvocationType;
use crate::analytics_client::ThreadCreateSource;
use crate::analytics_client::build_track_events_context;
use crate::apps::render_apps_section;
use crate::auth_env_telemetry::collect_auth_env_telemetry;
@@ -630,6 +634,13 @@ impl Codex {
user_shell_override,
};
let thread_create_source = classify_thread_create_source(
&conversation_history,
&session_configuration.session_source,
);
let thread_session_source = session_configuration.session_source.clone();
let thread_started_configuration = session_configuration.clone();
// Generate a unique ID for the lifetime of this Codex session.
let session_source_clone = session_configuration.session_source.clone();
let (agent_status_tx, agent_status_rx) = watch::channel(AgentStatus::PendingInit);
@@ -657,6 +668,46 @@ impl Codex {
})?;
let thread_id = session.conversation_id;
if let Some(create_source) = thread_create_source {
session
.services
.analytics_events_client
.track_thread_started(CodexThreadStartedEvent {
thread_id: thread_id.to_string(),
model: thread_started_configuration
.collaboration_mode
.model()
.to_string(),
model_provider: thread_started_configuration
.original_config_do_not_use
.model_provider_id
.clone(),
reasoning_effort: thread_started_configuration
.collaboration_mode
.reasoning_effort(),
reasoning_summary: thread_started_configuration.model_reasoning_summary,
service_tier: thread_started_configuration.service_tier,
approval_policy: thread_started_configuration.approval_policy.value(),
approvals_reviewer: thread_started_configuration.approvals_reviewer,
sandbox_policy: thread_started_configuration.sandbox_policy.get().clone(),
sandbox_network_access: thread_started_configuration
.network_sandbox_policy
.is_enabled(),
collaboration_mode: thread_started_configuration.collaboration_mode.mode,
personality: thread_started_configuration.personality,
ephemeral: thread_started_configuration
.original_config_do_not_use
.ephemeral,
parent_thread_id: session_source_parent_thread_id(&thread_session_source),
session_source: thread_session_source,
create_source,
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
});
}
// This task will run until Op::Shutdown is received.
let session_for_loop = Arc::clone(&session);
let session_loop_handle = tokio::spawn(async move {
@@ -771,6 +822,33 @@ impl Codex {
}
}
fn classify_thread_create_source(
conversation_history: &InitialHistory,
session_source: &SessionSource,
) -> Option<ThreadCreateSource> {
if matches!(
session_source,
SessionSource::SubAgent(SubAgentSource::Review)
) {
return Some(ThreadCreateSource::DetachedReview);
}
match conversation_history {
InitialHistory::New => Some(ThreadCreateSource::ThreadStart),
InitialHistory::Forked(_) => Some(ThreadCreateSource::ThreadFork),
InitialHistory::Resumed(_) => None,
}
}
fn session_source_parent_thread_id(session_source: &SessionSource) -> Option<String> {
match session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id, ..
}) => Some(parent_thread_id.to_string()),
_ => None,
}
}
#[cfg(test)]
pub(crate) fn completed_session_loop_termination() -> SessionLoopTermination {
futures::future::ready(()).boxed().shared()