relay_spans/
name.rs

1use relay_conventions::name_for_op_and_attributes;
2use relay_event_schema::protocol::Span;
3use relay_protocol::{Getter, GetterIter, Val};
4
5/// Constructs a name attribute for a span, following the rules defined in sentry-conventions.
6pub fn name_for_span(span: &Span) -> Option<String> {
7    let op = span.op.value()?;
8
9    let Some(data) = span.data.value() else {
10        return Some(name_for_op_and_attributes(op, &EmptyGetter {}));
11    };
12
13    Some(name_for_op_and_attributes(
14        op,
15        // SpanData's Getter impl treats dots in attribute names as object traversals.
16        // They have to be escaped in order for an attribute name with dots to be treated as a root
17        // attribute.
18        &EscapedGetter(data),
19    ))
20}
21
22struct EmptyGetter {}
23
24impl Getter for EmptyGetter {
25    fn get_value(&self, _path: &str) -> Option<Val<'_>> {
26        None
27    }
28}
29
30struct EscapedGetter<'a, T: Getter>(&'a T);
31
32impl<'a, T: Getter> Getter for EscapedGetter<'a, T> {
33    fn get_value(&self, path: &str) -> Option<Val<'_>> {
34        self.0.get_value(&path.replace(".", "\\."))
35    }
36
37    fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
38        self.0.get_iter(&path.replace(".", "\\."))
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use relay_event_schema::protocol::SpanData;
45    use relay_protocol::{Annotated, Object, Value};
46
47    use super::*;
48
49    #[test]
50    fn falls_back_to_op_when_no_templates_defined() {
51        let span = Span {
52            op: Annotated::new("foo".to_owned()),
53            ..Default::default()
54        };
55        assert_eq!(name_for_span(&span), Some("foo".to_owned()));
56    }
57
58    #[test]
59    fn uses_the_first_matching_template() {
60        let span = Span {
61            op: Annotated::new("db".to_owned()),
62            data: Annotated::new(SpanData {
63                other: Object::from([
64                    (
65                        "db.query.summary".into(),
66                        Value::String("SELECT users".into()).into(),
67                    ),
68                    (
69                        "db.operation.name".into(),
70                        Value::String("INSERT".into()).into(),
71                    ),
72                    (
73                        "db.collection.name".into(),
74                        Value::String("widgets".into()).into(),
75                    ),
76                ]),
77                ..Default::default()
78            }),
79            ..Default::default()
80        };
81        assert_eq!(name_for_span(&span), Some("SELECT users".to_owned()));
82    }
83
84    #[test]
85    fn uses_fallback_templates_when_data_is_missing() {
86        let span = Span {
87            op: Annotated::new("db".to_owned()),
88            data: Annotated::new(SpanData {
89                other: Object::from([
90                    (
91                        "db.operation.name".into(),
92                        Value::String("INSERT".into()).into(),
93                    ),
94                    (
95                        "db.collection.name".into(),
96                        Value::String("widgets".into()).into(),
97                    ),
98                ]),
99                ..Default::default()
100            }),
101            ..Default::default()
102        };
103        assert_eq!(name_for_span(&span), Some("INSERT widgets".to_owned()));
104    }
105
106    #[test]
107    fn falls_back_to_hardcoded_name_when_nothing_matches() {
108        let span = Span {
109            op: Annotated::new("db".to_owned()),
110            ..Default::default()
111        };
112        assert_eq!(name_for_span(&span), Some("Database operation".to_owned()));
113    }
114}