mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Record bucketed terminal sizes in TUI telemetry (#53859)
## What changed - Add `terminal_columns` and `terminal_rows` buckets to `codex.tui.start`. - Record `codex.tui.terminal_size` for new turns in the active thread and for resize bucket changes after the terminal size settles for 15 seconds. Reuse launch context and suppress duplicate turn notifications. - Respect analytics consent for the built-in metrics exporter while allowing explicitly configured OTLP collectors. Apply consent checks to the active exporter across provider replacements. ## Testing Add coverage for exporter replacement and consent after config reloads. Extend the PTY integration test to verify startup size buckets, turn-start observations, and delayed resize observations with analytics disabled and an explicit OTLP collector. GitOrigin-RevId: e01a900cd3f04a36731078f725c75374596cb01b
This commit is contained in:
@@ -9,6 +9,7 @@ use std::io::Write as _;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
#[cfg(not(unix))]
|
||||
use tempfile::TempDir;
|
||||
use wiremock::Mock;
|
||||
@@ -252,6 +253,10 @@ trust_level = "trusted"
|
||||
env.insert("TERM".into(), "xterm-256color".into());
|
||||
env.insert("TERM_PROGRAM".into(), "unrecognized-terminal".into());
|
||||
env.insert("OTEL_METRIC_EXPORT_INTERVAL".into(), "100".into());
|
||||
env.insert(
|
||||
"OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE".into(),
|
||||
"delta".into(),
|
||||
);
|
||||
for key in [
|
||||
"CODEX_EXEC_SERVER_URL",
|
||||
"CODEX_ACCESS_TOKEN",
|
||||
@@ -335,8 +340,8 @@ trust_level = "trusted"
|
||||
}
|
||||
let (metric_tx, mut metric_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
Mock::given(wiremock::matchers::path("/metrics"))
|
||||
.respond_with(move |_: &wiremock::Request| {
|
||||
let _ = metric_tx.send(());
|
||||
.respond_with(move |request: &wiremock::Request| {
|
||||
let _ = metric_tx.send(request.body.clone());
|
||||
ResponseTemplate::new(200).set_body_json(serde_json::json!({}))
|
||||
})
|
||||
.mount(&server)
|
||||
@@ -409,7 +414,8 @@ trust_level = "trusted"
|
||||
if analytics {
|
||||
args.extend([
|
||||
"-c".into(),
|
||||
"analytics.enabled=true".into(),
|
||||
// An explicit collector is independent of built-in analytics consent.
|
||||
"analytics.enabled=false".into(),
|
||||
"-c".into(),
|
||||
format!(
|
||||
"otel.metrics_exporter={{otlp-http={{endpoint=\"{}/metrics\",protocol=\"json\"}}}}",
|
||||
@@ -551,9 +557,44 @@ trust_level = "trusted"
|
||||
.with_context(|| format!("daemon status timed out: {output}"))??;
|
||||
}
|
||||
if analytics {
|
||||
tokio::time::timeout(Duration::from_secs(/*secs*/ 10), metric_rx.recv())
|
||||
.await?
|
||||
.context("metrics export while the TUI is running")?;
|
||||
for reason in ["turn_start", "resize"] {
|
||||
let started = Instant::now();
|
||||
let wait = if reason == "resize" {
|
||||
session.resize(codex_utils_pty::TerminalSize {
|
||||
rows: 80,
|
||||
cols: 240,
|
||||
})?;
|
||||
Duration::from_secs(/*secs*/ 30)
|
||||
} else {
|
||||
Duration::from_secs(/*secs*/ 10)
|
||||
};
|
||||
tokio::time::timeout(wait, async {
|
||||
loop {
|
||||
tokio::select! {
|
||||
body = metric_rx.recv() => {
|
||||
let payload: Value = serde_json::from_slice(&body.context("metric body")?)?;
|
||||
let found = 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.terminal_size")
|
||||
.flat_map(|metric| metric["sum"]["dataPoints"].as_array().into_iter().flatten())
|
||||
.any(|point| point["attributes"].as_array().into_iter().flatten().any(|attribute|
|
||||
attribute["key"] == "observation_reason" && attribute["value"]["stringValue"] == reason));
|
||||
if found { return Ok::<_, anyhow::Error>(()); }
|
||||
}
|
||||
bytes = stdout.recv() => {
|
||||
let bytes = bytes.context("TUI exited before size observation")?;
|
||||
output.push_str(&String::from_utf8_lossy(&bytes));
|
||||
let replies = queries.feed(&bytes);
|
||||
if !replies.is_empty() { session.writer_sender().send(replies).await?; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await.with_context(|| format!("waiting for {reason} observation"))??;
|
||||
if reason == "resize" {
|
||||
assert!(started.elapsed() >= Duration::from_secs(/*secs*/ 15));
|
||||
}
|
||||
}
|
||||
}
|
||||
let startup_result = observed.as_ref().map(|result| result.as_ref().map(|_| ()));
|
||||
assert!(
|
||||
@@ -637,6 +678,7 @@ trust_level = "trusted"
|
||||
.flat_map(|metric| metric["sum"]["dataPoints"].as_array().into_iter().flatten())
|
||||
.next()
|
||||
.context("codex.tui.start data point")?;
|
||||
assert_eq!(point["asInt"], 1);
|
||||
let tags = point["attributes"]
|
||||
.as_array()
|
||||
.context("codex.tui.start attributes")?
|
||||
@@ -653,6 +695,8 @@ trust_level = "trusted"
|
||||
let mut expected_tags = HashMap::from([
|
||||
("app_server_mode", "in_process"),
|
||||
("terminal_name", "unknown"),
|
||||
("terminal_columns", "120-239"),
|
||||
("terminal_rows", "40-79"),
|
||||
("multiplexer", "none"),
|
||||
("daemon_selection_reason", "incompatible_option"),
|
||||
(
|
||||
@@ -680,6 +724,40 @@ trust_level = "trusted"
|
||||
]);
|
||||
}
|
||||
assert_eq!(tags, expected_tags);
|
||||
let observations = exported
|
||||
.iter()
|
||||
.filter(|metric| metric["name"] == "codex.tui.terminal_size")
|
||||
.flat_map(|metric| metric["sum"]["dataPoints"].as_array().into_iter().flatten())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(observations.len(), 2);
|
||||
for observation in observations {
|
||||
assert_eq!(observation["asInt"], 1);
|
||||
let observation_tags = observation["attributes"]
|
||||
.as_array()
|
||||
.context("observation attributes")?
|
||||
.iter()
|
||||
.map(|attribute| {
|
||||
Ok((
|
||||
attribute["key"].as_str().context("observation key")?,
|
||||
attribute["value"]["stringValue"]
|
||||
.as_str()
|
||||
.context("observation value")?,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<HashMap<_, _>>>()?;
|
||||
let reason = observation_tags["observation_reason"];
|
||||
let mut expected = expected_tags.clone();
|
||||
expected.insert("observation_reason", reason);
|
||||
match reason {
|
||||
"turn_start" => {}
|
||||
"resize" => {
|
||||
expected.insert("terminal_columns", "240-479");
|
||||
expected.insert("terminal_rows", "80-119");
|
||||
}
|
||||
other => anyhow::bail!("unexpected observation: {other}"),
|
||||
}
|
||||
assert_eq!(observation_tags, expected);
|
||||
}
|
||||
} else {
|
||||
assert!(metrics.is_empty(), "metrics exporter disabled");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user