From a4a7ab780d97ee5961780e7209656043b7db7344 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 25 Mar 2026 11:08:05 -0700 Subject: [PATCH] Extract codex-analytics crate (#15748) - move the analytics events client into codex-analytics - update codex-core and app-server callsites to use the new crate - CI --------- Co-authored-by: Codex --- codex-rs/Cargo.lock | 19 +- codex-rs/Cargo.toml | 2 + codex-rs/analytics/BUILD.bazel | 6 + codex-rs/analytics/Cargo.toml | 30 ++ .../src/analytics_client.rs | 349 ++++++++++-------- .../src/analytics_client_tests.rs | 21 +- codex-rs/analytics/src/lib.rs | 11 + codex-rs/app-server/src/config_api.rs | 13 +- codex-rs/app-server/src/message_processor.rs | 7 +- codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/codex.rs | 20 +- codex-rs/core/src/codex_tests.rs | 6 +- codex-rs/core/src/lib.rs | 3 +- codex-rs/core/src/mcp_tool_call.rs | 6 +- codex-rs/core/src/skills/injection.rs | 8 +- codex-rs/core/src/skills/invocation_utils.rs | 6 +- codex-rs/core/src/state/service.rs | 2 +- 17 files changed, 312 insertions(+), 198 deletions(-) create mode 100644 codex-rs/analytics/BUILD.bazel create mode 100644 codex-rs/analytics/Cargo.toml rename codex-rs/{core => analytics}/src/analytics_client.rs (78%) rename codex-rs/{core => analytics}/src/analytics_client_tests.rs (96%) create mode 100644 codex-rs/analytics/src/lib.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b9760de416..61b328d84c 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1384,6 +1384,22 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9b18233253483ce2f65329a24072ec414db782531bdbb7d0bbc4bd2ce6b7e21" +[[package]] +name = "codex-analytics" +version = "0.0.0" +dependencies = [ + "codex-git-utils", + "codex-login", + "codex-plugin", + "codex-protocol", + "pretty_assertions", + "serde", + "serde_json", + "sha1", + "tokio", + "tracing", +] + [[package]] name = "codex-ansi-escape" version = "0.0.0" @@ -1869,6 +1885,7 @@ dependencies = [ "chardetng", "chrono", "clap", + "codex-analytics", "codex-api", "codex-app-server-protocol", "codex-apply-patch", @@ -2401,8 +2418,6 @@ version = "0.0.0" dependencies = [ "codex-utils-absolute-path", "codex-utils-plugins", - "serde", - "serde_json", "thiserror 2.0.18", ] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 4b23f48658..10b9485d20 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "analytics", "backend-client", "ansi-escape", "async-utils", @@ -98,6 +99,7 @@ license = "Apache-2.0" # Internal app_test_support = { path = "app-server/tests/common" } codex-ansi-escape = { path = "ansi-escape" } +codex-analytics = { path = "analytics" } codex-api = { path = "codex-api" } codex-artifacts = { path = "artifacts" } codex-code-mode = { path = "code-mode" } diff --git a/codex-rs/analytics/BUILD.bazel b/codex-rs/analytics/BUILD.bazel new file mode 100644 index 0000000000..aec07c8746 --- /dev/null +++ b/codex-rs/analytics/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "analytics", + crate_name = "codex_analytics", +) diff --git a/codex-rs/analytics/Cargo.toml b/codex-rs/analytics/Cargo.toml new file mode 100644 index 0000000000..9633eebb14 --- /dev/null +++ b/codex-rs/analytics/Cargo.toml @@ -0,0 +1,30 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-analytics" +version.workspace = true + +[lib] +doctest = false +name = "codex_analytics" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +codex-git-utils = { workspace = true } +codex-login = { workspace = true } +codex-plugin = { workspace = true } +codex-protocol = { workspace = true } +serde = { workspace = true, features = ["derive"] } +sha1 = { workspace = true } +tokio = { workspace = true, features = [ + "macros", + "rt-multi-thread", +] } +tracing = { workspace = true, features = ["log"] } + +[dev-dependencies] +pretty_assertions = { workspace = true } +serde_json = { workspace = true } diff --git a/codex-rs/core/src/analytics_client.rs b/codex-rs/analytics/src/analytics_client.rs similarity index 78% rename from codex-rs/core/src/analytics_client.rs rename to codex-rs/analytics/src/analytics_client.rs index 3c42336681..0eecd5c3a7 100644 --- a/codex-rs/core/src/analytics_client.rs +++ b/codex-rs/analytics/src/analytics_client.rs @@ -1,9 +1,9 @@ -use crate::AuthManager; -use crate::config::Config; -use crate::default_client::create_client; -use crate::plugins::PluginTelemetryMetadata; use codex_git_utils::collect_git_info; use codex_git_utils::get_git_repo_root; +use codex_login::AuthManager; +use codex_login::default_client::create_client; +use codex_login::default_client::originator; +use codex_plugin::PluginTelemetryMetadata; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; @@ -28,60 +28,60 @@ use std::time::Duration; use tokio::sync::mpsc; #[derive(Clone)] -pub(crate) struct TrackEventsContext { - pub(crate) model_slug: String, - pub(crate) thread_id: String, - pub(crate) turn_id: String, +pub struct TrackEventsContext { + pub model_slug: String, + pub thread_id: String, + pub turn_id: String, } #[derive(Clone)] -pub(crate) struct CodexThreadInitializedEvent { - pub(crate) thread_id: String, - pub(crate) model: String, - pub(crate) model_provider: String, - pub(crate) reasoning_effort: Option, - pub(crate) reasoning_summary: Option, - pub(crate) service_tier: Option, - 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, - pub(crate) ephemeral: bool, - pub(crate) session_source: SessionSource, - pub(crate) initialization_mode: InitializationMode, - pub(crate) subagent_source: Option, - pub(crate) parent_thread_id: Option, - pub(crate) created_at: u64, +pub struct CodexThreadInitializedEvent { + pub thread_id: String, + pub model: String, + pub model_provider: String, + pub reasoning_effort: Option, + pub reasoning_summary: Option, + pub service_tier: Option, + pub approval_policy: AskForApproval, + pub approvals_reviewer: ApprovalsReviewer, + pub sandbox_policy: SandboxPolicy, + pub sandbox_network_access: bool, + pub collaboration_mode: ModeKind, + pub personality: Option, + pub ephemeral: bool, + pub session_source: SessionSource, + pub initialization_mode: InitializationMode, + pub subagent_source: Option, + pub parent_thread_id: Option, + pub created_at: u64, } #[derive(Clone)] -pub(crate) struct CodexTurnEvent { - pub(crate) submission_type: Option, - pub(crate) model_provider: String, - pub(crate) sandbox_policy: SandboxPolicy, - pub(crate) reasoning_effort: Option, - pub(crate) reasoning_summary: Option, - pub(crate) service_tier: Option, - pub(crate) approval_policy: AskForApproval, - pub(crate) approvals_reviewer: ApprovalsReviewer, - pub(crate) sandbox_network_access: bool, - pub(crate) collaboration_mode: ModeKind, - pub(crate) personality: Option, - pub(crate) num_input_images: usize, - pub(crate) is_first_turn: bool, +pub struct CodexTurnEvent { + pub submission_type: Option, + pub model_provider: String, + pub sandbox_policy: SandboxPolicy, + pub reasoning_effort: Option, + pub reasoning_summary: Option, + pub service_tier: Option, + pub approval_policy: AskForApproval, + pub approvals_reviewer: ApprovalsReviewer, + pub sandbox_network_access: bool, + pub collaboration_mode: ModeKind, + pub personality: Option, + pub num_input_images: usize, + pub is_first_turn: bool, } #[derive(Clone, Copy, Debug, Serialize)] #[serde(rename_all = "snake_case")] -pub(crate) enum InitializationMode { +pub enum InitializationMode { New, Forked, Resumed, } -pub(crate) fn build_track_events_context( +pub fn build_track_events_context( model_slug: String, thread_id: String, turn_id: String, @@ -94,24 +94,24 @@ pub(crate) fn build_track_events_context( } #[derive(Clone, Debug)] -pub(crate) struct SkillInvocation { - pub(crate) skill_name: String, - pub(crate) skill_scope: SkillScope, - pub(crate) skill_path: PathBuf, - pub(crate) invocation_type: InvocationType, +pub struct SkillInvocation { + pub skill_name: String, + pub skill_scope: SkillScope, + pub skill_path: PathBuf, + pub invocation_type: InvocationType, } #[derive(Clone, Copy, Debug, Serialize)] #[serde(rename_all = "lowercase")] -pub(crate) enum InvocationType { +pub enum InvocationType { Explicit, Implicit, } -pub(crate) struct AppInvocation { - pub(crate) connector_id: Option, - pub(crate) app_name: Option, - pub(crate) invocation_type: Option, +pub struct AppInvocation { + pub connector_id: Option, + pub app_name: Option, + pub invocation_type: Option, } #[derive(Clone)] @@ -124,44 +124,44 @@ pub(crate) struct AnalyticsEventsQueue { #[derive(Clone)] pub struct AnalyticsEventsClient { queue: AnalyticsEventsQueue, - config: Arc, + analytics_enabled: Option, } impl AnalyticsEventsQueue { - pub(crate) fn new(auth_manager: Arc) -> Self { + pub(crate) fn new(auth_manager: Arc, base_url: String) -> Self { let (sender, mut receiver) = mpsc::channel(ANALYTICS_EVENTS_QUEUE_SIZE); tokio::spawn(async move { while let Some(job) = receiver.recv().await { match job { TrackEventsJob::SkillInvocations(job) => { - send_track_skill_invocations(&auth_manager, job).await; + send_track_skill_invocations(&auth_manager, &base_url, job).await; } TrackEventsJob::ThreadInitialized(job) => { send_track_thread_initialized(&auth_manager, job).await; } TrackEventsJob::AppMentioned(job) => { - send_track_app_mentioned(&auth_manager, job).await; + send_track_app_mentioned(&auth_manager, &base_url, job).await; } TrackEventsJob::AppUsed(job) => { - send_track_app_used(&auth_manager, job).await; + send_track_app_used(&auth_manager, &base_url, job).await; } TrackEventsJob::TurnEvent(job) => { send_track_turn_event(&auth_manager, job).await; } TrackEventsJob::PluginUsed(job) => { - send_track_plugin_used(&auth_manager, job).await; + send_track_plugin_used(&auth_manager, &base_url, job).await; } TrackEventsJob::PluginInstalled(job) => { - send_track_plugin_installed(&auth_manager, job).await; + send_track_plugin_installed(&auth_manager, &base_url, job).await; } TrackEventsJob::PluginUninstalled(job) => { - send_track_plugin_uninstalled(&auth_manager, job).await; + send_track_plugin_uninstalled(&auth_manager, &base_url, job).await; } TrackEventsJob::PluginEnabled(job) => { - send_track_plugin_enabled(&auth_manager, job).await; + send_track_plugin_enabled(&auth_manager, &base_url, job).await; } TrackEventsJob::PluginDisabled(job) => { - send_track_plugin_disabled(&auth_manager, job).await; + send_track_plugin_disabled(&auth_manager, &base_url, job).await; } } } @@ -211,58 +211,49 @@ impl AnalyticsEventsQueue { } impl AnalyticsEventsClient { - pub fn new(config: Arc, auth_manager: Arc) -> Self { + pub fn new( + auth_manager: Arc, + base_url: String, + analytics_enabled: Option, + ) -> Self { Self { - queue: AnalyticsEventsQueue::new(Arc::clone(&auth_manager)), - config, + queue: AnalyticsEventsQueue::new(Arc::clone(&auth_manager), base_url), + analytics_enabled, } } - pub(crate) fn track_skill_invocations( + pub fn track_skill_invocations( &self, tracking: TrackEventsContext, invocations: Vec, ) { track_skill_invocations( &self.queue, - Arc::clone(&self.config), + self.analytics_enabled, Some(tracking), invocations, ); } - pub(crate) fn track_thread_initialized(&self, thread_event: CodexThreadInitializedEvent) { - track_thread_initialized(&self.queue, Arc::clone(&self.config), thread_event); + pub fn track_thread_initialized(&self, thread_event: CodexThreadInitializedEvent) { + track_thread_initialized(&self.queue, self.analytics_enabled, thread_event); } - pub(crate) fn track_app_mentioned( - &self, - tracking: TrackEventsContext, - mentions: Vec, - ) { + pub fn track_app_mentioned(&self, tracking: TrackEventsContext, mentions: Vec) { track_app_mentioned( &self.queue, - Arc::clone(&self.config), + self.analytics_enabled, Some(tracking), mentions, ); } - pub(crate) fn track_app_used(&self, tracking: TrackEventsContext, app: AppInvocation) { - track_app_used(&self.queue, Arc::clone(&self.config), Some(tracking), app); + pub fn track_app_used(&self, tracking: TrackEventsContext, app: AppInvocation) { + track_app_used(&self.queue, self.analytics_enabled, Some(tracking), app); } - pub(crate) fn track_plugin_used( - &self, - tracking: TrackEventsContext, - plugin: PluginTelemetryMetadata, - ) { - track_plugin_used( - &self.queue, - Arc::clone(&self.config), - Some(tracking), - plugin, - ); + pub fn track_plugin_used(&self, tracking: TrackEventsContext, plugin: PluginTelemetryMetadata) { + track_plugin_used(&self.queue, self.analytics_enabled, Some(tracking), plugin); } pub(crate) fn track_turn_event( @@ -272,7 +263,7 @@ impl AnalyticsEventsClient { ) { track_turn_event( &self.queue, - Arc::clone(&self.config), + self.analytics_enabled, Some(tracking), turn_event, ); @@ -281,7 +272,7 @@ impl AnalyticsEventsClient { pub fn track_plugin_installed(&self, plugin: PluginTelemetryMetadata) { track_plugin_management( &self.queue, - Arc::clone(&self.config), + self.analytics_enabled, PluginManagementEventType::Installed, plugin, ); @@ -290,7 +281,7 @@ impl AnalyticsEventsClient { pub fn track_plugin_uninstalled(&self, plugin: PluginTelemetryMetadata) { track_plugin_management( &self.queue, - Arc::clone(&self.config), + self.analytics_enabled, PluginManagementEventType::Uninstalled, plugin, ); @@ -299,7 +290,7 @@ impl AnalyticsEventsClient { pub fn track_plugin_enabled(&self, plugin: PluginTelemetryMetadata) { track_plugin_management( &self.queue, - Arc::clone(&self.config), + self.analytics_enabled, PluginManagementEventType::Enabled, plugin, ); @@ -308,7 +299,7 @@ impl AnalyticsEventsClient { pub fn track_plugin_disabled(&self, plugin: PluginTelemetryMetadata) { track_plugin_management( &self.queue, - Arc::clone(&self.config), + self.analytics_enabled, PluginManagementEventType::Disabled, plugin, ); @@ -329,42 +320,42 @@ enum TrackEventsJob { } struct TrackSkillInvocationsJob { - config: Arc, + analytics_enabled: Option, tracking: TrackEventsContext, invocations: Vec, } struct TrackThreadInitializedJob { - config: Arc, + analytics_enabled: Option, thread_event: CodexThreadInitializedEvent, } struct TrackAppMentionedJob { - config: Arc, + analytics_enabled: Option, tracking: TrackEventsContext, mentions: Vec, } struct TrackAppUsedJob { - config: Arc, + analytics_enabled: Option, tracking: TrackEventsContext, app: AppInvocation, } struct TrackTurnEventJob { - config: Arc, + analytics_enabled: Option, tracking: TrackEventsContext, turn_event: CodexTurnEvent, } struct TrackPluginUsedJob { - config: Arc, + analytics_enabled: Option, tracking: TrackEventsContext, plugin: PluginTelemetryMetadata, } struct TrackPluginManagementJob { - config: Arc, + analytics_enabled: Option, plugin: PluginTelemetryMetadata, } @@ -531,11 +522,11 @@ struct CodexPluginUsedEventRequest { pub(crate) fn track_skill_invocations( queue: &AnalyticsEventsQueue, - config: Arc, + analytics_enabled: Option, tracking: Option, invocations: Vec, ) { - if config.analytics_enabled == Some(false) { + if analytics_enabled == Some(false) { return; } let Some(tracking) = tracking else { @@ -545,7 +536,7 @@ pub(crate) fn track_skill_invocations( return; } let job = TrackEventsJob::SkillInvocations(TrackSkillInvocationsJob { - config, + analytics_enabled, tracking, invocations, }); @@ -554,14 +545,14 @@ pub(crate) fn track_skill_invocations( pub(crate) fn track_thread_initialized( queue: &AnalyticsEventsQueue, - config: Arc, + analytics_enabled: Option, thread_event: CodexThreadInitializedEvent, ) { - if config.analytics_enabled == Some(false) { + if analytics_enabled == Some(false) { return; } let job = TrackEventsJob::ThreadInitialized(TrackThreadInitializedJob { - config, + analytics_enabled, thread_event, }); queue.try_send(job); @@ -569,11 +560,11 @@ pub(crate) fn track_thread_initialized( pub(crate) fn track_app_mentioned( queue: &AnalyticsEventsQueue, - config: Arc, + analytics_enabled: Option, tracking: Option, mentions: Vec, ) { - if config.analytics_enabled == Some(false) { + if analytics_enabled == Some(false) { return; } let Some(tracking) = tracking else { @@ -583,7 +574,7 @@ pub(crate) fn track_app_mentioned( return; } let job = TrackEventsJob::AppMentioned(TrackAppMentionedJob { - config, + analytics_enabled, tracking, mentions, }); @@ -592,11 +583,11 @@ pub(crate) fn track_app_mentioned( pub(crate) fn track_app_used( queue: &AnalyticsEventsQueue, - config: Arc, + analytics_enabled: Option, tracking: Option, app: AppInvocation, ) { - if config.analytics_enabled == Some(false) { + if analytics_enabled == Some(false) { return; } let Some(tracking) = tracking else { @@ -606,7 +597,7 @@ pub(crate) fn track_app_used( return; } let job = TrackEventsJob::AppUsed(TrackAppUsedJob { - config, + analytics_enabled, tracking, app, }); @@ -615,18 +606,18 @@ pub(crate) fn track_app_used( pub(crate) fn track_turn_event( queue: &AnalyticsEventsQueue, - config: Arc, + analytics_enabled: Option, tracking: Option, turn_event: CodexTurnEvent, ) { - if config.analytics_enabled == Some(false) { + if analytics_enabled == Some(false) { return; } let Some(tracking) = tracking else { return; }; let job = TrackEventsJob::TurnEvent(TrackTurnEventJob { - config, + analytics_enabled, tracking, turn_event, }); @@ -635,11 +626,11 @@ pub(crate) fn track_turn_event( pub(crate) fn track_plugin_used( queue: &AnalyticsEventsQueue, - config: Arc, + analytics_enabled: Option, tracking: Option, plugin: PluginTelemetryMetadata, ) { - if config.analytics_enabled == Some(false) { + if analytics_enabled == Some(false) { return; } let Some(tracking) = tracking else { @@ -649,7 +640,7 @@ pub(crate) fn track_plugin_used( return; } let job = TrackEventsJob::PluginUsed(TrackPluginUsedJob { - config, + analytics_enabled, tracking, plugin, }); @@ -658,14 +649,17 @@ pub(crate) fn track_plugin_used( fn track_plugin_management( queue: &AnalyticsEventsQueue, - config: Arc, + analytics_enabled: Option, event_type: PluginManagementEventType, plugin: PluginTelemetryMetadata, ) { - if config.analytics_enabled == Some(false) { + if analytics_enabled == Some(false) { return; } - let job = TrackPluginManagementJob { config, plugin }; + let job = TrackPluginManagementJob { + analytics_enabled, + plugin, + }; let job = match event_type { PluginManagementEventType::Installed => TrackEventsJob::PluginInstalled(job), PluginManagementEventType::Uninstalled => TrackEventsJob::PluginUninstalled(job), @@ -675,9 +669,13 @@ fn track_plugin_management( queue.try_send(job); } -async fn send_track_skill_invocations(auth_manager: &AuthManager, job: TrackSkillInvocationsJob) { +async fn send_track_skill_invocations( + auth_manager: &AuthManager, + base_url: &str, + job: TrackSkillInvocationsJob, +) { let TrackSkillInvocationsJob { - config, + analytics_enabled, tracking, invocations, } = job; @@ -712,7 +710,7 @@ async fn send_track_skill_invocations(auth_manager: &AuthManager, job: TrackSkil thread_id: Some(tracking.thread_id.clone()), invoke_type: Some(invocation.invocation_type), model_slug: Some(tracking.model_slug.clone()), - product_client_id: Some(crate::default_client::originator().value), + product_client_id: Some(originator().value), repo_url, skill_scope: Some(skill_scope.to_string()), }, @@ -720,12 +718,16 @@ async fn send_track_skill_invocations(auth_manager: &AuthManager, job: TrackSkil )); } - send_track_events(auth_manager, config, events).await; + send_track_events(auth_manager, analytics_enabled, base_url, events).await; } -async fn send_track_thread_initialized(auth_manager: &AuthManager, job: TrackThreadInitializedJob) { +async fn send_track_thread_initialized( + auth_manager: &AuthManager, + base_url: &str, + job: TrackThreadInitializedJob, +) { let TrackThreadInitializedJob { - config, + analytics_enabled, thread_event, } = job; let events = vec![TrackEventRequest::ThreadInitialized( @@ -735,12 +737,16 @@ async fn send_track_thread_initialized(auth_manager: &AuthManager, job: TrackThr }, )]; - send_track_events(auth_manager, config, events).await; + send_track_events(auth_manager, analytics_enabled, base_url, events).await; } -async fn send_track_app_mentioned(auth_manager: &AuthManager, job: TrackAppMentionedJob) { +async fn send_track_app_mentioned( + auth_manager: &AuthManager, + base_url: &str, + job: TrackAppMentionedJob, +) { let TrackAppMentionedJob { - config, + analytics_enabled, tracking, mentions, } = job; @@ -755,12 +761,12 @@ async fn send_track_app_mentioned(auth_manager: &AuthManager, job: TrackAppMenti }) .collect::>(); - send_track_events(auth_manager, config, events).await; + send_track_events(auth_manager, analytics_enabled, base_url, events).await; } -async fn send_track_app_used(auth_manager: &AuthManager, job: TrackAppUsedJob) { +async fn send_track_app_used(auth_manager: &AuthManager, base_url: &str, job: TrackAppUsedJob) { let TrackAppUsedJob { - config, + analytics_enabled, tracking, app, } = job; @@ -770,12 +776,12 @@ async fn send_track_app_used(auth_manager: &AuthManager, job: TrackAppUsedJob) { event_params, })]; - send_track_events(auth_manager, config, events).await; + send_track_events(auth_manager, analytics_enabled, base_url, events).await; } -async fn send_track_turn_event(auth_manager: &AuthManager, job: TrackTurnEventJob) { +async fn send_track_turn_event(auth_manager: &AuthManager, base_url: &str, job: TrackTurnEventJob) { let TrackTurnEventJob { - config, + analytics_enabled, tracking, turn_event, } = job; @@ -784,12 +790,16 @@ async fn send_track_turn_event(auth_manager: &AuthManager, job: TrackTurnEventJo event_params: codex_turn_event_params(&tracking, turn_event), })]; - send_track_events(auth_manager, config, events).await; + send_track_events(auth_manager, analytics_enabled, base_url, events).await; } -async fn send_track_plugin_used(auth_manager: &AuthManager, job: TrackPluginUsedJob) { +async fn send_track_plugin_used( + auth_manager: &AuthManager, + base_url: &str, + job: TrackPluginUsedJob, +) { let TrackPluginUsedJob { - config, + analytics_enabled, tracking, plugin, } = job; @@ -798,31 +808,52 @@ async fn send_track_plugin_used(auth_manager: &AuthManager, job: TrackPluginUsed event_params: codex_plugin_used_metadata(&tracking, plugin), })]; - send_track_events(auth_manager, config, events).await; + send_track_events(auth_manager, analytics_enabled, base_url, events).await; } -async fn send_track_plugin_installed(auth_manager: &AuthManager, job: TrackPluginManagementJob) { - send_track_plugin_management_event(auth_manager, job, "codex_plugin_installed").await; +async fn send_track_plugin_installed( + auth_manager: &AuthManager, + base_url: &str, + job: TrackPluginManagementJob, +) { + send_track_plugin_management_event(auth_manager, base_url, job, "codex_plugin_installed").await; } -async fn send_track_plugin_uninstalled(auth_manager: &AuthManager, job: TrackPluginManagementJob) { - send_track_plugin_management_event(auth_manager, job, "codex_plugin_uninstalled").await; +async fn send_track_plugin_uninstalled( + auth_manager: &AuthManager, + base_url: &str, + job: TrackPluginManagementJob, +) { + send_track_plugin_management_event(auth_manager, base_url, job, "codex_plugin_uninstalled") + .await; } -async fn send_track_plugin_enabled(auth_manager: &AuthManager, job: TrackPluginManagementJob) { - send_track_plugin_management_event(auth_manager, job, "codex_plugin_enabled").await; +async fn send_track_plugin_enabled( + auth_manager: &AuthManager, + base_url: &str, + job: TrackPluginManagementJob, +) { + send_track_plugin_management_event(auth_manager, base_url, job, "codex_plugin_enabled").await; } -async fn send_track_plugin_disabled(auth_manager: &AuthManager, job: TrackPluginManagementJob) { - send_track_plugin_management_event(auth_manager, job, "codex_plugin_disabled").await; +async fn send_track_plugin_disabled( + auth_manager: &AuthManager, + base_url: &str, + job: TrackPluginManagementJob, +) { + send_track_plugin_management_event(auth_manager, base_url, job, "codex_plugin_disabled").await; } async fn send_track_plugin_management_event( auth_manager: &AuthManager, + base_url: &str, job: TrackPluginManagementJob, event_type: &'static str, ) { - let TrackPluginManagementJob { config, plugin } = job; + let TrackPluginManagementJob { + analytics_enabled, + plugin, + } = job; let event_params = codex_plugin_metadata(plugin); let event = CodexPluginEventRequest { event_type, @@ -836,7 +867,7 @@ async fn send_track_plugin_management_event( _ => unreachable!("unknown plugin management event type"), }]; - send_track_events(auth_manager, config, events).await; + send_track_events(auth_manager, analytics_enabled, base_url, events).await; } fn codex_app_metadata(tracking: &TrackEventsContext, app: AppInvocation) -> CodexAppMetadata { @@ -845,7 +876,7 @@ fn codex_app_metadata(tracking: &TrackEventsContext, app: AppInvocation) -> Code thread_id: Some(tracking.thread_id.clone()), turn_id: Some(tracking.turn_id.clone()), app_name: app.app_name, - product_client_id: Some(crate::default_client::originator().value), + product_client_id: Some(originator().value), invoke_type: app.invocation_type, model_slug: Some(tracking.model_slug.clone()), } @@ -858,7 +889,7 @@ fn codex_turn_event_params( CodexTurnEventParams { thread_id: tracking.thread_id.clone(), turn_id: tracking.turn_id.clone(), - product_client_id: Some(crate::default_client::originator().value), + product_client_id: Some(originator().value), model: Some(tracking.model_slug.clone()), submission_type: turn_event.submission_type, model_provider: turn_event.model_provider, @@ -914,7 +945,7 @@ fn codex_thread_initialized_event_params( ) -> CodexThreadInitializedEventParams { CodexThreadInitializedEventParams { thread_id: thread_event.thread_id, - product_client_id: crate::default_client::originator().value, + product_client_id: originator().value, model: thread_event.model, model_provider: thread_event.model_provider, reasoning_effort: thread_event.reasoning_effort.map(|value| value.to_string()), @@ -959,7 +990,7 @@ fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPluginMetadata .map(|connector_id| connector_id.0) .collect() }), - product_client_id: Some(crate::default_client::originator().value), + product_client_id: Some(originator().value), } } @@ -1013,9 +1044,13 @@ fn subagent_source_name(subagent_source: SubAgentSource) -> String { async fn send_track_events( auth_manager: &AuthManager, - config: Arc, + analytics_enabled: Option, + base_url: &str, events: Vec, ) { + if analytics_enabled == Some(false) { + return; + } if events.is_empty() { return; } @@ -1033,7 +1068,7 @@ async fn send_track_events( return; }; - let base_url = config.chatgpt_base_url.trim_end_matches('/'); + let base_url = base_url.trim_end_matches('/'); let url = format!("{base_url}/codex/analytics-events/events"); let payload = TrackEventsRequest { events }; diff --git a/codex-rs/core/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs similarity index 96% rename from codex-rs/core/src/analytics_client_tests.rs rename to codex-rs/analytics/src/analytics_client_tests.rs index eddbf7830a..047eb49ba8 100644 --- a/codex-rs/core/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -19,10 +19,11 @@ use super::codex_plugin_used_metadata; use super::codex_thread_initialized_event_params; use super::codex_turn_event_params; use super::normalize_path_for_skill_id; -use crate::plugins::AppConnectorId; -use crate::plugins::PluginCapabilitySummary; -use crate::plugins::PluginId; -use crate::plugins::PluginTelemetryMetadata; +use codex_login::default_client::originator; +use codex_plugin::AppConnectorId; +use codex_plugin::PluginCapabilitySummary; +use codex_plugin::PluginId; +use codex_plugin::PluginTelemetryMetadata; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; @@ -127,7 +128,7 @@ fn app_mentioned_event_serializes_expected_shape() { "thread_id": "thread-1", "turn_id": "turn-1", "app_name": "Calendar", - "product_client_id": crate::default_client::originator().value, + "product_client_id": originator().value, "invoke_type": "explicit", "model_slug": "gpt-5" } @@ -165,7 +166,7 @@ fn app_used_event_serializes_expected_shape() { "thread_id": "thread-2", "turn_id": "turn-2", "app_name": "Google Drive", - "product_client_id": crate::default_client::originator().value, + "product_client_id": originator().value, "invoke_type": "implicit", "model_slug": "gpt-5" } @@ -241,7 +242,7 @@ fn turn_event_serializes_expected_shape() { "event_params": { "thread_id": "thread-2", "turn_id": "turn-2", - "product_client_id": crate::default_client::originator().value, + "product_client_id": originator().value, "model": "gpt-5", "submission_type": "prompt", "model_provider": "openai", @@ -295,7 +296,7 @@ fn thread_initialized_event_serializes_expected_shape() { "event_type": "codex_thread_initialized", "event_params": { "thread_id": "thread-0", - "product_client_id": crate::default_client::originator().value, + "product_client_id": originator().value, "model": "gpt-5", "model_provider": "openai", "reasoning_effort": "high", @@ -408,7 +409,7 @@ fn plugin_used_event_serializes_expected_shape() { "has_skills": true, "mcp_server_count": 2, "connector_ids": ["calendar", "drive"], - "product_client_id": crate::default_client::originator().value, + "product_client_id": originator().value, "thread_id": "thread-3", "turn_id": "turn-3", "model_slug": "gpt-5" @@ -437,7 +438,7 @@ fn plugin_management_event_serializes_expected_shape() { "has_skills": true, "mcp_server_count": 2, "connector_ids": ["calendar", "drive"], - "product_client_id": crate::default_client::originator().value + "product_client_id": originator().value } }) ); diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs new file mode 100644 index 0000000000..b76546753a --- /dev/null +++ b/codex-rs/analytics/src/lib.rs @@ -0,0 +1,11 @@ +mod analytics_client; + +pub use analytics_client::AnalyticsEventsClient; +pub use analytics_client::AppInvocation; +pub use analytics_client::CodexThreadInitializedEvent; +pub use analytics_client::CodexTurnEvent; +pub use analytics_client::InitializationMode; +pub use analytics_client::InvocationType; +pub use analytics_client::SkillInvocation; +pub use analytics_client::TrackEventsContext; +pub use analytics_client::build_track_events_context; diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index 4aa2982a81..2be0bb859c 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -450,6 +450,8 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into) -> mod tests { use super::*; use codex_core::AnalyticsEventsClient; + use codex_core::AuthManager; + use codex_core::CodexAuth; use codex_core::config_loader::NetworkRequirementsToml as CoreNetworkRequirementsToml; use codex_features::Feature; use codex_protocol::protocol::AskForApproval as CoreAskForApproval; @@ -651,6 +653,7 @@ mod tests { .await .expect("load analytics config"), ); + let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); let config_api = ConfigApi::new( codex_home.path().to_path_buf(), Arc::new(RwLock::new(Vec::new())), @@ -659,10 +662,12 @@ mod tests { Arc::new(RwLock::new(CloudRequirementsLoader::default())), reloader.clone(), AnalyticsEventsClient::new( - analytics_config, - codex_core::test_support::auth_manager_from_auth( - codex_core::CodexAuth::from_api_key("test"), - ), + auth_manager, + analytics_config + .chatgpt_base_url + .trim_end_matches('/') + .to_string(), + analytics_config.analytics_enabled, ), ); diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index d070738fba..feca709513 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -222,8 +222,11 @@ impl MessageProcessor { auth_manager.set_external_auth_refresher(Arc::new(ExternalAuthRefreshBridge { outgoing: outgoing.clone(), })); - let analytics_events_client = - AnalyticsEventsClient::new(Arc::clone(&config), Arc::clone(&auth_manager)); + let analytics_events_client = AnalyticsEventsClient::new( + Arc::clone(&auth_manager), + config.chatgpt_base_url.trim_end_matches('/').to_string(), + config.analytics_enabled, + ); thread_manager .plugins_manager() .set_analytics_events_client(analytics_events_client.clone()); diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 9e01e6662f..42ca739fa2 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -27,6 +27,7 @@ bm25 = { workspace = true } chardetng = { workspace = true } chrono = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive"] } +codex-analytics = { workspace = true } codex-api = { workspace = true } codex-app-server-protocol = { workspace = true } codex-apply-patch = { workspace = true } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d58f6fdd3c..947e90beb2 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -14,13 +14,6 @@ use crate::SandboxState; use crate::agent::AgentControl; 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::CodexThreadInitializedEvent; -use crate::analytics_client::CodexTurnEvent; -use crate::analytics_client::InitializationMode; -use crate::analytics_client::InvocationType; -use crate::analytics_client::build_track_events_context; use crate::apps::render_apps_section; use crate::auth_env_telemetry::collect_auth_env_telemetry; use crate::commit_attribution::commit_message_trailer_instruction; @@ -58,6 +51,12 @@ use async_channel::Receiver; use async_channel::Sender; use chrono::Local; use chrono::Utc; +use codex_analytics::AppInvocation; +use codex_analytics::CodexThreadInitializedEvent; +use codex_analytics::CodexTurnEvent; +use codex_analytics::InitializationMode; +use codex_analytics::InvocationType; +use codex_analytics::build_track_events_context; use codex_app_server_protocol::McpServerElicitationRequest; use codex_app_server_protocol::McpServerElicitationRequestParams; use codex_exec_server::Environment; @@ -356,6 +355,10 @@ use crate::turn_timing::record_turn_ttft_metric; use crate::unified_exec::UnifiedExecProcessManager; use crate::util::backoff; use crate::windows_sandbox::WindowsSandboxLevelExt; +use codex_analytics::AnalyticsEventsClient; +use codex_analytics::AppInvocation; +use codex_analytics::InvocationType; +use codex_analytics::build_track_events_context; use codex_async_utils::OrCancelExt; use codex_git_utils::get_git_repo_root; use codex_otel::SessionTelemetry; @@ -1936,8 +1939,9 @@ 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(&config), Arc::clone(&auth_manager), + config.chatgpt_base_url.trim_end_matches('/').to_string(), + config.analytics_enabled, ), hooks, rollout: Mutex::new(rollout_recorder), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index d7fff99ea1..b46674a17e 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2668,8 +2668,9 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { shell_zsh_path: None, main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(), analytics_events_client: AnalyticsEventsClient::new( - Arc::clone(&config), Arc::clone(&auth_manager), + config.chatgpt_base_url.trim_end_matches('/').to_string(), + config.analytics_enabled, ), hooks: Hooks::new(HooksConfig { legacy_notify_argv: config.notify.clone(), @@ -3504,8 +3505,9 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( shell_zsh_path: None, main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(), analytics_events_client: AnalyticsEventsClient::new( - Arc::clone(&config), Arc::clone(&auth_manager), + config.chatgpt_base_url.trim_end_matches('/').to_string(), + config.analytics_enabled, ), hooks: Hooks::new(HooksConfig { legacy_notify_argv: config.notify.clone(), diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 71f4328aed..b4426c9cdf 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -5,7 +5,6 @@ // the TUI or the tracing stack). #![deny(clippy::print_stdout, clippy::print_stderr)] -mod analytics_client; pub mod api_bridge; mod apply_patch; mod apps; @@ -108,9 +107,9 @@ pub type NewConversation = NewThread; #[deprecated(note = "use CodexThread")] pub type CodexConversation = CodexThread; // Re-export common auth types for workspace consumers -pub use analytics_client::AnalyticsEventsClient; pub use auth::AuthManager; pub use auth::CodexAuth; +pub use codex_analytics::AnalyticsEventsClient; mod default_client_forwarding; /// Default Codex HTTP client headers and reqwest construction. diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 18a4843578..d31696db5c 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -8,9 +8,6 @@ use codex_app_server_protocol::McpServerElicitationRequest; use codex_app_server_protocol::McpServerElicitationRequestParams; use tracing::error; -use crate::analytics_client::AppInvocation; -use crate::analytics_client::InvocationType; -use crate::analytics_client::build_track_events_context; use crate::arc_monitor::ArcMonitorOutcome; use crate::arc_monitor::monitor_action; use crate::codex::Session; @@ -32,6 +29,9 @@ use crate::protocol::McpInvocation; use crate::protocol::McpToolCallBeginEvent; use crate::protocol::McpToolCallEndEvent; use crate::state_db; +use codex_analytics::AppInvocation; +use codex_analytics::InvocationType; +use codex_analytics::build_track_events_context; use codex_features::Feature; use codex_protocol::mcp::CallToolResult; use codex_protocol::openai_models::InputModality; diff --git a/codex-rs/core/src/skills/injection.rs b/codex-rs/core/src/skills/injection.rs index b83be2322c..dac05f8368 100644 --- a/codex-rs/core/src/skills/injection.rs +++ b/codex-rs/core/src/skills/injection.rs @@ -2,14 +2,14 @@ use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; -use crate::analytics_client::AnalyticsEventsClient; -use crate::analytics_client::InvocationType; -use crate::analytics_client::SkillInvocation; -use crate::analytics_client::TrackEventsContext; use crate::instructions::SkillInstructions; use crate::mention_syntax::TOOL_MENTION_SIGIL; use crate::mentions::build_skill_name_counts; use crate::skills::SkillMetadata; +use codex_analytics::AnalyticsEventsClient; +use codex_analytics::InvocationType; +use codex_analytics::SkillInvocation; +use codex_analytics::TrackEventsContext; use codex_otel::SessionTelemetry; use codex_protocol::models::ResponseItem; use codex_protocol::user_input::UserInput; diff --git a/codex-rs/core/src/skills/invocation_utils.rs b/codex-rs/core/src/skills/invocation_utils.rs index 122158bda7..084670fa86 100644 --- a/codex-rs/core/src/skills/invocation_utils.rs +++ b/codex-rs/core/src/skills/invocation_utils.rs @@ -2,13 +2,13 @@ use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; -use crate::analytics_client::InvocationType; -use crate::analytics_client::SkillInvocation; -use crate::analytics_client::build_track_events_context; use crate::codex::Session; use crate::codex::TurnContext; use crate::skills::SkillLoadOutcome; use crate::skills::SkillMetadata; +use codex_analytics::InvocationType; +use codex_analytics::SkillInvocation; +use codex_analytics::build_track_events_context; pub(crate) fn build_implicit_skill_path_indexes( skills: Vec, diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index f12f7ef971..ad980db1a7 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use crate::AuthManager; use crate::RolloutRecorder; use crate::agent::AgentControl; -use crate::analytics_client::AnalyticsEventsClient; use crate::client::ModelClient; use crate::config::StartedNetworkProxy; use crate::exec_policy::ExecPolicyManager; @@ -20,6 +19,7 @@ use crate::tools::network_approval::NetworkApprovalService; use crate::tools::runtimes::ExecveSessionApproval; use crate::tools::sandboxing::ApprovalStore; use crate::unified_exec::UnifiedExecProcessManager; +use codex_analytics::AnalyticsEventsClient; use codex_exec_server::Environment; use codex_hooks::Hooks; use codex_otel::SessionTelemetry;