Skip to content

Commit f079596

Browse files
authored
fix(spansv2): Correctly map the span name from v1 to v2 (#5253)
The span name was only mapped but not inferred correctly (like we do for standalone spans). Also the span name was left in the attributes, when it should've been removed, as it is redundant. The segment processor in Sentry, then maps the top level `name` field again correctly to a `sentry.name` attribute.
1 parent 20fe4ee commit f079596

File tree

6 files changed

+162
-30
lines changed

6 files changed

+162
-30
lines changed

relay-event-schema/src/protocol/attributes.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ impl Attribute {
3030
other: Object::new(),
3131
}
3232
}
33+
34+
/// Returns the string value of this attribute.
35+
pub fn into_string(self) -> Option<String> {
36+
self.value.value.into_value()?.into_string()
37+
}
3338
}
3439

3540
impl fmt::Debug for Attribute {
@@ -42,8 +47,12 @@ impl fmt::Debug for Attribute {
4247
}
4348
}
4449

45-
impl From<AttributeValue> for Attribute {
46-
fn from(value: AttributeValue) -> Self {
50+
impl<T> From<T> for Attribute
51+
where
52+
AttributeValue: From<T>,
53+
{
54+
fn from(value: T) -> Self {
55+
let value = value.into();
4756
Self {
4857
value,
4958
other: Default::default(),
@@ -267,6 +276,15 @@ impl Attributes {
267276
self.0.contains_key(key)
268277
}
269278

279+
/// Removes an attribute from this collection.
280+
pub fn remove<Q>(&mut self, key: &Q) -> Option<Annotated<Attribute>>
281+
where
282+
String: Borrow<Q>,
283+
Q: Ord + ?Sized,
284+
{
285+
self.0.remove(key)
286+
}
287+
270288
/// Iterates over this collection's attribute keys and values.
271289
pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, Annotated<Attribute>> {
272290
self.0.iter()
@@ -296,6 +314,12 @@ impl FromIterator<(String, Annotated<Attribute>)> for Attributes {
296314
}
297315
}
298316

317+
impl<const N: usize> From<[(String, Annotated<Attribute>); N]> for Attributes {
318+
fn from(value: [(String, Annotated<Attribute>); N]) -> Self {
319+
value.into_iter().collect()
320+
}
321+
}
322+
299323
impl ProcessValue for Attributes {
300324
#[inline]
301325
fn value_type(&self) -> EnumSet<ValueType> {

relay-protocol/src/annotated.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,17 @@ impl<T> Annotated<T> {
238238
}
239239
}
240240

241+
impl<T> Annotated<Option<T>> {
242+
/// Transposes an `Annotated` of [`Option`] into a [`Option`] of `Annotated`.
243+
pub fn transpose(self) -> Option<Annotated<T>> {
244+
match self {
245+
Annotated(Some(Some(value)), meta) => Some(Annotated(Some(value), meta)),
246+
Annotated(Some(None), _) => None,
247+
Annotated(None, meta) => Some(Annotated(None, meta)),
248+
}
249+
}
250+
}
251+
241252
impl<T> Annotated<T>
242253
where
243254
T: AsRef<str>,

relay-protocol/src/value.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ impl Value {
7070
}
7171
}
7272

73+
/// Returns the string if this value is a string, otherwise `None`.
74+
pub fn into_string(self) -> Option<String> {
75+
match self {
76+
Value::String(string) => Some(string),
77+
_ => None,
78+
}
79+
}
80+
7381
/// Returns a f64 if the value can be converted to it, otherwise `None`.
7482
pub fn as_f64(&self) -> Option<f64> {
7583
match self {

relay-spans/src/name.rs

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use relay_conventions::name_for_op_and_attributes;
2-
use relay_event_schema::protocol::Span;
2+
use relay_event_schema::protocol::{Attributes, Span};
33
use relay_protocol::{Getter, GetterIter, Val};
44

55
/// Constructs a name attribute for a span, following the rules defined in sentry-conventions.
@@ -19,6 +19,12 @@ pub fn name_for_span(span: &Span) -> Option<String> {
1919
))
2020
}
2121

22+
/// Constructs a name attribute for a span, following the rules defined in sentry-conventions.
23+
pub fn name_for_attributes(attributes: &Attributes) -> Option<String> {
24+
let op = attributes.get_value("sentry.op")?.as_str()?;
25+
Some(name_for_op_and_attributes(op, &AttributeGetter(attributes)))
26+
}
27+
2228
struct EmptyGetter {}
2329

2430
impl Getter for EmptyGetter {
@@ -39,6 +45,18 @@ impl<'a, T: Getter> Getter for EscapedGetter<'a, T> {
3945
}
4046
}
4147

48+
/// A custom getter for [`Attributes`] which only resolves values based on the attribute name.
49+
///
50+
/// This [`Getter`] does not implement nested traversals, which is the behaviour required for
51+
/// [`name_for_op_and_attributes`].
52+
struct AttributeGetter<'a>(&'a Attributes);
53+
54+
impl<'a> Getter for AttributeGetter<'a> {
55+
fn get_value(&self, path: &str) -> Option<Val<'_>> {
56+
self.0.get_value(path).map(|value| value.into())
57+
}
58+
}
59+
4260
#[cfg(test)]
4361
mod tests {
4462
use relay_event_schema::protocol::SpanData;
@@ -47,7 +65,7 @@ mod tests {
4765
use super::*;
4866

4967
#[test]
50-
fn falls_back_to_op_when_no_templates_defined() {
68+
fn test_span_falls_back_to_op_when_no_templates_defined() {
5169
let span = Span {
5270
op: Annotated::new("foo".to_owned()),
5371
..Default::default()
@@ -56,22 +74,32 @@ mod tests {
5674
}
5775

5876
#[test]
59-
fn uses_the_first_matching_template() {
77+
fn test_attributes_falls_back_to_op_when_no_templates_defined() {
78+
let attributes = Attributes::from([(
79+
"sentry.op".to_owned(),
80+
Annotated::new("foo".to_owned().into()),
81+
)]);
82+
83+
assert_eq!(name_for_attributes(&attributes), Some("foo".to_owned()));
84+
}
85+
86+
#[test]
87+
fn test_span_uses_the_first_matching_template() {
6088
let span = Span {
6189
op: Annotated::new("db".to_owned()),
6290
data: Annotated::new(SpanData {
6391
other: Object::from([
6492
(
65-
"db.query.summary".into(),
66-
Value::String("SELECT users".into()).into(),
93+
"db.query.summary".to_owned(),
94+
Value::String("SELECT users".to_owned()).into(),
6795
),
6896
(
69-
"db.operation.name".into(),
70-
Value::String("INSERT".into()).into(),
97+
"db.operation.name".to_owned(),
98+
Value::String("INSERT".to_owned()).into(),
7199
),
72100
(
73-
"db.collection.name".into(),
74-
Value::String("widgets".into()).into(),
101+
"db.collection.name".to_owned(),
102+
Value::String("widgets".to_owned()).into(),
75103
),
76104
]),
77105
..Default::default()
@@ -82,18 +110,45 @@ mod tests {
82110
}
83111

84112
#[test]
85-
fn uses_fallback_templates_when_data_is_missing() {
113+
fn test_attributes_uses_the_first_matching_template() {
114+
let attributes = Attributes::from([
115+
(
116+
"sentry.op".to_owned(),
117+
Annotated::new("db".to_owned().into()),
118+
),
119+
(
120+
"db.query.summary".to_owned(),
121+
Annotated::new("SELECT users".to_owned().into()),
122+
),
123+
(
124+
"db.operation.name".to_owned(),
125+
Annotated::new("INSERT".to_owned().into()),
126+
),
127+
(
128+
"db.collection.name".to_owned(),
129+
Annotated::new("widgets".to_owned().into()),
130+
),
131+
]);
132+
133+
assert_eq!(
134+
name_for_attributes(&attributes),
135+
Some("SELECT users".to_owned())
136+
);
137+
}
138+
139+
#[test]
140+
fn test_span_uses_fallback_templates_when_data_is_missing() {
86141
let span = Span {
87142
op: Annotated::new("db".to_owned()),
88143
data: Annotated::new(SpanData {
89144
other: Object::from([
90145
(
91-
"db.operation.name".into(),
92-
Value::String("INSERT".into()).into(),
146+
"db.operation.name".to_owned(),
147+
Value::String("INSERT".to_owned()).into(),
93148
),
94149
(
95-
"db.collection.name".into(),
96-
Value::String("widgets".into()).into(),
150+
"db.collection.name".to_owned(),
151+
Value::String("widgets".to_owned()).into(),
97152
),
98153
]),
99154
..Default::default()
@@ -104,11 +159,47 @@ mod tests {
104159
}
105160

106161
#[test]
107-
fn falls_back_to_hardcoded_name_when_nothing_matches() {
162+
fn test_attributes_uses_fallback_templates_when_data_is_missing() {
163+
let attributes = Attributes::from([
164+
(
165+
"sentry.op".to_owned(),
166+
Annotated::new("db".to_owned().into()),
167+
),
168+
(
169+
"db.operation.name".to_owned(),
170+
Annotated::new("INSERT".to_owned().into()),
171+
),
172+
(
173+
"db.collection.name".to_owned(),
174+
Annotated::new("widgets".to_owned().into()),
175+
),
176+
]);
177+
178+
assert_eq!(
179+
name_for_attributes(&attributes),
180+
Some("INSERT widgets".to_owned())
181+
);
182+
}
183+
184+
#[test]
185+
fn test_span_falls_back_to_hardcoded_name_when_nothing_matches() {
108186
let span = Span {
109187
op: Annotated::new("db".to_owned()),
110188
..Default::default()
111189
};
112190
assert_eq!(name_for_span(&span), Some("Database operation".to_owned()));
113191
}
192+
193+
#[test]
194+
fn test_attributes_falls_back_to_hardcoded_name_when_nothing_matches() {
195+
let attributes = Attributes::from([(
196+
"sentry.op".to_owned(),
197+
Annotated::new("db".to_owned().into()),
198+
)]);
199+
200+
assert_eq!(
201+
name_for_attributes(&attributes),
202+
Some("Database operation".to_owned())
203+
);
204+
}
114205
}

relay-spans/src/v1_to_v2.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use relay_event_schema::protocol::{
66
};
77
use relay_protocol::{Annotated, Empty, Error, IntoValue, Meta, Value};
88

9+
use crate::name::name_for_attributes;
10+
911
/// Converts a legacy span to the new Span V2 schema.
1012
///
1113
/// - `tags`, `sentry_tags`, `measurements` and `data` are transferred to `attributes`.
@@ -99,14 +101,16 @@ pub fn span_v1_to_span_v2(span_v1: SpanV1) -> SpanV2 {
99101
}
100102
}
101103

104+
let name = attributes
105+
.remove("sentry.name")
106+
.and_then(|name| name.map_value(|attr| attr.into_string()).transpose())
107+
.unwrap_or_else(|| name_for_attributes(attributes).into());
108+
102109
SpanV2 {
103110
trace_id,
104111
parent_span_id,
105112
span_id,
106-
name: attributes
107-
.get_value("sentry.name")
108-
.and_then(|v| Some(v.as_str()?.to_owned()))
109-
.into(),
113+
name,
110114
status: Annotated::map_value(status, span_v1_status_to_span_v2_status)
111115
.or_else(|| SpanV2Status::Ok.into()),
112116
is_remote: is_remote.or_else(|| false.into()),
@@ -287,11 +291,12 @@ mod tests {
287291
let span_v2 = span_v1_to_span_v2(span_v1);
288292

289293
let annotated_span_v2: Annotated<SpanV2> = Annotated::new(span_v2);
290-
insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span_v2), @r###"
294+
insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span_v2), @r#"
291295
{
292296
"trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
293297
"parent_span_id": "fa90fdead5f74051",
294298
"span_id": "fa90fdead5f74052",
299+
"name": "operation",
295300
"status": "ok",
296301
"is_remote": true,
297302
"kind": "server",
@@ -410,7 +415,7 @@ mod tests {
410415
}
411416
}
412417
}
413-
"###);
418+
"#);
414419
}
415420

416421
#[test]

tests/integration/test_spans.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ def test_span_extraction(
146146
"attributes": { # Backfilled from `sentry_tags`
147147
"sentry.category": {"type": "string", "value": "http"},
148148
"sentry.exclusive_time": {"type": "double", "value": 500.0},
149-
"sentry.name": {"type": "string", "value": "http"},
150149
"sentry.normalized_description": {"type": "string", "value": "GET *"},
151150
"sentry.group": {"type": "string", "value": "37e3d9fab1ae9162"},
152151
"sentry.op": {"type": "string", "value": "http"},
@@ -223,7 +222,6 @@ def test_span_extraction(
223222
"sentry.description": {"type": "string", "value": "hi"},
224223
"sentry.exclusive_time": {"type": "double", "value": 1500.0},
225224
"sentry.is_segment": {"type": "boolean", "value": True},
226-
"sentry.name": {"type": "string", "value": "hi"},
227225
"sentry.op": {"type": "string", "value": "hi"},
228226
"sentry.origin": {"type": "string", "value": "manual"},
229227
"sentry.platform": {"type": "string", "value": "other"},
@@ -682,7 +680,6 @@ def test_span_ingestion(
682680
"sentry.file_extension": {"type": "string", "value": "js"},
683681
"sentry.group": {"type": "string", "value": "8a97a9e43588e2bd"},
684682
"sentry.is_segment": {"type": "boolean", "value": True},
685-
"sentry.name": {"type": "string", "value": "resource.script"},
686683
"sentry.normalized_description": {
687684
"type": "string",
688685
"value": "https://example.com/*/blah.js",
@@ -726,7 +723,6 @@ def test_span_ingestion(
726723
},
727724
"sentry.exclusive_time": {"type": "double", "value": 345.0},
728725
"sentry.is_segment": {"type": "boolean", "value": False},
729-
"sentry.name": {"type": "string", "value": "default"},
730726
"sentry.op": {"type": "string", "value": "default"},
731727
"sentry.segment.id": {"type": "string", "value": "968cff94913ebb07"},
732728
"user_agent.original": {
@@ -755,7 +751,6 @@ def test_span_ingestion(
755751
"sentry.description": {"type": "string", "value": "my 2nd OTel span"},
756752
"sentry.exclusive_time": {"type": "double", "value": 500.0},
757753
"sentry.is_segment": {"type": "boolean", "value": True},
758-
"sentry.name": {"type": "string", "value": "my 2nd OTel span"},
759754
"sentry.op": {"type": "string", "value": "default"},
760755
"sentry.segment.id": {"type": "string", "value": "d342abb1214ca182"},
761756
"sentry.status": {"type": "string", "value": "ok"},
@@ -793,7 +788,6 @@ def test_span_ingestion(
793788
"sentry.browser.name": {"type": "string", "value": "Chrome"},
794789
"sentry.exclusive_time": {"type": "double", "value": 345.0},
795790
"sentry.is_segment": {"type": "boolean", "value": False},
796-
"sentry.name": {"type": "string", "value": "default"},
797791
"sentry.op": {"type": "string", "value": "default"},
798792
"sentry.segment.id": {"type": "string", "value": "968cff94913ebb07"},
799793
"user_agent.original": {
@@ -825,7 +819,6 @@ def test_span_ingestion(
825819
"value": "my 3rd protobuf OTel span",
826820
},
827821
"sentry.exclusive_time": {"type": "double", "value": 500.0},
828-
"sentry.name": {"type": "string", "value": "my 3rd protobuf OTel span"},
829822
"sentry.op": {"type": "string", "value": "default"},
830823
"sentry.status": {"type": "string", "value": "ok"},
831824
"ui.component_name": {"type": "string", "value": "MyComponent"},

0 commit comments

Comments
 (0)