Compare commits

...

1 Commits

Author SHA1 Message Date
Sayan Sisodiya
fac99a8cab add chatgpt user id into OTLP metric tags 2026-02-27 01:09:06 -08:00
6 changed files with 77 additions and 2 deletions

View File

@@ -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<String> {
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

View File

@@ -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);
}

View File

@@ -44,6 +44,7 @@ pub struct OtelEventMetadata {
pub(crate) auth_mode: Option<String>,
pub(crate) account_id: Option<String>,
pub(crate) account_email: Option<String>,
pub(crate) chatgpt_user_id: Option<String>,
pub(crate) originator: String,
pub(crate) service_name: Option<String>,
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)
}

View File

@@ -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(),

View File

@@ -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(())
}

View File

@@ -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()