Skip to main content

relay_spans/
name.rs

1use relay_conventions::attributes::{SENTRY__DESCRIPTION, SENTRY__OP, SENTRY__ORIGIN};
2use relay_conventions::name::name_for_op_and_attributes;
3use relay_event_schema::protocol::Attributes;
4use relay_protocol::{Getter, Val};
5
6/// Constructs a name attribute for a V2 span, based on its attributes.
7///
8/// If the attributes contain [`SENTRY__ORIGIN`] with the value `"manual"`,
9/// the description (contained in [`SENTRY__DESCRIPTION`]) is used as the name.
10/// Otherwise, the name is constructed following the rules defined in sentry-conventions.
11///
12/// If no rule in `sentry-conventions` matches the span's [`SENTRY__OP`], the op is
13/// returned as the name.
14///
15/// Finally, if the span doesn't have an op, `None` is returned.
16pub fn name_for_attributes(attributes: &Attributes) -> Option<String> {
17    let origin = attributes
18        .get_value(SENTRY__ORIGIN)
19        .and_then(|o| o.as_str());
20    let description = attributes
21        .get_value(SENTRY__DESCRIPTION)
22        .and_then(|d| d.as_str());
23
24    if let Some(description) = description
25        && origin == Some("manual")
26    {
27        return Some(description.to_owned());
28    }
29
30    let op = attributes.get_value(SENTRY__OP)?.as_str()?;
31    Some(name_for_op_and_attributes(op, &AttributeGetter(attributes)).unwrap_or(op.to_owned()))
32}
33
34/// A custom getter for [`Attributes`] which only resolves values based on the attribute name.
35///
36/// This [`Getter`] does not implement nested traversals, which is the behaviour required for
37/// [`name_for_op_and_attributes`].
38struct AttributeGetter<'a>(&'a Attributes);
39
40impl<'a> Getter for AttributeGetter<'a> {
41    fn get_value(&self, path: &str) -> Option<Val<'_>> {
42        self.0.get_value(path).map(|value| value.into())
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use relay_protocol::Annotated;
49
50    use super::*;
51
52    #[test]
53    fn test_attributes_falls_back_to_op_when_no_templates_defined() {
54        let attributes = Attributes::from([(
55            "sentry.op".to_owned(),
56            Annotated::new("foo".to_owned().into()),
57        )]);
58
59        assert_eq!(name_for_attributes(&attributes), Some("foo".to_owned()));
60    }
61
62    #[test]
63    fn test_attributes_uses_the_first_matching_template() {
64        let attributes = Attributes::from([
65            (
66                "sentry.op".to_owned(),
67                Annotated::new("db".to_owned().into()),
68            ),
69            (
70                "db.query.summary".to_owned(),
71                Annotated::new("SELECT users".to_owned().into()),
72            ),
73            (
74                "db.operation.name".to_owned(),
75                Annotated::new("INSERT".to_owned().into()),
76            ),
77            (
78                "db.collection.name".to_owned(),
79                Annotated::new("widgets".to_owned().into()),
80            ),
81        ]);
82
83        assert_eq!(
84            name_for_attributes(&attributes),
85            Some("SELECT users".to_owned())
86        );
87    }
88
89    #[test]
90    fn test_attributes_uses_fallback_templates_when_data_is_missing() {
91        let attributes = Attributes::from([
92            (
93                "sentry.op".to_owned(),
94                Annotated::new("db".to_owned().into()),
95            ),
96            (
97                "db.operation.name".to_owned(),
98                Annotated::new("INSERT".to_owned().into()),
99            ),
100            (
101                "db.collection.name".to_owned(),
102                Annotated::new("widgets".to_owned().into()),
103            ),
104        ]);
105
106        assert_eq!(
107            name_for_attributes(&attributes),
108            Some("INSERT widgets".to_owned())
109        );
110    }
111
112    #[test]
113    fn test_attributes_falls_back_to_hardcoded_name_when_nothing_matches() {
114        let attributes = Attributes::from([(
115            "sentry.op".to_owned(),
116            Annotated::new("db".to_owned().into()),
117        )]);
118
119        assert_eq!(
120            name_for_attributes(&attributes),
121            Some("Database operation".to_owned())
122        );
123    }
124
125    #[test]
126    fn test_manual_spans_use_description_v2() {
127        let attributes = Attributes::from([
128            (
129                "sentry.origin".to_owned(),
130                Annotated::new("manual".to_owned().into()),
131            ),
132            (
133                "sentry.description".to_owned(),
134                Annotated::new("Custom name".to_owned().into()),
135            ),
136            (
137                "sentry.op".to_owned(),
138                Annotated::new("db".to_owned().into()),
139            ),
140            (
141                "db.query.summary".to_owned(),
142                Annotated::new("SELECT users".to_owned().into()),
143            ),
144            (
145                "db.operation.name".to_owned(),
146                Annotated::new("INSERT".to_owned().into()),
147            ),
148            (
149                "db.collection.name".to_owned(),
150                Annotated::new("widgets".to_owned().into()),
151            ),
152        ]);
153
154        assert_eq!(
155            name_for_attributes(&attributes),
156            Some("Custom name".to_owned())
157        );
158    }
159}