1use std::borrow::Cow;
2
3use relay_conventions::{IS_REMOTE, SPAN_KIND};
4use relay_event_schema::protocol::{
5 Attribute, AttributeType, AttributeValue, Attributes, JsonLenientString, Span as SpanV1,
6 SpanData, SpanLink, SpanStatus as SpanV1Status, SpanV2, SpanV2Link, SpanV2Status,
7};
8use relay_protocol::{Annotated, Empty, Error, IntoValue, Meta, Value};
9
10use crate::name::name_for_attributes;
11
12pub fn span_v1_to_span_v2(span_v1: SpanV1) -> SpanV2 {
17 let SpanV1 {
18 timestamp,
19 start_timestamp,
20 exclusive_time,
21 op,
22 span_id,
23 parent_span_id,
24 trace_id,
25 segment_id,
26 is_segment,
27 is_remote,
28 status,
29 description,
30 tags,
31 origin,
32 profile_id,
33 data,
34 links,
35 sentry_tags,
36 received: _, measurements,
38 platform,
39 was_transaction,
40 kind,
41 performance_issues_spans,
42 other: _,
43 } = span_v1;
44
45 let mut annotated_attributes = attributes_from_data(data);
46 let attributes = annotated_attributes.get_or_insert_with(Default::default);
47
48 attributes.insert("sentry.exclusive_time", exclusive_time);
50 attributes.insert("sentry.op", op);
51
52 attributes.insert("sentry.segment.id", segment_id.map_value(|v| v.to_string()));
53 attributes.insert("sentry.description", description);
54 attributes.insert("sentry.origin", origin);
55 attributes.insert("sentry.profile_id", profile_id.map_value(|v| v.to_string()));
56 attributes.insert("sentry.platform", platform);
57 attributes.insert("sentry.was_transaction", was_transaction);
58 attributes.insert(
59 "sentry._internal.performance_issues_spans",
60 performance_issues_spans,
61 );
62
63 if let Some(measurements) = measurements.into_value() {
65 for (key, measurement) in measurements.0 {
66 let key = match key.as_str() {
67 "client_sample_rate" => "sentry.client_sample_rate",
68 "server_sample_rate" => "sentry.server_sample_rate",
69 other => other,
70 };
71
72 attributes.insert_if_missing(key, || match measurement {
73 Annotated(Some(measurement), _) => measurement.value.map_value(|f| f.to_f64()),
74 Annotated(None, meta) => Annotated(None, meta),
75 });
76 }
77 }
78 if let Some(tags) = tags.into_value() {
79 for (key, value) in tags {
80 if !attributes.contains_key(&key) {
81 attributes.0.insert(
82 key,
83 value
84 .map_value(|JsonLenientString(s)| AttributeValue::from(s))
85 .and_then(Attribute::from),
86 );
87 }
88 }
89 }
90 if let Some(tags) = sentry_tags.into_value()
91 && let Value::Object(tags) = tags.into_value()
92 {
93 for (key, value) in tags {
94 let key = match key.as_str() {
95 "description" => "sentry.normalized_description".into(),
96 other => Cow::Owned(format!("sentry.{}", other)),
97 };
98 if !value.is_empty() && !attributes.contains_key(key.as_ref()) {
99 attributes
100 .0
101 .insert(key.into_owned(), attribute_from_value(value));
102 }
103 }
104 }
105
106 let name = attributes
107 .remove("sentry.name")
108 .and_then(|name| name.map_value(|attr| attr.into_string()).transpose())
109 .unwrap_or_else(|| name_for_attributes(attributes).into());
110
111 if let Some(is_remote) = is_remote.value() {
112 attributes.insert(IS_REMOTE, *is_remote);
113 }
114 attributes.insert(SPAN_KIND, kind.map_value(|kind| kind.to_string()));
115
116 let is_segment = match (is_segment.value(), is_remote.value()) {
117 (None, Some(true)) => is_remote,
118 _ => is_segment,
119 };
120
121 SpanV2 {
122 trace_id,
123 parent_span_id,
124 span_id,
125 name,
126 status: Annotated::map_value(status, span_v1_status_to_span_v2_status)
127 .or_else(|| SpanV2Status::Ok.into()),
128 is_segment,
129 start_timestamp,
130 end_timestamp: timestamp,
131 links: links.map_value(span_v1_links_to_span_v2_links),
132 attributes: annotated_attributes,
133 other: Default::default(), }
135}
136
137fn span_v1_status_to_span_v2_status(status: SpanV1Status) -> SpanV2Status {
138 match status {
139 SpanV1Status::Ok => SpanV2Status::Ok,
140 _ => SpanV2Status::Error,
141 }
142}
143
144fn span_v1_links_to_span_v2_links(links: Vec<Annotated<SpanLink>>) -> Vec<Annotated<SpanV2Link>> {
145 links
146 .into_iter()
147 .map(|link| {
148 link.map_value(
149 |SpanLink {
150 trace_id,
151 span_id,
152 sampled,
153 attributes,
154 other,
155 }| {
156 SpanV2Link {
157 trace_id,
158 span_id,
159 sampled,
160 attributes: attributes.map_value(|attrs| {
161 Attributes::from_iter(
162 attrs
163 .into_iter()
164 .map(|(key, value)| (key, attribute_from_value(value))),
165 )
166 }),
167 other,
168 }
169 },
170 )
171 })
172 .collect()
173}
174
175fn attributes_from_data(data: Annotated<SpanData>) -> Annotated<Attributes> {
176 let Annotated(data, meta) = data;
177 let Some(data) = data else {
178 return Annotated(None, meta);
179 };
180 let Value::Object(data) = data.into_value() else {
181 debug_assert!(false, "`SpanData` must convert to Object");
182 return Annotated(None, meta);
183 };
184
185 Annotated::new(Attributes::from_iter(data.into_iter().filter_map(
186 |(key, value)| (!value.is_empty()).then_some((key, attribute_from_value(value))),
187 )))
188}
189
190fn attribute_from_value(value: Annotated<Value>) -> Annotated<Attribute> {
191 let value: Annotated<AttributeValue> = value.and_then(attribute_value_from_value);
192 value.map_value(Attribute::from)
193}
194
195fn attribute_value_from_value(value: Value) -> Annotated<AttributeValue> {
200 match value {
201 Value::Bool(v) => AttributeValue::from(v),
202 Value::I64(v) => AttributeValue::from(v),
203 Value::U64(v) => match i64::try_from(v) {
204 Ok(i) => AttributeValue::from(i),
205 Err(_) => return Annotated::from_error(Error::invalid("integer too large"), None),
206 },
207 Value::F64(v) => AttributeValue::from(v),
208 Value::String(v) => AttributeValue::from(v),
209 Value::Array(_) | Value::Object(_) => {
210 return match serde_json::to_string(&NoMeta(&value)) {
211 Ok(s) => Annotated(
212 Some(AttributeValue {
213 ty: AttributeType::String.into(),
214 value: Value::String(s).into(),
215 }),
216 Meta::from_error(Error::expected("scalar attribute")),
217 ),
218 Err(_) => Annotated::from_error(
219 Error::invalid("failed to serialize nested attribute"),
220 None,
221 ),
222 };
223 }
224 }
225 .into()
226}
227
228struct NoMeta<'a, T>(&'a T);
230
231impl<T> serde::Serialize for NoMeta<'_, T>
232where
233 T: IntoValue,
234{
235 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
236 where
237 S: serde::Serializer,
238 {
239 self.0.serialize_payload(serializer, Default::default())
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use chrono::DateTime;
247 use relay_event_schema::protocol::{Event, Timestamp};
248 use relay_protocol::{FromValue, SerializableAnnotated};
249
250 #[test]
251 fn parse() {
252 let json = serde_json::json!({
253 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
254 "parent_span_id": "fa90fdead5f74051",
255 "span_id": "fa90fdead5f74052",
256 "status": "ok",
257 "is_remote": true,
258 "kind": "server",
259 "start_timestamp": -63158400.0,
260 "timestamp": 0.0,
261 "links": [
262 {
263 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
264 "span_id": "fa90fdead5f74052",
265 "sampled": true,
266 "attributes": {
267 "boolAttr": true,
268 "numAttr": 123,
269 "stringAttr": "foo"
270 }
271 }
272 ],
273 "tags": {
274 "foo": "bar"
275 },
276 "measurements": {
277 "memory": {
278 "value": 9001.0,
279 "unit": "byte"
280 },
281 "client_sample_rate": {
282 "value": 0.11
283 },
284 "server_sample_rate": {
285 "value": 0.22
286 }
287 },
288 "data": {
289 "my.data.field": "my.data.value",
290 "my.array": ["str", 123],
291 "my.nested": {
292 "numbers": [
293 1,
294 2,
295 3
296 ]
297 }
298 },
299 "_performance_issues_spans": true,
300 "description": "raw description",
301 "exclusive_time": 1.23,
302 "is_segment": true,
303 "sentry_tags": {
304 "description": "normalized description",
305 "user": "id:user123",
306 },
307 "op": "operation",
308 "origin": "auto.http",
309 "platform": "javascript",
310 "profile_id": "4c79f60c11214eb38604f4ae0781bfb0",
311 "segment_id": "fa90fdead5f74050",
312 "was_transaction": true,
313
314 "received": 0.2,
315 "additional_field": "additional field value"
316 });
317
318 let span_v1 = SpanV1::from_value(json.into()).into_value().unwrap();
319 let span_v2 = span_v1_to_span_v2(span_v1);
320
321 let annotated_span_v2: Annotated<SpanV2> = Annotated::new(span_v2);
322 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span_v2), @r#"
323 {
324 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
325 "parent_span_id": "fa90fdead5f74051",
326 "span_id": "fa90fdead5f74052",
327 "name": "operation",
328 "status": "ok",
329 "is_segment": true,
330 "start_timestamp": -63158400.0,
331 "end_timestamp": 0.0,
332 "links": [
333 {
334 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
335 "span_id": "fa90fdead5f74052",
336 "sampled": true,
337 "attributes": {
338 "boolAttr": {
339 "type": "boolean",
340 "value": true
341 },
342 "numAttr": {
343 "type": "integer",
344 "value": 123
345 },
346 "stringAttr": {
347 "type": "string",
348 "value": "foo"
349 }
350 }
351 }
352 ],
353 "attributes": {
354 "foo": {
355 "type": "string",
356 "value": "bar"
357 },
358 "memory": {
359 "type": "double",
360 "value": 9001.0
361 },
362 "my.array": {
363 "type": "string",
364 "value": "[\"str\",123]"
365 },
366 "my.data.field": {
367 "type": "string",
368 "value": "my.data.value"
369 },
370 "my.nested": {
371 "type": "string",
372 "value": "{\"numbers\":[1,2,3]}"
373 },
374 "sentry._internal.performance_issues_spans": {
375 "type": "boolean",
376 "value": true
377 },
378 "sentry.client_sample_rate": {
379 "type": "double",
380 "value": 0.11
381 },
382 "sentry.description": {
383 "type": "string",
384 "value": "raw description"
385 },
386 "sentry.exclusive_time": {
387 "type": "double",
388 "value": 1.23
389 },
390 "sentry.is_remote": {
391 "type": "boolean",
392 "value": true
393 },
394 "sentry.kind": {
395 "type": "string",
396 "value": "server"
397 },
398 "sentry.normalized_description": {
399 "type": "string",
400 "value": "normalized description"
401 },
402 "sentry.op": {
403 "type": "string",
404 "value": "operation"
405 },
406 "sentry.origin": {
407 "type": "string",
408 "value": "auto.http"
409 },
410 "sentry.platform": {
411 "type": "string",
412 "value": "javascript"
413 },
414 "sentry.profile_id": {
415 "type": "string",
416 "value": "4c79f60c11214eb38604f4ae0781bfb0"
417 },
418 "sentry.segment.id": {
419 "type": "string",
420 "value": "fa90fdead5f74050"
421 },
422 "sentry.server_sample_rate": {
423 "type": "double",
424 "value": 0.22
425 },
426 "sentry.user": {
427 "type": "string",
428 "value": "id:user123"
429 },
430 "sentry.was_transaction": {
431 "type": "boolean",
432 "value": true
433 }
434 },
435 "_meta": {
436 "attributes": {
437 "my.array": {
438 "": {
439 "err": [
440 [
441 "invalid_data",
442 {
443 "reason": "expected scalar attribute"
444 }
445 ]
446 ]
447 }
448 },
449 "my.nested": {
450 "": {
451 "err": [
452 [
453 "invalid_data",
454 {
455 "reason": "expected scalar attribute"
456 }
457 ]
458 ]
459 }
460 }
461 }
462 }
463 }
464 "#);
465 }
466
467 #[test]
468 fn transaction_conversion() {
469 let txn = Annotated::<Event>::from_json(r#"{"transaction": "hi"}"#)
470 .unwrap()
471 .0
472 .unwrap();
473 assert_eq!(txn.transaction.as_str(), Some("hi"));
474 let span_v1 = SpanV1::from(&txn);
475 assert_eq!(
476 span_v1.data.value().unwrap().segment_name.as_str(),
477 Some("hi")
478 );
479 let span_v2 = span_v1_to_span_v2(span_v1);
480 assert_eq!(
481 span_v2
482 .attributes
483 .value()
484 .unwrap()
485 .get_value("sentry.segment.name")
486 .and_then(Value::as_str),
487 Some("hi")
488 );
489 }
490
491 #[test]
492 fn start_timestamp() {
493 let json = r#"{"timestamp": 123, "end_timestamp": "invalid data"}"#;
494 let span_v1 = Annotated::<SpanV1>::from_json(json).unwrap();
495 let span_v2 = span_v1_to_span_v2(span_v1.into_value().unwrap());
496
497 assert_eq!(
499 span_v2.end_timestamp.value().unwrap(),
500 &Timestamp(DateTime::from_timestamp_secs(123).unwrap())
501 );
502
503 let serialized = Annotated::from(span_v2).payload_to_json().unwrap();
504 assert_eq!(
505 &serialized,
506 r#"{"status":"ok","end_timestamp":123.0,"attributes":{}}"#
507 );
508 }
509}