Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions relay-conventions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
}
Expand Down
145 changes: 142 additions & 3 deletions relay-event-schema/src/protocol/span/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -422,4 +448,117 @@ mod tests {
assert!(!data.contains(SENTRY__EVENT__SERIALIZED_EXTRA));
}
}

#[test]
fn convert_preserves_meta() {
let event = Annotated::<Event>::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::<Event>::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::<Event>::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));
}
}
}
8 changes: 5 additions & 3 deletions tests/integration/test_spans.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading