Skip to content

Commit 4b886a8

Browse files
authored
fix: Allow for Contexts data type to be infered from the key value (#588)
1 parent 117f2d0 commit 4b886a8

File tree

2 files changed

+134
-4
lines changed

2 files changed

+134
-4
lines changed

sentry-types/src/protocol/v7.rs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ use std::str;
1616
use std::time::SystemTime;
1717

1818
use self::debugid::{CodeId, DebugId};
19-
use serde::Serializer;
20-
use serde::{Deserialize, Serialize};
19+
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
2120
use thiserror::Error;
2221
use url::Url;
2322
use uuid::Uuid;
@@ -1477,6 +1476,60 @@ into_context!(Trace, TraceContext);
14771476
into_context!(Gpu, GpuContext);
14781477
into_context!(Profile, ProfileContext);
14791478

1479+
const INFERABLE_CONTEXTS: &[&str] = &[
1480+
"device", "os", "runtime", "app", "browser", "trace", "gpu", "profile",
1481+
];
1482+
1483+
struct ContextsVisitor;
1484+
1485+
impl<'de> de::Visitor<'de> for ContextsVisitor {
1486+
type Value = Map<String, Context>;
1487+
1488+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1489+
formatter.write_str("contexts object")
1490+
}
1491+
1492+
fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
1493+
where
1494+
A: de::MapAccess<'de>,
1495+
{
1496+
let mut map: Map<String, Context> = Map::new();
1497+
1498+
while let Some((key, mut value)) = access.next_entry::<String, Value>()? {
1499+
let typed_value = value
1500+
.as_object_mut()
1501+
.map(|ctx| {
1502+
if !ctx.contains_key("type") {
1503+
let type_key = if INFERABLE_CONTEXTS.contains(&key.as_str()) {
1504+
key.clone().into()
1505+
} else {
1506+
Value::String("unknown".into())
1507+
};
1508+
ctx.insert(String::from("type"), type_key);
1509+
}
1510+
ctx.to_owned()
1511+
})
1512+
.ok_or_else(|| de::Error::custom("expected valid `context` object"))?;
1513+
1514+
match serde_json::from_value(serde_json::to_value(typed_value).unwrap()) {
1515+
Ok(context) => {
1516+
map.insert(key, context);
1517+
}
1518+
Err(e) => return Err(de::Error::custom(e.to_string())),
1519+
}
1520+
}
1521+
1522+
Ok(map)
1523+
}
1524+
}
1525+
1526+
fn deserialize_contexts<'de, D>(deserializer: D) -> Result<Map<String, Context>, D::Error>
1527+
where
1528+
D: Deserializer<'de>,
1529+
{
1530+
deserializer.deserialize_map(ContextsVisitor {})
1531+
}
1532+
14801533
mod event {
14811534
use super::*;
14821535

@@ -1578,7 +1631,11 @@ pub struct Event<'a> {
15781631
#[serde(default, skip_serializing_if = "Option::is_none")]
15791632
pub request: Option<Request>,
15801633
/// Optional contexts.
1581-
#[serde(default, skip_serializing_if = "Map::is_empty")]
1634+
#[serde(
1635+
default,
1636+
skip_serializing_if = "Map::is_empty",
1637+
deserialize_with = "deserialize_contexts"
1638+
)]
15821639
pub contexts: Map<String, Context>,
15831640
/// List of breadcrumbs to send along.
15841641
#[serde(default, skip_serializing_if = "Values::is_empty")]
@@ -1943,7 +2000,11 @@ pub struct Transaction<'a> {
19432000
/// The collection of finished spans part of this transaction.
19442001
pub spans: Vec<Span>,
19452002
/// Optional contexts.
1946-
#[serde(default, skip_serializing_if = "Map::is_empty")]
2003+
#[serde(
2004+
default,
2005+
skip_serializing_if = "Map::is_empty",
2006+
deserialize_with = "deserialize_contexts"
2007+
)]
19472008
pub contexts: Map<String, Context>,
19482009
/// Optionally HTTP request data to be sent along.
19492010
#[serde(default, skip_serializing_if = "Option::is_none")]

sentry-types/tests/test_protocol_v7.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,6 +1379,75 @@ mod test_contexts {
13791379
\"contexts\":{\"other\":{\"type\":\"unknown\",\"aha\":\"oho\"}}}"
13801380
);
13811381
}
1382+
1383+
#[test]
1384+
fn test_typeless_context() {
1385+
let payload = r#"
1386+
{
1387+
"event_id": "d43e86c96e424a93a4fbda156dd17341",
1388+
"timestamp": 1514103120,
1389+
"contexts": {
1390+
"device": {
1391+
"name": "iphone",
1392+
"family": "iphone",
1393+
"model": "iphone7,3",
1394+
"model_id": "AH223",
1395+
"arch": "arm64"
1396+
},
1397+
"os": { "name": "iOS", "version": "11.4.2", "build": "ADSA23" },
1398+
"runtime": { "name": "magicvm", "version": "5.3" },
1399+
"app": {
1400+
"app_start_time": "2018-02-08T22:21:57Z",
1401+
"build_type": "testflight",
1402+
"app_name": "Baz App",
1403+
"app_version": "1.0",
1404+
"app_build": "100001"
1405+
},
1406+
"browser": { "name": "Chrome", "version": "59.0.3071" },
1407+
"gpu": {
1408+
"name": "AMD Radeon Pro 560",
1409+
"vendor_name": "Apple",
1410+
"memory_size": 4096
1411+
},
1412+
"trace": {
1413+
"trace_id": "12312012123120121231201212312012",
1414+
"span_id": "0415201309082013",
1415+
"parent_span_id": null,
1416+
"description": "<OrganizationContext>",
1417+
"op": "http.server"
1418+
},
1419+
"random": { "aha": "oho" }
1420+
}
1421+
}
1422+
"#;
1423+
1424+
let event: v7::Event = serde_json::from_str(payload).unwrap();
1425+
let ctx = event.contexts;
1426+
1427+
assert!(ctx.contains_key("device"));
1428+
assert_eq!(ctx.get("device").unwrap().type_name(), "device");
1429+
1430+
assert!(ctx.contains_key("os"));
1431+
assert_eq!(ctx.get("os").unwrap().type_name(), "os");
1432+
1433+
assert!(ctx.contains_key("runtime"));
1434+
assert_eq!(ctx.get("runtime").unwrap().type_name(), "runtime");
1435+
1436+
assert!(ctx.contains_key("app"));
1437+
assert_eq!(ctx.get("app").unwrap().type_name(), "app");
1438+
1439+
assert!(ctx.contains_key("browser"));
1440+
assert_eq!(ctx.get("browser").unwrap().type_name(), "browser");
1441+
1442+
assert!(ctx.contains_key("trace"));
1443+
assert_eq!(ctx.get("trace").unwrap().type_name(), "trace");
1444+
1445+
assert!(ctx.contains_key("gpu"));
1446+
assert_eq!(ctx.get("gpu").unwrap().type_name(), "gpu");
1447+
1448+
assert!(ctx.contains_key("random"));
1449+
assert_eq!(ctx.get("random").unwrap().type_name(), "unknown");
1450+
}
13821451
}
13831452

13841453
#[test]

0 commit comments

Comments
 (0)