relay_event_normalization/normalize/
contexts.rs

1//! Computation and normalization of contexts from event data.
2
3use std::collections::HashMap;
4
5use once_cell::sync::Lazy;
6use regex::Regex;
7use relay_event_schema::protocol::{
8    BrowserContext, Context, Cookies, OsContext, ResponseContext, RuntimeContext,
9};
10use relay_protocol::{Annotated, Empty, Value};
11
12/// Environment.OSVersion (GetVersionEx) or RuntimeInformation.OSDescription on Windows
13static OS_WINDOWS_REGEX1: Lazy<Regex> = Lazy::new(|| {
14    Regex::new(r"^(Microsoft\s+)?Windows\s+(NT\s+)?(?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$")
15        .unwrap()
16});
17static OS_WINDOWS_REGEX2: Lazy<Regex> = Lazy::new(|| {
18    Regex::new(r"^Windows\s+\d+\s+\((?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$").unwrap()
19});
20
21static OS_ANDROID_REGEX: Lazy<Regex> = Lazy::new(|| {
22    Regex::new(r"^Android (OS )?(?P<version>\d+(\.\d+){0,2}) / API-(?P<api>(\d+))").unwrap()
23});
24
25/// Format sent by Unreal Engine on macOS
26static OS_MACOS_REGEX: Lazy<Regex> = Lazy::new(|| {
27    Regex::new(r"^Mac OS X (?P<version>\d+\.\d+\.\d+)( \((?P<build>[a-fA-F0-9]+)\))?$").unwrap()
28});
29
30/// Format sent by Unity on iOS
31static OS_IOS_REGEX: Lazy<Regex> =
32    Lazy::new(|| Regex::new(r"^iOS (?P<version>\d+\.\d+\.\d+)").unwrap());
33
34/// Format sent by Unity on iPadOS
35static OS_IPADOS_REGEX: Lazy<Regex> =
36    Lazy::new(|| Regex::new(r"^iPadOS (?P<version>\d+\.\d+\.\d+)").unwrap());
37
38/// Specific regex to parse Linux distros
39static OS_LINUX_DISTRO_UNAME_REGEX: Lazy<Regex> = Lazy::new(|| {
40    Regex::new(r"^Linux (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?) (?P<name>[a-zA-Z]+) (?P<version>\d+(\.\d+){0,2})").unwrap()
41});
42
43/// Environment.OSVersion or RuntimeInformation.OSDescription (uname) on Mono and CoreCLR on
44/// macOS, iOS, Linux, etc.
45static OS_UNAME_REGEX: Lazy<Regex> = Lazy::new(|| {
46    Regex::new(r"^(?P<name>[a-zA-Z]+) (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?)").unwrap()
47});
48
49/// Mono 5.4, .NET Core 2.0
50static RUNTIME_DOTNET_REGEX: Lazy<Regex> =
51    Lazy::new(|| Regex::new(r"^(?P<name>.*) (?P<version>\d+\.\d+(\.\d+){0,2}).*$").unwrap());
52
53/// A hashmap that translates from the android model to the more human-friendly product-names.
54/// E.g. NE2211 -> OnePlus 10 Pro 5G
55static ANDROID_MODEL_NAMES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
56    let mut map = HashMap::new();
57    // Note that windows paths with backslashes '\' won't work on unix systems.
58    let android_str = include_str!("android_models.csv");
59
60    let mut lines = android_str.lines();
61
62    let header = lines.next().expect("CSV file should have a header");
63
64    let header_fields: Vec<&str> = header.split(',').collect();
65    let model_index = header_fields.iter().position(|&s| s.trim() == "Model");
66    let product_name_index = header_fields
67        .iter()
68        .position(|&s| s.trim() == "Marketing Name");
69
70    let (model_index, product_name_index) = match (model_index, product_name_index) {
71        (Some(model_index), Some(product_name_index)) => (model_index, product_name_index),
72        (_, _) => {
73            relay_log::error!(
74                "failed to find model and/or marketing name headers for android-model map",
75            );
76
77            return HashMap::new();
78        }
79    };
80
81    for line in lines {
82        let fields: Vec<&str> = line.split(',').collect();
83        if fields.len() > std::cmp::max(model_index, product_name_index) {
84            map.insert(
85                fields[model_index].trim(),
86                fields[product_name_index].trim(),
87            );
88        }
89    }
90    map
91});
92
93fn normalize_runtime_context(runtime: &mut RuntimeContext) {
94    if runtime.name.value().is_empty() && runtime.version.value().is_empty() {
95        if let Some(raw_description) = runtime.raw_description.as_str() {
96            if let Some(captures) = RUNTIME_DOTNET_REGEX.captures(raw_description) {
97                runtime.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
98                runtime.version = captures
99                    .name("version")
100                    .map(|m| m.as_str().to_owned())
101                    .into();
102            }
103        }
104    }
105
106    // RuntimeInformation.FrameworkDescription doesn't return a very useful value.
107    // Example: ".NET Framework 4.7.3056.0"
108    // Use release keys from registry sent as #build
109    if let Some(name) = runtime.name.as_str() {
110        if let Some(build) = runtime.build.as_str() {
111            if name.starts_with(".NET Framework") {
112                let version = match build {
113                    "378389" => Some("4.5".to_owned()),
114                    "378675" => Some("4.5.1".to_owned()),
115                    "378758" => Some("4.5.1".to_owned()),
116                    "379893" => Some("4.5.2".to_owned()),
117                    "393295" => Some("4.6".to_owned()),
118                    "393297" => Some("4.6".to_owned()),
119                    "394254" => Some("4.6.1".to_owned()),
120                    "394271" => Some("4.6.1".to_owned()),
121                    "394802" => Some("4.6.2".to_owned()),
122                    "394806" => Some("4.6.2".to_owned()),
123                    "460798" => Some("4.7".to_owned()),
124                    "460805" => Some("4.7".to_owned()),
125                    "461308" => Some("4.7.1".to_owned()),
126                    "461310" => Some("4.7.1".to_owned()),
127                    "461808" => Some("4.7.2".to_owned()),
128                    "461814" => Some("4.7.2".to_owned()),
129                    "528040" => Some("4.8".to_owned()),
130                    "528049" => Some("4.8".to_owned()),
131                    "528209" => Some("4.8".to_owned()),
132                    "528372" => Some("4.8".to_owned()),
133                    "528449" => Some("4.8".to_owned()),
134                    _ => None,
135                };
136
137                if let Some(version) = version {
138                    runtime.version = version.into();
139                }
140            }
141        }
142    }
143
144    // Calculation of the computed context for the runtime.
145    // The equivalent calculation is done in `sentry` in `src/sentry/interfaces/contexts.py`.
146    if runtime.runtime.value().is_none() {
147        if let (Some(name), Some(version)) = (runtime.name.value(), runtime.version.value()) {
148            runtime.runtime = Annotated::from(format!("{name} {version}"));
149        }
150    }
151}
152
153/// Parses the Windows build number from the description and maps it to a marketing name.
154/// Source: <https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions>.
155/// Note: We cannot distinguish between Windows Server and PC versions, so we map to the PC versions
156/// here.
157fn get_windows_version(description: &str) -> Option<(&str, &str)> {
158    let captures = OS_WINDOWS_REGEX1
159        .captures(description)
160        .or_else(|| OS_WINDOWS_REGEX2.captures(description))?;
161
162    let full_version = captures.name("version")?.as_str();
163    let build_number_str = captures.name("build_number")?.as_str();
164    let build_number = build_number_str.parse::<u64>().ok()?;
165
166    let version_name = match build_number {
167        // Not considering versions below Windows XP
168        2600..=3790 => "XP",
169        6002 => "Vista",
170        7601 => "7",
171        9200 => "8",
172        9600 => "8.1",
173        10240..=19044 => "10",
174        22000..=22999 => "11",
175        // Fall back to raw version:
176        _ => full_version,
177    };
178
179    Some((version_name, build_number_str))
180}
181
182/// Simple marketing names in the form `<OS> <version>`.
183fn get_marketing_name(description: &str) -> Option<(&str, &str)> {
184    let (name, version) = description.split_once(' ')?;
185    let name = name.trim();
186    let version = version.trim();
187
188    // Validate if it looks like a reasonable name.
189    if name.bytes().any(|c| !c.is_ascii_alphabetic()) {
190        return None;
191    }
192
193    // Validate if it looks like a reasonable version.
194    if version
195        .bytes()
196        .any(|c| !matches!(c, b'0'..=b'9' | b'.' | b'-'))
197    {
198        return None;
199    }
200
201    Some((name, version))
202}
203
204#[allow(dead_code)]
205/// Returns the API version of an Android description.
206///
207/// TODO use this to add a tag `android.api` to the message (not yet 100% decided)
208pub fn get_android_api_version(description: &str) -> Option<&str> {
209    if let Some(captures) = OS_ANDROID_REGEX.captures(description) {
210        captures.name("api").map(|m| m.as_str())
211    } else {
212        None
213    }
214}
215
216fn normalize_os_context(os: &mut OsContext) {
217    if os.name.value().is_some() || os.version.value().is_some() {
218        compute_os_context(os);
219        return;
220    }
221
222    if let Some(raw_description) = os.raw_description.as_str() {
223        if let Some((version, build_number)) = get_windows_version(raw_description) {
224            os.name = "Windows".to_owned().into();
225            os.version = version.to_owned().into();
226            if os.build.is_empty() {
227                // Keep raw version as build
228                os.build.set_value(Some(build_number.to_owned().into()));
229            }
230        } else if let Some(captures) = OS_MACOS_REGEX.captures(raw_description) {
231            os.name = "macOS".to_owned().into();
232            os.version = captures
233                .name("version")
234                .map(|m| m.as_str().to_owned())
235                .into();
236            os.build = captures
237                .name("build")
238                .map(|m| m.as_str().to_owned().into())
239                .into();
240        } else if let Some(captures) = OS_IOS_REGEX.captures(raw_description) {
241            os.name = "iOS".to_owned().into();
242            os.version = captures
243                .name("version")
244                .map(|m| m.as_str().to_owned())
245                .into();
246        } else if let Some(captures) = OS_IPADOS_REGEX.captures(raw_description) {
247            os.name = "iPadOS".to_owned().into();
248            os.version = captures
249                .name("version")
250                .map(|m| m.as_str().to_owned())
251                .into();
252        } else if let Some(captures) = OS_LINUX_DISTRO_UNAME_REGEX.captures(raw_description) {
253            os.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
254            os.version = captures
255                .name("version")
256                .map(|m| m.as_str().to_owned())
257                .into();
258            os.kernel_version = captures
259                .name("kernel_version")
260                .map(|m| m.as_str().to_owned())
261                .into();
262        } else if let Some(captures) = OS_UNAME_REGEX.captures(raw_description) {
263            os.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
264            os.kernel_version = captures
265                .name("kernel_version")
266                .map(|m| m.as_str().to_owned())
267                .into();
268        } else if let Some(captures) = OS_ANDROID_REGEX.captures(raw_description) {
269            os.name = "Android".to_owned().into();
270            os.version = captures
271                .name("version")
272                .map(|m| m.as_str().to_owned())
273                .into();
274        } else if raw_description == "Nintendo Switch" {
275            os.name = "Nintendo OS".to_owned().into();
276        } else if let Some((name, version)) = get_marketing_name(raw_description) {
277            os.name = name.to_owned().into();
278            os.version = version.to_owned().into();
279        }
280    }
281
282    compute_os_context(os);
283}
284
285fn compute_os_context(os: &mut OsContext) {
286    // Calculation of the computed context for the os.
287    // The equivalent calculation is done in `sentry` in `src/sentry/interfaces/contexts.py`.
288    if os.os.value().is_none() {
289        let name = match (os.name.value(), os.version.value()) {
290            (Some(name), Some(version)) => Some(format!("{name} {version}")),
291            (Some(name), _) => Some(name.to_owned()),
292            _ => None,
293        };
294
295        if let Some(name) = name {
296            os.os = Annotated::new(name);
297        }
298    }
299}
300
301fn normalize_browser_context(browser: &mut BrowserContext) {
302    // Calculation of the computed context for the browser.
303    // The equivalent calculation is done in `sentry` in `src/sentry/interfaces/contexts.py`.
304    if browser.browser.value().is_none() {
305        let name = match (browser.name.value(), browser.version.value()) {
306            (Some(name), Some(version)) => Some(format!("{name} {version}")),
307            (Some(name), _) => Some(name.to_owned()),
308            _ => None,
309        };
310
311        if let Some(name) = name {
312            browser.browser = Annotated::new(name);
313        }
314    }
315}
316
317fn parse_raw_response_data(response: &ResponseContext) -> Option<(&'static str, Value)> {
318    let raw = response.data.as_str()?;
319
320    serde_json::from_str(raw)
321        .ok()
322        .map(|value| ("application/json", value))
323}
324
325fn normalize_response_data(response: &mut ResponseContext) {
326    // Always derive the `inferred_content_type` from the response body, even if there is a
327    // `Content-Type` header present. This value can technically be ingested (due to the schema) but
328    // should always be overwritten in normalization. Only if inference fails, fall back to the
329    // content type header.
330    if let Some((content_type, parsed_data)) = parse_raw_response_data(response) {
331        // Retain meta data on the body (e.g. trimming annotations) but remove anything on the
332        // inferred content type.
333        response.data.set_value(Some(parsed_data));
334        response.inferred_content_type = Annotated::from(content_type.to_owned());
335    } else {
336        response.inferred_content_type = response
337            .headers
338            .value()
339            .and_then(|headers| headers.get_header("Content-Type"))
340            .map(|value| value.split(';').next().unwrap_or(value).to_owned())
341            .into();
342    }
343}
344
345fn normalize_response(response: &mut ResponseContext) {
346    normalize_response_data(response);
347
348    let headers = match response.headers.value_mut() {
349        Some(headers) => headers,
350        None => return,
351    };
352
353    if response.cookies.value().is_some() {
354        headers.remove("Set-Cookie");
355        return;
356    }
357
358    let cookie_header = match headers.get_header("Set-Cookie") {
359        Some(header) => header,
360        None => return,
361    };
362
363    if let Ok(new_cookies) = Cookies::parse(cookie_header) {
364        response.cookies = Annotated::from(new_cookies);
365        headers.remove("Set-Cookie");
366    }
367}
368
369/// Normalizes the given context.
370pub fn normalize_context(context: &mut Context) {
371    match context {
372        Context::Runtime(runtime) => normalize_runtime_context(runtime),
373        Context::Os(os) => normalize_os_context(os),
374        Context::Browser(browser) => normalize_browser_context(browser),
375        Context::Response(response) => normalize_response(response),
376        Context::Device(device) => {
377            if let Some(product_name) = device
378                .as_ref()
379                .model
380                .value()
381                .and_then(|model| ANDROID_MODEL_NAMES.get(model.as_str()))
382            {
383                device.name.set_value(Some(product_name.to_string()))
384            }
385        }
386        _ => {}
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use relay_event_schema::protocol::{Headers, LenientString, PairList};
393    use relay_protocol::SerializableAnnotated;
394    use similar_asserts::assert_eq;
395
396    use super::*;
397
398    macro_rules! assert_json_context {
399        ($ctx:expr, $($tt:tt)*) => {
400            insta::assert_json_snapshot!(SerializableAnnotated(&Annotated::new($ctx)), $($tt)*)
401
402        };
403    }
404
405    #[test]
406    fn test_get_product_name() {
407        assert_eq!(
408            ANDROID_MODEL_NAMES.get("NE2211").unwrap(),
409            &"OnePlus 10 Pro 5G"
410        );
411
412        assert_eq!(
413            ANDROID_MODEL_NAMES.get("MP04").unwrap(),
414            &"A13 Pro Max 5G EEA"
415        );
416
417        assert_eq!(ANDROID_MODEL_NAMES.get("ZT216_7").unwrap(), &"zyrex");
418
419        assert!(ANDROID_MODEL_NAMES.get("foobar").is_none());
420    }
421
422    #[test]
423    fn test_dotnet_framework_48_without_build_id() {
424        let mut runtime = RuntimeContext {
425            raw_description: ".NET Framework 4.8.4250.0".to_owned().into(),
426            ..RuntimeContext::default()
427        };
428
429        normalize_runtime_context(&mut runtime);
430        assert_json_context!(runtime, @r###"
431        {
432          "runtime": ".NET Framework 4.8.4250.0",
433          "name": ".NET Framework",
434          "version": "4.8.4250.0",
435          "raw_description": ".NET Framework 4.8.4250.0"
436        }
437        "###);
438    }
439
440    #[test]
441    fn test_dotnet_framework_472() {
442        let mut runtime = RuntimeContext {
443            raw_description: ".NET Framework 4.7.3056.0".to_owned().into(),
444            build: LenientString("461814".to_owned()).into(),
445            ..RuntimeContext::default()
446        };
447
448        normalize_runtime_context(&mut runtime);
449        assert_json_context!(runtime, @r###"
450        {
451          "runtime": ".NET Framework 4.7.2",
452          "name": ".NET Framework",
453          "version": "4.7.2",
454          "build": "461814",
455          "raw_description": ".NET Framework 4.7.3056.0"
456        }
457        "###);
458    }
459
460    #[test]
461    fn test_dotnet_framework_future_version() {
462        let mut runtime = RuntimeContext {
463            raw_description: ".NET Framework 200.0".to_owned().into(),
464            build: LenientString("999999".to_owned()).into(),
465            ..RuntimeContext::default()
466        };
467
468        // Unmapped build number doesn't override version
469        normalize_runtime_context(&mut runtime);
470        assert_json_context!(runtime, @r###"
471        {
472          "runtime": ".NET Framework 200.0",
473          "name": ".NET Framework",
474          "version": "200.0",
475          "build": "999999",
476          "raw_description": ".NET Framework 200.0"
477        }
478        "###);
479    }
480
481    #[test]
482    fn test_dotnet_native() {
483        let mut runtime = RuntimeContext {
484            raw_description: ".NET Native 2.0".to_owned().into(),
485            ..RuntimeContext::default()
486        };
487
488        normalize_runtime_context(&mut runtime);
489        assert_json_context!(runtime, @r###"
490        {
491          "runtime": ".NET Native 2.0",
492          "name": ".NET Native",
493          "version": "2.0",
494          "raw_description": ".NET Native 2.0"
495        }
496        "###);
497    }
498
499    #[test]
500    fn test_dotnet_core() {
501        let mut runtime = RuntimeContext {
502            raw_description: ".NET Core 2.0".to_owned().into(),
503            ..RuntimeContext::default()
504        };
505
506        normalize_runtime_context(&mut runtime);
507        assert_json_context!(runtime, @r###"
508        {
509          "runtime": ".NET Core 2.0",
510          "name": ".NET Core",
511          "version": "2.0",
512          "raw_description": ".NET Core 2.0"
513        }
514        "###);
515    }
516
517    #[test]
518    fn test_windows_7_or_server_2008() {
519        // Environment.OSVersion on Windows 7 (CoreCLR 1.0+, .NET Framework 1.1+, Mono 1+)
520        let mut os = OsContext {
521            raw_description: "Microsoft Windows NT 6.1.7601 Service Pack 1"
522                .to_owned()
523                .into(),
524            ..OsContext::default()
525        };
526
527        normalize_os_context(&mut os);
528        assert_json_context!(os, @r###"
529        {
530          "os": "Windows 7",
531          "name": "Windows",
532          "version": "7",
533          "build": "7601",
534          "raw_description": "Microsoft Windows NT 6.1.7601 Service Pack 1"
535        }
536        "###);
537    }
538
539    #[test]
540    fn test_windows_8_or_server_2012_or_later() {
541        // Environment.OSVersion on Windows 10 (CoreCLR 1.0+, .NET Framework 1.1+, Mono 1+)
542        // *or later, due to GetVersionEx deprecated on Windows 8.1
543        // It's a potentially really misleading API on newer platforms
544        // Only used if RuntimeInformation.OSDescription is not available (old runtimes)
545        let mut os = OsContext {
546            raw_description: "Microsoft Windows NT 6.2.9200.0".to_owned().into(),
547            ..OsContext::default()
548        };
549
550        normalize_os_context(&mut os);
551        assert_json_context!(os, @r###"
552        {
553          "os": "Windows 8",
554          "name": "Windows",
555          "version": "8",
556          "build": "9200",
557          "raw_description": "Microsoft Windows NT 6.2.9200.0"
558        }
559        "###);
560    }
561
562    #[test]
563    fn test_windows_10() {
564        // RuntimeInformation.OSDescription on Windows 10 (CoreCLR 2.0+, .NET
565        // Framework 4.7.1+, Mono 5.4+)
566        let mut os = OsContext {
567            raw_description: "Microsoft Windows 10.0.16299".to_owned().into(),
568            ..OsContext::default()
569        };
570
571        normalize_os_context(&mut os);
572        assert_json_context!(os, @r###"
573        {
574          "os": "Windows 10",
575          "name": "Windows",
576          "version": "10",
577          "build": "16299",
578          "raw_description": "Microsoft Windows 10.0.16299"
579        }
580        "###);
581    }
582
583    #[test]
584    fn test_windows_11() {
585        // https://github.com/getsentry/relay/issues/1201
586        let mut os = OsContext {
587            raw_description: "Microsoft Windows 10.0.22000".to_owned().into(),
588            ..OsContext::default()
589        };
590
591        normalize_os_context(&mut os);
592        assert_json_context!(os, @r###"
593        {
594          "os": "Windows 11",
595          "name": "Windows",
596          "version": "11",
597          "build": "22000",
598          "raw_description": "Microsoft Windows 10.0.22000"
599        }
600        "###);
601    }
602
603    #[test]
604    fn test_windows_11_future1() {
605        // This is fictional as of today, but let's be explicit about the behavior we expect.
606        let mut os = OsContext {
607            raw_description: "Microsoft Windows 10.0.22001".to_owned().into(),
608            ..OsContext::default()
609        };
610
611        normalize_os_context(&mut os);
612        assert_json_context!(os, @r###"
613        {
614          "os": "Windows 11",
615          "name": "Windows",
616          "version": "11",
617          "build": "22001",
618          "raw_description": "Microsoft Windows 10.0.22001"
619        }
620        "###);
621    }
622
623    #[test]
624    fn test_windows_11_future2() {
625        // This is fictional, but let's be explicit about the behavior we expect.
626        let mut os = OsContext {
627            raw_description: "Microsoft Windows 10.1.23456".to_owned().into(),
628            ..OsContext::default()
629        };
630
631        normalize_os_context(&mut os);
632        assert_json_context!(os, @r###"
633        {
634          "os": "Windows 10.1.23456",
635          "name": "Windows",
636          "version": "10.1.23456",
637          "build": "23456",
638          "raw_description": "Microsoft Windows 10.1.23456"
639        }
640        "###);
641    }
642
643    #[test]
644    fn test_macos_os_version() {
645        // Environment.OSVersion on macOS (CoreCLR 1.0+, Mono 1+)
646        let mut os = OsContext {
647            raw_description: "Unix 17.5.0.0".to_owned().into(),
648            ..OsContext::default()
649        };
650
651        normalize_os_context(&mut os);
652        assert_json_context!(os, @r###"
653        {
654          "os": "Unix",
655          "name": "Unix",
656          "kernel_version": "17.5.0",
657          "raw_description": "Unix 17.5.0.0"
658        }
659        "###);
660    }
661
662    #[test]
663    fn test_macos_runtime() {
664        // RuntimeInformation.OSDescription on macOS (CoreCLR 2.0+, Mono 5.4+)
665        let mut os = OsContext {
666        raw_description: "Darwin 17.5.0 Darwin Kernel Version 17.5.0: Mon Mar  5 22:24:32 PST 2018; root:xnu-4570.51.1~1/RELEASE_X86_64".to_owned().into(),
667        ..OsContext::default()
668    };
669
670        normalize_os_context(&mut os);
671        assert_json_context!(os, @r###"
672        {
673          "os": "Darwin",
674          "name": "Darwin",
675          "kernel_version": "17.5.0",
676          "raw_description": "Darwin 17.5.0 Darwin Kernel Version 17.5.0: Mon Mar  5 22:24:32 PST 2018; root:xnu-4570.51.1~1/RELEASE_X86_64"
677        }
678        "###);
679    }
680
681    #[test]
682    fn test_centos_os_version() {
683        // Environment.OSVersion on CentOS 7 (CoreCLR 1.0+, Mono 1+)
684        let mut os = OsContext {
685            raw_description: "Unix 3.10.0.693".to_owned().into(),
686            ..OsContext::default()
687        };
688
689        normalize_os_context(&mut os);
690        assert_json_context!(os, @r###"
691        {
692          "os": "Unix",
693          "name": "Unix",
694          "kernel_version": "3.10.0.693",
695          "raw_description": "Unix 3.10.0.693"
696        }
697        "###);
698    }
699
700    #[test]
701    fn test_centos_runtime_info() {
702        // RuntimeInformation.OSDescription on CentOS 7 (CoreCLR 2.0+, Mono 5.4+)
703        let mut os = OsContext {
704            raw_description: "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
705                .to_owned()
706                .into(),
707            ..OsContext::default()
708        };
709
710        normalize_os_context(&mut os);
711        assert_json_context!(os, @r###"
712        {
713          "os": "Linux",
714          "name": "Linux",
715          "kernel_version": "3.10.0",
716          "raw_description": "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
717        }
718        "###);
719    }
720
721    #[test]
722    fn test_wsl_ubuntu() {
723        // RuntimeInformation.OSDescription on Windows Subsystem for Linux (Ubuntu)
724        // (CoreCLR 2.0+, Mono 5.4+)
725        let mut os = OsContext {
726            raw_description: "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
727                .to_owned()
728                .into(),
729            ..OsContext::default()
730        };
731
732        normalize_os_context(&mut os);
733        assert_json_context!(os, @r###"
734        {
735          "os": "Linux",
736          "name": "Linux",
737          "kernel_version": "4.4.0",
738          "raw_description": "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
739        }
740        "###);
741    }
742
743    #[test]
744    fn test_macos_with_build() {
745        let mut os = OsContext {
746            raw_description: "Mac OS X 10.14.2 (18C54)".to_owned().into(),
747            ..OsContext::default()
748        };
749
750        normalize_os_context(&mut os);
751        assert_json_context!(os, @r###"
752        {
753          "os": "macOS 10.14.2",
754          "name": "macOS",
755          "version": "10.14.2",
756          "build": "18C54",
757          "raw_description": "Mac OS X 10.14.2 (18C54)"
758        }
759        "###);
760    }
761
762    #[test]
763    fn test_macos_without_build() {
764        let mut os = OsContext {
765            raw_description: "Mac OS X 10.14.2".to_owned().into(),
766            ..OsContext::default()
767        };
768
769        normalize_os_context(&mut os);
770        assert_json_context!(os, @r###"
771        {
772          "os": "macOS 10.14.2",
773          "name": "macOS",
774          "version": "10.14.2",
775          "raw_description": "Mac OS X 10.14.2"
776        }
777        "###);
778    }
779
780    #[test]
781    fn test_name_not_overwritten() {
782        let mut os = OsContext {
783            name: "Properly defined name".to_owned().into(),
784            raw_description: "Linux 4.4.0".to_owned().into(),
785            ..OsContext::default()
786        };
787
788        normalize_os_context(&mut os);
789        assert_json_context!(os, @r###"
790        {
791          "os": "Properly defined name",
792          "name": "Properly defined name",
793          "raw_description": "Linux 4.4.0"
794        }
795        "###);
796    }
797
798    #[test]
799    fn test_version_not_overwritten() {
800        let mut os = OsContext {
801            version: "Properly defined version".to_owned().into(),
802            raw_description: "Linux 4.4.0".to_owned().into(),
803            ..OsContext::default()
804        };
805
806        normalize_os_context(&mut os);
807        assert_json_context!(os, @r###"
808        {
809          "version": "Properly defined version",
810          "raw_description": "Linux 4.4.0"
811        }
812        "###);
813    }
814
815    #[test]
816    fn test_no_name() {
817        let mut os = OsContext::default();
818
819        normalize_os_context(&mut os);
820        assert_json_context!(os, @"{}");
821    }
822
823    #[test]
824    fn test_unity_mac_os() {
825        let mut os = OsContext {
826            raw_description: "Mac OS X 10.16.0".to_owned().into(),
827            ..OsContext::default()
828        };
829        normalize_os_context(&mut os);
830        assert_json_context!(os, @r###"
831        {
832          "os": "macOS 10.16.0",
833          "name": "macOS",
834          "version": "10.16.0",
835          "raw_description": "Mac OS X 10.16.0"
836        }
837        "###);
838    }
839
840    #[test]
841    fn test_unity_ios() {
842        let mut os = OsContext {
843            raw_description: "iOS 17.5.1".to_owned().into(),
844            ..OsContext::default()
845        };
846
847        normalize_os_context(&mut os);
848        assert_json_context!(os, @r###"
849        {
850          "os": "iOS 17.5.1",
851          "name": "iOS",
852          "version": "17.5.1",
853          "raw_description": "iOS 17.5.1"
854        }
855        "###);
856    }
857
858    #[test]
859    fn test_unity_ipados() {
860        let mut os = OsContext {
861            raw_description: "iPadOS 17.5.1".to_owned().into(),
862            ..OsContext::default()
863        };
864
865        normalize_os_context(&mut os);
866        assert_json_context!(os, @r###"
867        {
868          "os": "iPadOS 17.5.1",
869          "name": "iPadOS",
870          "version": "17.5.1",
871          "raw_description": "iPadOS 17.5.1"
872        }
873        "###);
874    }
875
876    //OS_WINDOWS_REGEX = r#"^(Microsoft )?Windows (NT )?(?P<version>\d+\.\d+\.\d+).*$"#;
877    #[test]
878    fn test_unity_windows_os() {
879        let mut os = OsContext {
880            raw_description: "Windows 10  (10.0.19042) 64bit".to_owned().into(),
881            ..OsContext::default()
882        };
883
884        normalize_os_context(&mut os);
885        assert_json_context!(os, @r###"
886        {
887          "os": "Windows 10",
888          "name": "Windows",
889          "version": "10",
890          "build": "19042",
891          "raw_description": "Windows 10  (10.0.19042) 64bit"
892        }
893        "###);
894    }
895
896    #[test]
897    fn test_unity_android_os() {
898        let mut os = OsContext {
899            raw_description: "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
900                .to_owned()
901                .into(),
902            ..OsContext::default()
903        };
904
905        normalize_os_context(&mut os);
906        assert_json_context!(os, @r###"
907        {
908          "os": "Android 11",
909          "name": "Android",
910          "version": "11",
911          "raw_description": "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
912        }
913        "###);
914    }
915
916    #[test]
917    fn test_unity_android_api_version() {
918        let description = "Android OS 11 / API-30 (RP1A.201005.001/2107031736)";
919        assert_eq!(Some("30"), get_android_api_version(description));
920    }
921
922    #[test]
923    fn test_unreal_windows_os() {
924        let mut os = OsContext {
925            raw_description: "Windows 10".to_owned().into(),
926            ..OsContext::default()
927        };
928
929        normalize_os_context(&mut os);
930        assert_json_context!(os, @r###"
931        {
932          "os": "Windows 10",
933          "name": "Windows",
934          "version": "10",
935          "raw_description": "Windows 10"
936        }
937        "###);
938    }
939
940    #[test]
941    fn test_linux_5_11() {
942        let mut os = OsContext {
943            raw_description: "Linux 5.11 Ubuntu 20.04 64bit".to_owned().into(),
944            ..OsContext::default()
945        };
946
947        normalize_os_context(&mut os);
948        assert_json_context!(os, @r###"
949        {
950          "os": "Ubuntu 20.04",
951          "name": "Ubuntu",
952          "version": "20.04",
953          "kernel_version": "5.11",
954          "raw_description": "Linux 5.11 Ubuntu 20.04 64bit"
955        }
956        "###);
957    }
958
959    #[test]
960    fn test_unity_nintendo_switch() {
961        // Format sent by Unity on Nintendo Switch
962        let mut os = OsContext {
963            raw_description: "Nintendo Switch".to_owned().into(),
964            ..OsContext::default()
965        };
966
967        normalize_os_context(&mut os);
968        assert_json_context!(os, @r###"
969        {
970          "os": "Nintendo OS",
971          "name": "Nintendo OS",
972          "raw_description": "Nintendo Switch"
973        }
974        "###);
975    }
976
977    #[test]
978    fn test_android_4_4_2() {
979        let mut os = OsContext {
980            raw_description: "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
981                .to_owned()
982                .into(),
983            ..OsContext::default()
984        };
985
986        normalize_os_context(&mut os);
987        assert_json_context!(os, @r###"
988        {
989          "os": "Android 4.4.2",
990          "name": "Android",
991          "version": "4.4.2",
992          "raw_description": "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
993        }
994        "###);
995    }
996
997    #[test]
998    fn test_infer_json() {
999        let mut response = ResponseContext {
1000            data: Annotated::from(Value::String(r#"{"foo":"bar"}"#.to_owned())),
1001            ..ResponseContext::default()
1002        };
1003
1004        normalize_response(&mut response);
1005        assert_json_context!(response, @r###"
1006        {
1007          "data": {
1008            "foo": "bar"
1009          },
1010          "inferred_content_type": "application/json"
1011        }
1012        "###);
1013    }
1014
1015    #[test]
1016    fn test_broken_json_with_fallback() {
1017        let mut response = ResponseContext {
1018            data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1019            headers: Annotated::from(Headers(PairList(vec![Annotated::new((
1020                Annotated::new("Content-Type".to_owned().into()),
1021                Annotated::new("text/plain; encoding=utf-8".to_owned().into()),
1022            ))]))),
1023            ..ResponseContext::default()
1024        };
1025
1026        normalize_response(&mut response);
1027        assert_json_context!(response, @r###"
1028        {
1029          "headers": [
1030            [
1031              "Content-Type",
1032              "text/plain; encoding=utf-8"
1033            ]
1034          ],
1035          "data": "{\"foo\":\"b",
1036          "inferred_content_type": "text/plain"
1037        }
1038        "###);
1039    }
1040
1041    #[test]
1042    fn test_broken_json_without_fallback() {
1043        let mut response = ResponseContext {
1044            data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1045            ..ResponseContext::default()
1046        };
1047
1048        normalize_response(&mut response);
1049        assert_json_context!(response, @r###"
1050        {
1051          "data": "{\"foo\":\"b"
1052        }
1053        "###);
1054    }
1055
1056    #[test]
1057    fn test_os_computed_context() {
1058        let mut os = OsContext {
1059            name: "Windows".to_owned().into(),
1060            version: "10".to_owned().into(),
1061            ..OsContext::default()
1062        };
1063
1064        normalize_os_context(&mut os);
1065        assert_json_context!(os, @r###"
1066        {
1067          "os": "Windows 10",
1068          "name": "Windows",
1069          "version": "10"
1070        }
1071        "###);
1072    }
1073
1074    #[test]
1075    fn test_os_computed_context_missing_version() {
1076        let mut os = OsContext {
1077            name: "Windows".to_owned().into(),
1078            ..OsContext::default()
1079        };
1080
1081        normalize_os_context(&mut os);
1082        assert_json_context!(os, @r###"
1083        {
1084          "os": "Windows",
1085          "name": "Windows"
1086        }
1087        "###);
1088    }
1089
1090    #[test]
1091    fn test_runtime_computed_context() {
1092        let mut runtime = RuntimeContext {
1093            name: "Python".to_owned().into(),
1094            version: "3.9.0".to_owned().into(),
1095            ..RuntimeContext::default()
1096        };
1097
1098        normalize_runtime_context(&mut runtime);
1099        assert_json_context!(runtime, @r###"
1100        {
1101          "runtime": "Python 3.9.0",
1102          "name": "Python",
1103          "version": "3.9.0"
1104        }
1105        "###);
1106    }
1107
1108    #[test]
1109    fn test_runtime_computed_context_missing_version() {
1110        let mut runtime = RuntimeContext {
1111            name: "Python".to_owned().into(),
1112            ..RuntimeContext::default()
1113        };
1114
1115        normalize_runtime_context(&mut runtime);
1116        assert_json_context!(runtime, @r###"
1117        {
1118          "name": "Python"
1119        }
1120        "###);
1121    }
1122
1123    #[test]
1124    fn test_browser_computed_context() {
1125        let mut browser = BrowserContext {
1126            name: "Firefox".to_owned().into(),
1127            version: "89.0".to_owned().into(),
1128            ..BrowserContext::default()
1129        };
1130
1131        normalize_browser_context(&mut browser);
1132        assert_json_context!(browser, @r###"
1133        {
1134          "browser": "Firefox 89.0",
1135          "name": "Firefox",
1136          "version": "89.0"
1137        }
1138        "###);
1139    }
1140
1141    #[test]
1142    fn test_browser_computed_context_missing_version() {
1143        let mut browser = BrowserContext {
1144            name: "Firefox".to_owned().into(),
1145            ..BrowserContext::default()
1146        };
1147
1148        normalize_browser_context(&mut browser);
1149        assert_json_context!(browser, @r###"
1150        {
1151          "browser": "Firefox",
1152          "name": "Firefox"
1153        }
1154        "###);
1155    }
1156}