diff --git a/codex-rs/cli/tests/worktree.rs b/codex-rs/cli/tests/worktree.rs index 593569493f..7031791918 100644 --- a/codex-rs/cli/tests/worktree.rs +++ b/codex-rs/cli/tests/worktree.rs @@ -214,12 +214,18 @@ trust_level = "trusted" env.insert("NO_PROXY".into(), "127.0.0.1,localhost".into()); env.insert("no_proxy".into(), "127.0.0.1,localhost".into()); env.insert("TERM".into(), "xterm-256color".into()); + env.insert("TERM_PROGRAM".into(), "unrecognized-terminal".into()); env.insert("OTEL_METRIC_EXPORT_INTERVAL".into(), "100".into()); for key in [ "CODEX_EXEC_SERVER_URL", "CODEX_ACCESS_TOKEN", "OPENAI_API_KEY", "CODEX_API_KEY", + "TMUX", + "TMUX_PANE", + "ZELLIJ", + "ZELLIJ_SESSION_NAME", + "ZELLIJ_VERSION", ] { env.remove(key); } @@ -461,17 +467,42 @@ trust_level = "trusted" .context("requests")? .into_iter() .filter(|request| request.url.path() == "/metrics") - .flat_map(|request| request.body) - .collect::>(); - assert!( - if analytics { - String::from_utf8_lossy(&metrics).contains("codex.tui.start") - } else { - metrics.is_empty() - }, - "analytics={analytics}, metrics_bytes={}", - metrics.len() - ); + .map(|request| serde_json::from_slice::(&request.body)) + .collect::>>()?; + if analytics { + let point = metrics + .iter() + .flat_map(|payload| payload["resourceMetrics"].as_array().into_iter().flatten()) + .flat_map(|resource| resource["scopeMetrics"].as_array().into_iter().flatten()) + .flat_map(|scope| scope["metrics"].as_array().into_iter().flatten()) + .filter(|metric| metric["name"] == "codex.tui.start") + .flat_map(|metric| metric["sum"]["dataPoints"].as_array().into_iter().flatten()) + .next() + .context("codex.tui.start data point")?; + let tags = point["attributes"] + .as_array() + .context("codex.tui.start attributes")? + .iter() + .map(|attribute| { + Ok(( + attribute["key"].as_str().context("metric tag key")?, + attribute["value"]["stringValue"] + .as_str() + .context("metric tag value")?, + )) + }) + .collect::>>()?; + assert_eq!( + tags, + HashMap::from([ + ("app_server_mode", "in_process"), + ("terminal_name", "unknown"), + ("multiplexer", "none"), + ]) + ); + } else { + assert!(metrics.is_empty(), "analytics disabled"); + } let (body, checkout, metadata) = observed .with_context(|| { format!( diff --git a/codex-rs/tui/src/startup_orchestration.rs b/codex-rs/tui/src/startup_orchestration.rs index c9fed10be0..3ccc92c4f9 100644 --- a/codex-rs/tui/src/startup_orchestration.rs +++ b/codex-rs/tui/src/startup_orchestration.rs @@ -4,6 +4,8 @@ //! configuration and app-server initialization remain responsive to safe local editing. use super::*; +use codex_terminal_detection::Multiplexer; +use codex_terminal_detection::TerminalName; pub(super) async fn run_main_inner( mut cli: Cli, @@ -456,10 +458,37 @@ pub(super) async fn run_main_inner( AppServerTarget::LocalDaemon { .. } => "local_daemon", AppServerTarget::Remote { .. } => "remote", }; + // Use a fixed category, not the versioned or user-provided terminal identifier. + let terminal_info = codex_terminal_detection::terminal_info(); + let terminal_name = match terminal_info.name { + TerminalName::AppleTerminal => "apple_terminal", + TerminalName::Ghostty => "ghostty", + TerminalName::Iterm2 => "iterm2", + TerminalName::WarpTerminal => "warp", + TerminalName::VsCode => "vscode", + TerminalName::WezTerm => "wezterm", + TerminalName::Kitty => "kitty", + TerminalName::Alacritty => "alacritty", + TerminalName::Konsole => "konsole", + TerminalName::GnomeTerminal => "gnome_terminal", + TerminalName::Vte => "vte", + TerminalName::WindowsTerminal => "windows_terminal", + TerminalName::Dumb => "dumb", + TerminalName::Unknown => "unknown", + }; + let multiplexer = match terminal_info.multiplexer { + Some(Multiplexer::Tmux { .. }) => "tmux", + Some(Multiplexer::Zellij { .. }) => "zellij", + None => "none", + }; let _ = metrics.counter( "codex.tui.start", /*inc*/ 1, - &[("app_server_mode", app_server_mode)], + &[ + ("app_server_mode", app_server_mode), + ("terminal_name", terminal_name), + ("multiplexer", multiplexer), + ], ); let telemetry = codex_rollout::sqlite_telemetry_recorder(metrics.clone(), otel_originator.as_str());