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