From fac99a8cabbe0fc32c53fa7c5180852c72adc99b Mon Sep 17 00:00:00 2001 From: Sayan Sisodiya Date: Fri, 27 Feb 2026 01:09:06 -0800 Subject: [PATCH] add chatgpt user id into OTLP metric tags --- codex-rs/core/src/auth.rs | 6 +++ codex-rs/core/src/codex.rs | 4 ++ codex-rs/otel/src/lib.rs | 20 +++++++++- codex-rs/otel/src/traces/otel_manager.rs | 1 + codex-rs/otel/tests/suite/manager_metrics.rs | 42 ++++++++++++++++++++ codex-rs/tui/src/app.rs | 6 ++- 6 files changed, 77 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index 217334b2a5..cbf87ab2b5 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -262,6 +262,12 @@ impl CodexAuth { self.get_current_token_data().and_then(|t| t.id_token.email) } + /// Returns `None` if `is_chatgpt_auth()` is false. + pub fn get_chatgpt_user_id(&self) -> Option { + self.get_current_token_data() + .and_then(|t| t.id_token.chatgpt_user_id) + } + /// Account-facing plan classification derived from the current token. /// Returns a high-level `AccountPlanType` (e.g., Free/Plus/Pro/Team/…) /// mapped from the ID token's internal plan value. Prefer this when you diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c511739f3a..e2ab6c013c 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1269,6 +1269,7 @@ impl Session { let auth_mode = auth.map(CodexAuth::auth_mode).map(TelemetryAuthMode::from); let account_id = auth.and_then(CodexAuth::get_account_id); let account_email = auth.and_then(CodexAuth::get_account_email); + let chatgpt_user_id = auth.and_then(CodexAuth::get_chatgpt_user_id); let originator = crate::default_client::originator().value; let terminal_type = terminal::user_agent(); let session_model = session_configuration.collaboration_mode.model().to_string(); @@ -1284,6 +1285,9 @@ impl Session { terminal_type.clone(), session_configuration.session_source.clone(), ); + if let Some(chatgpt_user_id) = chatgpt_user_id.as_deref() { + otel_manager = otel_manager.with_chatgpt_user_id(chatgpt_user_id); + } if let Some(service_name) = session_configuration.metrics_service_name.as_deref() { otel_manager = otel_manager.with_metrics_service_name(service_name); } diff --git a/codex-rs/otel/src/lib.rs b/codex-rs/otel/src/lib.rs index 5aaee5f5a9..30ed47f947 100644 --- a/codex-rs/otel/src/lib.rs +++ b/codex-rs/otel/src/lib.rs @@ -44,6 +44,7 @@ pub struct OtelEventMetadata { pub(crate) auth_mode: Option, pub(crate) account_id: Option, pub(crate) account_email: Option, + pub(crate) chatgpt_user_id: Option, pub(crate) originator: String, pub(crate) service_name: Option, pub(crate) session_source: String, @@ -73,6 +74,11 @@ impl OtelManager { self } + pub fn with_chatgpt_user_id(mut self, chatgpt_user_id: &str) -> Self { + self.metadata.chatgpt_user_id = Some(sanitize_metric_tag_value(chatgpt_user_id)); + self + } + pub fn with_metrics(mut self, metrics: MetricsClient) -> Self { self.metrics = Some(metrics); self.metrics_use_metadata_tags = true; @@ -203,7 +209,7 @@ impl OtelManager { if !self.metrics_use_metadata_tags { return Ok(Vec::new()); } - let mut tags = Vec::with_capacity(7); + let mut tags = Vec::with_capacity(9); Self::push_metadata_tag(&mut tags, "auth_mode", self.metadata.auth_mode.as_deref())?; Self::push_metadata_tag( &mut tags, @@ -222,6 +228,18 @@ impl OtelManager { )?; Self::push_metadata_tag(&mut tags, "model", Some(self.metadata.model.as_str()))?; Self::push_metadata_tag(&mut tags, "app.version", Some(self.metadata.app_version))?; + // Emit both tag names for the same public ChatGPT user id until we confirm + // which downstream metrics pipeline reads: `enduser.id` or `user_id`. + Self::push_metadata_tag( + &mut tags, + "enduser.id", + self.metadata.chatgpt_user_id.as_deref(), + )?; + Self::push_metadata_tag( + &mut tags, + "user_id", + self.metadata.chatgpt_user_id.as_deref(), + )?; Ok(tags) } diff --git a/codex-rs/otel/src/traces/otel_manager.rs b/codex-rs/otel/src/traces/otel_manager.rs index 32b9fba860..b4492e28e6 100644 --- a/codex-rs/otel/src/traces/otel_manager.rs +++ b/codex-rs/otel/src/traces/otel_manager.rs @@ -78,6 +78,7 @@ impl OtelManager { auth_mode: auth_mode.map(|m| m.to_string()), account_id, account_email, + chatgpt_user_id: None, originator: sanitize_metric_tag_value(originator.as_str()), service_name: None, session_source: session_source.to_string(), diff --git a/codex-rs/otel/tests/suite/manager_metrics.rs b/codex-rs/otel/tests/suite/manager_metrics.rs index 53a9cc89d6..6ac43f6edc 100644 --- a/codex-rs/otel/tests/suite/manager_metrics.rs +++ b/codex-rs/otel/tests/suite/manager_metrics.rs @@ -153,3 +153,45 @@ fn manager_attaches_optional_service_name_tag() -> Result<()> { Ok(()) } + +#[test] +fn manager_attaches_chatgpt_user_id_tags_to_metrics() -> Result<()> { + let (metrics, exporter) = build_metrics_with_defaults(&[])?; + let manager = OtelManager::new( + ThreadId::new(), + "gpt-5.1", + "gpt-5.1", + Some("account-id".to_string()), + None, + Some(TelemetryAuthMode::Chatgpt), + "test_originator".to_string(), + false, + "tty".to_string(), + SessionSource::Cli, + ) + .with_chatgpt_user_id("user-12345") + .with_metrics(metrics); + + manager.counter("codex.session_started", 1, &[]); + manager.shutdown_metrics()?; + + let resource_metrics = latest_metrics(&exporter); + let metric = + find_metric(&resource_metrics, "codex.session_started").expect("counter metric missing"); + let attrs = match metric.data() { + AggregatedMetrics::U64(data) => match data { + MetricData::Sum(sum) => { + let points: Vec<_> = sum.data_points().collect(); + assert_eq!(points.len(), 1); + attributes_to_map(points[0].attributes()) + } + _ => panic!("unexpected counter aggregation"), + }, + _ => panic!("unexpected counter data type"), + }; + + assert_eq!(attrs.get("enduser.id"), Some(&"user-12345".to_string())); + assert_eq!(attrs.get("user_id"), Some(&"user-12345".to_string())); + + Ok(()) +} diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index e32a76da0f..153148a61c 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1366,7 +1366,8 @@ impl App { let auth_mode = auth_ref .map(CodexAuth::auth_mode) .map(TelemetryAuthMode::from); - let otel_manager = OtelManager::new( + let chatgpt_user_id = auth_ref.and_then(CodexAuth::get_chatgpt_user_id); + let mut otel_manager = OtelManager::new( ThreadId::new(), model.as_str(), model.as_str(), @@ -1378,6 +1379,9 @@ impl App { codex_core::terminal::user_agent(), SessionSource::Cli, ); + if let Some(chatgpt_user_id) = chatgpt_user_id.as_deref() { + otel_manager = otel_manager.with_chatgpt_user_id(chatgpt_user_id); + } if config .tui_status_line .as_ref()