1use relay_conventions::name_for_op_and_attributes;
2use relay_event_schema::protocol::{Attributes, Span};
3use relay_protocol::{Getter, GetterIter, Val};
4
5pub 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 &EscapedGetter(data),
19 ))
20}
21
22pub 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
48struct 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}