relay_filter/
error_messages.rs

1//! Implements event filtering based on the error message
2//!
3//! Specific values in the error message or in the exception values can be used to
4//! filter messages with this filter.
5
6use std::borrow::Cow;
7
8use relay_pattern::Patterns;
9
10use crate::{ErrorMessagesFilterConfig, FilterStatKey, Filterable};
11
12/// Checks events by patterns in their error messages.
13fn matches<F: Filterable>(item: &F, patterns: &Patterns) -> bool {
14    if let Some(logentry) = item.logentry() {
15        if let Some(message) = logentry.formatted.value() {
16            if patterns.is_match(message.as_ref()) {
17                return true;
18            }
19        } else if let Some(message) = logentry.message.value() {
20            if patterns.is_match(message.as_ref()) {
21                return true;
22            }
23        }
24    }
25
26    if let Some(exception_values) = item.exceptions() {
27        if let Some(exceptions) = exception_values.values.value() {
28            for exception in exceptions {
29                if let Some(exception) = exception.value() {
30                    let ty = exception.ty.as_str().unwrap_or_default();
31                    let value = exception.value.as_str().unwrap_or_default();
32                    let message = match (ty, value) {
33                        ("", value) => Cow::Borrowed(value),
34                        (ty, "") => Cow::Borrowed(ty),
35                        (ty, value) => Cow::Owned(format!("{ty}: {value}")),
36                    };
37                    if patterns.is_match(message.as_ref()) {
38                        return true;
39                    }
40                }
41            }
42        }
43    }
44    false
45}
46
47/// Filters events by patterns in their error messages.
48pub fn should_filter<F: Filterable>(
49    item: &F,
50    config: &ErrorMessagesFilterConfig,
51) -> Result<(), FilterStatKey> {
52    if matches(item, &config.patterns) {
53        Err(FilterStatKey::ErrorMessage)
54    } else {
55        Ok(())
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use relay_event_schema::protocol::{Event, Exception, LogEntry, Values};
62    use relay_pattern::TypedPatterns;
63    use relay_protocol::Annotated;
64
65    use super::*;
66
67    #[test]
68    fn test_should_filter_exception() {
69        let configs = &[
70            // with globs
71            ErrorMessagesFilterConfig {
72                patterns: TypedPatterns::from([
73                    "filteredexception*".to_owned(),
74                    "*this is a filtered exception.".to_owned(),
75                    "".to_owned(),
76                    "this is".to_owned(),
77                ]),
78            },
79            // without globs
80            ErrorMessagesFilterConfig {
81                patterns: TypedPatterns::from([
82                    "filteredexception: this is a filtered exception.".to_owned(),
83                    "filteredexception".to_owned(),
84                    "this is a filtered exception.".to_owned(),
85                ]),
86            },
87        ];
88
89        let cases = &[
90            (
91                Some("UnfilteredException"),
92                None,
93                "UnfilteredException",
94                true,
95            ),
96            (
97                None,
98                Some("This is an unfiltered exception."),
99                "This is an unfiltered exception.",
100                true,
101            ),
102            (None, None, "This is an unfiltered exception.", true),
103            (None, None, "", true),
104            (
105                Some("UnfilteredException"),
106                Some("This is an unfiltered exception."),
107                "UnfilteredException: This is an unfiltered exception.",
108                true,
109            ),
110            (Some("FilteredException"), None, "FilteredException", false),
111            (
112                None,
113                Some("This is a filtered exception."),
114                "This is a filtered exception.",
115                false,
116            ),
117            (None, None, "This is a filtered exception.", false),
118            (
119                Some("FilteredException"),
120                Some("This is a filtered exception."),
121                "FilteredException: This is a filtered exception.",
122                false,
123            ),
124            (
125                Some("OtherException"),
126                Some("This is a random exception."),
127                "FilteredException: This is a filtered exception.",
128                false,
129            ),
130            (
131                None,
132                None,
133                "FilteredException: This is a filtered exception.",
134                false,
135            ),
136            (
137                Some("FilteredException"),
138                Some("This is a filtered exception."),
139                "hi this is a legit log message",
140                false,
141            ),
142        ];
143
144        for config in &configs[..] {
145            for &case in &cases[..] {
146                let (exc_type, exc_value, logentry_formatted, should_ingest) = case;
147                let event = Event {
148                    exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
149                        ty: Annotated::from(exc_type.map(str::to_string)),
150                        value: Annotated::from(exc_value.map(str::to_owned).map(From::from)),
151                        ..Default::default()
152                    })])),
153                    logentry: Annotated::new(LogEntry {
154                        formatted: Annotated::new(logentry_formatted.to_string().into()),
155                        ..Default::default()
156                    }),
157                    ..Default::default()
158                };
159
160                assert_eq!(
161                    should_filter(&event, config),
162                    if should_ingest {
163                        Ok(())
164                    } else {
165                        Err(FilterStatKey::ErrorMessage)
166                    }
167                );
168            }
169        }
170    }
171
172    #[test]
173    fn test_filter_hydration_error() {
174        let pattern =
175            "*https://reactjs.org/docs/error-decoder.html?invariant={418,419,422,423,425}*";
176        let config = ErrorMessagesFilterConfig {
177            patterns: TypedPatterns::from([pattern.to_owned()]),
178        };
179
180        let event = Annotated::<Event>::from_json(
181            r#"{
182                "exception": {
183                    "values": [
184                        {
185                            "type": "Error",
186                            "value": "Minified React error #423; visit https://reactjs.org/docs/error-decoder.html?invariant=423 for the full message or use the non-minified dev environment for full errors and additional helpful warnings."
187                        }
188                    ]
189                }
190            }"#,
191        ).unwrap();
192
193        assert!(should_filter(&event.0.unwrap(), &config) == Err(FilterStatKey::ErrorMessage));
194    }
195
196    #[test]
197    fn test_filter_chunk_load_error() {
198        let errors = [
199            "Error: Uncaught (in promise): ChunkLoadError: Loading chunk 175 failed.",
200            "Uncaught (in promise): ChunkLoadError: Loading chunk 175 failed.",
201            "ChunkLoadError: Loading chunk 552 failed.",
202        ];
203
204        let config = ErrorMessagesFilterConfig {
205            patterns: TypedPatterns::from([
206                "ChunkLoadError: Loading chunk *".to_owned(),
207                "*Uncaught *: ChunkLoadError: Loading chunk *".to_owned(),
208            ]),
209        };
210
211        for error in errors {
212            let event = Event {
213                logentry: Annotated::new(LogEntry {
214                    formatted: Annotated::new(error.to_owned().into()),
215                    ..Default::default()
216                }),
217                ..Default::default()
218            };
219
220            assert_eq!(
221                should_filter(&event, &config),
222                Err(FilterStatKey::ErrorMessage)
223            );
224        }
225    }
226}