1use relay_conventions::{DESCRIPTION, PROFILE_ID, SEGMENT_ID};
2use relay_protocol::{Annotated, Empty, Error, FromValue, IntoValue, Object, Value};
3
4use crate::protocol::{Attributes, EventId, SpanV2, Timestamp};
5
6#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue)]
9pub struct CompatSpan {
10 #[metastructure(flatten)]
11 pub span_v2: SpanV2,
12
13 pub data: Annotated<Object<Value>>,
14 pub description: Annotated<String>,
15 pub duration_ms: Annotated<u64>,
16 pub end_timestamp_precise: Annotated<Timestamp>,
17 pub profile_id: Annotated<EventId>,
18 pub segment_id: Annotated<String>,
19 pub start_timestamp_ms: Annotated<u64>, pub start_timestamp_precise: Annotated<Timestamp>,
21
22 #[metastructure(field = "_performance_issues_spans")]
23 pub performance_issues_spans: Annotated<bool>, }
25
26impl TryFrom<SpanV2> for CompatSpan {
27 type Error = uuid::Error;
28
29 fn try_from(span_v2: SpanV2) -> Result<Self, uuid::Error> {
30 let mut compat_span = CompatSpan {
31 start_timestamp_precise: span_v2.start_timestamp.clone(),
32 start_timestamp_ms: span_v2
33 .start_timestamp
34 .clone()
35 .map_value(|ts| ts.0.timestamp_millis() as u64),
36 end_timestamp_precise: span_v2.end_timestamp.clone(),
37 ..Default::default()
38 };
39
40 if let (Some(start_timestamp), Some(end_timestamp)) = (
41 span_v2.start_timestamp.value(),
42 span_v2.end_timestamp.value(),
43 ) {
44 let delta = (*end_timestamp - *start_timestamp).num_milliseconds();
45 compat_span.duration_ms = u64::try_from(delta).unwrap_or(0).into();
46 }
47
48 if let Some(attributes) = span_v2.attributes.value() {
49 for (key, value) in attributes.iter() {
51 compat_span
52 .data
53 .get_or_insert_with(Default::default)
54 .insert(key.clone(), value.clone().and_then(|attr| attr.value.value));
55 }
56
57 if let Some(description) = get_string_or_error(attributes, DESCRIPTION)
59 {
61 compat_span.description = description;
62 }
63 if let Some(profile_id) = get_string_or_error(attributes, PROFILE_ID) {
64 compat_span.profile_id = profile_id.and_then(|s| match s.parse::<EventId>() {
65 Ok(id) => Annotated::from(id),
66 Err(_) => Annotated::from_error(Error::invalid("profile_id"), None),
67 });
68 }
69 if let Some(segment_id) = get_string_or_error(attributes, SEGMENT_ID) {
70 compat_span.segment_id = segment_id;
71 }
72 if let Some(Value::Bool(b)) =
73 attributes.get_value("sentry._internal.performance_issues_spans")
74 {
75 compat_span.performance_issues_spans = Annotated::new(*b);
77 }
78 }
79
80 compat_span.span_v2 = span_v2;
81 Ok(compat_span)
82 }
83}
84
85fn get_string_or_error(attributes: &Attributes, key: &str) -> Option<Annotated<String>> {
86 let annotated = attributes.0.get(key)?;
87 let value: Annotated<Value> = annotated.clone().and_then(|attr| attr.value.value);
88 match value {
89 Annotated(Some(Value::String(description)), meta) => {
90 Some(Annotated(Some(description), meta))
91 }
92 Annotated(None, meta) => Some(Annotated(None, meta)),
93 _ => None,
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use std::collections::BTreeMap;
101
102 use chrono::DateTime;
103 use insta::assert_debug_snapshot;
104 use relay_protocol::{Error, SerializableAnnotated};
105
106 use crate::protocol::Attributes;
107
108 use super::*;
109
110 #[test]
111 fn basic_conversion() {
112 let json = r#"{
113 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
114 "span_id": "fa90fdead5f74052",
115 "parent_span_id": "fa90fdead5f74051",
116 "start_timestamp": 123,
117 "end_timestamp": 123.5,
118 "name": "myname",
119 "status": "ok",
120 "links": [],
121 "attributes": {
122 "browser.name": {
123 "value": "Chrome",
124 "type": "string"
125 },
126 "sentry.description": {
127 "value": "mydescription",
128 "type": "string"
129 },
130 "sentry.environment": {
131 "value": "prod",
132 "type": "string"
133 },
134 "sentry.op": {
135 "value": "myop",
136 "type": "string"
137 },
138 "sentry.platform": {
139 "value": "php",
140 "type": "string"
141 },
142 "sentry.profile_id": {
143 "value": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab",
144 "type": "string"
145 },
146 "sentry.release": {
147 "value": "myapp@1.0.0",
148 "type": "string"
149 },
150 "sentry.sdk.name": {
151 "value": "sentry.php",
152 "type": "string"
153 },
154 "sentry.segment.id": {
155 "value": "FA90FDEAD5F74052",
156 "type": "string"
157 },
158 "sentry.segment.name": {
159 "value": "my 1st transaction",
160 "type": "string"
161 },
162 "sentry._internal.performance_issues_spans": {
163 "value": true,
164 "type": "bool"
165 }
166 }
167 }"#;
168
169 let span_v2: SpanV2 = Annotated::from_json(json).unwrap().into_value().unwrap();
170 let compat_span = CompatSpan::try_from(span_v2).unwrap();
171
172 insta::assert_json_snapshot!(SerializableAnnotated(&Annotated::from(compat_span)), @r###"
173 {
174 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
175 "parent_span_id": "fa90fdead5f74051",
176 "span_id": "fa90fdead5f74052",
177 "name": "myname",
178 "status": "ok",
179 "start_timestamp": 123.0,
180 "end_timestamp": 123.5,
181 "links": [],
182 "attributes": {
183 "browser.name": {
184 "type": "string",
185 "value": "Chrome"
186 },
187 "sentry._internal.performance_issues_spans": {
188 "type": "bool",
189 "value": true
190 },
191 "sentry.description": {
192 "type": "string",
193 "value": "mydescription"
194 },
195 "sentry.environment": {
196 "type": "string",
197 "value": "prod"
198 },
199 "sentry.op": {
200 "type": "string",
201 "value": "myop"
202 },
203 "sentry.platform": {
204 "type": "string",
205 "value": "php"
206 },
207 "sentry.profile_id": {
208 "type": "string",
209 "value": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab"
210 },
211 "sentry.release": {
212 "type": "string",
213 "value": "myapp@1.0.0"
214 },
215 "sentry.sdk.name": {
216 "type": "string",
217 "value": "sentry.php"
218 },
219 "sentry.segment.id": {
220 "type": "string",
221 "value": "FA90FDEAD5F74052"
222 },
223 "sentry.segment.name": {
224 "type": "string",
225 "value": "my 1st transaction"
226 }
227 },
228 "data": {
229 "browser.name": "Chrome",
230 "sentry._internal.performance_issues_spans": true,
231 "sentry.description": "mydescription",
232 "sentry.environment": "prod",
233 "sentry.op": "myop",
234 "sentry.platform": "php",
235 "sentry.profile_id": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab",
236 "sentry.release": "myapp@1.0.0",
237 "sentry.sdk.name": "sentry.php",
238 "sentry.segment.id": "FA90FDEAD5F74052",
239 "sentry.segment.name": "my 1st transaction"
240 },
241 "description": "mydescription",
242 "duration_ms": 500,
243 "end_timestamp_precise": 123.5,
244 "profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
245 "segment_id": "FA90FDEAD5F74052",
246 "start_timestamp_ms": 123000,
247 "start_timestamp_precise": 123.0,
248 "_performance_issues_spans": true
249 }
250 "###);
251 }
252
253 #[test]
254 fn negative_duration() {
255 let span_v2 = SpanV2 {
256 start_timestamp: Timestamp(DateTime::from_timestamp_nanos(100)).into(),
257 end_timestamp: Timestamp(DateTime::from_timestamp_nanos(50)).into(),
258 ..Default::default()
259 };
260
261 let compat_span = CompatSpan::try_from(span_v2).unwrap();
262 assert_eq!(compat_span.duration_ms.value(), Some(&0));
263 }
264
265 #[test]
266 fn meta_conversion() {
267 let span_v2 = SpanV2 {
268 trace_id: Annotated::from_error(Error::invalid("trace_id"), None),
269 parent_span_id: Annotated::from_error(Error::invalid("parent_span_id"), None),
270 span_id: Annotated::from_error(Error::invalid("span_id"), None),
271 name: Annotated::from_error(Error::invalid("name"), None),
272 status: Annotated::from_error(Error::invalid("status"), None),
273 is_remote: Annotated::from_error(Error::invalid("is_remote"), None),
274 kind: Annotated::from_error(Error::invalid("kind"), None),
275 start_timestamp: Annotated::from_error(Error::invalid("start_timestamp"), None),
276 end_timestamp: Annotated::from_error(Error::invalid("end_timestamp"), None),
277 links: Annotated::from_error(Error::invalid("links"), None),
278 attributes: Annotated::new(Attributes::from_iter([
279 (
280 "sentry.description".to_owned(),
281 Annotated::from_error(Error::invalid("description"), None),
282 ),
283 (
284 "sentry.profile_id".to_owned(),
285 Annotated::from_error(Error::invalid("profile ID"), None),
286 ),
287 (
288 "sentry.segment.id".to_owned(),
289 Annotated::from_error(Error::invalid("segment ID"), None),
290 ),
291 (
292 "performance_issues_spans".to_owned(),
293 Annotated::from_error(Error::invalid("flag"), None),
294 ),
295 (
296 "other_attribute".to_owned(),
297 Annotated::from_error(Error::invalid("other_attribute"), None),
298 ),
299 ])),
300 other: BTreeMap::from([(
301 "foo".to_owned(),
302 Annotated::from_error(Error::invalid("other"), None),
303 )]),
304 };
305
306 let compat_span = CompatSpan::try_from(span_v2).unwrap();
307 assert_debug_snapshot!(compat_span);
308 }
309}