diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e9b12d..dedcc18d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## Unreleased + +## Features + +- feat(core): add Response context ([#874](https://github.com/getsentry/sentry-rust/pull/874)) by @lcian + - The `Response` context can now be attached to events, to include information about HTTP responses such as headers, cookies and status code. + - Example: + ```rust + let mut event = Event::new(); + let response = ResponseContext { + cookies: Some(r#""csrftoken": "1234567""#.to_owned()), + headers: Some(headers_map), + status_code: Some(500), + body_size: Some(15), + data: Some("Invalid request"), + }; + event + .contexts + .insert("response".to_owned(), response.into()); + ``` + ## 0.42.0 ### Features diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index ced3bd3e..617fd5e8 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -1100,6 +1100,8 @@ pub enum Context { Gpu(Box), /// OpenTelemetry data. Otel(Box), + /// HTTP response data. + Response(Box), /// Generic other context data. #[serde(rename = "unknown")] Other(Map), @@ -1117,6 +1119,7 @@ impl Context { Context::Trace(..) => "trace", Context::Gpu(..) => "gpu", Context::Otel(..) => "otel", + Context::Response(..) => "response", Context::Other(..) => "unknown", } } @@ -1351,6 +1354,29 @@ pub struct OtelContext { pub other: Map, } +/// Holds information about an HTTP response. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +pub struct ResponseContext { + /// The unparsed cookie values. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cookies: Option, + /// A map of submitted headers. + /// + /// If a header appears multiple times, it needs to be merged according to the HTTP standard + /// for header merging. Header names are treated case-insensitively by Sentry. + #[serde(default, skip_serializing_if = "Map::is_empty")] + pub headers: Map, + /// The HTTP response status code. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status_code: Option, + /// The response body size in bytes. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub body_size: Option, + /// Response data in any format that makes sense. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data: Option, +} + /// Holds the identifier for a Span #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)] #[serde(try_from = "String", into = "String")] @@ -1501,9 +1527,10 @@ into_context!(Browser, BrowserContext); into_context!(Trace, TraceContext); into_context!(Gpu, GpuContext); into_context!(Otel, OtelContext); +into_context!(Response, ResponseContext); const INFERABLE_CONTEXTS: &[&str] = &[ - "device", "os", "runtime", "app", "browser", "trace", "gpu", "otel", + "device", "os", "runtime", "app", "browser", "trace", "gpu", "otel", "response", ]; struct ContextsVisitor; diff --git a/sentry-types/tests/test_protocol_v7.rs b/sentry-types/tests/test_protocol_v7.rs index 5638d314..841cf19d 100644 --- a/sentry-types/tests/test_protocol_v7.rs +++ b/sentry-types/tests/test_protocol_v7.rs @@ -1313,6 +1313,43 @@ mod test_contexts { ); } + #[test] + fn test_response_context() { + let event = v7::Event { + event_id: event_id(), + timestamp: event_time(), + contexts: { + let mut m = v7::Map::new(); + m.insert( + "response".into(), + v7::ResponseContext { + status_code: Some(400), + cookies: Some("sessionId=abc123; Path=/; HttpOnly,authToken=xyz789; Secure; SameSite=Strict".into()), + headers: { + let mut hm = v7::Map::new(); + hm.insert("Content-Type".into(), "text/plain".into()); + hm + }, + body_size: Some(1000), + data: Some("lol".into()), + } + .into(), + ); + m + }, + ..Default::default() + }; + + assert_roundtrip(&event); + assert_eq!( + serde_json::to_string(&event).unwrap(), + "{\"event_id\":\"d43e86c96e424a93a4fbda156dd17341\",\"timestamp\":1514103120,\ + \"contexts\":{\"response\":{\"type\":\"response\",\ + \"cookies\":\"sessionId=abc123; Path=/; HttpOnly,authToken=xyz789; Secure; SameSite=Strict\",\ + \"headers\":{\"Content-Type\":\"text/plain\"},\"status_code\":400,\"body_size\":1000,\"data\":\"lol\"}}}" + ); + } + #[test] fn test_renamed_contexts() { let event = v7::Event {