diff --git a/relay-conventions/src/lib.rs b/relay-conventions/src/lib.rs index f70e17d3c8c..8014d7c6a58 100644 --- a/relay-conventions/src/lib.rs +++ b/relay-conventions/src/lib.rs @@ -65,6 +65,10 @@ pub mod attributes { // TODO(buenaflor): Add as sentry convention once mobile SDKs can migrate to it. // Tracking issue: https://github.com/getsentry/sentry-conventions/issues/318 pub const APP__VITALS__START__VALUE: &str = "app.vitals.start.value"; + + // TODO: Add as sentry convention. Stores the event's `_meta` (remarks/errors) for the + // preserved `contexts`, `extra`, and `breadcrumbs`. + pub const SENTRY__EVENT__SERIALIZED_META: &str = "sentry.event.serialized_meta"; } pub use self::not_yet_defined::*; } diff --git a/relay-event-schema/src/protocol/span/convert.rs b/relay-event-schema/src/protocol/span/convert.rs index 2177457784d..dd847ff909b 100644 --- a/relay-event-schema/src/protocol/span/convert.rs +++ b/relay-event-schema/src/protocol/span/convert.rs @@ -2,10 +2,11 @@ use relay_conventions::attributes::{ BROWSER__NAME, HTTP__QUERY, SENTRY__ENVIRONMENT, SENTRY__EVENT__SERIALIZED_BREADCRUMBS, - SENTRY__EVENT__SERIALIZED_CONTEXTS, SENTRY__EVENT__SERIALIZED_EXTRA, SENTRY__RELEASE, - SENTRY__SDK__NAME, SENTRY__SDK__VERSION, SENTRY__SEGMENT__NAME, URL__QUERY, + SENTRY__EVENT__SERIALIZED_CONTEXTS, SENTRY__EVENT__SERIALIZED_EXTRA, + SENTRY__EVENT__SERIALIZED_META, SENTRY__RELEASE, SENTRY__SDK__NAME, SENTRY__SDK__VERSION, + SENTRY__SEGMENT__NAME, URL__QUERY, }; -use relay_protocol::{IntoValue, Object, SerializePayload, SkipSerialization}; +use relay_protocol::{IntoValue, MetaTree, Object, SerializePayload, SkipSerialization}; use serde::ser::SerializeMap; use serde::{Serialize, Serializer}; @@ -136,6 +137,31 @@ impl From<&Event> for Span { span_data.insert_value(SENTRY__EVENT__SERIALIZED_EXTRA, json); } + // Preserve the event's `_meta` (remarks/errors) for the fields serialized above, so the + // annotations PII scrubbing and normalization produced aren't lost when the transaction is + // turned into an EAP span. + let mut meta = MetaTree::default(); + let mut contexts_meta = IntoValue::extract_meta_tree(contexts); + // Keep this consistent with the serialized contexts payload, which omits the trace context. + contexts_meta.children.remove(TraceContext::default_key()); + if !contexts_meta.is_empty() { + meta.children.insert("contexts".to_owned(), contexts_meta); + } + let breadcrumbs_meta = IntoValue::extract_meta_tree(breadcrumbs); + if !breadcrumbs_meta.is_empty() { + meta.children + .insert("breadcrumbs".to_owned(), breadcrumbs_meta); + } + let extra_meta = IntoValue::extract_meta_tree(extra); + if !extra_meta.is_empty() { + meta.children.insert("extra".to_owned(), extra_meta); + } + if !meta.is_empty() + && let Ok(json) = serde_json::to_string(&meta) + { + span_data.insert_value(SENTRY__EVENT__SERIALIZED_META, json); + } + Self { timestamp: timestamp.clone(), start_timestamp: start_timestamp.clone(), @@ -422,4 +448,117 @@ mod tests { assert!(!data.contains(SENTRY__EVENT__SERIALIZED_EXTRA)); } } + + #[test] + fn convert_preserves_meta() { + let event = Annotated::::from_json( + r#"{ + "type": "transaction", + "transaction": "my transaction", + "contexts": { + "browser": {"name": "Chrome"}, + "trace": { + "trace_id": "4c79f60c11214eb38604f4ae0781bfb2", + "span_id": "fa90fdead5f74052" + } + }, + "breadcrumbs": [ + {"type": "default", "category": "auth", "message": "login"} + ], + "extra": { + "my_key": "[Filtered]" + }, + "_meta": { + "contexts": { + "browser": { + "name": {"": {"rem": [["browser_rule", "s"]]}} + }, + "trace": { + "trace_id": {"": {"rem": [["trace_rule", "s"]]}} + } + }, + "breadcrumbs": { + "0": { + "message": {"": {"rem": [["breadcrumb_rule", "s"]]}} + } + }, + "extra": { + "my_key": {"": {"rem": [["extra_rule", "s", 0, 10]]}} + } + } + }"#, + ) + .unwrap() + .into_value() + .unwrap(); + + let span = Span::from(&event); + let data = span.data.value().unwrap(); + + let meta = data.get_str(SENTRY__EVENT__SERIALIZED_META).unwrap(); + + assert_eq!( + meta, + r#"{"breadcrumbs":{"values":{"0":{"message":{"":{"rem":[["breadcrumb_rule","s"]]}}}}},"contexts":{"browser":{"name":{"":{"rem":[["browser_rule","s"]]}}}},"extra":{"my_key":{"":{"rem":[["extra_rule","s",0,10]]}}}}"# + ); + } + + #[test] + fn convert_omits_meta_when_absent() { + let event = Annotated::::from_json( + r#"{ + "type": "transaction", + "transaction": "my transaction", + "contexts": { + "browser": {"name": "Chrome"} + }, + "extra": { + "my_key": 1 + } + }"#, + ) + .unwrap() + .into_value() + .unwrap(); + + let span = Span::from(&event); + + if let Some(data) = span.data.value() { + assert!(!data.contains(SENTRY__EVENT__SERIALIZED_META)); + } + } + + #[test] + fn convert_omits_meta_when_only_trace_context_has_meta() { + // The trace context is excluded from the serialized payload, so meta attached only to it + // must not produce a `serialized_meta` attribute. + let event = Annotated::::from_json( + r#"{ + "type": "transaction", + "transaction": "my transaction", + "contexts": { + "trace": { + "trace_id": "4c79f60c11214eb38604f4ae0781bfb2", + "span_id": "fa90fdead5f74052" + } + }, + "_meta": { + "contexts": { + "trace": { + "trace_id": {"": {"rem": [["trace_rule", "s"]]}} + } + } + } + }"#, + ) + .unwrap() + .into_value() + .unwrap(); + + let span = Span::from(&event); + + if let Some(data) = span.data.value() { + assert!(!data.contains(SENTRY__EVENT__SERIALIZED_META)); + } + } } diff --git a/tests/integration/test_spans.py b/tests/integration/test_spans.py index 01c08613252..edcfe038b09 100644 --- a/tests/integration/test_spans.py +++ b/tests/integration/test_spans.py @@ -1525,11 +1525,13 @@ def test_segment_span_scrubs_extra_before_serializing( relay.send_event(project_id, event) segment_span = spans_consumer.get_span() - extra = json.loads( - segment_span["attributes"]["sentry.event.serialized_extra"]["value"] - ) + attributes = segment_span["attributes"] + extra = json.loads(attributes["sentry.event.serialized_extra"]["value"]) assert "john.doe@company.com" not in extra["note"] assert "[email]" in extra["note"] + meta = json.loads(attributes["sentry.event.serialized_meta"]["value"]) + assert meta["extra"]["note"][""]["rem"][0][0] == "@email" + spans_consumer.assert_empty()