use std::fmt;
use std::str::FromStr;
use relay_common::time;
use relay_protocol::{
Annotated, Array, Empty, FromValue, Getter, GetterIter, IntoValue, Object, Val, Value,
};
use sentry_release_parser::Release as ParsedRelease;
use uuid::Uuid;
use crate::processor::ProcessValue;
use crate::protocol::{
AppContext, Breadcrumb, Breakdowns, BrowserContext, ClientSdkInfo, Contexts, Csp, DebugMeta,
DefaultContext, DeviceContext, EventType, Exception, ExpectCt, ExpectStaple, Fingerprint,
GpuContext, Hpkp, LenientString, Level, LogEntry, Measurements, Metrics, MetricsSummary,
MonitorContext, OsContext, ProfileContext, RelayInfo, Request, ResponseContext, RuntimeContext,
Span, SpanId, Stacktrace, Tags, TemplateInfo, Thread, Timestamp, TraceContext, TransactionInfo,
User, Values,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EventId(pub Uuid);
impl EventId {
#[inline]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[inline]
pub fn is_nil(&self) -> bool {
self.0.is_nil()
}
}
impl Default for EventId {
#[inline]
fn default() -> Self {
Self::new()
}
}
relay_protocol::derive_string_meta_structure!(EventId, "event id");
impl ProcessValue for EventId {}
impl fmt::Display for EventId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.as_simple())
}
}
impl FromStr for EventId {
type Err = <Uuid as FromStr>::Err;
fn from_str(uuid_str: &str) -> Result<Self, Self::Err> {
uuid_str.parse().map(EventId)
}
}
relay_common::impl_str_serde!(EventId, "an event identifier");
impl TryFrom<&SpanId> for EventId {
type Error = <EventId as FromStr>::Err;
fn try_from(value: &SpanId) -> Result<Self, Self::Error> {
let s = format!("0000000000000000{value}");
s.parse()
}
}
#[derive(Debug, FromValue, IntoValue, ProcessValue, Empty, Clone, PartialEq)]
pub struct ExtraValue(#[metastructure(max_depth = 7, max_bytes = 16_384)] pub Value);
impl<T: Into<Value>> From<T> for ExtraValue {
fn from(value: T) -> ExtraValue {
ExtraValue(value.into())
}
}
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
pub struct EventProcessingError {
#[metastructure(field = "type", required = "true")]
pub ty: Annotated<String>,
pub name: Annotated<String>,
pub value: Annotated<Value>,
#[metastructure(additional_properties, pii = "maybe")]
pub other: Object<Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
pub struct GroupingConfig {
#[metastructure(max_chars = 128)]
pub id: Annotated<String>,
pub enhancements: Annotated<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
#[metastructure(process_func = "process_event", value_type = "Event")]
pub struct Event {
#[metastructure(field = "event_id")]
pub id: Annotated<EventId>,
pub level: Annotated<Level>,
pub version: Annotated<String>,
#[metastructure(field = "type")]
pub ty: Annotated<EventType>,
#[metastructure(skip_serialization = "empty")]
pub fingerprint: Annotated<Fingerprint>,
#[metastructure(max_chars = 200, pii = "maybe")]
pub culprit: Annotated<String>,
#[metastructure(max_chars = 200, trim_whitespace = "true")]
pub transaction: Annotated<String>,
#[metastructure(skip_serialization = "null")]
pub transaction_info: Annotated<TransactionInfo>,
pub time_spent: Annotated<u64>,
#[metastructure(legacy_alias = "sentry.interfaces.Message", legacy_alias = "message")]
#[metastructure(skip_serialization = "empty")]
pub logentry: Annotated<LogEntry>,
#[metastructure(
max_chars = 64, deny_chars = "\r\n",
)]
pub logger: Annotated<String>,
#[metastructure(skip_serialization = "empty_deep", max_depth = 7, max_bytes = 8192)]
pub modules: Annotated<Object<String>>,
pub platform: Annotated<String>,
pub timestamp: Annotated<Timestamp>,
#[metastructure(omit_from_schema)] pub start_timestamp: Annotated<Timestamp>,
pub received: Annotated<Timestamp>,
#[metastructure(pii = "true", max_chars = 256, max_chars_allowance = 20)]
pub server_name: Annotated<String>,
#[metastructure(
max_chars = 200, required = "false",
trim_whitespace = "true",
nonempty = "true",
skip_serialization = "empty"
)]
pub release: Annotated<LenientString>,
#[metastructure(
allow_chars = "a-zA-Z0-9_.-",
trim_whitespace = "true",
required = "false",
nonempty = "true"
)]
pub dist: Annotated<String>,
#[metastructure(
max_chars = 64,
nonempty = "true",
required = "false",
trim_whitespace = "true"
)]
pub environment: Annotated<String>,
#[metastructure(max_chars = 256, max_chars_allowance = 20)]
#[metastructure(omit_from_schema)] pub site: Annotated<String>,
#[metastructure(legacy_alias = "sentry.interfaces.User")]
#[metastructure(skip_serialization = "empty")]
pub user: Annotated<User>,
#[metastructure(legacy_alias = "sentry.interfaces.Http")]
#[metastructure(skip_serialization = "empty")]
pub request: Annotated<Request>,
#[metastructure(legacy_alias = "sentry.interfaces.Contexts")]
pub contexts: Annotated<Contexts>,
#[metastructure(legacy_alias = "sentry.interfaces.Breadcrumbs")]
#[metastructure(skip_serialization = "empty")]
pub breadcrumbs: Annotated<Values<Breadcrumb>>,
#[metastructure(legacy_alias = "sentry.interfaces.Exception")]
#[metastructure(field = "exception")]
#[metastructure(skip_serialization = "empty")]
pub exceptions: Annotated<Values<Exception>>,
#[metastructure(skip_serialization = "empty")]
#[metastructure(legacy_alias = "sentry.interfaces.Stacktrace")]
pub stacktrace: Annotated<Stacktrace>,
#[metastructure(legacy_alias = "sentry.interfaces.Template")]
#[metastructure(omit_from_schema)]
pub template: Annotated<TemplateInfo>,
#[metastructure(skip_serialization = "empty")]
pub threads: Annotated<Values<Thread>>,
#[metastructure(skip_serialization = "empty", pii = "maybe")]
pub tags: Annotated<Tags>,
#[metastructure(max_depth = 7, max_bytes = 262_144)]
#[metastructure(pii = "true", skip_serialization = "empty")]
pub extra: Annotated<Object<ExtraValue>>,
#[metastructure(skip_serialization = "empty")]
pub debug_meta: Annotated<DebugMeta>,
#[metastructure(field = "sdk")]
#[metastructure(skip_serialization = "empty")]
pub client_sdk: Annotated<ClientSdkInfo>,
#[metastructure(max_depth = 5, max_bytes = 2048)]
#[metastructure(skip_serialization = "empty", omit_from_schema)]
pub ingest_path: Annotated<Array<RelayInfo>>,
#[metastructure(skip_serialization = "empty_deep")]
pub errors: Annotated<Array<EventProcessingError>>,
#[metastructure(omit_from_schema)] pub key_id: Annotated<String>,
#[metastructure(omit_from_schema)] pub project: Annotated<u64>,
#[metastructure(omit_from_schema)] pub grouping_config: Annotated<Object<Value>>,
#[metastructure(max_chars = 128)]
#[metastructure(omit_from_schema)] pub checksum: Annotated<String>,
#[metastructure(legacy_alias = "sentry.interfaces.Csp")]
#[metastructure(omit_from_schema)] pub csp: Annotated<Csp>,
#[metastructure(pii = "true", legacy_alias = "sentry.interfaces.Hpkp")]
#[metastructure(omit_from_schema)] pub hpkp: Annotated<Hpkp>,
#[metastructure(pii = "true", legacy_alias = "sentry.interfaces.ExpectCT")]
#[metastructure(omit_from_schema)] pub expectct: Annotated<ExpectCt>,
#[metastructure(pii = "true", legacy_alias = "sentry.interfaces.ExpectStaple")]
#[metastructure(omit_from_schema)] pub expectstaple: Annotated<ExpectStaple>,
#[metastructure(max_bytes = 819200)]
#[metastructure(omit_from_schema)] pub spans: Annotated<Array<Span>>,
#[metastructure(skip_serialization = "empty")]
#[metastructure(omit_from_schema)] pub measurements: Annotated<Measurements>,
#[metastructure(skip_serialization = "empty")]
#[metastructure(omit_from_schema)] pub breakdowns: Annotated<Breakdowns>,
#[metastructure(omit_from_schema)] pub scraping_attempts: Annotated<Value>,
#[metastructure(omit_from_schema)]
pub _metrics: Annotated<Metrics>,
#[metastructure(omit_from_schema)]
pub _metrics_summary: Annotated<MetricsSummary>,
#[metastructure(omit_from_schema)]
pub _dsc: Annotated<Value>,
#[metastructure(additional_properties, pii = "true")]
pub other: Object<Value>,
}
impl Event {
pub fn tag_value(&self, tag_key: &str) -> Option<&str> {
if let Some(tags) = self.tags.value() {
tags.get(tag_key)
} else {
None
}
}
pub fn has_module(&self, module_name: &str) -> bool {
self.modules
.value()
.map(|m| m.contains_key(module_name))
.unwrap_or(false)
}
pub fn sdk_name(&self) -> &str {
if let Some(client_sdk) = self.client_sdk.value() {
if let Some(name) = client_sdk.name.as_str() {
return name;
}
}
"unknown"
}
pub fn sdk_version(&self) -> &str {
if let Some(client_sdk) = self.client_sdk.value() {
if let Some(version) = client_sdk.version.as_str() {
return version;
}
}
"unknown"
}
pub fn user_agent(&self) -> Option<&str> {
let headers = self.request.value()?.headers.value()?;
for item in headers.iter() {
if let Some((ref o_k, ref v)) = item.value() {
if let Some(k) = o_k.as_str() {
if k.eq_ignore_ascii_case("user-agent") {
return v.as_str();
}
}
}
}
None
}
pub fn extra_at(&self, path: &str) -> Option<&Value> {
let mut path = path.split('.');
let mut value = &self.extra.value()?.get(path.next()?)?.value()?.0;
for key in path {
if let Value::Object(ref object) = value {
value = object.get(key)?.value()?;
} else {
return None;
}
}
Some(value)
}
pub fn parse_release(&self) -> Option<ParsedRelease<'_>> {
sentry_release_parser::Release::parse(self.release.as_str()?).ok()
}
pub fn measurement(&self, name: &str) -> Option<f64> {
let annotated = self.measurements.value()?.get(name)?;
Some(*annotated.value()?.value.value()?)
}
pub fn breakdown(&self, breakdown: &str, measurement: &str) -> Option<f64> {
let breakdown = self.breakdowns.value()?.get(breakdown)?.value()?;
Some(*breakdown.get(measurement)?.value()?.value.value()?)
}
pub fn context<C: DefaultContext>(&self) -> Option<&C> {
self.contexts.value()?.get()
}
pub fn context_mut<C: DefaultContext>(&mut self) -> Option<&mut C> {
self.contexts.value_mut().as_mut()?.get_mut()
}
}
fn or_none(string: &Annotated<impl AsRef<str>>) -> Option<&str> {
match string.as_str() {
None | Some("") => None,
Some(other) => Some(other),
}
}
impl Getter for Event {
fn get_value(&self, path: &str) -> Option<Val<'_>> {
Some(match path.strip_prefix("event.")? {
"level" => self.level.value()?.name().into(),
"release" => self.release.as_str()?.into(),
"dist" => self.dist.as_str()?.into(),
"environment" => self.environment.as_str()?.into(),
"transaction" => self.transaction.as_str()?.into(),
"logger" => self.logger.as_str()?.into(),
"platform" => self.platform.as_str().unwrap_or("other").into(),
"logentry.formatted" => self.logentry.value()?.formatted.value()?.as_ref().into(),
"logentry.message" => self.logentry.value()?.message.value()?.as_ref().into(),
"user.email" => or_none(&self.user.value()?.email)?.into(),
"user.id" => or_none(&self.user.value()?.id)?.into(),
"user.ip_address" => self.user.value()?.ip_address.as_str()?.into(),
"user.name" => self.user.value()?.name.as_str()?.into(),
"user.segment" => or_none(&self.user.value()?.segment)?.into(),
"user.geo.city" => self.user.value()?.geo.value()?.city.as_str()?.into(),
"user.geo.country_code" => self
.user
.value()?
.geo
.value()?
.country_code
.as_str()?
.into(),
"user.geo.region" => self.user.value()?.geo.value()?.region.as_str()?.into(),
"user.geo.subdivision" => self.user.value()?.geo.value()?.subdivision.as_str()?.into(),
"request.method" => self.request.value()?.method.as_str()?.into(),
"request.url" => self.request.value()?.url.as_str()?.into(),
"transaction.source" => self
.transaction_info
.value()?
.source
.value()?
.as_str()
.into(),
"sdk.name" => self.client_sdk.value()?.name.as_str()?.into(),
"sdk.version" => self.client_sdk.value()?.version.as_str()?.into(),
"sentry_user" => self.user.value()?.sentry_user.as_str()?.into(),
"contexts.app.in_foreground" => {
self.context::<AppContext>()?.in_foreground.value()?.into()
}
"contexts.app.device_app_hash" => self
.context::<AppContext>()?
.device_app_hash
.as_str()?
.into(),
"contexts.device.arch" => self.context::<DeviceContext>()?.arch.as_str()?.into(),
"contexts.device.battery_level" => self
.context::<DeviceContext>()?
.battery_level
.value()?
.into(),
"contexts.device.brand" => self.context::<DeviceContext>()?.brand.as_str()?.into(),
"contexts.device.charging" => self.context::<DeviceContext>()?.charging.value()?.into(),
"contexts.device.family" => self.context::<DeviceContext>()?.family.as_str()?.into(),
"contexts.device.model" => self.context::<DeviceContext>()?.model.as_str()?.into(),
"contexts.device.locale" => self.context::<DeviceContext>()?.locale.as_str()?.into(),
"contexts.device.online" => self.context::<DeviceContext>()?.online.value()?.into(),
"contexts.device.orientation" => self
.context::<DeviceContext>()?
.orientation
.as_str()?
.into(),
"contexts.device.name" => self.context::<DeviceContext>()?.name.as_str()?.into(),
"contexts.device.screen_density" => self
.context::<DeviceContext>()?
.screen_density
.value()?
.into(),
"contexts.device.screen_dpi" => {
self.context::<DeviceContext>()?.screen_dpi.value()?.into()
}
"contexts.device.screen_width_pixels" => self
.context::<DeviceContext>()?
.screen_width_pixels
.value()?
.into(),
"contexts.device.screen_height_pixels" => self
.context::<DeviceContext>()?
.screen_height_pixels
.value()?
.into(),
"contexts.device.simulator" => {
self.context::<DeviceContext>()?.simulator.value()?.into()
}
"contexts.gpu.vendor_name" => {
self.context::<GpuContext>()?.vendor_name.as_str()?.into()
}
"contexts.gpu.name" => self.context::<GpuContext>()?.name.as_str()?.into(),
"contexts.monitor.id" => self.context::<MonitorContext>()?.get("id")?.value()?.into(),
"contexts.monitor.slug" => self
.context::<MonitorContext>()?
.get("slug")?
.value()?
.into(),
"contexts.os" => self.context::<OsContext>()?.os.as_str()?.into(),
"contexts.os.build" => self.context::<OsContext>()?.build.as_str()?.into(),
"contexts.os.kernel_version" => {
self.context::<OsContext>()?.kernel_version.as_str()?.into()
}
"contexts.os.name" => self.context::<OsContext>()?.name.as_str()?.into(),
"contexts.os.version" => self.context::<OsContext>()?.version.as_str()?.into(),
"contexts.os.rooted" => self.context::<OsContext>()?.rooted.value()?.into(),
"contexts.browser" => self.context::<BrowserContext>()?.browser.as_str()?.into(),
"contexts.browser.name" => self.context::<BrowserContext>()?.name.as_str()?.into(),
"contexts.browser.version" => {
self.context::<BrowserContext>()?.version.as_str()?.into()
}
"contexts.profile.profile_id" => self
.context::<ProfileContext>()?
.profile_id
.value()?
.0
.into(),
"contexts.device.uuid" => self.context::<DeviceContext>()?.uuid.value()?.into(),
"contexts.trace.status" => self
.context::<TraceContext>()?
.status
.value()?
.as_str()
.into(),
"contexts.trace.op" => self.context::<TraceContext>()?.op.as_str()?.into(),
"contexts.response.status_code" => self
.context::<ResponseContext>()?
.status_code
.value()?
.into(),
"contexts.unreal.crash_type" => match self.contexts.value()?.get_key("unreal")? {
super::Context::Other(context) => context.get("crash_type")?.value()?.into(),
_ => return None,
},
"contexts.runtime" => self.context::<RuntimeContext>()?.runtime.as_str()?.into(),
"contexts.runtime.name" => self.context::<RuntimeContext>()?.name.as_str()?.into(),
"duration" => {
let start = self.start_timestamp.value()?;
let end = self.timestamp.value()?;
if start <= end && self.ty.value() == Some(&EventType::Transaction) {
time::chrono_to_positive_millis(*end - *start).into()
} else {
return None;
}
}
path => {
if let Some(rest) = path.strip_prefix("release.") {
let release = self.parse_release()?;
match rest {
"build" => release.build_hash()?.into(),
"package" => release.package()?.into(),
"version.short" => release.version()?.raw_short().into(),
_ => return None,
}
} else if let Some(rest) = path.strip_prefix("measurements.") {
let name = rest.strip_suffix(".value")?;
self.measurement(name)?.into()
} else if let Some(rest) = path.strip_prefix("breakdowns.") {
let (breakdown, measurement) = rest.split_once('.')?;
self.breakdown(breakdown, measurement)?.into()
} else if let Some(rest) = path.strip_prefix("extra.") {
self.extra_at(rest)?.into()
} else if let Some(rest) = path.strip_prefix("tags.") {
self.tags.value()?.get(rest)?.into()
} else if let Some(rest) = path.strip_prefix("request.headers.") {
self.request
.value()?
.headers
.value()?
.get_header(rest)?
.into()
} else {
return None;
}
}
})
}
fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
Some(match path.strip_prefix("event.")? {
"exception.values" => {
GetterIter::new_annotated(self.exceptions.value()?.values.value()?)
}
_ => return None,
})
}
}
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};
use relay_protocol::{ErrorKind, Map, Meta};
use similar_asserts::assert_eq;
use std::collections::BTreeMap;
use uuid::uuid;
use super::*;
use crate::protocol::{
Headers, IpAddr, JsonLenientString, PairList, TagEntry, TransactionSource,
};
#[test]
fn test_event_roundtrip() {
let json = r#"{
"event_id": "52df9022835246eeb317dbd739ccd059",
"level": "debug",
"fingerprint": [
"myprint"
],
"culprit": "myculprit",
"transaction": "mytransaction",
"logentry": {
"formatted": "mymessage"
},
"logger": "mylogger",
"modules": {
"mymodule": "1.0.0"
},
"platform": "myplatform",
"timestamp": 946684800.0,
"server_name": "myhost",
"release": "myrelease",
"dist": "mydist",
"environment": "myenv",
"tags": [
[
"tag",
"value"
]
],
"extra": {
"extra": "value"
},
"other": "value",
"_meta": {
"event_id": {
"": {
"err": [
"invalid_data"
]
}
}
}
}"#;
let event = Annotated::new(Event {
id: Annotated(
Some("52df9022-8352-46ee-b317-dbd739ccd059".parse().unwrap()),
Meta::from_error(ErrorKind::InvalidData),
),
level: Annotated::new(Level::Debug),
fingerprint: Annotated::new(vec!["myprint".to_string()].into()),
culprit: Annotated::new("myculprit".to_string()),
transaction: Annotated::new("mytransaction".to_string()),
logentry: Annotated::new(LogEntry {
formatted: Annotated::new("mymessage".to_string().into()),
..Default::default()
}),
logger: Annotated::new("mylogger".to_string()),
modules: {
let mut map = Map::new();
map.insert("mymodule".to_string(), Annotated::new("1.0.0".to_string()));
Annotated::new(map)
},
platform: Annotated::new("myplatform".to_string()),
timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
server_name: Annotated::new("myhost".to_string()),
release: Annotated::new("myrelease".to_string().into()),
dist: Annotated::new("mydist".to_string()),
environment: Annotated::new("myenv".to_string()),
tags: {
let items = vec![Annotated::new(TagEntry(
Annotated::new("tag".to_string()),
Annotated::new("value".to_string()),
))];
Annotated::new(Tags(items.into()))
},
extra: {
let mut map = Map::new();
map.insert(
"extra".to_string(),
Annotated::new(ExtraValue(Value::String("value".to_string()))),
);
Annotated::new(map)
},
other: {
let mut map = Map::new();
map.insert(
"other".to_string(),
Annotated::new(Value::String("value".to_string())),
);
map
},
..Default::default()
});
assert_eq!(event, Annotated::from_json(json).unwrap());
assert_eq!(json, event.to_json_pretty().unwrap());
}
#[test]
fn test_event_default_values() {
let json = "{}";
let event = Annotated::new(Event::default());
assert_eq!(event, Annotated::from_json(json).unwrap());
assert_eq!(json, event.to_json_pretty().unwrap());
}
#[test]
fn test_event_default_values_with_meta() {
let json = r#"{
"event_id": "52df9022835246eeb317dbd739ccd059",
"fingerprint": [
"{{ default }}"
],
"platform": "other",
"_meta": {
"event_id": {
"": {
"err": [
"invalid_data"
]
}
},
"fingerprint": {
"": {
"err": [
"invalid_data"
]
}
},
"platform": {
"": {
"err": [
"invalid_data"
]
}
}
}
}"#;
let event = Annotated::new(Event {
id: Annotated(
Some("52df9022-8352-46ee-b317-dbd739ccd059".parse().unwrap()),
Meta::from_error(ErrorKind::InvalidData),
),
fingerprint: Annotated(
Some(vec!["{{ default }}".to_string()].into()),
Meta::from_error(ErrorKind::InvalidData),
),
platform: Annotated(
Some("other".to_string()),
Meta::from_error(ErrorKind::InvalidData),
),
..Default::default()
});
assert_eq!(event, Annotated::<Event>::from_json(json).unwrap());
assert_eq!(json, event.to_json_pretty().unwrap());
}
#[test]
fn test_event_type() {
assert_eq!(
EventType::Default,
*Annotated::<EventType>::from_json("\"default\"")
.unwrap()
.value()
.unwrap()
);
}
#[test]
fn test_fingerprint_empty_string() {
let json = r#"{"fingerprint":[""]}"#;
let event = Annotated::new(Event {
fingerprint: Annotated::new(vec!["".to_string()].into()),
..Default::default()
});
assert_eq!(json, event.to_json().unwrap());
assert_eq!(event, Annotated::from_json(json).unwrap());
}
#[test]
fn test_fingerprint_null_values() {
let input = r#"{"fingerprint":[null]}"#;
let output = r#"{}"#;
let event = Annotated::new(Event {
fingerprint: Annotated::new(vec![].into()),
..Default::default()
});
assert_eq!(event, Annotated::from_json(input).unwrap());
assert_eq!(output, event.to_json().unwrap());
}
#[test]
fn test_empty_threads() {
let input = r#"{"threads": {}}"#;
let output = r#"{}"#;
let event = Annotated::new(Event::default());
assert_eq!(event, Annotated::from_json(input).unwrap());
assert_eq!(output, event.to_json().unwrap());
}
#[test]
fn test_lenient_release() {
let input = r#"{"release":42}"#;
let output = r#"{"release":"42"}"#;
let event = Annotated::new(Event {
release: Annotated::new("42".to_string().into()),
..Default::default()
});
assert_eq!(event, Annotated::from_json(input).unwrap());
assert_eq!(output, event.to_json().unwrap());
}
#[test]
fn test_extra_at() {
let json = serde_json::json!({
"extra": {
"a": "string1",
"b": 42,
"c": {
"d": "string2",
"e": null,
},
},
});
let event = Event::from_value(json.into());
let event = event.value().unwrap();
assert_eq!(
Some(&Value::String("string1".to_owned())),
event.extra_at("a")
);
assert_eq!(Some(&Value::I64(42)), event.extra_at("b"));
assert!(matches!(event.extra_at("c"), Some(&Value::Object(_))));
assert_eq!(None, event.extra_at("d"));
assert_eq!(
Some(&Value::String("string2".to_owned())),
event.extra_at("c.d")
);
assert_eq!(None, event.extra_at("c.e"));
assert_eq!(None, event.extra_at("c.f"));
}
#[test]
fn test_scrape_attempts() {
let json = serde_json::json!({
"scraping_attempts": [
{"status": "not_attempted", "url": "http://example.com/embedded.js"},
{"status": "not_attempted", "url": "http://example.com/embedded.js.map"},
]
});
let event = Event::from_value(json.into());
assert!(!event.value().unwrap().scraping_attempts.meta().has_errors());
}
#[test]
fn test_field_value_provider_event_filled() {
let event = Event {
level: Annotated::new(Level::Info),
release: Annotated::new(LenientString("1.1.1".to_owned())),
environment: Annotated::new("prod".to_owned()),
user: Annotated::new(User {
ip_address: Annotated::new(IpAddr("127.0.0.1".to_owned())),
id: Annotated::new(LenientString("user-id".into())),
segment: Annotated::new("user-seg".into()),
sentry_user: Annotated::new("id:user-id".into()),
..Default::default()
}),
client_sdk: Annotated::new(ClientSdkInfo {
name: Annotated::new("sentry-javascript".into()),
version: Annotated::new("1.87.0".into()),
..Default::default()
}),
exceptions: Annotated::new(Values {
values: Annotated::new(vec![Annotated::new(Exception {
value: Annotated::new(JsonLenientString::from(
"canvas.contentDocument".to_owned(),
)),
..Default::default()
})]),
..Default::default()
}),
logentry: Annotated::new(LogEntry {
formatted: Annotated::new("formatted".to_string().into()),
message: Annotated::new("message".to_string().into()),
..Default::default()
}),
request: Annotated::new(Request {
headers: Annotated::new(Headers(PairList(vec![Annotated::new((
Annotated::new("user-agent".into()),
Annotated::new("Slurp".into()),
))]))),
url: Annotated::new("https://sentry.io".into()),
..Default::default()
}),
transaction: Annotated::new("some-transaction".into()),
transaction_info: Annotated::new(TransactionInfo {
source: Annotated::new(TransactionSource::Route),
..Default::default()
}),
tags: {
let items = vec![Annotated::new(TagEntry(
Annotated::new("custom".to_string()),
Annotated::new("custom-value".to_string()),
))];
Annotated::new(Tags(items.into()))
},
contexts: Annotated::new({
let mut contexts = Contexts::new();
contexts.add(DeviceContext {
name: Annotated::new("iphone".to_string()),
family: Annotated::new("iphone-fam".to_string()),
model: Annotated::new("iphone7,3".to_string()),
screen_dpi: Annotated::new(560),
screen_width_pixels: Annotated::new(1920),
screen_height_pixels: Annotated::new(1080),
locale: Annotated::new("US".into()),
uuid: Annotated::new(uuid!("abadcade-feed-dead-beef-baddadfeeded")),
charging: Annotated::new(true),
..DeviceContext::default()
});
contexts.add(OsContext {
name: Annotated::new("iOS".to_string()),
version: Annotated::new("11.4.2".to_string()),
kernel_version: Annotated::new("17.4.0".to_string()),
..OsContext::default()
});
contexts.add(ProfileContext {
profile_id: Annotated::new(EventId(uuid!(
"abadcade-feed-dead-beef-8addadfeedaa"
))),
..ProfileContext::default()
});
let mut monitor_context_fields = BTreeMap::new();
monitor_context_fields.insert(
"id".to_string(),
Annotated::new(Value::String("123".to_string())),
);
monitor_context_fields.insert(
"slug".to_string(),
Annotated::new(Value::String("my_monitor".to_string())),
);
contexts.add(MonitorContext(monitor_context_fields));
contexts
}),
..Default::default()
};
assert_eq!(Some(Val::String("info")), event.get_value("event.level"));
assert_eq!(Some(Val::String("1.1.1")), event.get_value("event.release"));
assert_eq!(
Some(Val::String("prod")),
event.get_value("event.environment")
);
assert_eq!(
Some(Val::String("user-id")),
event.get_value("event.user.id")
);
assert_eq!(
Some(Val::String("id:user-id")),
event.get_value("event.sentry_user")
);
assert_eq!(
Some(Val::String("user-seg")),
event.get_value("event.user.segment")
);
assert_eq!(
Some(Val::String("some-transaction")),
event.get_value("event.transaction")
);
assert_eq!(
Some(Val::String("iphone")),
event.get_value("event.contexts.device.name")
);
assert_eq!(
Some(Val::String("iphone-fam")),
event.get_value("event.contexts.device.family")
);
assert_eq!(
Some(Val::String("iOS")),
event.get_value("event.contexts.os.name")
);
assert_eq!(
Some(Val::String("11.4.2")),
event.get_value("event.contexts.os.version")
);
assert_eq!(
Some(Val::String("custom-value")),
event.get_value("event.tags.custom")
);
assert_eq!(None, event.get_value("event.tags.doesntexist"));
assert_eq!(
Some(Val::String("sentry-javascript")),
event.get_value("event.sdk.name")
);
assert_eq!(
Some(Val::String("1.87.0")),
event.get_value("event.sdk.version")
);
assert_eq!(
Some(Val::String("17.4.0")),
event.get_value("event.contexts.os.kernel_version")
);
assert_eq!(
Some(Val::I64(560)),
event.get_value("event.contexts.device.screen_dpi")
);
assert_eq!(
Some(Val::Bool(true)),
event.get_value("event.contexts.device.charging")
);
assert_eq!(
Some(Val::U64(1920)),
event.get_value("event.contexts.device.screen_width_pixels")
);
assert_eq!(
Some(Val::U64(1080)),
event.get_value("event.contexts.device.screen_height_pixels")
);
assert_eq!(
Some(Val::String("US")),
event.get_value("event.contexts.device.locale")
);
assert_eq!(
Some(Val::Uuid(uuid!("abadcade-feed-dead-beef-baddadfeeded"))),
event.get_value("event.contexts.device.uuid")
);
assert_eq!(
Some(Val::String("https://sentry.io")),
event.get_value("event.request.url")
);
assert_eq!(
Some(Val::Uuid(uuid!("abadcade-feed-dead-beef-8addadfeedaa"))),
event.get_value("event.contexts.profile.profile_id")
);
assert_eq!(
Some(Val::String("route")),
event.get_value("event.transaction.source")
);
let mut exceptions = event.get_iter("event.exception.values").unwrap();
let exception = exceptions.next().unwrap();
assert_eq!(
Some(Val::String("canvas.contentDocument")),
exception.get_value("value")
);
assert!(exceptions.next().is_none());
assert_eq!(
Some(Val::String("formatted")),
event.get_value("event.logentry.formatted")
);
assert_eq!(
Some(Val::String("message")),
event.get_value("event.logentry.message")
);
assert_eq!(
Some(Val::String("123")),
event.get_value("event.contexts.monitor.id")
);
assert_eq!(
Some(Val::String("my_monitor")),
event.get_value("event.contexts.monitor.slug")
);
}
#[test]
fn test_field_value_provider_event_empty() {
let event = Event::default();
assert_eq!(None, event.get_value("event.release"));
assert_eq!(None, event.get_value("event.environment"));
assert_eq!(None, event.get_value("event.user.id"));
assert_eq!(None, event.get_value("event.user.segment"));
let event = Event {
user: Annotated::new(User {
..Default::default()
}),
..Default::default()
};
assert_eq!(None, event.get_value("event.user.id"));
assert_eq!(None, event.get_value("event.user.segment"));
assert_eq!(None, event.get_value("event.transaction"));
}
}