Skip to main content

relay_event_normalization/normalize/
contexts.rs

1//! Computation and normalization of contexts from event data.
2
3use std::collections::HashMap;
4use std::sync::LazyLock;
5
6use regex::Regex;
7use relay_base_schema::spans::SpanStatus;
8use relay_event_schema::protocol::{
9    BrowserContext, Context, Cookies, OsContext, ResponseContext, RuntimeContext,
10};
11use relay_protocol::{Annotated, Empty, Value};
12
13/// Environment.OSVersion (GetVersionEx) or RuntimeInformation.OSDescription on Windows
14static OS_WINDOWS_REGEX1: LazyLock<Regex> = LazyLock::new(|| {
15    Regex::new(r"^(Microsoft\s+)?Windows\s+(NT\s+)?(?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$")
16        .unwrap()
17});
18static OS_WINDOWS_REGEX2: LazyLock<Regex> = LazyLock::new(|| {
19    Regex::new(r"^Windows\s+\d+\s+\((?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$").unwrap()
20});
21
22static OS_ANDROID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
23    Regex::new(r"^Android (OS )?(?P<version>\d+(\.\d+){0,2}) / API-(?P<api>(\d+))").unwrap()
24});
25
26/// Format sent by Unreal Engine on macOS
27static OS_MACOS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
28    Regex::new(r"^Mac OS X (?P<version>\d+\.\d+\.\d+)( \((?P<build>[a-fA-F0-9]+)\))?$").unwrap()
29});
30
31/// Format sent by Unity on iOS
32static OS_IOS_REGEX: LazyLock<Regex> =
33    LazyLock::new(|| Regex::new(r"^iOS (?P<version>\d+\.\d+\.\d+)").unwrap());
34
35/// Format sent by Unity on iPadOS
36static OS_IPADOS_REGEX: LazyLock<Regex> =
37    LazyLock::new(|| Regex::new(r"^iPadOS (?P<version>\d+\.\d+\.\d+)").unwrap());
38
39/// Specific regex to parse Linux distros
40static OS_LINUX_DISTRO_UNAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
41    Regex::new(r"^Linux (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?) (?P<name>[a-zA-Z]+) (?P<version>\d+(\.\d+){0,2})").unwrap()
42});
43
44/// Environment.OSVersion or RuntimeInformation.OSDescription (uname) on Mono and CoreCLR on
45/// macOS, iOS, Linux, etc.
46static OS_UNAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
47    Regex::new(r"^(?P<name>[a-zA-Z]+) (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?)").unwrap()
48});
49
50/// Mono 5.4, .NET Core 2.0
51static RUNTIME_DOTNET_REGEX: LazyLock<Regex> =
52    LazyLock::new(|| Regex::new(r"^(?P<name>.*) (?P<version>\d+\.\d+(\.\d+){0,2}).*$").unwrap());
53
54/// A hashmap that translates from the android model to the more human-friendly product-names.
55/// E.g. NE2211 -> OnePlus 10 Pro 5G
56static ANDROID_MODEL_NAMES: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
57    let mut map = HashMap::new();
58    // Note that windows paths with backslashes '\' won't work on unix systems.
59    let android_str = include_str!("android_models.csv");
60
61    let mut lines = android_str.lines();
62
63    let header = lines.next().expect("CSV file should have a header");
64
65    let header_fields: Vec<&str> = header.split(',').collect();
66    let model_index = header_fields.iter().position(|&s| s.trim() == "Model");
67    let product_name_index = header_fields
68        .iter()
69        .position(|&s| s.trim() == "Marketing Name");
70
71    let (model_index, product_name_index) = match (model_index, product_name_index) {
72        (Some(model_index), Some(product_name_index)) => (model_index, product_name_index),
73        (_, _) => {
74            relay_log::error!(
75                "failed to find model and/or marketing name headers for android-model map",
76            );
77
78            return HashMap::new();
79        }
80    };
81
82    for line in lines {
83        let fields: Vec<&str> = line.split(',').collect();
84        if fields.len() > std::cmp::max(model_index, product_name_index) {
85            map.insert(
86                fields[model_index].trim(),
87                fields[product_name_index].trim(),
88            );
89        }
90    }
91    map
92});
93
94fn normalize_runtime_context(runtime: &mut RuntimeContext) {
95    if runtime.name.value().is_empty()
96        && runtime.version.value().is_empty()
97        && let Some(raw_description) = runtime.raw_description.as_str()
98        && let Some(captures) = RUNTIME_DOTNET_REGEX.captures(raw_description)
99    {
100        runtime.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
101        runtime.version = captures
102            .name("version")
103            .map(|m| m.as_str().to_owned())
104            .into();
105    }
106
107    // RuntimeInformation.FrameworkDescription doesn't return a very useful value.
108    // Example: ".NET Framework 4.7.3056.0"
109    // Use release keys from registry sent as #build
110    if let Some(name) = runtime.name.as_str()
111        && let Some(build) = runtime.build.as_str()
112        && name.starts_with(".NET Framework")
113    {
114        let version = match build {
115            "378389" => Some("4.5".to_owned()),
116            "378675" => Some("4.5.1".to_owned()),
117            "378758" => Some("4.5.1".to_owned()),
118            "379893" => Some("4.5.2".to_owned()),
119            "393295" => Some("4.6".to_owned()),
120            "393297" => Some("4.6".to_owned()),
121            "394254" => Some("4.6.1".to_owned()),
122            "394271" => Some("4.6.1".to_owned()),
123            "394802" => Some("4.6.2".to_owned()),
124            "394806" => Some("4.6.2".to_owned()),
125            "460798" => Some("4.7".to_owned()),
126            "460805" => Some("4.7".to_owned()),
127            "461308" => Some("4.7.1".to_owned()),
128            "461310" => Some("4.7.1".to_owned()),
129            "461808" => Some("4.7.2".to_owned()),
130            "461814" => Some("4.7.2".to_owned()),
131            "528040" => Some("4.8".to_owned()),
132            "528049" => Some("4.8".to_owned()),
133            "528209" => Some("4.8".to_owned()),
134            "528372" => Some("4.8".to_owned()),
135            "528449" => Some("4.8".to_owned()),
136            _ => None,
137        };
138
139        if let Some(version) = version {
140            runtime.version = version.into();
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        && let (Some(name), Some(version)) = (runtime.name.value(), runtime.version.value())
148    {
149        runtime.runtime = Annotated::from(format!("{name} {version}"));
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        Context::Trace(trace) => {
387            trace.status.get_or_insert_with(|| SpanStatus::Unknown);
388        }
389        _ => {}
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use relay_event_schema::protocol::{Headers, LenientString, PairList};
396    use relay_protocol::SerializableAnnotated;
397    use similar_asserts::assert_eq;
398
399    use super::*;
400
401    macro_rules! assert_json_context {
402        ($ctx:expr, $($tt:tt)*) => {
403            insta::assert_json_snapshot!(SerializableAnnotated(&Annotated::new($ctx)), $($tt)*)
404
405        };
406    }
407
408    #[test]
409    fn test_get_product_name() {
410        assert_eq!(
411            ANDROID_MODEL_NAMES.get("NE2211").unwrap(),
412            &"OnePlus 10 Pro 5G"
413        );
414
415        assert_eq!(
416            ANDROID_MODEL_NAMES.get("MP04").unwrap(),
417            &"A13 Pro Max 5G EEA"
418        );
419
420        assert_eq!(ANDROID_MODEL_NAMES.get("ZT216_7").unwrap(), &"zyrex");
421
422        assert!(ANDROID_MODEL_NAMES.get("foobar").is_none());
423    }
424
425    #[test]
426    fn test_dotnet_framework_48_without_build_id() {
427        let mut runtime = RuntimeContext {
428            raw_description: ".NET Framework 4.8.4250.0".to_owned().into(),
429            ..RuntimeContext::default()
430        };
431
432        normalize_runtime_context(&mut runtime);
433        assert_json_context!(runtime, @r###"
434        {
435          "runtime": ".NET Framework 4.8.4250.0",
436          "name": ".NET Framework",
437          "version": "4.8.4250.0",
438          "raw_description": ".NET Framework 4.8.4250.0"
439        }
440        "###);
441    }
442
443    #[test]
444    fn test_dotnet_framework_472() {
445        let mut runtime = RuntimeContext {
446            raw_description: ".NET Framework 4.7.3056.0".to_owned().into(),
447            build: LenientString("461814".to_owned()).into(),
448            ..RuntimeContext::default()
449        };
450
451        normalize_runtime_context(&mut runtime);
452        assert_json_context!(runtime, @r###"
453        {
454          "runtime": ".NET Framework 4.7.2",
455          "name": ".NET Framework",
456          "version": "4.7.2",
457          "build": "461814",
458          "raw_description": ".NET Framework 4.7.3056.0"
459        }
460        "###);
461    }
462
463    #[test]
464    fn test_dotnet_framework_future_version() {
465        let mut runtime = RuntimeContext {
466            raw_description: ".NET Framework 200.0".to_owned().into(),
467            build: LenientString("999999".to_owned()).into(),
468            ..RuntimeContext::default()
469        };
470
471        // Unmapped build number doesn't override version
472        normalize_runtime_context(&mut runtime);
473        assert_json_context!(runtime, @r###"
474        {
475          "runtime": ".NET Framework 200.0",
476          "name": ".NET Framework",
477          "version": "200.0",
478          "build": "999999",
479          "raw_description": ".NET Framework 200.0"
480        }
481        "###);
482    }
483
484    #[test]
485    fn test_dotnet_native() {
486        let mut runtime = RuntimeContext {
487            raw_description: ".NET Native 2.0".to_owned().into(),
488            ..RuntimeContext::default()
489        };
490
491        normalize_runtime_context(&mut runtime);
492        assert_json_context!(runtime, @r###"
493        {
494          "runtime": ".NET Native 2.0",
495          "name": ".NET Native",
496          "version": "2.0",
497          "raw_description": ".NET Native 2.0"
498        }
499        "###);
500    }
501
502    #[test]
503    fn test_dotnet_core() {
504        let mut runtime = RuntimeContext {
505            raw_description: ".NET Core 2.0".to_owned().into(),
506            ..RuntimeContext::default()
507        };
508
509        normalize_runtime_context(&mut runtime);
510        assert_json_context!(runtime, @r###"
511        {
512          "runtime": ".NET Core 2.0",
513          "name": ".NET Core",
514          "version": "2.0",
515          "raw_description": ".NET Core 2.0"
516        }
517        "###);
518    }
519
520    #[test]
521    fn test_windows_7_or_server_2008() {
522        // Environment.OSVersion on Windows 7 (CoreCLR 1.0+, .NET Framework 1.1+, Mono 1+)
523        let mut os = OsContext {
524            raw_description: "Microsoft Windows NT 6.1.7601 Service Pack 1"
525                .to_owned()
526                .into(),
527            ..OsContext::default()
528        };
529
530        normalize_os_context(&mut os);
531        assert_json_context!(os, @r###"
532        {
533          "os": "Windows 7",
534          "name": "Windows",
535          "version": "7",
536          "build": "7601",
537          "raw_description": "Microsoft Windows NT 6.1.7601 Service Pack 1"
538        }
539        "###);
540    }
541
542    #[test]
543    fn test_windows_8_or_server_2012_or_later() {
544        // Environment.OSVersion on Windows 10 (CoreCLR 1.0+, .NET Framework 1.1+, Mono 1+)
545        // *or later, due to GetVersionEx deprecated on Windows 8.1
546        // It's a potentially really misleading API on newer platforms
547        // Only used if RuntimeInformation.OSDescription is not available (old runtimes)
548        let mut os = OsContext {
549            raw_description: "Microsoft Windows NT 6.2.9200.0".to_owned().into(),
550            ..OsContext::default()
551        };
552
553        normalize_os_context(&mut os);
554        assert_json_context!(os, @r###"
555        {
556          "os": "Windows 8",
557          "name": "Windows",
558          "version": "8",
559          "build": "9200",
560          "raw_description": "Microsoft Windows NT 6.2.9200.0"
561        }
562        "###);
563    }
564
565    #[test]
566    fn test_windows_10() {
567        // RuntimeInformation.OSDescription on Windows 10 (CoreCLR 2.0+, .NET
568        // Framework 4.7.1+, Mono 5.4+)
569        let mut os = OsContext {
570            raw_description: "Microsoft Windows 10.0.16299".to_owned().into(),
571            ..OsContext::default()
572        };
573
574        normalize_os_context(&mut os);
575        assert_json_context!(os, @r###"
576        {
577          "os": "Windows 10",
578          "name": "Windows",
579          "version": "10",
580          "build": "16299",
581          "raw_description": "Microsoft Windows 10.0.16299"
582        }
583        "###);
584    }
585
586    #[test]
587    fn test_windows_11() {
588        // https://github.com/getsentry/relay/issues/1201
589        let mut os = OsContext {
590            raw_description: "Microsoft Windows 10.0.22000".to_owned().into(),
591            ..OsContext::default()
592        };
593
594        normalize_os_context(&mut os);
595        assert_json_context!(os, @r###"
596        {
597          "os": "Windows 11",
598          "name": "Windows",
599          "version": "11",
600          "build": "22000",
601          "raw_description": "Microsoft Windows 10.0.22000"
602        }
603        "###);
604    }
605
606    #[test]
607    fn test_windows_11_future1() {
608        // This is fictional as of today, but let's be explicit about the behavior we expect.
609        let mut os = OsContext {
610            raw_description: "Microsoft Windows 10.0.22001".to_owned().into(),
611            ..OsContext::default()
612        };
613
614        normalize_os_context(&mut os);
615        assert_json_context!(os, @r###"
616        {
617          "os": "Windows 11",
618          "name": "Windows",
619          "version": "11",
620          "build": "22001",
621          "raw_description": "Microsoft Windows 10.0.22001"
622        }
623        "###);
624    }
625
626    #[test]
627    fn test_windows_11_future2() {
628        // This is fictional, but let's be explicit about the behavior we expect.
629        let mut os = OsContext {
630            raw_description: "Microsoft Windows 10.1.23456".to_owned().into(),
631            ..OsContext::default()
632        };
633
634        normalize_os_context(&mut os);
635        assert_json_context!(os, @r###"
636        {
637          "os": "Windows 10.1.23456",
638          "name": "Windows",
639          "version": "10.1.23456",
640          "build": "23456",
641          "raw_description": "Microsoft Windows 10.1.23456"
642        }
643        "###);
644    }
645
646    #[test]
647    fn test_macos_os_version() {
648        // Environment.OSVersion on macOS (CoreCLR 1.0+, Mono 1+)
649        let mut os = OsContext {
650            raw_description: "Unix 17.5.0.0".to_owned().into(),
651            ..OsContext::default()
652        };
653
654        normalize_os_context(&mut os);
655        assert_json_context!(os, @r###"
656        {
657          "os": "Unix",
658          "name": "Unix",
659          "kernel_version": "17.5.0",
660          "raw_description": "Unix 17.5.0.0"
661        }
662        "###);
663    }
664
665    #[test]
666    fn test_macos_runtime() {
667        // RuntimeInformation.OSDescription on macOS (CoreCLR 2.0+, Mono 5.4+)
668        let mut os = OsContext {
669        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(),
670        ..OsContext::default()
671    };
672
673        normalize_os_context(&mut os);
674        assert_json_context!(os, @r###"
675        {
676          "os": "Darwin",
677          "name": "Darwin",
678          "kernel_version": "17.5.0",
679          "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"
680        }
681        "###);
682    }
683
684    #[test]
685    fn test_centos_os_version() {
686        // Environment.OSVersion on CentOS 7 (CoreCLR 1.0+, Mono 1+)
687        let mut os = OsContext {
688            raw_description: "Unix 3.10.0.693".to_owned().into(),
689            ..OsContext::default()
690        };
691
692        normalize_os_context(&mut os);
693        assert_json_context!(os, @r###"
694        {
695          "os": "Unix",
696          "name": "Unix",
697          "kernel_version": "3.10.0.693",
698          "raw_description": "Unix 3.10.0.693"
699        }
700        "###);
701    }
702
703    #[test]
704    fn test_centos_runtime_info() {
705        // RuntimeInformation.OSDescription on CentOS 7 (CoreCLR 2.0+, Mono 5.4+)
706        let mut os = OsContext {
707            raw_description: "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
708                .to_owned()
709                .into(),
710            ..OsContext::default()
711        };
712
713        normalize_os_context(&mut os);
714        assert_json_context!(os, @r###"
715        {
716          "os": "Linux",
717          "name": "Linux",
718          "kernel_version": "3.10.0",
719          "raw_description": "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
720        }
721        "###);
722    }
723
724    #[test]
725    fn test_wsl_ubuntu() {
726        // RuntimeInformation.OSDescription on Windows Subsystem for Linux (Ubuntu)
727        // (CoreCLR 2.0+, Mono 5.4+)
728        let mut os = OsContext {
729            raw_description: "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
730                .to_owned()
731                .into(),
732            ..OsContext::default()
733        };
734
735        normalize_os_context(&mut os);
736        assert_json_context!(os, @r###"
737        {
738          "os": "Linux",
739          "name": "Linux",
740          "kernel_version": "4.4.0",
741          "raw_description": "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
742        }
743        "###);
744    }
745
746    #[test]
747    fn test_macos_with_build() {
748        let mut os = OsContext {
749            raw_description: "Mac OS X 10.14.2 (18C54)".to_owned().into(),
750            ..OsContext::default()
751        };
752
753        normalize_os_context(&mut os);
754        assert_json_context!(os, @r###"
755        {
756          "os": "macOS 10.14.2",
757          "name": "macOS",
758          "version": "10.14.2",
759          "build": "18C54",
760          "raw_description": "Mac OS X 10.14.2 (18C54)"
761        }
762        "###);
763    }
764
765    #[test]
766    fn test_macos_without_build() {
767        let mut os = OsContext {
768            raw_description: "Mac OS X 10.14.2".to_owned().into(),
769            ..OsContext::default()
770        };
771
772        normalize_os_context(&mut os);
773        assert_json_context!(os, @r###"
774        {
775          "os": "macOS 10.14.2",
776          "name": "macOS",
777          "version": "10.14.2",
778          "raw_description": "Mac OS X 10.14.2"
779        }
780        "###);
781    }
782
783    #[test]
784    fn test_name_not_overwritten() {
785        let mut os = OsContext {
786            name: "Properly defined name".to_owned().into(),
787            raw_description: "Linux 4.4.0".to_owned().into(),
788            ..OsContext::default()
789        };
790
791        normalize_os_context(&mut os);
792        assert_json_context!(os, @r###"
793        {
794          "os": "Properly defined name",
795          "name": "Properly defined name",
796          "raw_description": "Linux 4.4.0"
797        }
798        "###);
799    }
800
801    #[test]
802    fn test_version_not_overwritten() {
803        let mut os = OsContext {
804            version: "Properly defined version".to_owned().into(),
805            raw_description: "Linux 4.4.0".to_owned().into(),
806            ..OsContext::default()
807        };
808
809        normalize_os_context(&mut os);
810        assert_json_context!(os, @r###"
811        {
812          "version": "Properly defined version",
813          "raw_description": "Linux 4.4.0"
814        }
815        "###);
816    }
817
818    #[test]
819    fn test_no_name() {
820        let mut os = OsContext::default();
821
822        normalize_os_context(&mut os);
823        assert_json_context!(os, @"{}");
824    }
825
826    #[test]
827    fn test_unity_mac_os() {
828        let mut os = OsContext {
829            raw_description: "Mac OS X 10.16.0".to_owned().into(),
830            ..OsContext::default()
831        };
832        normalize_os_context(&mut os);
833        assert_json_context!(os, @r###"
834        {
835          "os": "macOS 10.16.0",
836          "name": "macOS",
837          "version": "10.16.0",
838          "raw_description": "Mac OS X 10.16.0"
839        }
840        "###);
841    }
842
843    #[test]
844    fn test_unity_ios() {
845        let mut os = OsContext {
846            raw_description: "iOS 17.5.1".to_owned().into(),
847            ..OsContext::default()
848        };
849
850        normalize_os_context(&mut os);
851        assert_json_context!(os, @r###"
852        {
853          "os": "iOS 17.5.1",
854          "name": "iOS",
855          "version": "17.5.1",
856          "raw_description": "iOS 17.5.1"
857        }
858        "###);
859    }
860
861    #[test]
862    fn test_unity_ipados() {
863        let mut os = OsContext {
864            raw_description: "iPadOS 17.5.1".to_owned().into(),
865            ..OsContext::default()
866        };
867
868        normalize_os_context(&mut os);
869        assert_json_context!(os, @r###"
870        {
871          "os": "iPadOS 17.5.1",
872          "name": "iPadOS",
873          "version": "17.5.1",
874          "raw_description": "iPadOS 17.5.1"
875        }
876        "###);
877    }
878
879    //OS_WINDOWS_REGEX = r#"^(Microsoft )?Windows (NT )?(?P<version>\d+\.\d+\.\d+).*$"#;
880    #[test]
881    fn test_unity_windows_os() {
882        let mut os = OsContext {
883            raw_description: "Windows 10  (10.0.19042) 64bit".to_owned().into(),
884            ..OsContext::default()
885        };
886
887        normalize_os_context(&mut os);
888        assert_json_context!(os, @r###"
889        {
890          "os": "Windows 10",
891          "name": "Windows",
892          "version": "10",
893          "build": "19042",
894          "raw_description": "Windows 10  (10.0.19042) 64bit"
895        }
896        "###);
897    }
898
899    #[test]
900    fn test_unity_android_os() {
901        let mut os = OsContext {
902            raw_description: "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
903                .to_owned()
904                .into(),
905            ..OsContext::default()
906        };
907
908        normalize_os_context(&mut os);
909        assert_json_context!(os, @r###"
910        {
911          "os": "Android 11",
912          "name": "Android",
913          "version": "11",
914          "raw_description": "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
915        }
916        "###);
917    }
918
919    #[test]
920    fn test_unity_android_api_version() {
921        let description = "Android OS 11 / API-30 (RP1A.201005.001/2107031736)";
922        assert_eq!(Some("30"), get_android_api_version(description));
923    }
924
925    #[test]
926    fn test_unreal_windows_os() {
927        let mut os = OsContext {
928            raw_description: "Windows 10".to_owned().into(),
929            ..OsContext::default()
930        };
931
932        normalize_os_context(&mut os);
933        assert_json_context!(os, @r###"
934        {
935          "os": "Windows 10",
936          "name": "Windows",
937          "version": "10",
938          "raw_description": "Windows 10"
939        }
940        "###);
941    }
942
943    #[test]
944    fn test_linux_5_11() {
945        let mut os = OsContext {
946            raw_description: "Linux 5.11 Ubuntu 20.04 64bit".to_owned().into(),
947            ..OsContext::default()
948        };
949
950        normalize_os_context(&mut os);
951        assert_json_context!(os, @r###"
952        {
953          "os": "Ubuntu 20.04",
954          "name": "Ubuntu",
955          "version": "20.04",
956          "kernel_version": "5.11",
957          "raw_description": "Linux 5.11 Ubuntu 20.04 64bit"
958        }
959        "###);
960    }
961
962    #[test]
963    fn test_unity_nintendo_switch() {
964        // Format sent by Unity on Nintendo Switch
965        let mut os = OsContext {
966            raw_description: "Nintendo Switch".to_owned().into(),
967            ..OsContext::default()
968        };
969
970        normalize_os_context(&mut os);
971        assert_json_context!(os, @r###"
972        {
973          "os": "Nintendo OS",
974          "name": "Nintendo OS",
975          "raw_description": "Nintendo Switch"
976        }
977        "###);
978    }
979
980    #[test]
981    fn test_android_4_4_2() {
982        let mut os = OsContext {
983            raw_description: "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
984                .to_owned()
985                .into(),
986            ..OsContext::default()
987        };
988
989        normalize_os_context(&mut os);
990        assert_json_context!(os, @r###"
991        {
992          "os": "Android 4.4.2",
993          "name": "Android",
994          "version": "4.4.2",
995          "raw_description": "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
996        }
997        "###);
998    }
999
1000    #[test]
1001    fn test_infer_json() {
1002        let mut response = ResponseContext {
1003            data: Annotated::from(Value::String(r#"{"foo":"bar"}"#.to_owned())),
1004            ..ResponseContext::default()
1005        };
1006
1007        normalize_response(&mut response);
1008        assert_json_context!(response, @r###"
1009        {
1010          "data": {
1011            "foo": "bar"
1012          },
1013          "inferred_content_type": "application/json"
1014        }
1015        "###);
1016    }
1017
1018    #[test]
1019    fn test_broken_json_with_fallback() {
1020        let mut response = ResponseContext {
1021            data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1022            headers: Annotated::from(Headers(PairList(vec![Annotated::new((
1023                Annotated::new("Content-Type".to_owned().into()),
1024                Annotated::new("text/plain; encoding=utf-8".to_owned().into()),
1025            ))]))),
1026            ..ResponseContext::default()
1027        };
1028
1029        normalize_response(&mut response);
1030        assert_json_context!(response, @r###"
1031        {
1032          "headers": [
1033            [
1034              "Content-Type",
1035              "text/plain; encoding=utf-8"
1036            ]
1037          ],
1038          "data": "{\"foo\":\"b",
1039          "inferred_content_type": "text/plain"
1040        }
1041        "###);
1042    }
1043
1044    #[test]
1045    fn test_broken_json_without_fallback() {
1046        let mut response = ResponseContext {
1047            data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1048            ..ResponseContext::default()
1049        };
1050
1051        normalize_response(&mut response);
1052        assert_json_context!(response, @r###"
1053        {
1054          "data": "{\"foo\":\"b"
1055        }
1056        "###);
1057    }
1058
1059    #[test]
1060    fn test_os_computed_context() {
1061        let mut os = OsContext {
1062            name: "Windows".to_owned().into(),
1063            version: "10".to_owned().into(),
1064            ..OsContext::default()
1065        };
1066
1067        normalize_os_context(&mut os);
1068        assert_json_context!(os, @r###"
1069        {
1070          "os": "Windows 10",
1071          "name": "Windows",
1072          "version": "10"
1073        }
1074        "###);
1075    }
1076
1077    #[test]
1078    fn test_os_computed_context_missing_version() {
1079        let mut os = OsContext {
1080            name: "Windows".to_owned().into(),
1081            ..OsContext::default()
1082        };
1083
1084        normalize_os_context(&mut os);
1085        assert_json_context!(os, @r###"
1086        {
1087          "os": "Windows",
1088          "name": "Windows"
1089        }
1090        "###);
1091    }
1092
1093    #[test]
1094    fn test_runtime_computed_context() {
1095        let mut runtime = RuntimeContext {
1096            name: "Python".to_owned().into(),
1097            version: "3.9.0".to_owned().into(),
1098            ..RuntimeContext::default()
1099        };
1100
1101        normalize_runtime_context(&mut runtime);
1102        assert_json_context!(runtime, @r###"
1103        {
1104          "runtime": "Python 3.9.0",
1105          "name": "Python",
1106          "version": "3.9.0"
1107        }
1108        "###);
1109    }
1110
1111    #[test]
1112    fn test_runtime_computed_context_missing_version() {
1113        let mut runtime = RuntimeContext {
1114            name: "Python".to_owned().into(),
1115            ..RuntimeContext::default()
1116        };
1117
1118        normalize_runtime_context(&mut runtime);
1119        assert_json_context!(runtime, @r###"
1120        {
1121          "name": "Python"
1122        }
1123        "###);
1124    }
1125
1126    #[test]
1127    fn test_browser_computed_context() {
1128        let mut browser = BrowserContext {
1129            name: "Firefox".to_owned().into(),
1130            version: "89.0".to_owned().into(),
1131            ..BrowserContext::default()
1132        };
1133
1134        normalize_browser_context(&mut browser);
1135        assert_json_context!(browser, @r###"
1136        {
1137          "browser": "Firefox 89.0",
1138          "name": "Firefox",
1139          "version": "89.0"
1140        }
1141        "###);
1142    }
1143
1144    #[test]
1145    fn test_browser_computed_context_missing_version() {
1146        let mut browser = BrowserContext {
1147            name: "Firefox".to_owned().into(),
1148            ..BrowserContext::default()
1149        };
1150
1151        normalize_browser_context(&mut browser);
1152        assert_json_context!(browser, @r###"
1153        {
1154          "browser": "Firefox",
1155          "name": "Firefox"
1156        }
1157        "###);
1158    }
1159}