relay_event_schema/protocol/contexts/
response.rs

1use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};
2
3use crate::processor::ProcessValue;
4use crate::protocol::{Cookies, Headers};
5
6/// Response interface that contains information on a HTTP response related to the event.
7///
8/// The data variable should only contain the response body. It can either be
9/// a dictionary (for standard HTTP responses) or a raw response body.
10#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
11pub struct ResponseContext {
12    /// The cookie values.
13    ///
14    /// Can be given unparsed as string, as dictionary, or as a list of tuples.
15    #[metastructure(pii = "true", max_depth = 5, max_bytes = 2048)]
16    #[metastructure(skip_serialization = "empty")]
17    pub cookies: Annotated<Cookies>,
18
19    /// A dictionary of submitted headers.
20    ///
21    /// If a header appears multiple times it, needs to be merged according to the HTTP standard
22    /// for header merging. Header names are treated case-insensitively by Sentry.
23    #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
24    #[metastructure(skip_serialization = "empty")]
25    pub headers: Annotated<Headers>,
26
27    /// HTTP status code.
28    pub status_code: Annotated<u64>,
29
30    /// HTTP response body size.
31    pub body_size: Annotated<u64>,
32
33    /// Response data in any format that makes sense.
34    ///
35    /// SDKs should discard large and binary bodies by default. Can be given as a string or
36    /// structural data of any format.
37    #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
38    pub data: Annotated<Value>,
39
40    /// The inferred content type of the response payload.
41    #[metastructure(skip_serialization = "empty")]
42    pub inferred_content_type: Annotated<String>,
43
44    /// Additional arbitrary fields for forwards compatibility.
45    /// These fields are retained (`retain = true`) to keep supporting the format that the Dio integration sends:
46    /// <https://github.com/getsentry/sentry-dart/blob/7011abe27ac69bd160bdc6ecf3314974b8340b97/dart/lib/src/protocol/sentry_response.dart#L4-L8>
47    #[metastructure(additional_properties, retain = true, pii = "maybe")]
48    pub other: Object<Value>,
49}
50
51impl super::DefaultContext for ResponseContext {
52    fn default_key() -> &'static str {
53        "response"
54    }
55
56    fn from_context(context: super::Context) -> Option<Self> {
57        match context {
58            super::Context::Response(c) => Some(*c),
59            _ => None,
60        }
61    }
62
63    fn cast(context: &super::Context) -> Option<&Self> {
64        match context {
65            super::Context::Response(c) => Some(c),
66            _ => None,
67        }
68    }
69
70    fn cast_mut(context: &mut super::Context) -> Option<&mut Self> {
71        match context {
72            super::Context::Response(c) => Some(c),
73            _ => None,
74        }
75    }
76
77    fn into_context(self) -> super::Context {
78        super::Context::Response(Box::new(self))
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::protocol::{Context, PairList};
86
87    #[test]
88    fn test_response_context_roundtrip() {
89        let json = r#"{
90  "cookies": [
91    [
92      "PHPSESSID",
93      "298zf09hf012fh2"
94    ],
95    [
96      "csrftoken",
97      "u32t4o3tb3gg43"
98    ],
99    [
100      "_gat",
101      "1"
102    ]
103  ],
104  "headers": [
105    [
106      "Content-Type",
107      "text/html"
108    ]
109  ],
110  "status_code": 500,
111  "body_size": 1000,
112  "data": {
113    "some": 1
114  },
115  "inferred_content_type": "application/json",
116  "arbitrary_field": "arbitrary",
117  "type": "response"
118}"#;
119
120        let cookies =
121            Cookies::parse("PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;;")
122                .unwrap();
123        let headers = vec![Annotated::new((
124            Annotated::new("content-type".to_string().into()),
125            Annotated::new("text/html".to_string().into()),
126        ))];
127        let context = Annotated::new(Context::Response(Box::new(ResponseContext {
128            cookies: Annotated::new(cookies),
129            headers: Annotated::new(Headers(PairList(headers))),
130            status_code: Annotated::new(500),
131            body_size: Annotated::new(1000),
132            data: {
133                let mut map = Object::new();
134                map.insert("some".to_string(), Annotated::new(Value::I64(1)));
135                Annotated::new(Value::Object(map))
136            },
137            inferred_content_type: Annotated::new("application/json".to_string()),
138            other: {
139                let mut map = Object::new();
140                map.insert(
141                    "arbitrary_field".to_string(),
142                    Annotated::new(Value::String("arbitrary".to_string())),
143                );
144                map
145            },
146        })));
147
148        assert_eq!(context, Annotated::from_json(json).unwrap());
149        assert_eq!(json, context.to_json_pretty().unwrap());
150    }
151}