relay_filter/
legacy_browsers.rs

1//! Implements filtering for events originating from legacy browsers.
2
3use std::collections::BTreeSet;
4
5use relay_ua::UserAgent;
6
7use crate::{FilterStatKey, Filterable, LegacyBrowser, LegacyBrowsersFilterConfig};
8
9/// Checks if the event originates from legacy browsers.
10fn matches(user_agent: &str, browsers: &BTreeSet<LegacyBrowser>) -> bool {
11    let user_agent = relay_ua::parse_user_agent(user_agent);
12
13    // remap IE Mobile to IE (sentry python, filter compatibility)
14    let family = match user_agent.family.as_ref() {
15        "IE Mobile" => "IE",
16        other => other,
17    };
18
19    if browsers.contains(&LegacyBrowser::Default) {
20        return default_filter(family, &user_agent);
21    }
22
23    for browser_type in browsers {
24        let should_filter = match browser_type {
25            LegacyBrowser::IePre9 => filter_browser(family, &user_agent, "IE", |x| x <= 8),
26            LegacyBrowser::Ie9 => filter_browser(family, &user_agent, "IE", |x| x == 9),
27            LegacyBrowser::Ie10 => filter_browser(family, &user_agent, "IE", |x| x == 10),
28            LegacyBrowser::Ie11 => filter_browser(family, &user_agent, "IE", |x| x == 11),
29            LegacyBrowser::OperaMiniPre8 => {
30                filter_browser(family, &user_agent, "Opera Mini", |x| x < 8)
31            }
32            LegacyBrowser::OperaPre15 => filter_browser(family, &user_agent, "Opera", |x| x < 15),
33            LegacyBrowser::AndroidPre4 => filter_browser(family, &user_agent, "Android", |x| x < 4),
34            LegacyBrowser::SafariPre6 => filter_browser(family, &user_agent, "Safari", |x| x < 6),
35            LegacyBrowser::EdgePre79 => filter_browser(family, &user_agent, "Edge", |x| x < 79),
36            LegacyBrowser::Ie => filter_browser(family, &user_agent, "IE", |x| x < 12),
37            LegacyBrowser::OperaMini => {
38                filter_browser(family, &user_agent, "Opera Mini", |x| x < 35)
39            }
40            LegacyBrowser::Opera => filter_browser(family, &user_agent, "Opera", |x| x < 51),
41            LegacyBrowser::Android => filter_browser(family, &user_agent, "Android", |x| x < 4),
42            LegacyBrowser::Safari => filter_browser(family, &user_agent, "Safari", |x| x < 12),
43            LegacyBrowser::Edge => filter_browser(family, &user_agent, "Edge", |x| x < 79),
44            LegacyBrowser::Chrome => filter_browser(family, &user_agent, "Chrome", |x| x < 64),
45            LegacyBrowser::Firefox => filter_browser(family, &user_agent, "Firefox", |x| x < 67),
46            LegacyBrowser::Unknown(_) => {
47                // unknown browsers should not be filtered
48                false
49            }
50            LegacyBrowser::Default => unreachable!(),
51        };
52        if should_filter {
53            return true;
54        }
55    }
56    false
57}
58
59/// Filters events originating from legacy browsers.
60pub fn should_filter<F: Filterable>(
61    item: &F,
62    config: &LegacyBrowsersFilterConfig,
63) -> Result<(), FilterStatKey> {
64    if !config.is_enabled || config.browsers.is_empty() {
65        return Ok(()); // globally disabled or no individual browser enabled
66    }
67
68    let browsers = &config.browsers;
69    if item.user_agent().is_some_and(|ua| matches(ua, browsers)) {
70        Err(FilterStatKey::LegacyBrowsers)
71    } else {
72        Ok(())
73    }
74}
75
76fn get_browser_major_version(user_agent: &UserAgent) -> Option<i32> {
77    if let Some(browser_major_version_str) = &user_agent.major {
78        if let Ok(browser_major_version) = browser_major_version_str.parse::<i32>() {
79            return Some(browser_major_version);
80        }
81    }
82
83    None
84}
85
86fn min_version(family: &str) -> Option<i32> {
87    match family {
88        "Chrome" => Some(0),
89        "IE" => Some(10),
90        "Firefox" => Some(0),
91        "Safari" => Some(6),
92        "Edge" => Some(0),
93        "Opera" => Some(15),
94        "Android" => Some(4),
95        "Opera Mini" => Some(8),
96        _ => None,
97    }
98}
99
100fn default_filter(mapped_family: &str, user_agent: &UserAgent) -> bool {
101    if let Some(browser_major_version) = get_browser_major_version(user_agent) {
102        if let Some(min_version) = min_version(mapped_family) {
103            if min_version > browser_major_version {
104                return true;
105            }
106        }
107    }
108    false
109}
110
111fn filter_browser<F>(
112    mapped_family: &str,
113    user_agent: &UserAgent,
114    family: &str,
115    should_filter: F,
116) -> bool
117where
118    F: FnOnce(i32) -> bool,
119{
120    if mapped_family == family {
121        if let Some(browser_major_version) = get_browser_major_version(user_agent) {
122            if should_filter(browser_major_version) {
123                return true;
124            }
125        }
126    }
127    false
128}
129
130#[cfg(test)]
131mod tests {
132    const IE8_UA: &str = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)";
133    const IE_MOBILE9_UA: &str = "Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; NOKIA; Lumia 710)";
134    const IE9_UA: &str = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)";
135    const IE10_UA: &str = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)";
136    const IE11_UA: &str = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko";
137    const OPERA_MINI_PRE8_UA: &str =
138        "Opera/9.80 (J2ME/MIDP; Opera Mini/7.0.32796/59.323; U; fr) Presto/2.12.423 Version/12.16";
139    const OPERA_MINI_8_UA: &str =
140        "Opera/9.80 (J2ME/MIDP; Opera Mini/8.0.35158/36.2534; U; en) Presto/2.12.423 Version/12.16";
141    const OPERA_PRE15_UA: &str = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.12 Safari/537.36 OPR/14.0.1116.4";
142    const OPERA_15_UA: &str = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.45 Safari/537.36 OPR/15.0.1147.61 (Edition Next)";
143    const ANDROID_PRE4_UA: &str = "Mozilla/5.0 (Linux; U; Android 3.2; nl-nl; GT-P6800 Build/HTJ85B) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13";
144    const ANDROID_4_UA: &str = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Build/KLP) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30";
145    const SAFARI_PRE6_UA: &str = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 1063; tr-DE) AppleWebKit/533.16 (KHTML like Gecko) Version/5.0 Safari/533.16";
146    const SAFARI_6_UA: &str = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.17.4; en-GB) AppleWebKit/605.1.5 (KHTML, like Gecko) Version/6.0 Safari/605.1.5";
147    const EDGE_ANDROID_118_UA: &str = "Mozilla/5.0 (Linux; Android 10; Pixel 3 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.80 Mobile Safari/537.36 EdgA/118.0.2088.58";
148    const EDGE_79_UA: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3919.0 Safari/537.36 Edg/79.0.294.1";
149    const EDGE_18_UA: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582";
150    const EDGE_12_UA: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246";
151    const OPERA_UA: &str = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.45 Safari/537.36 OPR/49.0.1147.61";
152    const OPERA_MINI_UA: &str = "Opera/20.80 (J2ME/MIDP; Opera Mini/16.0.35158/36.2534; U; en) Presto/2.12.423 Version/12.16";
153    const CHROME_UA: &str = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.2228.0 Safari/537.36";
154    const FIREFOX_UA: &str = "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0";
155    const IE_UA: &str = "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko";
156    const EDGE_UA: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582";
157    const SAFARI_UA: &str = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.17.4; en-GB) AppleWebKit/605.1.5 (KHTML, like Gecko) Version/6.0 Safari/605.1.5";
158    const ANDROID_UA: &str = "Mozilla/5.0 (Linux; U; Android 3.2; nl-nl; GT-P6800 Build/HTJ85B) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13";
159
160    use super::*;
161    use crate::testutils;
162
163    fn get_legacy_browsers_config(
164        is_enabled: bool,
165        legacy_browsers: &[LegacyBrowser],
166    ) -> LegacyBrowsersFilterConfig {
167        LegacyBrowsersFilterConfig {
168            is_enabled,
169            browsers: {
170                let mut browsers = BTreeSet::<LegacyBrowser>::new();
171                for elm in legacy_browsers {
172                    browsers.insert(elm.clone());
173                }
174                browsers
175            },
176        }
177    }
178
179    #[test]
180    fn test_dont_filter_when_disabled() {
181        let evt = testutils::get_event_with_user_agent(IE8_UA);
182        let filter_result = should_filter(
183            &evt,
184            &get_legacy_browsers_config(false, &[LegacyBrowser::Default]),
185        );
186        assert_eq!(
187            filter_result,
188            Ok(()),
189            "Event filtered although filter should have been disabled"
190        )
191    }
192
193    #[test]
194    fn test_filter_default_browsers() {
195        for old_user_agent in &[
196            IE9_UA,
197            IE_MOBILE9_UA,
198            SAFARI_PRE6_UA,
199            OPERA_PRE15_UA,
200            ANDROID_PRE4_UA,
201            OPERA_MINI_PRE8_UA,
202        ] {
203            let evt = testutils::get_event_with_user_agent(old_user_agent);
204            let filter_result = should_filter(
205                &evt,
206                &get_legacy_browsers_config(true, &[LegacyBrowser::Default]),
207            );
208            assert_ne!(
209                filter_result,
210                Ok(()),
211                "Default filter should have filtered User Agent\n{old_user_agent}"
212            )
213        }
214    }
215
216    #[test]
217    fn test_dont_filter_default_above_minimum_versions() {
218        for old_user_agent in &[
219            IE10_UA,
220            SAFARI_6_UA,
221            OPERA_15_UA,
222            ANDROID_4_UA,
223            OPERA_MINI_8_UA,
224        ] {
225            let evt = testutils::get_event_with_user_agent(old_user_agent);
226            let filter_result = should_filter(
227                &evt,
228                &get_legacy_browsers_config(true, &[LegacyBrowser::Default]),
229            );
230            assert_eq!(
231                filter_result,
232                Ok(()),
233                "Default filter shouldn't have filtered User Agent\n{old_user_agent}"
234            )
235        }
236    }
237
238    #[test]
239    fn test_filter_configured_browsers() {
240        let test_configs = [
241            (
242                IE10_UA,
243                &[LegacyBrowser::AndroidPre4, LegacyBrowser::Ie10][..],
244            ),
245            (IE11_UA, &[LegacyBrowser::Ie11][..]),
246            (IE10_UA, &[LegacyBrowser::Ie10][..]),
247            (IE9_UA, &[LegacyBrowser::Ie9][..]),
248            (IE_MOBILE9_UA, &[LegacyBrowser::Ie9][..]),
249            (
250                IE9_UA,
251                &[LegacyBrowser::AndroidPre4, LegacyBrowser::Ie9][..],
252            ),
253            (IE8_UA, &[LegacyBrowser::IePre9][..]),
254            (
255                IE8_UA,
256                &[LegacyBrowser::OperaPre15, LegacyBrowser::IePre9][..],
257            ),
258            (OPERA_PRE15_UA, &[LegacyBrowser::OperaPre15][..]),
259            (
260                OPERA_PRE15_UA,
261                &[LegacyBrowser::Ie10, LegacyBrowser::OperaPre15][..],
262            ),
263            (OPERA_MINI_PRE8_UA, &[LegacyBrowser::OperaMiniPre8][..]),
264            (
265                OPERA_MINI_PRE8_UA,
266                &[LegacyBrowser::Ie10, LegacyBrowser::OperaMiniPre8][..],
267            ),
268            (ANDROID_PRE4_UA, &[LegacyBrowser::AndroidPre4][..]),
269            (
270                ANDROID_PRE4_UA,
271                &[LegacyBrowser::Ie10, LegacyBrowser::AndroidPre4][..],
272            ),
273            (SAFARI_PRE6_UA, &[LegacyBrowser::SafariPre6][..]),
274            (
275                SAFARI_PRE6_UA,
276                &[LegacyBrowser::OperaPre15, LegacyBrowser::SafariPre6][..],
277            ),
278            (EDGE_12_UA, &[LegacyBrowser::EdgePre79][..]),
279            (
280                EDGE_18_UA,
281                &[LegacyBrowser::OperaPre15, LegacyBrowser::EdgePre79][..],
282            ),
283            (OPERA_UA, &[LegacyBrowser::Opera][..]),
284            (OPERA_MINI_UA, &[LegacyBrowser::OperaMini][..]),
285            (CHROME_UA, &[LegacyBrowser::Chrome][..]),
286            (FIREFOX_UA, &[LegacyBrowser::Firefox][..]),
287            (IE_UA, &[LegacyBrowser::Ie][..]),
288            (EDGE_UA, &[LegacyBrowser::Edge][..]),
289            (SAFARI_UA, &[LegacyBrowser::Safari][..]),
290            (ANDROID_UA, &[LegacyBrowser::Android][..]),
291        ];
292
293        for (user_agent, active_filters) in &test_configs {
294            let evt = testutils::get_event_with_user_agent(user_agent);
295            let filter_result =
296                should_filter(&evt, &get_legacy_browsers_config(true, active_filters));
297            assert_ne!(
298                filter_result,
299                Ok(()),
300                "Filters {active_filters:?} should have filtered User Agent\n{user_agent} for "
301            )
302        }
303    }
304
305    #[test]
306    fn test_dont_filter_unconfigured_browsers() {
307        let test_configs = [
308            (IE11_UA, LegacyBrowser::Ie10),
309            (IE10_UA, LegacyBrowser::Ie9),
310            (IE10_UA, LegacyBrowser::Ie11),
311            (IE9_UA, LegacyBrowser::IePre9),
312            (OPERA_15_UA, LegacyBrowser::OperaPre15),
313            (OPERA_MINI_8_UA, LegacyBrowser::OperaMiniPre8),
314            (ANDROID_4_UA, LegacyBrowser::AndroidPre4),
315            (SAFARI_6_UA, LegacyBrowser::SafariPre6),
316            (EDGE_12_UA, LegacyBrowser::Ie10),
317            (EDGE_18_UA, LegacyBrowser::Ie10),
318            (EDGE_79_UA, LegacyBrowser::EdgePre79),
319            (EDGE_ANDROID_118_UA, LegacyBrowser::EdgePre79),
320        ];
321
322        for (user_agent, active_filter) in &test_configs {
323            let evt = testutils::get_event_with_user_agent(user_agent);
324            let filter_result = should_filter(
325                &evt,
326                &get_legacy_browsers_config(true, &[active_filter.clone()]),
327            );
328            assert_eq!(
329                filter_result,
330                Ok(()),
331                "Filter {active_filter:?} shouldn't have filtered User Agent\n{user_agent} for "
332            )
333        }
334    }
335
336    /// Test to ensure Sentry filter compatibility.
337    ///
338    /// To be remove if/when Sentry backward compatibility is no longer required.
339    mod sentry_compatibility {
340        use super::*;
341
342        // User agents used by Sentry tests
343        const ANDROID2_S_UA: &str = "Mozilla/5.0 (Linux; U; Android 2.3.5; en-us; HTC Vision Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1";
344        const ANDROID4_S_UA: &str = "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19";
345        const IE5_S_UA: &str =
346            "Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.9; SiteCoach 1.0)";
347        const IE8_S_UA: &str = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Win64; x64; Trident/4.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MDDC; Tablet PC 2.0)";
348        const IE9_S_UA: &str = "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))";
349        const IE_MOBILE9_S_UA: &str = "Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; NOKIA; Lumia 710)";
350        const IE10_S_UA: &str = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 7.0; InfoPath.3; .NET CLR 3.1.40767; Trident/6.0; en-IN)";
351        const IE_MOBILE10_S_UA: &str = "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)";
352        const OPERA11_S_UA: &str = "Opera/9.80 (Windows NT 5.1; U; it) Presto/2.7.62 Version/11.00";
353        const OPERA_12_S_UA: &str =
354            "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16";
355        const OPERA_15_S_UA: &str = "Mozilla/5.0 (X11; Linux x86_64; Debian) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 OPR/15.0.1147.100";
356        const CHROME_S_UA: &str = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36";
357        const EDGE_S_UA: &str = "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10136";
358        const SAFARI5_S_UA: &str = "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-HK) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5";
359        const SAFARI7_S_UA: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A";
360        const OPERA_MINI8_S_UA: &str = "Opera/9.80 (J2ME/MIDP; Opera Mini/8.0.35158/36.2534; U; en) Presto/2.12.423 Version/12.16";
361        const OPERA_MINI7_S_UA: &str = "Opera/9.80 (J2ME/MIDP; Opera Mini/7.0.32796/59.323; U; fr) Presto/2.12.423 Version/12.16";
362
363        #[test]
364        fn test_filter_sentry_user_agents() {
365            let test_configs = [
366                (ANDROID2_S_UA, LegacyBrowser::Default),
367                (IE9_S_UA, LegacyBrowser::Default),
368                (IE_MOBILE9_S_UA, LegacyBrowser::Default),
369                (IE5_S_UA, LegacyBrowser::Default),
370                (OPERA11_S_UA, LegacyBrowser::Default),
371                (OPERA_12_S_UA, LegacyBrowser::Default),
372                (OPERA_MINI7_S_UA, LegacyBrowser::Default),
373                (OPERA_12_S_UA, LegacyBrowser::OperaPre15),
374                (OPERA_MINI7_S_UA, LegacyBrowser::OperaMiniPre8),
375                (OPERA_MINI7_S_UA, LegacyBrowser::Default),
376                (OPERA_MINI7_S_UA, LegacyBrowser::Default),
377                (IE8_S_UA, LegacyBrowser::IePre9),
378                (IE8_S_UA, LegacyBrowser::Default),
379                (IE9_S_UA, LegacyBrowser::Ie9),
380                (IE_MOBILE9_S_UA, LegacyBrowser::Ie9),
381                (IE10_S_UA, LegacyBrowser::Ie10),
382                (IE_MOBILE10_S_UA, LegacyBrowser::Ie10),
383                (SAFARI5_S_UA, LegacyBrowser::SafariPre6),
384                (SAFARI5_S_UA, LegacyBrowser::Default),
385                (ANDROID2_S_UA, LegacyBrowser::AndroidPre4),
386            ];
387
388            for (user_agent, active_filter) in &test_configs {
389                let evt = testutils::get_event_with_user_agent(user_agent);
390                let filter_result = should_filter(
391                    &evt,
392                    &get_legacy_browsers_config(true, &[active_filter.clone()]),
393                );
394                assert_ne!(
395                    filter_result,
396                    Ok(()),
397                    "Filter <{active_filter:?}> should have filtered User Agent\n{user_agent} for "
398                )
399            }
400        }
401
402        #[test]
403        fn test_dont_filter_sentry_allowed_user_agents() {
404            let test_configs = [
405                (ANDROID4_S_UA, LegacyBrowser::Default),
406                (IE10_S_UA, LegacyBrowser::Default),
407                (IE_MOBILE10_S_UA, LegacyBrowser::Default),
408                (CHROME_S_UA, LegacyBrowser::Default),
409                (EDGE_S_UA, LegacyBrowser::Default),
410                (OPERA_15_S_UA, LegacyBrowser::Default),
411                (OPERA_MINI8_S_UA, LegacyBrowser::Default),
412                (IE10_S_UA, LegacyBrowser::Default),
413                (IE_MOBILE10_S_UA, LegacyBrowser::Default),
414                (SAFARI7_S_UA, LegacyBrowser::Default),
415            ];
416
417            for (user_agent, active_filter) in &test_configs {
418                let evt = testutils::get_event_with_user_agent(user_agent);
419                let filter_result = should_filter(
420                    &evt,
421                    &get_legacy_browsers_config(true, &[active_filter.clone()]),
422                );
423                assert_eq!(
424                    filter_result,
425                    Ok(()),
426                    "Filter {active_filter:?} shouldn't have filtered User Agent\n{user_agent} for "
427                )
428            }
429        }
430    }
431}