Skip to content

Commit 3292e9a

Browse files
untitakerjan-auer
andauthored
ref(transactions): Ensure that every span's parent exists (#4661)
In the span buffer we rely on the parent_span_id to cluster spans back into transactions/segments. If we send out transactions that contain "dangling" spans, then flatten those transactions into individual spans, we won't be able to build the same segment back together. --------- Co-authored-by: Jan Michael Auer <[email protected]>
1 parent d45e661 commit 3292e9a

File tree

10 files changed

+300
-131
lines changed

10 files changed

+300
-131
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- Normalize trace context information before writing it into transaction and span data. This ensures the correct sampling rates are stored for extrapolation in Sentry. ([#4625](https://github.com/getsentry/relay/pull/4625))
1919
- Adds u16 validation to the replay protocol's segment_id field. ([#4635](https://github.com/getsentry/relay/pull/4635))
2020
- Exposes all service utilization with instance labels instead of the last. ([#4654](https://github.com/getsentry/relay/pull/4654))
21+
- Ensure that every span's parent exists. ([#4661](https://github.com/getsentry/relay/pull/4661))
2122

2223
**Internal**:
2324

relay-event-normalization/src/event.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ fn normalize(event: &mut Event, meta: &mut Meta, config: &NormalizationConfig) {
335335
normalize_contexts(&mut event.contexts);
336336

337337
if config.normalize_spans && event.ty.value() == Some(&EventType::Transaction) {
338+
span::reparent_broken_spans::reparent_broken_spans(event);
338339
crate::normalize::normalize_app_start_spans(event);
339340
span::exclusive_time::compute_span_exclusive_time(event);
340341
}

relay-event-normalization/src/normalize/span/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod ai;
77
pub mod country_subregion;
88
pub mod description;
99
pub mod exclusive_time;
10+
pub mod reparent_broken_spans;
1011
pub mod tag_extraction;
1112

1213
/// Regex used to scrub hex IDs and multi-digit numbers from table names and other identifiers.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//! Span tree normalization.
2+
3+
use std::collections::BTreeSet;
4+
use std::mem;
5+
6+
use relay_event_schema::protocol::{Event, TraceContext};
7+
use relay_protocol::Error;
8+
9+
/// Enforce that every span has a valid parent. If any `parent_span_id` is pointing nowhere, the
10+
/// span is re-parented onto the root span.
11+
///
12+
/// This is to avoid any nasty surprises in the span buffer specifically. Other readers of spans
13+
/// (such as the frontend's tree component) were already able to deal with detached spans.
14+
pub fn reparent_broken_spans(event: &mut Event) {
15+
let Some(ref mut spans) = event.spans.value_mut() else {
16+
return;
17+
};
18+
19+
let Some(contexts) = event.contexts.value_mut() else {
20+
return;
21+
};
22+
23+
let Some(trace_context) = contexts.get_mut::<TraceContext>() else {
24+
return;
25+
};
26+
27+
let Some(root_span_id) = trace_context.span_id.value() else {
28+
return;
29+
};
30+
31+
let valid_span_ids = spans
32+
.iter()
33+
.filter_map(|span| span.value())
34+
.filter_map(|span| span.span_id.value())
35+
.chain(Some(root_span_id))
36+
.cloned()
37+
.collect::<BTreeSet<_>>();
38+
39+
for span in spans {
40+
let Some(span) = span.value_mut() else {
41+
continue;
42+
};
43+
44+
let Some(parent_span_id) = span.parent_span_id.value_mut() else {
45+
continue;
46+
};
47+
48+
if valid_span_ids.contains(&*parent_span_id) {
49+
continue;
50+
};
51+
52+
let invalid_parent = mem::replace(parent_span_id, root_span_id.clone());
53+
let meta = span.parent_span_id.meta_mut();
54+
meta.add_error(Error::invalid("span ID does not exist"));
55+
meta.set_original_value(Some(invalid_parent));
56+
}
57+
}
58+
59+
#[cfg(test)]
60+
mod tests {
61+
use relay_protocol::FromValue;
62+
use relay_protocol::SerializableAnnotated;
63+
64+
use super::*;
65+
66+
#[test]
67+
fn basic() {
68+
let mut data = Event::from_value(
69+
serde_json::json!({
70+
"type": "transaction",
71+
"start_timestamp": 1609455600,
72+
"end_timestamp": 1609455605,
73+
"contexts": {
74+
"trace": {
75+
"trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
76+
"span_id": "aaaaaaaaaaaaaaaa",
77+
"parent_span_id": "ffffffffffffffff",
78+
}
79+
},
80+
"spans": [
81+
{
82+
"span_id": "bbbbbbbbbbbbbbbb",
83+
"parent_span_id": "aaaaaaaaaaaaaaaa"
84+
},
85+
{
86+
"span_id": "bbbbbbbbbbbbbbbb",
87+
"parent_span_id": "dddddddddddddddd",
88+
},
89+
{
90+
"span_id": "bbbbbbbbbbbbbbbb",
91+
"parent_span_id": "eeeeeeeeeeeeeeee",
92+
},
93+
],
94+
})
95+
.into(),
96+
);
97+
98+
reparent_broken_spans(data.value_mut().as_mut().unwrap());
99+
100+
insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
101+
{
102+
"type": "transaction",
103+
"start_timestamp": 1609455600.0,
104+
"contexts": {
105+
"trace": {
106+
"trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
107+
"span_id": "aaaaaaaaaaaaaaaa",
108+
"parent_span_id": "ffffffffffffffff",
109+
"type": "trace"
110+
}
111+
},
112+
"spans": [
113+
{
114+
"span_id": "bbbbbbbbbbbbbbbb",
115+
"parent_span_id": "aaaaaaaaaaaaaaaa"
116+
},
117+
{
118+
"span_id": "bbbbbbbbbbbbbbbb",
119+
"parent_span_id": "aaaaaaaaaaaaaaaa"
120+
},
121+
{
122+
"span_id": "bbbbbbbbbbbbbbbb",
123+
"parent_span_id": "aaaaaaaaaaaaaaaa"
124+
}
125+
],
126+
"end_timestamp": 1609455605,
127+
"_meta": {
128+
"spans": {
129+
"1": {
130+
"parent_span_id": {
131+
"": {
132+
"err": [
133+
[
134+
"invalid_data",
135+
{
136+
"reason": "span ID does not exist"
137+
}
138+
]
139+
],
140+
"val": "dddddddddddddddd"
141+
}
142+
}
143+
},
144+
"2": {
145+
"parent_span_id": {
146+
"": {
147+
"err": [
148+
[
149+
"invalid_data",
150+
{
151+
"reason": "span ID does not exist"
152+
}
153+
]
154+
],
155+
"val": "eeeeeeeeeeeeeeee"
156+
}
157+
}
158+
}
159+
}
160+
}
161+
}
162+
"###);
163+
}
164+
}

relay-event-schema/src/protocol/contexts/trace.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ impl AsRef<str> for TraceId {
4040
}
4141

4242
/// A 16-character hex string as described in the W3C trace context spec.
43-
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Empty, IntoValue, ProcessValue)]
43+
#[derive(
44+
Clone, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Empty, IntoValue, ProcessValue,
45+
)]
4446
pub struct SpanId(pub String);
4547

4648
relay_common::impl_str_serde!(SpanId, "a span identifier");

0 commit comments

Comments
 (0)