mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Make feedback uploads proxy-aware
This commit is contained in:
24
MODULE.bazel.lock
generated
24
MODULE.bazel.lock
generated
File diff suppressed because one or more lines are too long
332
codex-rs/Cargo.lock
generated
332
codex-rs/Cargo.lock
generated
@@ -12,154 +12,6 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-codec"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"bitflags 2.13.1",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more 2.1.1",
|
||||
"encoding_rs",
|
||||
"foldhash 0.1.5",
|
||||
"futures-core",
|
||||
"http 0.2.12",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-router"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
|
||||
dependencies = [
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
"http 0.2.12",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-rt"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-server"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"mio",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-service"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-utils"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
|
||||
dependencies = [
|
||||
"local-waker",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "4.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
"actix-router",
|
||||
"actix-rt",
|
||||
"actix-server",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
"derive_more 2.1.1",
|
||||
"encoding_rs",
|
||||
"foldhash 0.1.5",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"impl-more",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2 0.6.3",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
@@ -1536,15 +1388,6 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytestring"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.5.2"
|
||||
@@ -3179,14 +3022,18 @@ name = "codex-feedback"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-http-client",
|
||||
"codex-login",
|
||||
"codex-protocol",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"pretty_assertions",
|
||||
"sentry",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5656,7 +5503,6 @@ dependencies = [
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.117",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6340,18 +6186,6 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "findshlibs"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "finl_unicode"
|
||||
version = "1.4.0"
|
||||
@@ -7910,17 +7744,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@@ -8464,12 +8287,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "impl-more"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.4"
|
||||
@@ -8932,12 +8749,6 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language-tags"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -9064,12 +8875,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "local-waker"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -12338,56 +12143,7 @@ version = "0.46.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f925d575b468e88b079faf590a8dd0c9c99e2ec29e9bab663ceb8b45056312f"
|
||||
dependencies = [
|
||||
"httpdate",
|
||||
"native-tls",
|
||||
"reqwest 0.12.28",
|
||||
"sentry-actix",
|
||||
"sentry-backtrace",
|
||||
"sentry-contexts",
|
||||
"sentry-core",
|
||||
"sentry-debug-images",
|
||||
"sentry-panic",
|
||||
"sentry-tracing",
|
||||
"tokio",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-actix"
|
||||
version = "0.46.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18bac0f6b8621fa0f85e298901e51161205788322e1a995e3764329020368058"
|
||||
dependencies = [
|
||||
"actix-http",
|
||||
"actix-web",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"sentry-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-backtrace"
|
||||
version = "0.46.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb1ef7534f583af20452b1b1bf610a60ed9c8dd2d8485e7bd064efc556a78fb"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"regex",
|
||||
"sentry-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-contexts"
|
||||
version = "0.46.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebd6be899d9938390b6d1ec71e2f53bd9e57b6a9d8b1d5b049e5c364e7da9078"
|
||||
dependencies = [
|
||||
"hostname",
|
||||
"libc",
|
||||
"os_info",
|
||||
"rustc_version",
|
||||
"sentry-core",
|
||||
"uname",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12403,39 +12159,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-debug-images"
|
||||
version = "0.46.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5637ec550dc6f8c49a711537950722d3fc4baa6fd433c371912104eaff31e2a5"
|
||||
dependencies = [
|
||||
"findshlibs",
|
||||
"sentry-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-panic"
|
||||
version = "0.46.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f02c7162f7b69b8de872b439d4696dc1d65f80b13ddd3c3831723def4756b63"
|
||||
dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-tracing"
|
||||
version = "0.46.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1dd47df349a80025819f3d25c3d2f751df705d49c65a4cdc0f130f700972a48"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-types"
|
||||
version = "0.46.1"
|
||||
@@ -14597,15 +14320,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uname"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unarray"
|
||||
version = "0.1.4"
|
||||
@@ -14739,35 +14453,6 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"der",
|
||||
"log",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"rustls-pki-types",
|
||||
"ureq-proto",
|
||||
"utf-8",
|
||||
"webpki-root-certs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"http 1.4.0",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -15173,15 +14858,6 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.5"
|
||||
|
||||
@@ -262,9 +262,9 @@ impl FeedbackRequestProcessor {
|
||||
|
||||
let session_source = self.thread_manager.session_source();
|
||||
|
||||
let upload_result = tokio::task::spawn_blocking(move || {
|
||||
let prepared_upload = tokio::task::spawn_blocking(move || {
|
||||
let tags = (!upload_tags.is_empty()).then_some(&upload_tags);
|
||||
snapshot.upload_feedback(FeedbackUploadOptions {
|
||||
snapshot.prepare_feedback_upload(FeedbackUploadOptions {
|
||||
classification: &classification,
|
||||
reason: reason.as_deref(),
|
||||
tags,
|
||||
@@ -277,7 +277,7 @@ impl FeedbackRequestProcessor {
|
||||
})
|
||||
.await;
|
||||
|
||||
let upload_result = match upload_result {
|
||||
let prepared_upload = match prepared_upload {
|
||||
Ok(result) => result,
|
||||
Err(join_err) => {
|
||||
return Err(internal_error(format!(
|
||||
@@ -286,7 +286,11 @@ impl FeedbackRequestProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
upload_result.map_err(|err| internal_error(format!("failed to upload feedback: {err}")))?;
|
||||
prepared_upload
|
||||
.map_err(|err| internal_error(format!("failed to prepare feedback: {err}")))?
|
||||
.send(&self.config.http_client_factory())
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to upload feedback: {err}")))?;
|
||||
Ok(FeedbackUploadResponse { thread_id })
|
||||
}
|
||||
|
||||
|
||||
@@ -252,7 +252,6 @@ deny = [
|
||||
"opentelemetry-http",
|
||||
"opentelemetry-otlp",
|
||||
"rmcp",
|
||||
"sentry",
|
||||
"webrtc-sys-build",
|
||||
], reason = "new direct reqwest dependencies must use codex-http-client; temporary wrappers track migration debt" },
|
||||
]
|
||||
|
||||
@@ -9,16 +9,20 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-http-client = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
mime_guess = { workspace = true }
|
||||
sentry = { version = "0.46" }
|
||||
sentry = { version = "0.46", default-features = false }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
log = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||
wiremock = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
@@ -8,10 +8,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use codex_login::AuthEnvTelemetry;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -25,9 +22,11 @@ use tracing_subscriber::fmt::writer::MakeWriter;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
|
||||
pub(crate) mod feedback_diagnostics;
|
||||
mod upload;
|
||||
pub use feedback_diagnostics::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME;
|
||||
pub use feedback_diagnostics::FeedbackDiagnostic;
|
||||
pub use feedback_diagnostics::FeedbackDiagnostics;
|
||||
pub use upload::PreparedFeedbackUpload;
|
||||
|
||||
/// Filename used for the redacted `codex doctor --json` feedback attachment.
|
||||
pub const DOCTOR_REPORT_ATTACHMENT_FILENAME: &str = "codex-doctor-report.json";
|
||||
@@ -419,78 +418,6 @@ impl FeedbackSnapshot {
|
||||
self.feedback_diagnostics.attachment_text()
|
||||
}
|
||||
|
||||
/// Upload feedback to Sentry with optional attachments.
|
||||
pub fn upload_feedback(&self, options: FeedbackUploadOptions<'_>) -> Result<()> {
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use sentry::Client;
|
||||
use sentry::ClientOptions;
|
||||
use sentry::protocol::Envelope;
|
||||
use sentry::protocol::EnvelopeItem;
|
||||
use sentry::protocol::Event;
|
||||
use sentry::protocol::Level;
|
||||
use sentry::transports::DefaultTransportFactory;
|
||||
use sentry::types::Dsn;
|
||||
|
||||
// Build Sentry client
|
||||
let client = Client::from_config(ClientOptions {
|
||||
dsn: Some(Dsn::from_str(SENTRY_DSN).map_err(|e| anyhow!("invalid DSN: {e}"))?),
|
||||
transport: Some(Arc::new(DefaultTransportFactory {})),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let tags = self.upload_tags(
|
||||
options.classification,
|
||||
options.reason,
|
||||
options.tags,
|
||||
options.session_source.as_ref(),
|
||||
);
|
||||
|
||||
let level = match options.classification {
|
||||
"bug" | "bad_result" | "safety_check" => Level::Error,
|
||||
_ => Level::Info,
|
||||
};
|
||||
|
||||
let mut envelope = Envelope::new();
|
||||
let title = format!(
|
||||
"[{}]: Codex session {}",
|
||||
display_classification(options.classification),
|
||||
self.thread_id
|
||||
);
|
||||
|
||||
let mut event = Event {
|
||||
level,
|
||||
message: Some(title.clone()),
|
||||
tags,
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(r) = options.reason {
|
||||
use sentry::protocol::Exception;
|
||||
use sentry::protocol::Values;
|
||||
|
||||
event.exception = Values::from(vec![Exception {
|
||||
ty: title,
|
||||
value: Some(r.to_string()),
|
||||
..Default::default()
|
||||
}]);
|
||||
}
|
||||
envelope.add_item(EnvelopeItem::Event(event));
|
||||
|
||||
for attachment in self.feedback_attachments(
|
||||
options.include_logs,
|
||||
options.extra_attachments,
|
||||
options.extra_attachment_paths,
|
||||
options.logs_override,
|
||||
) {
|
||||
envelope.add_item(EnvelopeItem::Attachment(attachment));
|
||||
}
|
||||
|
||||
client.send_envelope(envelope);
|
||||
client.flush(Some(Duration::from_secs(UPLOAD_TIMEOUT_SECS)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_tags(
|
||||
&self,
|
||||
classification: &str,
|
||||
|
||||
128
codex-rs/feedback/src/upload.rs
Normal file
128
codex-rs/feedback/src/upload.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_http_client::ClientRouteClass;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use sentry::ClientOptions;
|
||||
use sentry::protocol::Envelope;
|
||||
use sentry::protocol::EnvelopeItem;
|
||||
use sentry::protocol::Event;
|
||||
use sentry::protocol::Exception;
|
||||
use sentry::protocol::Level;
|
||||
use sentry::protocol::Values;
|
||||
use sentry::types::Dsn;
|
||||
|
||||
use crate::FeedbackSnapshot;
|
||||
use crate::FeedbackUploadOptions;
|
||||
use crate::SENTRY_DSN;
|
||||
use crate::UPLOAD_TIMEOUT_SECS;
|
||||
use crate::display_classification;
|
||||
|
||||
/// Serialized feedback ready to upload without performing additional filesystem reads.
|
||||
pub struct PreparedFeedbackUpload {
|
||||
endpoint: String,
|
||||
authorization: String,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PreparedFeedbackUpload {
|
||||
/// Sends the prepared envelope using the application's configured outbound proxy policy.
|
||||
pub async fn send(self, http_client_factory: &HttpClientFactory) -> Result<()> {
|
||||
let client = http_client_factory
|
||||
.build_client(&self.endpoint, ClientRouteClass::Other)
|
||||
.context("failed to build Sentry feedback HTTP client")?;
|
||||
|
||||
client
|
||||
.post(&self.endpoint)
|
||||
.header("X-Sentry-Auth", self.authorization)
|
||||
.timeout(Duration::from_secs(UPLOAD_TIMEOUT_SECS))
|
||||
.body(self.body)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to send Sentry feedback envelope")?
|
||||
.error_for_status()
|
||||
.context("Sentry feedback upload returned an unsuccessful HTTP status")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FeedbackSnapshot {
|
||||
/// Builds a feedback envelope and synchronously reads any file-backed attachments.
|
||||
///
|
||||
/// Call this from a blocking task, then send the resulting upload asynchronously with the
|
||||
/// application's configured [`HttpClientFactory`].
|
||||
pub fn prepare_feedback_upload(
|
||||
&self,
|
||||
options: FeedbackUploadOptions<'_>,
|
||||
) -> Result<PreparedFeedbackUpload> {
|
||||
let dsn = SENTRY_DSN.parse::<Dsn>().context("invalid Sentry DSN")?;
|
||||
self.prepare_feedback_upload_with_dsn(options, &dsn)
|
||||
}
|
||||
|
||||
fn prepare_feedback_upload_with_dsn(
|
||||
&self,
|
||||
options: FeedbackUploadOptions<'_>,
|
||||
dsn: &Dsn,
|
||||
) -> Result<PreparedFeedbackUpload> {
|
||||
let tags = self.upload_tags(
|
||||
options.classification,
|
||||
options.reason,
|
||||
options.tags,
|
||||
options.session_source.as_ref(),
|
||||
);
|
||||
|
||||
let level = match options.classification {
|
||||
"bug" | "bad_result" | "safety_check" => Level::Error,
|
||||
_ => Level::Info,
|
||||
};
|
||||
|
||||
let mut envelope = Envelope::new();
|
||||
let title = format!(
|
||||
"[{}]: Codex session {}",
|
||||
display_classification(options.classification),
|
||||
self.thread_id
|
||||
);
|
||||
|
||||
let mut event = Event {
|
||||
level,
|
||||
message: Some(title.clone()),
|
||||
tags,
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(reason) = options.reason {
|
||||
event.exception = Values::from(vec![Exception {
|
||||
ty: title,
|
||||
value: Some(reason.to_string()),
|
||||
..Default::default()
|
||||
}]);
|
||||
}
|
||||
envelope.add_item(EnvelopeItem::Event(event));
|
||||
|
||||
for attachment in self.feedback_attachments(
|
||||
options.include_logs,
|
||||
options.extra_attachments,
|
||||
options.extra_attachment_paths,
|
||||
options.logs_override,
|
||||
) {
|
||||
envelope.add_item(EnvelopeItem::Attachment(attachment));
|
||||
}
|
||||
|
||||
let mut body = Vec::new();
|
||||
envelope
|
||||
.to_writer(&mut body)
|
||||
.context("failed to serialize Sentry feedback envelope")?;
|
||||
|
||||
let user_agent = ClientOptions::default().user_agent;
|
||||
Ok(PreparedFeedbackUpload {
|
||||
endpoint: dsn.envelope_api_url().to_string(),
|
||||
authorization: dsn.to_auth(Some(user_agent.as_ref())).to_string(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "upload_tests.rs"]
|
||||
mod tests;
|
||||
232
codex-rs/feedback/src/upload_tests.rs
Normal file
232
codex-rs/feedback/src/upload_tests.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_http_client::cache_system_proxy_route_for_test;
|
||||
use pretty_assertions::assert_eq;
|
||||
use sentry::ClientOptions;
|
||||
use sentry::types::Auth;
|
||||
use sentry::types::Dsn;
|
||||
use serde_json::Value;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
use crate::CodexFeedback;
|
||||
use crate::FeedbackAttachment;
|
||||
use crate::FeedbackDiagnostics;
|
||||
use crate::FeedbackUploadOptions;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sends_authenticated_envelope_with_existing_event_and_attachments() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/42/envelope/"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let dsn = format!("http://public@{}/42", server.address()).parse::<Dsn>()?;
|
||||
let snapshot = CodexFeedback::new()
|
||||
.snapshot(/*session_id*/ None)
|
||||
.with_feedback_diagnostics(FeedbackDiagnostics::default());
|
||||
let extra_attachments = [FeedbackAttachment {
|
||||
filename: "doctor.json".to_string(),
|
||||
content_type: Some("application/json".to_string()),
|
||||
buffer: br#"{"ok":true}"#.to_vec(),
|
||||
}];
|
||||
let upload = snapshot.prepare_feedback_upload_with_dsn(
|
||||
FeedbackUploadOptions {
|
||||
classification: "bug",
|
||||
reason: Some("proxy regression"),
|
||||
tags: None,
|
||||
include_logs: true,
|
||||
extra_attachments: &extra_attachments,
|
||||
extra_attachment_paths: &[],
|
||||
session_source: None,
|
||||
logs_override: Some(b"captured logs".to_vec()),
|
||||
},
|
||||
&dsn,
|
||||
)?;
|
||||
|
||||
upload
|
||||
.send(&HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault))
|
||||
.await?;
|
||||
|
||||
let requests = server.received_requests().await.unwrap_or_default();
|
||||
assert_eq!(requests.len(), 1);
|
||||
let request = &requests[0];
|
||||
let user_agent = ClientOptions::default().user_agent;
|
||||
let authorization = request
|
||||
.headers
|
||||
.get("x-sentry-auth")
|
||||
.context("feedback request should include Sentry authentication")?
|
||||
.to_str()?
|
||||
.parse::<Auth>()?;
|
||||
assert_eq!(
|
||||
(
|
||||
authorization.public_key(),
|
||||
authorization.version(),
|
||||
authorization.client_agent(),
|
||||
authorization.timestamp().is_some(),
|
||||
),
|
||||
("public", 7, Some(user_agent.as_ref()), true)
|
||||
);
|
||||
|
||||
let body = String::from_utf8(request.body.clone())?;
|
||||
let lines = body.lines().collect::<Vec<_>>();
|
||||
assert_eq!(lines.len(), 7);
|
||||
let event = serde_json::from_str::<Value>(lines[2])?;
|
||||
assert_eq!(
|
||||
(
|
||||
event["message"].as_str(),
|
||||
event["tags"]["classification"].as_str(),
|
||||
event["exception"]["values"][0]["value"].as_str(),
|
||||
),
|
||||
(
|
||||
Some(format!("[Bug]: Codex session {}", snapshot.thread_id).as_str()),
|
||||
Some("bug"),
|
||||
Some("proxy regression"),
|
||||
)
|
||||
);
|
||||
|
||||
let logs_header = serde_json::from_str::<Value>(lines[3])?;
|
||||
let doctor_header = serde_json::from_str::<Value>(lines[5])?;
|
||||
assert_eq!(
|
||||
vec![
|
||||
(
|
||||
logs_header["filename"].as_str(),
|
||||
logs_header["content_type"].as_str(),
|
||||
lines[4],
|
||||
),
|
||||
(
|
||||
doctor_header["filename"].as_str(),
|
||||
doctor_header["content_type"].as_str(),
|
||||
lines[6],
|
||||
),
|
||||
],
|
||||
vec![
|
||||
(Some("codex-logs.log"), Some("text/plain"), "captured logs"),
|
||||
(
|
||||
Some("doctor.json"),
|
||||
Some("application/json"),
|
||||
r#"{"ok":true}"#,
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn respects_system_proxy_for_sentry_envelope_destination() -> Result<()> {
|
||||
let proxy = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/42/envelope/"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(1)
|
||||
.mount(&proxy)
|
||||
.await;
|
||||
|
||||
let dsn = "http://public@feedback-proxy.invalid/42".parse::<Dsn>()?;
|
||||
let endpoint = dsn.envelope_api_url().to_string();
|
||||
cache_system_proxy_route_for_test(&endpoint, proxy.uri());
|
||||
let upload = CodexFeedback::new()
|
||||
.snapshot(/*session_id*/ None)
|
||||
.prepare_feedback_upload_with_dsn(
|
||||
FeedbackUploadOptions {
|
||||
classification: "bug",
|
||||
reason: None,
|
||||
tags: None,
|
||||
include_logs: false,
|
||||
extra_attachments: &[],
|
||||
extra_attachment_paths: &[],
|
||||
session_source: None,
|
||||
logs_override: None,
|
||||
},
|
||||
&dsn,
|
||||
)?;
|
||||
|
||||
upload
|
||||
.send(&HttpClientFactory::new(
|
||||
OutboundProxyPolicy::RespectSystemProxy,
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_sentry_http_failures_to_the_caller() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/42/envelope/"))
|
||||
.respond_with(ResponseTemplate::new(503))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let dsn = format!("http://public@{}/42", server.address()).parse::<Dsn>()?;
|
||||
let upload = CodexFeedback::new()
|
||||
.snapshot(/*session_id*/ None)
|
||||
.prepare_feedback_upload_with_dsn(
|
||||
FeedbackUploadOptions {
|
||||
classification: "bug",
|
||||
reason: None,
|
||||
tags: None,
|
||||
include_logs: false,
|
||||
extra_attachments: &[],
|
||||
extra_attachment_paths: &[],
|
||||
session_source: None,
|
||||
logs_override: None,
|
||||
},
|
||||
&dsn,
|
||||
)?;
|
||||
|
||||
let error = upload
|
||||
.send(&HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault))
|
||||
.await
|
||||
.expect_err("Sentry HTTP failures should fail the feedback upload");
|
||||
assert!(format!("{error:#}").contains("503"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_invalid_proxy_configuration_to_the_caller() -> Result<()> {
|
||||
let dsn = "http://public@invalid-feedback-proxy.invalid/42".parse::<Dsn>()?;
|
||||
let endpoint = dsn.envelope_api_url().to_string();
|
||||
cache_system_proxy_route_for_test(&endpoint, "not a valid proxy".to_string());
|
||||
let upload = CodexFeedback::new()
|
||||
.snapshot(/*session_id*/ None)
|
||||
.prepare_feedback_upload_with_dsn(
|
||||
FeedbackUploadOptions {
|
||||
classification: "bug",
|
||||
reason: None,
|
||||
tags: None,
|
||||
include_logs: false,
|
||||
extra_attachments: &[],
|
||||
extra_attachment_paths: &[],
|
||||
session_source: None,
|
||||
logs_override: None,
|
||||
},
|
||||
&dsn,
|
||||
)?;
|
||||
|
||||
let error = upload
|
||||
.send(&HttpClientFactory::new(
|
||||
OutboundProxyPolicy::RespectSystemProxy,
|
||||
))
|
||||
.await
|
||||
.expect_err("invalid proxy configuration should fail the feedback upload");
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("failed to build Sentry feedback HTTP client")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user