relay_filter/
browser_extensions.rs

1//! Implements filtering for events caused by problematic browsers extensions.
2
3use 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
83/// Frames which do not have defined function, method or type name. Or frames which come from the
84/// native V8 code.
85///
86/// These frames do not give us any information about the exception source and can be ignored.
87const ANONYMOUS_FRAMES: [&str; 2] = ["<anonymous>", "[native code]"];
88
89/// Check if the event originates from known problematic browser extensions.
90fn 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
104/// Filters events originating from known problematic browser extensions.
105pub 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    // Iterate from the tail and get the first frame which is not anonymous.
132    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    /// Returns an event with the specified exception on the last position in the stack.
152    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), // our exception
157                    // some dummy exception in the stack
158                    Annotated::from(Exception::default()),
159                    // another dummy exception
160                    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')", // Old iOS Chrome translation error
278            "undefined is not an object (evaluating 'a.J')", // New iOS Chrome translation error
279            "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            // Something that does not quite match the iOS Chrome translation error
301            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}