1use std::sync::LazyLock;
4
5use regex::Regex;
6use relay_event_schema::protocol::Exception;
7
8use crate::{FilterConfig, FilterStatKey, Filterable};
9
10static EXTENSION_EXC_VALUES: LazyLock<Regex> = LazyLock::new(|| {
11 Regex::new(
12 r#"(?ix)
13 # Random plugins/extensions
14 top\.GLOBALS|
15 # See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
16 originalCreateNotification|
17 canvas.contentDocument|
18 MyApp_RemoveAllHighlights|
19 http://tt\.epicplay\.com|
20 Can't\sfind\svariable:\sZiteReader|
21 jigsaw\sis\snot\sdefined|
22 ComboSearch\sis\snot\sdefined|
23 http://loading\.retry\.widdit\.com/|
24 atomicFindClose|
25 # Facebook borked
26 fb_xd_fragment|
27 # ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
28 # reduce this. (thanks @acdha)
29 # See http://stackoverflow.com/questions/4113268
30 bmi_SafeAddOnload|
31 EBCallBackMessageReceived|
32 # See https://groups.google.com/a/chromium.org/forum/#!topic/chromium-discuss/7VU0_VvC7mE
33 _gCrWeb|
34 # See http://toolbar.conduit.com/Debveloper/HtmlAndGadget/Methods/JSInjection.aspx
35 conduitPage|
36 # Google Search app (iOS)
37 # See: https://github.com/getsentry/raven-js/issues/756
38 null\sis\snot\san\sobject\s\(evaluating\s'elt.parentNode'\)|
39 # Dragon Web Extension from Nuance Communications
40 # See: https://forum.sentry.io/t/error-in-raven-js-plugin-setsuspendstate/481/
41 plugin\.setSuspendState\sis\snot\sa\sfunction|
42 # Chrome extension message passing failure
43 Extension\scontext\sinvalidated|
44 webkit-masked-url:|
45 # Firefox message when an extension tries to modify a no-longer-existing DOM node
46 # See https://blog.mozilla.org/addons/2012/09/12/what-does-cant-access-dead-object-mean/
47 can't\saccess\sdead\sobject|
48 # Cryptocurrency related extension errors solana|ethereum
49 # Googletag is also very similar, caused by adblockers
50 Cannot\sredefine\sproperty:\s(solana|ethereum|googletag)|
51 # Translation service errors in Chrome on iOS
52 undefined\sis\snot\san\sobject\s\(evaluating\s'a\..'\)|
53 # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Property_access_denied
54 # Usually caused by extensions that do stuff that isn't allowed
55 Permission\sdenied\sto\saccess\sproperty\s|
56 # Error from Google Search App https://issuetracker.google.com/issues/396043331
57 Can't\sfind\svariable:\sgmo
58 "#,
59 )
60 .expect("Invalid browser extensions filter (Exec Vals) Regex")
61});
62
63static EXTENSION_EXC_SOURCES: LazyLock<Regex> = LazyLock::new(|| {
64 Regex::new(
65 r"(?ix)
66 graph\.facebook\.com| # Facebook flakiness
67 connect\.facebook\.net| # Facebook blocked
68 eatdifferent\.com\.woopra-ns\.com| # Woopra flakiness
69 static\.woopra\.com/js/woopra\.js|
70 ^chrome(-extension)?://| # Chrome extensions
71 ^moz-extension://| # Firefox extensions
72 ^safari(-web)?-extension://| # Safari extensions
73 webkit-masked-url| # Safari extensions
74 127\.0\.0\.1:4001/isrunning| # Cacaoweb
75 webappstoolbarba\.texthelp\.com/| # Other
76 metrics\.itunes\.apple\.com\.edgesuite\.net/|
77 kaspersky-labs\.com # Kaspersky Protection browser extension
78 ",
79 )
80 .expect("Invalid browser extensions filter (Exec Sources) Regex")
81});
82
83const ANONYMOUS_FRAMES: [&str; 2] = ["<anonymous>", "[native code]"];
88
89fn matches<F: Filterable>(item: &F) -> bool {
91 if let Some(ex_val) = get_exception_value(item)
92 && EXTENSION_EXC_VALUES.is_match(ex_val)
93 {
94 return true;
95 }
96 if let Some(ex_source) = get_exception_source(item)
97 && EXTENSION_EXC_SOURCES.is_match(ex_source)
98 {
99 return true;
100 }
101 false
102}
103
104pub fn should_filter<F: Filterable>(item: &F, config: &FilterConfig) -> Result<(), FilterStatKey> {
106 if !config.is_enabled {
107 return Ok(());
108 }
109
110 if matches(item) {
111 Err(FilterStatKey::BrowserExtensions)
112 } else {
113 Ok(())
114 }
115}
116
117fn get_first_exception<F: Filterable>(item: &F) -> Option<&Exception> {
118 let values = item.exceptions()?;
119 let exceptions = values.values.value()?;
120 exceptions.first()?.value()
121}
122
123fn get_exception_value<F: Filterable>(item: &F) -> Option<&str> {
124 let exception = get_first_exception(item)?;
125 Some(exception.value.value()?.as_str())
126}
127
128fn get_exception_source<F: Filterable>(item: &F) -> Option<&str> {
129 let exception = get_first_exception(item)?;
130 let frames = exception.stacktrace.value()?.frames.value()?;
131 for f in frames.iter().rev() {
133 let abs_path = f.value()?.abs_path.value()?;
134 let path = abs_path.as_str();
135 if !ANONYMOUS_FRAMES.contains(&path) {
136 return Some(path);
137 }
138 }
139 None
140}
141
142#[cfg(test)]
143mod tests {
144 use relay_event_schema::protocol::{
145 Event, Frame, JsonLenientString, RawStacktrace, Stacktrace, Values,
146 };
147 use relay_protocol::Annotated;
148
149 use super::*;
150
151 fn get_event_with_exception(e: Exception) -> Event {
153 Event {
154 exceptions: Annotated::from(Values::<Exception> {
155 values: Annotated::from(vec![
156 Annotated::from(e), Annotated::from(Exception::default()),
159 Annotated::from(Exception::default()),
161 ]),
162 ..Values::default()
163 }),
164 ..Event::default()
165 }
166 }
167
168 fn get_event_with_exception_source(src: &str) -> Event {
169 let ex = Exception {
170 stacktrace: Annotated::from(Stacktrace(RawStacktrace {
171 frames: Annotated::new(vec![
172 Annotated::new(Frame {
173 abs_path: Annotated::new(src.into()),
174 ..Frame::default()
175 }),
176 Annotated::new(Frame {
177 abs_path: Annotated::new("<anonymous>".into()),
178 ..Frame::default()
179 }),
180 Annotated::new(Frame {
181 abs_path: Annotated::new("<anonymous>".into()),
182 ..Frame::default()
183 }),
184 ]),
185 ..RawStacktrace::default()
186 })),
187 ..Exception::default()
188 };
189 get_event_with_exception(ex)
190 }
191
192 fn get_event_with_exception_value(val: &str) -> Event {
193 let ex = Exception {
194 value: Annotated::from(JsonLenientString::from(val.to_owned())),
195 ..Exception::default()
196 };
197
198 get_event_with_exception(ex)
199 }
200
201 #[test]
202 fn test_dont_filter_when_disabled() {
203 let events = [
204 get_event_with_exception_source("https://fscr.kaspersky-labs.com/B-9B72-7B7/main.js"),
205 get_event_with_exception_value("fb_xd_fragment"),
206 ];
207
208 for event in &events {
209 let filter_result = should_filter(event, &FilterConfig { is_enabled: false });
210 assert_eq!(
211 filter_result,
212 Ok(()),
213 "Event filtered although filter should have been disabled"
214 )
215 }
216 }
217
218 #[test]
219 fn test_filter_known_browser_extension_source() {
220 let sources = [
221 "https://graph.facebook.com/",
222 "https://connect.facebook.net/en_US/sdk.js",
223 "https://eatdifferent.com.woopra-ns.com/main.js",
224 "https://static.woopra.com/js/woopra.js",
225 "chrome-extension://my-extension/or/something",
226 "chrome://my-extension/or/something",
227 "moz-extension://my-extension/or/something",
228 "safari-extension://my-extension/or/something",
229 "safari-web-extension://my-extension/or/something",
230 "127.0.0.1:4001/isrunning",
231 "webappstoolbarba.texthelp.com/",
232 "http://metrics.itunes.apple.com.edgesuite.net/itunespreview/itunes/browser:firefo",
233 "https://fscr.kaspersky-labs.com/B-9B72-7B7/main.js",
234 "webkit-masked-url:",
235 ];
236
237 for source_name in &sources {
238 let event = get_event_with_exception_source(source_name);
239 let filter_result = should_filter(&event, &FilterConfig { is_enabled: true });
240
241 assert_ne!(
242 filter_result,
243 Ok(()),
244 "Event filter not recognizing events with known source {source_name}"
245 )
246 }
247 }
248
249 #[test]
250 fn test_filter_known_browser_extension_values() {
251 let exceptions = [
252 "what does conduitPage even do",
253 "null is not an object (evaluating 'elt.parentNode')",
254 "some error on top.GLOBALS",
255 "biiig problem on originalCreateNotification",
256 "canvas.contentDocument",
257 "MyApp_RemoveAllHighlights",
258 "http://tt.epicplay.com/not/very/good",
259 "Can't find variable: ZiteReader, I wonder why?",
260 "jigsaw is not defined and I'm not happy about it",
261 "ComboSearch is not defined",
262 "http://loading.retry.widdit.com/some/obscure/error",
263 "atomicFindClose has messed up",
264 "bad news, we have a fb_xd_fragment",
265 "oh no! we have a case of: bmi_SafeAddOnload, again !",
266 "watch out ! EBCallBackMessageReceived",
267 "error _gCrWeb",
268 "conduitPage",
269 "null is not an object (evaluating 'elt.parentNode')",
270 "plugin.setSuspendState is not a function",
271 "Extension context invalidated",
272 "useless error webkit-masked-url: please filter",
273 "TypeError: can't access dead object because dead stuff smells bad",
274 "Cannot redefine property: solana",
275 "Cannot redefine property: ethereum",
276 "Cannot redefine property: googletag",
277 "undefined is not an object (evaluating 'a.L')", "undefined is not an object (evaluating 'a.J')", "Permission denied to access property \"correspondingUseElement\"",
280 "Permission denied to access property \"document\"",
281 "Can't find variable: gmo",
282 ];
283
284 for exc_value in &exceptions {
285 let event = get_event_with_exception_value(exc_value);
286 let filter_result = should_filter(&event, &FilterConfig { is_enabled: true });
287 assert_ne!(
288 filter_result,
289 Ok(()),
290 "Event filter not recognizing events with known value '{exc_value}'"
291 )
292 }
293 }
294
295 #[test]
296 fn test_dont_filter_unkown_browser_extension() {
297 let events = [
298 get_event_with_exception_source("https://some/resonable/source.js"),
299 get_event_with_exception_value("some perfectly reasonable value"),
300 get_event_with_exception_value("undefined is not an object (evaluating 'a!J')"),
302 ];
303
304 for event in &events {
305 let filter_result = should_filter(event, &FilterConfig { is_enabled: true });
306 assert_eq!(
307 filter_result,
308 Ok(()),
309 "Event filter although the source or value are ok "
310 )
311 }
312 }
313}