[codex-analytics] feature plumbing and emittance

This commit is contained in:
rhan-oai
2026-04-06 11:21:17 -07:00
parent 12d351fff9
commit 9743b13634
21 changed files with 1264 additions and 44 deletions
+67 -5
View File
@@ -51,6 +51,7 @@ use codex_analytics::AnalyticsEventsClient;
use codex_analytics::AppInvocation;
use codex_analytics::InvocationType;
use codex_analytics::SubAgentThreadStartedInput;
use codex_analytics::TurnResolvedConfigFact;
use codex_analytics::build_track_events_context;
use codex_app_server_protocol::McpServerElicitationRequest;
use codex_app_server_protocol::McpServerElicitationRequestParams;
@@ -185,6 +186,7 @@ use crate::config::StartedNetworkProxy;
use crate::config::resolve_web_search_mode_for_turn;
use crate::context_manager::ContextManager;
use crate::context_manager::TotalTokenUsageBreakdown;
use crate::context_manager::is_user_turn_boundary;
use crate::environment_context::EnvironmentContext;
use codex_config::CONFIG_TOML_FILE;
use codex_config::types::McpServerConfig;
@@ -430,6 +432,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) inherited_exec_policy: Option<Arc<ExecPolicyManager>>,
pub(crate) user_shell_override: Option<shell::Shell>,
pub(crate) parent_trace: Option<W3cTraceContext>,
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
}
pub(crate) const INITIAL_SUBMIT_ID: &str = "";
@@ -484,6 +487,7 @@ impl Codex {
user_shell_override,
inherited_exec_policy,
parent_trace: _,
analytics_events_client,
} = args;
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_event, rx_event) = async_channel::unbounded();
@@ -670,6 +674,7 @@ impl Codex {
mcp_manager.clone(),
skills_watcher,
agent_control,
analytics_events_client,
)
.await
.map_err(|e| {
@@ -809,6 +814,17 @@ pub(crate) fn session_loop_termination_from_handle(
.shared()
}
fn initial_history_has_prior_user_turns(conversation_history: &InitialHistory) -> bool {
conversation_history.scan_rollout_items(rollout_item_is_user_turn_boundary)
}
fn rollout_item_is_user_turn_boundary(item: &RolloutItem) -> bool {
match item {
RolloutItem::ResponseItem(item) => is_user_turn_boundary(item),
_ => false,
}
}
/// Context for an initialized model agent
///
/// A session has at most 1 running task at a time, and can be interrupted by user input.
@@ -1517,6 +1533,7 @@ impl Session {
mcp_manager: Arc<McpManager>,
skills_watcher: Arc<SkillsWatcher>,
agent_control: AgentControl,
analytics_events_client: Option<AnalyticsEventsClient>,
) -> anyhow::Result<Arc<Self>> {
debug!(
"Configuring session: model={}; provider={:?}",
@@ -1920,11 +1937,13 @@ impl Session {
),
shell_zsh_path: config.zsh_path.clone(),
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
analytics_events_client: AnalyticsEventsClient::new(
Arc::clone(&auth_manager),
config.chatgpt_base_url.trim_end_matches('/').to_string(),
config.analytics_enabled,
),
analytics_events_client: analytics_events_client.unwrap_or_else(|| {
AnalyticsEventsClient::new(
Arc::clone(&auth_manager),
config.chatgpt_base_url.trim_end_matches('/').to_string(),
config.analytics_enabled,
)
}),
hooks,
rollout: Mutex::new(rollout_recorder),
user_shell: Arc::new(default_shell),
@@ -2238,6 +2257,11 @@ impl Session {
SessionSource::SubAgent(_)
)
};
let has_prior_user_turns = initial_history_has_prior_user_turns(&conversation_history);
{
let mut state = self.state.lock().await;
state.set_next_turn_is_first(!has_prior_user_turns);
}
match conversation_history {
InitialHistory::New => {
// Defer initial context insertion until the first real turn starts so
@@ -6005,6 +6029,8 @@ pub(crate) async fn run_turn(
.await;
}
track_turn_resolved_config_analytics(&sess, &turn_context, &input).await;
let skills_outcome = Some(turn_context.turn_skills.outcome.as_ref());
sess.maybe_start_ghost_snapshot(Arc::clone(&turn_context), cancellation_token.child_token())
.await;
@@ -6291,6 +6317,42 @@ pub(crate) async fn run_turn(
last_agent_message
}
async fn track_turn_resolved_config_analytics(
sess: &Session,
turn_context: &TurnContext,
input: &[UserInput],
) {
let is_first_turn = {
let mut state = sess.state.lock().await;
state.take_next_turn_is_first()
};
sess.services
.analytics_events_client
.track_turn_resolved_config(TurnResolvedConfigFact {
turn_id: turn_context.sub_id.clone(),
thread_id: sess.conversation_id.to_string(),
num_input_images: input
.iter()
.filter(|item| {
matches!(item, UserInput::Image { .. } | UserInput::LocalImage { .. })
})
.count(),
submission_type: None,
model: turn_context.model_info.slug.clone(),
model_provider: turn_context.config.model_provider_id.clone(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
reasoning_effort: turn_context.reasoning_effort,
reasoning_summary: Some(turn_context.reasoning_summary),
service_tier: turn_context.config.service_tier,
approval_policy: turn_context.approval_policy.value(),
approvals_reviewer: turn_context.config.approvals_reviewer,
sandbox_network_access: turn_context.network_sandbox_policy.is_enabled(),
collaboration_mode: turn_context.collaboration_mode.mode,
personality: turn_context.personality,
is_first_turn,
});
}
async fn run_pre_sampling_compact(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
+1
View File
@@ -95,6 +95,7 @@ pub(crate) async fn run_codex_thread_interactive(
user_shell_override: None,
inherited_exec_policy: Some(Arc::clone(&parent_session.services.exec_policy)),
parent_trace: None,
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
})
.await?;
if parent_session.enabled(codex_features::Feature::GeneralAnalytics) {
+1
View File
@@ -2627,6 +2627,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
mcp_manager,
Arc::new(SkillsWatcher::noop()),
AgentControl::default(),
/*analytics_events_client*/ None,
)
.await;
@@ -457,6 +457,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
inherited_exec_policy: Some(Arc::new(parent_exec_policy)),
user_shell_override: None,
parent_trace: None,
analytics_events_client: None,
})
.await
.expect("spawn guardian subagent");
+12
View File
@@ -33,6 +33,7 @@ pub(crate) struct SessionState {
pub(crate) active_connector_selection: HashSet<String>,
pub(crate) pending_session_start_source: Option<codex_hooks::SessionStartSource>,
granted_permissions: Option<PermissionProfile>,
next_turn_is_first: bool,
}
impl SessionState {
@@ -51,6 +52,7 @@ impl SessionState {
active_connector_selection: HashSet::new(),
pending_session_start_source: None,
granted_permissions: None,
next_turn_is_first: true,
}
}
@@ -73,6 +75,16 @@ impl SessionState {
self.previous_turn_settings = previous_turn_settings;
}
pub(crate) fn set_next_turn_is_first(&mut self, value: bool) {
self.next_turn_is_first = value;
}
pub(crate) fn take_next_turn_is_first(&mut self) -> bool {
let is_first_turn = self.next_turn_is_first;
self.next_turn_is_first = false;
is_first_turn
}
pub(crate) fn clone_history(&self) -> ContextManager {
self.history.clone()
}
+23
View File
@@ -15,6 +15,7 @@ use crate::shell_snapshot::ShellSnapshot;
use crate::skills_watcher::SkillsWatcher;
use crate::skills_watcher::SkillsWatcherEvent;
use crate::tasks::interrupted_turn_history_marker;
use codex_analytics::AnalyticsEventsClient;
use codex_app_server_protocol::ThreadHistoryBuilder;
use codex_app_server_protocol::TurnStatus;
use codex_exec_server::EnvironmentManager;
@@ -208,6 +209,7 @@ pub(crate) struct ThreadManagerState {
mcp_manager: Arc<McpManager>,
skills_watcher: Arc<SkillsWatcher>,
session_source: SessionSource,
analytics_events_client: Option<AnalyticsEventsClient>,
// Captures submitted ops for testing purpose when test mode is enabled.
ops_log: Option<SharedCapturedOps>,
}
@@ -219,6 +221,24 @@ impl ThreadManager {
session_source: SessionSource,
collaboration_modes_config: CollaborationModesConfig,
environment_manager: Arc<EnvironmentManager>,
) -> Self {
Self::new_with_analytics_events_client(
config,
auth_manager,
session_source,
collaboration_modes_config,
environment_manager,
/*analytics_events_client*/ None,
)
}
pub fn new_with_analytics_events_client(
config: &Config,
auth_manager: Arc<AuthManager>,
session_source: SessionSource,
collaboration_modes_config: CollaborationModesConfig,
environment_manager: Arc<EnvironmentManager>,
analytics_events_client: Option<AnalyticsEventsClient>,
) -> Self {
let codex_home = config.codex_home.clone();
let restriction_product = session_source.restriction_product();
@@ -257,6 +277,7 @@ impl ThreadManager {
skills_watcher,
auth_manager,
session_source,
analytics_events_client,
ops_log: should_use_test_thread_manager_behavior()
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
}),
@@ -326,6 +347,7 @@ impl ThreadManager {
skills_watcher,
auth_manager,
session_source: SessionSource::Exec,
analytics_events_client: None,
ops_log: should_use_test_thread_manager_behavior()
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
}),
@@ -867,6 +889,7 @@ impl ThreadManagerState {
inherited_exec_policy,
user_shell_override,
parent_trace,
analytics_events_client: self.analytics_events_client.clone(),
})
.await?;
self.finalize_thread_spawn(codex, thread_id, watch_registration)