diff --git a/CHANGELOG.md b/CHANGELOG.md index fe4224f8a..84bab0bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ - Removed the public `ClientOptions::sample_rate` field. Use `ClientOptions::event_sampling_strategy` to inspect the configured event sampling strategy, and use the existing `ClientOptions::sample_rate(...)` builder setter to configure fixed-rate sampling ([#1228](https://github.com/getsentry/sentry-rust/pull/1228)). - Removed the public `ClientOptions::traces_sample_rate` and `ClientOptions::traces_sampler` fields. Use `ClientOptions::traces_sampling_strategy` to inspect the configured traces sampling strategy, and use the existing `ClientOptions::traces_sample_rate(...)` and `ClientOptions::traces_sampler(...)` builder setters to configure fixed-rate and callback-based sampling ([#1227](https://github.com/getsentry/sentry-rust/pull/1227)). +### Features + +- `SentryHttpLayer` now records the [`http.response.status_code`](https://getsentry.github.io/sentry-conventions/attributes/http/) attribute on transactions ([#1253](https://github.com/getsentry/sentry-rust/pull/1253)). + ## 0.48.5 ### Fixes diff --git a/Cargo.lock b/Cargo.lock index 5d6d69c8d..f00adfbb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3560,6 +3560,7 @@ dependencies = [ "curl", "embedded-svc", "esp-idf-svc", + "http 1.4.0", "httpdate", "log", "native-tls", diff --git a/sentry-tower/src/http.rs b/sentry-tower/src/http.rs index 5b7c2f4eb..b384dbeb7 100644 --- a/sentry-tower/src/http.rs +++ b/sentry-tower/src/http.rs @@ -138,12 +138,27 @@ where match slf.future.poll(cx) { Poll::Ready(res) => { if let Some((transaction, parent_span)) = slf.transaction.take() { - if transaction.get_status().is_none() { - let status = match &res { - Ok(res) => map_status(res.status()), - Err(_) => protocol::SpanStatus::UnknownError, - }; - transaction.set_status(status); + match &res { + Ok(res) => { + if !transaction + .get_trace_context() + .data + .contains_key("http.response.status_code") + { + transaction.set_data( + "http.response.status_code", + res.status().as_u16().into(), + ); + } + if transaction.get_status().is_none() { + transaction.set_status(map_status(res.status())); + } + } + Err(_) => { + if transaction.get_status().is_none() { + transaction.set_status(protocol::SpanStatus::UnknownError); + } + } } transaction.finish(); sentry_core::configure_scope(|scope| scope.set_span(parent_span)); diff --git a/sentry/Cargo.toml b/sentry/Cargo.toml index c1088101e..697b02be1 100644 --- a/sentry/Cargo.toml +++ b/sentry/Cargo.toml @@ -98,10 +98,11 @@ esp-idf-svc = { version = "0.51.0", optional = true } sentry-anyhow = { path = "../sentry-anyhow" } sentry-log = { path = "../sentry-log" } sentry-slog = { path = "../sentry-slog" } -sentry-tower = { path = "../sentry-tower" } +sentry-tower = { path = "../sentry-tower", features = ["http"] } sentry-tracing = { path = "../sentry-tracing" } actix-web = { version = "4", default-features = false } anyhow = { version = "1.0.30" } +http = "1.0.0" log = { version = "0.4.8", features = ["std"] } pretty_env_logger = "0.5.0" slog = { version = "2.5.2" } diff --git a/sentry/tests/test_tower.rs b/sentry/tests/test_tower.rs index 4023e2cd3..cc8c51c75 100644 --- a/sentry/tests/test_tower.rs +++ b/sentry/tests/test_tower.rs @@ -3,13 +3,64 @@ use std::sync::Arc; use sentry::{ - protocol::{Breadcrumb, Level}, + protocol::{Breadcrumb, Context, EnvelopeItem, Level, SpanStatus}, test::TestTransport, ClientOptions, Hub, }; -use sentry_tower::SentryLayer; +use sentry_tower::{SentryHttpLayer, SentryLayer}; use tower::{ServiceBuilder, ServiceExt}; +#[test] +fn test_tower_http_records_response_status_code() { + let options = ClientOptions::new().traces_sample_rate(1.0); + + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let service = ServiceBuilder::new() + .layer(SentryHttpLayer::new().enable_transaction()) + .service_fn(|_req: http::Request<()>| async move { + Ok::<_, std::convert::Infallible>( + http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .body(()) + .unwrap(), + ) + }); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let request = http::Request::builder() + .method(http::Method::GET) + .uri("http://example.com/missing") + .body(()) + .unwrap(); + let response = rt.block_on(service.oneshot(request)).unwrap(); + assert_eq!(response.status(), http::StatusCode::NOT_FOUND); + }, + options, + ); + + assert_eq!(envelopes.len(), 1); + let transaction = match envelopes[0].items().next().unwrap() { + EnvelopeItem::Transaction(transaction) => transaction, + _ => panic!("expected a transaction item"), + }; + + assert_eq!(transaction.name.as_deref(), Some("GET /missing")); + let Context::Trace(trace) = transaction.contexts.get("trace").unwrap() else { + panic!("expected a trace context"); + }; + assert_eq!(trace.op.as_deref(), Some("http.server")); + assert_eq!(trace.origin.as_deref(), Some("auto.http.tower")); + assert_eq!(trace.status, Some(SpanStatus::NotFound)); + assert_eq!( + trace.data.get("http.response.status_code"), + Some(&404.into()) + ); +} + #[test] fn test_tower_hub() { // Create a fake transport for new hubs