relay_server/services/processor/
report.rs

1//! Contains code related to validation and normalization of the user reports.
2
3use std::error::Error;
4
5use relay_event_schema::protocol::UserReport;
6
7use crate::envelope::{ContentType, ItemType};
8use crate::managed::{ItemAction, TypedEnvelope};
9use crate::services::outcome::{DiscardReason, Outcome};
10
11/// Validates and normalizes all user report items in the envelope.
12///
13/// User feedback items are removed from the envelope if they contain invalid JSON or if the
14/// JSON violates the schema (basic type validation). Otherwise, their normalized representation
15/// is written back into the item.
16pub fn process_user_reports<Group>(managed_envelope: &mut TypedEnvelope<Group>) {
17    managed_envelope.retain_items(|item| {
18        if item.ty() != &ItemType::UserReport {
19            return ItemAction::Keep;
20        };
21
22        let payload = item.payload();
23        // There is a customer SDK which sends invalid reports with a trailing `\n`,
24        // strip it here, even if they update/fix their SDK there will still be many old
25        // versions with the broken SDK out there.
26        let payload = trim_whitespaces(&payload);
27        let report = match serde_json::from_slice::<UserReport>(payload) {
28            Ok(report) => report,
29            Err(error) => {
30                relay_log::debug!(
31                    error = &error as &dyn Error,
32                    "failed to deserialize user report"
33                );
34                return ItemAction::Drop(Outcome::Invalid(DiscardReason::InvalidJson));
35            }
36        };
37
38        let json_string = match serde_json::to_string(&report) {
39            Ok(json) => json,
40            Err(err) => {
41                relay_log::error!(
42                    error = &err as &dyn Error,
43                    "failed to serialize user report"
44                );
45                return ItemAction::Drop(Outcome::Invalid(DiscardReason::Internal));
46            }
47        };
48
49        item.set_payload(ContentType::Json, json_string);
50        ItemAction::Keep
51    });
52}
53
54fn trim_whitespaces(data: &[u8]) -> &[u8] {
55    let Some(from) = data.iter().position(|x| !x.is_ascii_whitespace()) else {
56        return &[];
57    };
58    let Some(to) = data.iter().rposition(|x| !x.is_ascii_whitespace()) else {
59        return &[];
60    };
61    &data[from..to + 1]
62}
63
64#[cfg(test)]
65mod tests {
66    use relay_event_schema::protocol::EventId;
67    use relay_system::Addr;
68
69    use crate::envelope::{Envelope, Item};
70    use crate::extractors::RequestMeta;
71    use crate::managed::ManagedEnvelope;
72    use crate::processing;
73    use crate::services::processor::{ProcessEnvelopeGrouped, ProcessingGroup, Submit};
74    use crate::testutils::create_test_processor;
75
76    use super::*;
77
78    #[tokio::test]
79    async fn test_user_report_invalid() {
80        let processor = create_test_processor(Default::default()).await;
81        let outcome_aggregator = Addr::dummy();
82        let event_id = EventId::new();
83
84        let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
85            .parse()
86            .unwrap();
87
88        let request_meta = RequestMeta::new(dsn);
89        let mut envelope = Envelope::from_request(Some(event_id), request_meta);
90
91        envelope.add_item({
92            let mut item = Item::new(ItemType::UserReport);
93            item.set_payload(ContentType::Json, r#"{"foo": "bar"}"#);
94            item
95        });
96
97        envelope.add_item({
98            let mut item = Item::new(ItemType::Event);
99            item.set_payload(ContentType::Json, "{}");
100            item
101        });
102
103        let mut envelopes = ProcessingGroup::split_envelope(*envelope, &Default::default());
104        assert_eq!(envelopes.len(), 1);
105        let (group, envelope) = envelopes.pop().unwrap();
106        let envelope = ManagedEnvelope::new(envelope, outcome_aggregator);
107
108        let message = ProcessEnvelopeGrouped {
109            group,
110            envelope,
111            ctx: processing::Context::for_test(),
112        };
113
114        let Ok(Some(Submit::Envelope(new_envelope))) = processor.process(message).await else {
115            panic!();
116        };
117        let new_envelope = new_envelope.envelope();
118
119        assert_eq!(new_envelope.len(), 1);
120        assert_eq!(new_envelope.items().next().unwrap().ty(), &ItemType::Event);
121    }
122
123    #[test]
124    fn test_trim_whitespaces() {
125        assert_eq!(trim_whitespaces(b""), b"");
126        assert_eq!(trim_whitespaces(b" \n\r "), b"");
127        assert_eq!(trim_whitespaces(b" \nx\r "), b"x");
128        assert_eq!(trim_whitespaces(b" {foo: bar} "), b"{foo: bar}");
129        assert_eq!(trim_whitespaces(b"{ foo: bar}"), b"{ foo: bar}");
130    }
131}