1use std::borrow::Cow;
7
8use relay_pattern::Patterns;
9
10use crate::{ErrorMessagesFilterConfig, FilterStatKey, Filterable};
11
12fn 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
47pub 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 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 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}