diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 1fb2210c7..a8213441b 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -16,8 +16,7 @@ use std::str; use std::time::SystemTime; use self::debugid::{CodeId, DebugId}; -use serde::Serializer; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use url::Url; use uuid::Uuid; @@ -1477,6 +1476,60 @@ into_context!(Trace, TraceContext); into_context!(Gpu, GpuContext); into_context!(Profile, ProfileContext); +const INFERABLE_CONTEXTS: &[&str] = &[ + "device", "os", "runtime", "app", "browser", "trace", "gpu", "profile", +]; + +struct ContextsVisitor; + +impl<'de> de::Visitor<'de> for ContextsVisitor { + type Value = Map; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("contexts object") + } + + fn visit_map(self, mut access: A) -> Result + where + A: de::MapAccess<'de>, + { + let mut map: Map = Map::new(); + + while let Some((key, mut value)) = access.next_entry::()? { + let typed_value = value + .as_object_mut() + .map(|ctx| { + if !ctx.contains_key("type") { + let type_key = if INFERABLE_CONTEXTS.contains(&key.as_str()) { + key.clone().into() + } else { + Value::String("unknown".into()) + }; + ctx.insert(String::from("type"), type_key); + } + ctx.to_owned() + }) + .ok_or_else(|| de::Error::custom("expected valid `context` object"))?; + + match serde_json::from_value(serde_json::to_value(typed_value).unwrap()) { + Ok(context) => { + map.insert(key, context); + } + Err(e) => return Err(de::Error::custom(e.to_string())), + } + } + + Ok(map) + } +} + +fn deserialize_contexts<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_map(ContextsVisitor {}) +} + mod event { use super::*; @@ -1578,7 +1631,11 @@ pub struct Event<'a> { #[serde(default, skip_serializing_if = "Option::is_none")] pub request: Option, /// Optional contexts. - #[serde(default, skip_serializing_if = "Map::is_empty")] + #[serde( + default, + skip_serializing_if = "Map::is_empty", + deserialize_with = "deserialize_contexts" + )] pub contexts: Map, /// List of breadcrumbs to send along. #[serde(default, skip_serializing_if = "Values::is_empty")] @@ -1943,7 +2000,11 @@ pub struct Transaction<'a> { /// The collection of finished spans part of this transaction. pub spans: Vec, /// Optional contexts. - #[serde(default, skip_serializing_if = "Map::is_empty")] + #[serde( + default, + skip_serializing_if = "Map::is_empty", + deserialize_with = "deserialize_contexts" + )] pub contexts: Map, /// Optionally HTTP request data to be sent along. #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/sentry-types/tests/test_protocol_v7.rs b/sentry-types/tests/test_protocol_v7.rs index d3a59e48e..ea5b92756 100644 --- a/sentry-types/tests/test_protocol_v7.rs +++ b/sentry-types/tests/test_protocol_v7.rs @@ -1379,6 +1379,75 @@ mod test_contexts { \"contexts\":{\"other\":{\"type\":\"unknown\",\"aha\":\"oho\"}}}" ); } + + #[test] + fn test_typeless_context() { + let payload = r#" + { + "event_id": "d43e86c96e424a93a4fbda156dd17341", + "timestamp": 1514103120, + "contexts": { + "device": { + "name": "iphone", + "family": "iphone", + "model": "iphone7,3", + "model_id": "AH223", + "arch": "arm64" + }, + "os": { "name": "iOS", "version": "11.4.2", "build": "ADSA23" }, + "runtime": { "name": "magicvm", "version": "5.3" }, + "app": { + "app_start_time": "2018-02-08T22:21:57Z", + "build_type": "testflight", + "app_name": "Baz App", + "app_version": "1.0", + "app_build": "100001" + }, + "browser": { "name": "Chrome", "version": "59.0.3071" }, + "gpu": { + "name": "AMD Radeon Pro 560", + "vendor_name": "Apple", + "memory_size": 4096 + }, + "trace": { + "trace_id": "12312012123120121231201212312012", + "span_id": "0415201309082013", + "parent_span_id": null, + "description": "", + "op": "http.server" + }, + "random": { "aha": "oho" } + } + } + "#; + + let event: v7::Event = serde_json::from_str(payload).unwrap(); + let ctx = event.contexts; + + assert!(ctx.contains_key("device")); + assert_eq!(ctx.get("device").unwrap().type_name(), "device"); + + assert!(ctx.contains_key("os")); + assert_eq!(ctx.get("os").unwrap().type_name(), "os"); + + assert!(ctx.contains_key("runtime")); + assert_eq!(ctx.get("runtime").unwrap().type_name(), "runtime"); + + assert!(ctx.contains_key("app")); + assert_eq!(ctx.get("app").unwrap().type_name(), "app"); + + assert!(ctx.contains_key("browser")); + assert_eq!(ctx.get("browser").unwrap().type_name(), "browser"); + + assert!(ctx.contains_key("trace")); + assert_eq!(ctx.get("trace").unwrap().type_name(), "trace"); + + assert!(ctx.contains_key("gpu")); + assert_eq!(ctx.get("gpu").unwrap().type_name(), "gpu"); + + assert!(ctx.contains_key("random")); + assert_eq!(ctx.get("random").unwrap().type_name(), "unknown"); + } } #[test]