relay_filter/
browser_extensions.rs

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