relay_spans/
name.rs

1use relay_conventions::name_for_op_and_attributes;
2use relay_event_schema::protocol::{Attributes, 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
22/// Constructs a name attribute for a span, following the rules defined in sentry-conventions.
23pub 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
28struct EmptyGetter {}
29
30impl Getter for EmptyGetter {
31    fn get_value(&self, _path: &str) -> Option<Val<'_>> {
32        None
33    }
34}
35
36struct EscapedGetter<'a, T: Getter>(&'a T);
37
38impl<'a, T: Getter> Getter for EscapedGetter<'a, T> {
39    fn get_value(&self, path: &str) -> Option<Val<'_>> {
40        self.0.get_value(&path.replace(".", "\\."))
41    }
42
43    fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
44        self.0.get_iter(&path.replace(".", "\\."))
45    }
46}
47
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`].
52struct AttributeGetter<'a>(&'a Attributes);
53
54impl<'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
60#[cfg(test)]
61mod tests {
62    use relay_event_schema::protocol::SpanData;
63    use relay_protocol::{Annotated, Object, Value};
64
65    use super::*;
66
67    #[test]
68    fn test_span_falls_back_to_op_when_no_templates_defined() {
69        let span = Span {
70            op: Annotated::new("foo".to_owned()),
71            ..Default::default()
72        };
73        assert_eq!(name_for_span(&span), Some("foo".to_owned()));
74    }
75
76    #[test]
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() {
88        let span = Span {
89            op: Annotated::new("db".to_owned()),
90            data: Annotated::new(SpanData {
91                other: Object::from([
92                    (
93                        "db.query.summary".to_owned(),
94                        Value::String("SELECT users".to_owned()).into(),
95                    ),
96                    (
97                        "db.operation.name".to_owned(),
98                        Value::String("INSERT".to_owned()).into(),
99                    ),
100                    (
101                        "db.collection.name".to_owned(),
102                        Value::String("widgets".to_owned()).into(),
103                    ),
104                ]),
105                ..Default::default()
106            }),
107            ..Default::default()
108        };
109        assert_eq!(name_for_span(&span), Some("SELECT users".to_owned()));
110    }
111
112    #[test]
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() {
141        let span = Span {
142            op: Annotated::new("db".to_owned()),
143            data: Annotated::new(SpanData {
144                other: Object::from([
145                    (
146                        "db.operation.name".to_owned(),
147                        Value::String("INSERT".to_owned()).into(),
148                    ),
149                    (
150                        "db.collection.name".to_owned(),
151                        Value::String("widgets".to_owned()).into(),
152                    ),
153                ]),
154                ..Default::default()
155            }),
156            ..Default::default()
157        };
158        assert_eq!(name_for_span(&span), Some("INSERT widgets".to_owned()));
159    }
160
161    #[test]
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() {
186        let span = Span {
187            op: Annotated::new("db".to_owned()),
188            ..Default::default()
189        };
190        assert_eq!(name_for_span(&span), Some("Database operation".to_owned()));
191    }
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    }
205}