Make feedback uploads proxy-aware

This commit is contained in:
celia-oai
2026-08-17 16:22:49 -07:00
parent 050aa077b5
commit f385d036f3
8 changed files with 379 additions and 433 deletions

24
MODULE.bazel.lock generated

File diff suppressed because one or more lines are too long

332
codex-rs/Cargo.lock generated
View File

@@ -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"

View File

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

View File

@@ -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" },
]

View File

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

View File

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

View 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;

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