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