[feedback] diagnostics (#13292)

- added header logic to display diagnostics on cli
- added logic for collecting env vars

<img width="606" height="327" alt="Screenshot 2026-03-03 at 3 49 31 PM"
src="https://github.com/user-attachments/assets/05e78c56-8cb3-47fa-abaf-3e57f1fdd8e2"
/>

<img width="690" height="353" alt="Screenshot 2026-03-02 at 6 47 54 PM"
src="https://github.com/user-attachments/assets/e470b559-13f4-44d9-897f-bc398943c6d1"
/>
This commit is contained in:
rhan-oai
2026-03-03 16:34:11 -08:00
committed by GitHub
parent 082682a628
commit e951ef4374
10 changed files with 586 additions and 41 deletions
@@ -0,0 +1,247 @@
use std::collections::HashMap;
use url::Url;
const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
const OPENAI_BASE_URL_ENV_VAR: &str = "OPENAI_BASE_URL";
pub const FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME: &str = "codex-connectivity-diagnostics.txt";
const PROXY_ENV_VARS: &[&str] = &[
"HTTP_PROXY",
"http_proxy",
"HTTPS_PROXY",
"https_proxy",
"ALL_PROXY",
"all_proxy",
];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FeedbackDiagnostics {
diagnostics: Vec<FeedbackDiagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FeedbackDiagnostic {
pub headline: String,
pub details: Vec<String>,
}
impl FeedbackDiagnostics {
pub fn new(diagnostics: Vec<FeedbackDiagnostic>) -> Self {
Self { diagnostics }
}
pub fn collect_from_env() -> Self {
Self::collect_from_pairs(std::env::vars())
}
fn collect_from_pairs<I, K, V>(pairs: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let env = pairs
.into_iter()
.map(|(key, value)| (key.into(), value.into()))
.collect::<HashMap<_, _>>();
let mut diagnostics = Vec::new();
let proxy_details = PROXY_ENV_VARS
.iter()
.filter_map(|key| {
let value = env.get(*key)?.trim();
if value.is_empty() {
return None;
}
let detail = match sanitize_proxy_value(value) {
Some(sanitized) => format!("{key} = {sanitized}"),
None => format!("{key} = invalid value"),
};
Some(detail)
})
.collect::<Vec<_>>();
if !proxy_details.is_empty() {
diagnostics.push(FeedbackDiagnostic {
headline: "Proxy environment variables are set and may affect connectivity."
.to_string(),
details: proxy_details,
});
}
if let Some(value) = env.get(OPENAI_BASE_URL_ENV_VAR).map(String::as_str) {
let trimmed = value.trim();
if !trimmed.is_empty() && trimmed.trim_end_matches('/') != DEFAULT_OPENAI_BASE_URL {
let detail = match sanitize_url_for_display(trimmed) {
Some(sanitized) => format!("{OPENAI_BASE_URL_ENV_VAR} = {sanitized}"),
None => format!("{OPENAI_BASE_URL_ENV_VAR} = invalid value"),
};
diagnostics.push(FeedbackDiagnostic {
headline: "OPENAI_BASE_URL is set and may affect connectivity.".to_string(),
details: vec![detail],
});
}
}
Self { diagnostics }
}
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
pub fn diagnostics(&self) -> &[FeedbackDiagnostic] {
&self.diagnostics
}
pub fn attachment_text(&self) -> Option<String> {
if self.diagnostics.is_empty() {
return None;
}
let mut lines = vec!["Connectivity diagnostics".to_string(), String::new()];
for diagnostic in &self.diagnostics {
lines.push(format!("- {}", diagnostic.headline));
lines.extend(
diagnostic
.details
.iter()
.map(|detail| format!(" - {detail}")),
);
}
Some(lines.join("\n"))
}
}
pub fn sanitize_url_for_display(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let Ok(mut url) = Url::parse(trimmed) else {
return None;
};
let _ = url.set_username("");
let _ = url.set_password(None);
url.set_query(None);
url.set_fragment(None);
Some(url.to_string().trim_end_matches('/').to_string()).filter(|value| !value.is_empty())
}
fn sanitize_proxy_value(raw: &str) -> Option<String> {
if raw.contains("://") {
return sanitize_url_for_display(raw);
}
sanitize_url_for_display(&format!("http://{raw}"))
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::FeedbackDiagnostic;
use super::FeedbackDiagnostics;
use super::sanitize_url_for_display;
#[test]
fn collect_from_pairs_reports_sanitized_diagnostics_and_attachment() {
let diagnostics = FeedbackDiagnostics::collect_from_pairs([
(
"HTTPS_PROXY",
"https://user:password@secure-proxy.example.com:443?secret=1",
),
("http_proxy", "proxy.example.com:8080"),
("all_proxy", "socks5h://all-proxy.example.com:1080"),
("OPENAI_BASE_URL", "https://example.com/v1?token=secret"),
]);
assert_eq!(
diagnostics,
FeedbackDiagnostics {
diagnostics: vec![
FeedbackDiagnostic {
headline:
"Proxy environment variables are set and may affect connectivity."
.to_string(),
details: vec![
"http_proxy = http://proxy.example.com:8080".to_string(),
"HTTPS_PROXY = https://secure-proxy.example.com".to_string(),
"all_proxy = socks5h://all-proxy.example.com:1080".to_string(),
],
},
FeedbackDiagnostic {
headline: "OPENAI_BASE_URL is set and may affect connectivity.".to_string(),
details: vec!["OPENAI_BASE_URL = https://example.com/v1".to_string()],
},
],
}
);
assert_eq!(
diagnostics.attachment_text(),
Some(
"Connectivity diagnostics\n\n- Proxy environment variables are set and may affect connectivity.\n - http_proxy = http://proxy.example.com:8080\n - HTTPS_PROXY = https://secure-proxy.example.com\n - all_proxy = socks5h://all-proxy.example.com:1080\n- OPENAI_BASE_URL is set and may affect connectivity.\n - OPENAI_BASE_URL = https://example.com/v1"
.to_string()
)
);
}
#[test]
fn collect_from_pairs_ignores_absent_and_default_values() {
for diagnostics in [
FeedbackDiagnostics::collect_from_pairs(Vec::<(String, String)>::new()),
FeedbackDiagnostics::collect_from_pairs([(
"OPENAI_BASE_URL",
"https://api.openai.com/v1/",
)]),
] {
assert_eq!(diagnostics, FeedbackDiagnostics::default());
assert_eq!(diagnostics.attachment_text(), None);
}
}
#[test]
fn collect_from_pairs_reports_invalid_values_without_echoing_them() {
let invalid_proxy = "not a valid\nproxy";
let invalid_base_url = "not a valid\nurl";
let diagnostics = FeedbackDiagnostics::collect_from_pairs([
("HTTP_PROXY", invalid_proxy),
("OPENAI_BASE_URL", invalid_base_url),
]);
assert_eq!(
diagnostics,
FeedbackDiagnostics {
diagnostics: vec![
FeedbackDiagnostic {
headline:
"Proxy environment variables are set and may affect connectivity."
.to_string(),
details: vec!["HTTP_PROXY = invalid value".to_string()],
},
FeedbackDiagnostic {
headline: "OPENAI_BASE_URL is set and may affect connectivity.".to_string(),
details: vec!["OPENAI_BASE_URL = invalid value".to_string()],
},
],
}
);
let attachment_text = diagnostics
.attachment_text()
.expect("invalid diagnostics should still render attachment text");
assert!(!attachment_text.contains(invalid_proxy));
assert!(!attachment_text.contains(invalid_base_url));
}
#[test]
fn sanitize_url_for_display_strips_credentials_query_and_fragment() {
let sanitized = sanitize_url_for_display(
"https://user:password@example.com:8443/v1?token=secret#fragment",
);
assert_eq!(sanitized, Some("https://example.com:8443/v1".to_string()));
}
}
+127 -19
View File
@@ -13,6 +13,8 @@ use anyhow::Result;
use anyhow::anyhow;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionSource;
use feedback_diagnostics::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME;
use feedback_diagnostics::FeedbackDiagnostics;
use tracing::Event;
use tracing::Level;
use tracing::field::Visit;
@@ -21,6 +23,8 @@ use tracing_subscriber::filter::Targets;
use tracing_subscriber::fmt::writer::MakeWriter;
use tracing_subscriber::registry::LookupSpan;
pub mod feedback_diagnostics;
const DEFAULT_MAX_BYTES: usize = 4 * 1024 * 1024; // 4 MiB
const SENTRY_DSN: &str =
"https://ae32ed50620d7a7792c1ce5df38b3e3e@o33249.ingest.us.sentry.io/4510195390611458";
@@ -88,7 +92,7 @@ impl CodexFeedback {
.with_filter(Targets::new().with_target(FEEDBACK_TAGS_TARGET, Level::TRACE))
}
pub fn snapshot(&self, session_id: Option<ThreadId>) -> CodexLogSnapshot {
pub fn snapshot(&self, session_id: Option<ThreadId>) -> FeedbackSnapshot {
let bytes = {
let guard = self.inner.ring.lock().expect("mutex poisoned");
guard.snapshot_bytes()
@@ -97,9 +101,10 @@ impl CodexFeedback {
let guard = self.inner.tags.lock().expect("mutex poisoned");
guard.clone()
};
CodexLogSnapshot {
FeedbackSnapshot {
bytes,
tags,
feedback_diagnostics: FeedbackDiagnostics::collect_from_env(),
thread_id: session_id
.map(|id| id.to_string())
.unwrap_or("no-active-thread-".to_string() + &ThreadId::new().to_string()),
@@ -199,17 +204,35 @@ impl RingBuffer {
}
}
pub struct CodexLogSnapshot {
pub struct FeedbackSnapshot {
bytes: Vec<u8>,
tags: BTreeMap<String, String>,
feedback_diagnostics: FeedbackDiagnostics,
pub thread_id: String,
}
impl CodexLogSnapshot {
impl FeedbackSnapshot {
pub(crate) fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn feedback_diagnostics(&self) -> &FeedbackDiagnostics {
&self.feedback_diagnostics
}
pub fn with_feedback_diagnostics(mut self, feedback_diagnostics: FeedbackDiagnostics) -> Self {
self.feedback_diagnostics = feedback_diagnostics;
self
}
pub fn feedback_diagnostics_attachment_text(&self, include_logs: bool) -> Option<String> {
if !include_logs {
return None;
}
self.feedback_diagnostics.attachment_text()
}
pub fn save_to_temp_file(&self) -> io::Result<PathBuf> {
let dir = std::env::temp_dir();
let filename = format!("codex-feedback-{}.log", self.thread_id);
@@ -224,18 +247,16 @@ impl CodexLogSnapshot {
classification: &str,
reason: Option<&str>,
include_logs: bool,
extra_log_files: &[PathBuf],
extra_attachment_paths: &[PathBuf],
session_source: Option<SessionSource>,
logs_override: Option<Vec<u8>>,
) -> Result<()> {
use std::collections::BTreeMap;
use std::fs;
use std::str::FromStr;
use std::sync::Arc;
use sentry::Client;
use sentry::ClientOptions;
use sentry::protocol::Attachment;
use sentry::protocol::Envelope;
use sentry::protocol::EnvelopeItem;
use sentry::protocol::Event;
@@ -309,16 +330,46 @@ impl CodexLogSnapshot {
}
envelope.add_item(EnvelopeItem::Event(event));
for attachment in
self.feedback_attachments(include_logs, extra_attachment_paths, logs_override)
{
envelope.add_item(EnvelopeItem::Attachment(attachment));
}
client.send_envelope(envelope);
client.flush(Some(Duration::from_secs(UPLOAD_TIMEOUT_SECS)));
Ok(())
}
fn feedback_attachments(
&self,
include_logs: bool,
extra_attachment_paths: &[PathBuf],
logs_override: Option<Vec<u8>>,
) -> Vec<sentry::protocol::Attachment> {
use sentry::protocol::Attachment;
let mut attachments = Vec::new();
if include_logs {
envelope.add_item(EnvelopeItem::Attachment(Attachment {
attachments.push(Attachment {
buffer: logs_override.unwrap_or_else(|| self.bytes.clone()),
filename: String::from("codex-logs.log"),
content_type: Some("text/plain".to_string()),
ty: None,
}));
});
}
for path in extra_log_files {
if let Some(text) = self.feedback_diagnostics_attachment_text(include_logs) {
attachments.push(Attachment {
buffer: text.into_bytes(),
filename: FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME.to_string(),
content_type: Some("text/plain".to_string()),
ty: None,
});
}
for path in extra_attachment_paths {
let data = match fs::read(path) {
Ok(data) => data,
Err(err) => {
@@ -330,22 +381,19 @@ impl CodexLogSnapshot {
continue;
}
};
let fname = path
let filename = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "extra-log.log".to_string());
let content_type = "text/plain".to_string();
envelope.add_item(EnvelopeItem::Attachment(Attachment {
attachments.push(Attachment {
buffer: data,
filename: fname,
content_type: Some(content_type),
filename,
content_type: Some("text/plain".to_string()),
ty: None,
}));
});
}
client.send_envelope(envelope);
client.flush(Some(Duration::from_secs(UPLOAD_TIMEOUT_SECS)));
Ok(())
attachments
}
}
@@ -430,7 +478,12 @@ impl Visit for FeedbackTagsVisitor {
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use std::fs;
use super::*;
use feedback_diagnostics::FeedbackDiagnostic;
use pretty_assertions::assert_eq;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
@@ -460,4 +513,59 @@ mod tests {
pretty_assertions::assert_eq!(snap.tags.get("model").map(String::as_str), Some("gpt-5"));
pretty_assertions::assert_eq!(snap.tags.get("cached").map(String::as_str), Some("true"));
}
#[test]
fn feedback_attachments_gate_connectivity_diagnostics() {
let extra_filename = format!("codex-feedback-extra-{}.jsonl", ThreadId::new());
let extra_path = std::env::temp_dir().join(&extra_filename);
fs::write(&extra_path, "rollout").expect("extra attachment should be written");
let snapshot_with_diagnostics = CodexFeedback::new()
.snapshot(None)
.with_feedback_diagnostics(FeedbackDiagnostics::new(vec![FeedbackDiagnostic {
headline: "OPENAI_BASE_URL is set and may affect connectivity.".to_string(),
details: vec!["OPENAI_BASE_URL = https://example.com/v1".to_string()],
}]));
let attachments_with_diagnostics = snapshot_with_diagnostics.feedback_attachments(
true,
std::slice::from_ref(&extra_path),
Some(vec![1]),
);
assert_eq!(
attachments_with_diagnostics
.iter()
.map(|attachment| attachment.filename.as_str())
.collect::<Vec<_>>(),
vec![
"codex-logs.log",
FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME,
extra_filename.as_str()
]
);
assert_eq!(attachments_with_diagnostics[0].buffer, vec![1]);
assert_eq!(
attachments_with_diagnostics[1].buffer,
b"Connectivity diagnostics\n\n- OPENAI_BASE_URL is set and may affect connectivity.\n - OPENAI_BASE_URL = https://example.com/v1".to_vec()
);
assert_eq!(attachments_with_diagnostics[2].buffer, b"rollout".to_vec());
assert_eq!(
OsStr::new(attachments_with_diagnostics[2].filename.as_str()),
OsStr::new(extra_filename.as_str())
);
let attachments_without_diagnostics = CodexFeedback::new()
.snapshot(None)
.feedback_attachments(true, &[], Some(vec![1]));
assert_eq!(
attachments_without_diagnostics
.iter()
.map(|attachment| attachment.filename.as_str())
.collect::<Vec<_>>(),
vec!["codex-logs.log"]
);
assert_eq!(attachments_without_diagnostics[0].buffer, vec![1]);
fs::remove_file(extra_path).expect("extra attachment should be removed");
}
}