relay_spans/
otel_to_sentry.rs

1use crate::otel_to_sentry_v2;
2use crate::otel_trace::Span as OtelSpan;
3use crate::v2_to_v1;
4use relay_event_schema::protocol::Span as EventSpan;
5use relay_protocol::Error;
6
7/// Transforms an OTEL span to a Sentry span.
8///
9/// This uses attributes in the OTEL span to populate various fields in the Sentry span.
10/// * The Sentry span's `name` field may be set based on `db` or `http` attributes
11///   if the OTEL span's `name` is empty.
12/// * The Sentry span's `op` field will be inferred based on the OTEL span's `sentry.op` attribute,
13///   or other available attributes if `sentry.op` is not provided.
14/// * The Sentry span's `description` field will be inferred based on the OTEL span's
15///   `sentry.description` attribute, or other available attributes if `sentry.description` is not
16///   provided.
17/// * The Sentry span's `status` field is set based on the OTEL span's `status` field and
18///   `http.status_code` and `rpc.grpc.status_code` attributes.
19/// * The Sentry span's `exclusive_time` field is set based on the OTEL span's `exclusive_time_nano`
20///   attribute, or the difference between the start and end timestamp if that attribute is not set.
21/// * The Sentry span's `platform` field is set based on the OTEL span's `sentry.platform` attribute.
22/// * The Sentry span's `profile_id` field is set based on the OTEL span's `sentry.profile_id` attribute.
23/// * The Sentry span's `segment_id` field is set based on the OTEL span's `sentry.segment.id` attribute.
24///
25/// All other attributes are carried over from the OTEL span to the Sentry span's `data`.
26pub fn otel_to_sentry_span(otel_span: OtelSpan) -> Result<EventSpan, Error> {
27    let span_v2 = otel_to_sentry_v2::otel_to_sentry_span(otel_span)?;
28    Ok(v2_to_v1::span_v2_to_span_v1(span_v2))
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34    use relay_protocol::{Annotated, SerializableAnnotated};
35
36    #[test]
37    fn parse_span() {
38        let json = r#"{
39            "traceId": "89143b0763095bd9c9955e8175d1fb23",
40            "spanId": "e342abb1214ca181",
41            "parentSpanId": "0c7a7dea069bf5a6",
42            "name": "middleware - fastify -> @fastify/multipart",
43            "kind": 2,
44            "startTimeUnixNano": "1697620454980000000",
45            "endTimeUnixNano": "1697620454980078800",
46            "attributes": [
47                {
48                    "key": "http.route", "value": {
49                        "stringValue": "/home"
50                    }
51                },
52                {
53                    "key": "http.request.method",
54                    "value": {
55                        "stringValue": "GET"
56                        }
57                    },
58                {
59                    "key": "sentry.environment",
60                    "value": {
61                        "stringValue": "test"
62                    }
63                },
64                {
65                "key": "fastify.type",
66                    "value": {
67                        "stringValue": "middleware"
68                    }
69                },
70                {
71                    "key": "plugin.name",
72                    "value": {
73                        "stringValue": "fastify -> @fastify/multipart"
74                    }
75                },
76                {
77                    "key": "hook.name",
78                    "value": {
79                        "stringValue": "onResponse"
80                    }
81                },
82                {
83                    "key": "sentry.sample_rate",
84                    "value": {
85                        "intValue": "1"
86                    }
87                },
88                {
89                    "key": "sentry.parentSampled",
90                    "value": {
91                        "boolValue": true
92                    }
93                },
94                {
95                    "key": "sentry.exclusive_time_nano",
96                    "value": {
97                        "intValue": "1000000000"
98                    }
99                }
100            ],
101            "droppedAttributesCount": 0,
102            "events": [],
103            "droppedEventsCount": 0,
104            "status": {
105                "code": 0,
106                "message": "test"
107            },
108            "links": [],
109            "droppedLinksCount": 0
110        }"#;
111        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
112        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
113        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
114        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
115        {
116          "timestamp": 1697620454.980079,
117          "start_timestamp": 1697620454.98,
118          "exclusive_time": 1000.0,
119          "op": "http.server",
120          "span_id": "e342abb1214ca181",
121          "parent_span_id": "0c7a7dea069bf5a6",
122          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
123          "status": "ok",
124          "description": "GET /home",
125          "data": {
126            "sentry.environment": "test",
127            "fastify.type": "middleware",
128            "hook.name": "onResponse",
129            "http.request.method": "GET",
130            "http.route": "/home",
131            "plugin.name": "fastify -> @fastify/multipart",
132            "sentry.name": "middleware - fastify -> @fastify/multipart",
133            "sentry.parentSampled": true,
134            "sentry.sample_rate": 1,
135            "sentry.status.message": "test"
136          },
137          "links": [],
138          "kind": "server"
139        }
140        "###);
141    }
142
143    #[test]
144    fn parse_span_with_exclusive_time_nano_attribute() {
145        let json = r#"{
146            "traceId": "89143b0763095bd9c9955e8175d1fb23",
147            "spanId": "e342abb1214ca181",
148            "parentSpanId": "0c7a7dea069bf5a6",
149            "name": "middleware - fastify -> @fastify/multipart",
150            "kind": 1,
151            "startTimeUnixNano": "1697620454980000000",
152            "endTimeUnixNano": "1697620454980078800",
153            "attributes": [
154                {
155                    "key": "sentry.exclusive_time_nano",
156                    "value": {
157                        "intValue": "3200000000"
158                    }
159                }
160            ]
161        }"#;
162        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
163        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
164        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
165        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
166        {
167          "timestamp": 1697620454.980079,
168          "start_timestamp": 1697620454.98,
169          "exclusive_time": 3200.0,
170          "op": "default",
171          "span_id": "e342abb1214ca181",
172          "parent_span_id": "0c7a7dea069bf5a6",
173          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
174          "status": "unknown",
175          "description": "middleware - fastify -> @fastify/multipart",
176          "data": {
177            "sentry.name": "middleware - fastify -> @fastify/multipart"
178          },
179          "links": [],
180          "kind": "internal"
181        }
182        "###);
183    }
184
185    #[test]
186    fn parse_span_no_exclusive_time_nano_attribute() {
187        let json = r#"{
188            "traceId": "89143b0763095bd9c9955e8175d1fb23",
189            "spanId": "e342abb1214ca181",
190            "parentSpanId": "0c7a7dea069bf5a6",
191            "name": "middleware - fastify -> @fastify/multipart",
192            "kind": 1,
193            "startTimeUnixNano": "1697620454980000000",
194            "endTimeUnixNano": "1697620454980078800"
195        }"#;
196        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
197        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
198        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
199        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
200        {
201          "timestamp": 1697620454.980079,
202          "start_timestamp": 1697620454.98,
203          "exclusive_time": 0.0788,
204          "op": "default",
205          "span_id": "e342abb1214ca181",
206          "parent_span_id": "0c7a7dea069bf5a6",
207          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
208          "status": "unknown",
209          "description": "middleware - fastify -> @fastify/multipart",
210          "data": {
211            "sentry.name": "middleware - fastify -> @fastify/multipart"
212          },
213          "links": [],
214          "kind": "internal"
215        }
216        "###);
217    }
218
219    #[test]
220    fn parse_span_with_db_attributes() {
221        let json = r#"{
222            "traceId": "89143b0763095bd9c9955e8175d1fb23",
223            "spanId": "e342abb1214ca181",
224            "parentSpanId": "0c7a7dea069bf5a6",
225            "name": "database query",
226            "kind": 3,
227            "startTimeUnixNano": "1697620454980000000",
228            "endTimeUnixNano": "1697620454980078800",
229            "attributes": [
230                {
231                    "key" : "db.system",
232                    "value": {
233                        "stringValue": "mysql"
234                    }
235                },
236                {
237                    "key" : "db.name",
238                    "value": {
239                        "stringValue": "database"
240                    }
241                },
242                {
243                    "key" : "db.type",
244                    "value": {
245                        "stringValue": "sql"
246                    }
247                },
248                {
249                    "key" : "db.statement",
250                    "value": {
251                        "stringValue": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
252                    }
253                }
254            ]
255        }"#;
256        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
257        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
258        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
259        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
260        {
261          "timestamp": 1697620454.980079,
262          "start_timestamp": 1697620454.98,
263          "exclusive_time": 0.0788,
264          "op": "db",
265          "span_id": "e342abb1214ca181",
266          "parent_span_id": "0c7a7dea069bf5a6",
267          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
268          "status": "unknown",
269          "description": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s",
270          "data": {
271            "db.system": "mysql",
272            "db.name": "database",
273            "db.statement": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s",
274            "db.type": "sql",
275            "sentry.name": "database query"
276          },
277          "links": [],
278          "kind": "client"
279        }
280        "###);
281    }
282
283    #[test]
284    fn parse_span_with_db_attributes_and_description() {
285        let json = r#"{
286            "traceId": "89143b0763095bd9c9955e8175d1fb23",
287            "spanId": "e342abb1214ca181",
288            "parentSpanId": "0c7a7dea069bf5a6",
289            "name": "database query",
290            "kind": 3,
291            "startTimeUnixNano": "1697620454980000000",
292            "endTimeUnixNano": "1697620454980078800",
293            "attributes": [
294                {
295                    "key" : "db.name",
296                    "value": {
297                        "stringValue": "database"
298                    }
299                },
300                {
301                    "key" : "db.type",
302                    "value": {
303                        "stringValue": "sql"
304                    }
305                },
306                {
307                    "key" : "db.statement",
308                    "value": {
309                        "stringValue": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
310                    }
311                },
312                {
313                    "key": "sentry.description",
314                    "value": {
315                        "stringValue": "index view query"
316                    }
317                }
318            ]
319        }"#;
320        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
321        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
322        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
323        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
324        {
325          "timestamp": 1697620454.980079,
326          "start_timestamp": 1697620454.98,
327          "exclusive_time": 0.0788,
328          "op": "default",
329          "span_id": "e342abb1214ca181",
330          "parent_span_id": "0c7a7dea069bf5a6",
331          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
332          "status": "unknown",
333          "description": "index view query",
334          "data": {
335            "db.name": "database",
336            "db.statement": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s",
337            "db.type": "sql",
338            "sentry.name": "database query"
339          },
340          "links": [],
341          "kind": "client"
342        }
343        "###);
344    }
345
346    #[test]
347    fn parse_span_with_http_attributes() {
348        let json = r#"{
349            "traceId": "89143b0763095bd9c9955e8175d1fb23",
350            "spanId": "e342abb1214ca181",
351            "parentSpanId": "0c7a7dea069bf5a6",
352            "name": "http client request",
353            "kind": 2,
354            "startTimeUnixNano": "1697620454980000000",
355            "endTimeUnixNano": "1697620454980078800",
356            "attributes": [
357                {
358                    "key" : "http.request.method",
359                    "value": {
360                        "stringValue": "GET"
361                    }
362                },
363                {
364                    "key" : "url.path",
365                    "value": {
366                        "stringValue": "/api/search?q=foobar"
367                    }
368                }
369            ]
370        }"#;
371        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
372        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
373        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
374        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
375        {
376          "timestamp": 1697620454.980079,
377          "start_timestamp": 1697620454.98,
378          "exclusive_time": 0.0788,
379          "op": "http.server",
380          "span_id": "e342abb1214ca181",
381          "parent_span_id": "0c7a7dea069bf5a6",
382          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
383          "status": "unknown",
384          "description": "GET /api/search?q=foobar",
385          "data": {
386            "http.request.method": "GET",
387            "sentry.name": "http client request",
388            "url.path": "/api/search?q=foobar"
389          },
390          "links": [],
391          "kind": "server"
392        }
393        "###);
394    }
395
396    #[test]
397    fn parse_array_attribute() {
398        let json = r#"{
399            "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
400            "spanId": "fa90fdead5f74052",
401            "parentSpanId": "fa90fdead5f74051",
402            "startTimeUnixNano": "123000000000",
403            "endTimeUnixNano": "123500000000",
404            "name": "cmd.run",
405            "status": {"code": 0},
406            "attributes": [
407                {
408                    "key": "process.args",
409                    "value": {
410                        "arrayValue": {
411                            "values": [
412                                {"stringValue": "node"},
413                                {"stringValue": "--require"},
414                                {"stringValue": "preflight.cjs"}
415                            ]
416                        }
417                    }
418                },
419                {
420                    "key": "process.info",
421                    "value": {
422                        "arrayValue": {
423                            "values": [
424                                {"intValue": 41},
425                                {
426                                    "arrayValue": {
427                                        "values": [
428                                            {"intValue": 42}
429                                    ]}
430                                }
431                            ]
432                        }
433                    }
434                }
435            ]
436        }"#;
437
438        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
439        let event_span = otel_to_sentry_span(otel_span).unwrap();
440
441        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
442        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
443        {
444          "timestamp": 123.5,
445          "start_timestamp": 123.0,
446          "exclusive_time": 500.0,
447          "op": "default",
448          "span_id": "fa90fdead5f74052",
449          "parent_span_id": "fa90fdead5f74051",
450          "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
451          "status": "ok",
452          "description": "cmd.run",
453          "data": {
454            "process.args": "[\"node\",\"--require\",\"preflight.cjs\"]",
455            "process.info": "[41]",
456            "sentry.name": "cmd.run"
457          },
458          "links": []
459        }
460        "#);
461    }
462
463    /// Intended to be synced with `relay-event-schema::protocol::span::convert::tests::roundtrip`.
464    #[test]
465    fn parse_sentry_attributes() {
466        let json = r#"{
467            "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
468            "spanId": "fa90fdead5f74052",
469            "parentSpanId": "fa90fdead5f74051",
470            "startTimeUnixNano": "123000000000",
471            "endTimeUnixNano": "123500000000",
472            "name": "myname",
473            "status": {"code": 0, "message": "foo"},
474            "attributes": [
475                {
476                    "key" : "browser.name",
477                    "value": {
478                        "stringValue": "Chrome"
479                    }
480                },
481                {
482                    "key" : "sentry.description",
483                    "value": {
484                        "stringValue": "mydescription"
485                    }
486                },
487                {
488                    "key" : "sentry.environment",
489                    "value": {
490                        "stringValue": "prod"
491                    }
492                },
493                {
494                    "key" : "sentry.op",
495                    "value": {
496                        "stringValue": "myop"
497                    }
498                },
499                {
500                    "key" : "sentry.platform",
501                    "value": {
502                        "stringValue": "php"
503                    }
504                },
505                {
506                    "key" : "sentry.profile_id",
507                    "value": {
508                        "stringValue": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab"
509                    }
510                },
511                {
512                    "key" : "sentry.release",
513                    "value": {
514                        "stringValue": "myapp@1.0.0"
515                    }
516                },
517                {
518                    "key" : "sentry.sdk.name",
519                    "value": {
520                        "stringValue": "sentry.php"
521                    }
522                },
523                {
524                    "key" : "sentry.segment.id",
525                    "value": {
526                        "stringValue": "FA90FDEAD5F74052"
527                    }
528                },
529                {
530                    "key" : "sentry.segment.name",
531                    "value": {
532                        "stringValue": "my 1st transaction"
533                    }
534                },
535                {
536                    "key": "sentry.metrics_summary.some_metric",
537                    "value": {
538                        "arrayValue": {
539                            "values": [
540                                {
541                                    "kvlistValue": {
542                                        "values": [
543                                            {
544                                                "key": "min",
545                                                "value": {
546                                                    "doubleValue": 1.0
547                                                }
548                                            },
549                                            {
550                                                "key": "max",
551                                                "value": {
552                                                    "doubleValue": 2.0
553                                                }
554                                            },
555                                            {
556                                                "key": "sum",
557                                                "value": {
558                                                    "doubleValue": 3.0
559                                                }
560                                            },
561                                            {
562                                                "key": "count",
563                                                "value": {
564                                                    "intValue": "2"
565                                                }
566                                            },
567                                            {
568                                                "key": "tags",
569                                                "value": {
570                                                    "kvlistValue": {
571                                                        "values": [
572                                                            {
573                                                                "key": "environment",
574                                                                "value": {
575                                                                    "stringValue": "test"
576                                                                }
577                                                            }
578                                                        ]
579                                                    }
580                                                }
581                                            }
582                                        ]
583                                    }
584                                }
585                            ]
586                        }
587                    }
588                }
589            ]
590        }"#;
591
592        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
593        let event_span = otel_to_sentry_span(otel_span).unwrap();
594
595        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
596        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
597        {
598          "timestamp": 123.5,
599          "start_timestamp": 123.0,
600          "exclusive_time": 500.0,
601          "op": "myop",
602          "span_id": "fa90fdead5f74052",
603          "parent_span_id": "fa90fdead5f74051",
604          "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
605          "segment_id": "fa90fdead5f74052",
606          "status": "ok",
607          "description": "mydescription",
608          "profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
609          "data": {
610            "browser.name": "Chrome",
611            "sentry.environment": "prod",
612            "sentry.release": "myapp@1.0.0",
613            "sentry.segment.name": "my 1st transaction",
614            "sentry.sdk.name": "sentry.php",
615            "sentry.metrics_summary.some_metric": "[]",
616            "sentry.name": "myname",
617            "sentry.status.message": "foo"
618          },
619          "links": [],
620          "platform": "php"
621        }
622        "###);
623    }
624
625    #[test]
626    fn parse_span_is_remote() {
627        let json = r#"{
628            "traceId": "89143b0763095bd9c9955e8175d1fb23",
629            "spanId": "e342abb1214ca181",
630            "parentSpanId": "0c7a7dea069bf5a6",
631            "startTimeUnixNano": "123000000000",
632            "endTimeUnixNano": "123500000000",
633            "flags": 768
634        }"#;
635        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
636        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
637        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
638        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
639        {
640          "timestamp": 123.5,
641          "start_timestamp": 123.0,
642          "exclusive_time": 500.0,
643          "op": "default",
644          "span_id": "e342abb1214ca181",
645          "parent_span_id": "0c7a7dea069bf5a6",
646          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
647          "is_remote": true,
648          "status": "unknown",
649          "data": {},
650          "links": []
651        }
652        "###);
653    }
654
655    #[test]
656    fn parse_span_is_not_remote() {
657        let json = r#"{
658            "traceId": "89143b0763095bd9c9955e8175d1fb23",
659            "spanId": "e342abb1214ca181",
660            "parentSpanId": "0c7a7dea069bf5a6",
661            "startTimeUnixNano": "123000000000",
662            "endTimeUnixNano": "123500000000",
663            "flags": 256
664        }"#;
665        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
666        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
667        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
668        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
669        {
670          "timestamp": 123.5,
671          "start_timestamp": 123.0,
672          "exclusive_time": 500.0,
673          "op": "default",
674          "span_id": "e342abb1214ca181",
675          "parent_span_id": "0c7a7dea069bf5a6",
676          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
677          "is_remote": false,
678          "status": "unknown",
679          "data": {},
680          "links": []
681        }
682        "###);
683    }
684
685    #[test]
686    fn extract_span_kind() {
687        let json = r#"{
688            "traceId": "89143b0763095bd9c9955e8175d1fb23",
689            "spanId": "e342abb1214ca181",
690            "parentSpanId": "0c7a7dea069bf5a6",
691            "startTimeUnixNano": "123000000000",
692            "endTimeUnixNano": "123500000000",
693            "kind": 3
694        }"#;
695        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
696        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
697        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
698        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
699        {
700          "timestamp": 123.5,
701          "start_timestamp": 123.0,
702          "exclusive_time": 500.0,
703          "op": "default",
704          "span_id": "e342abb1214ca181",
705          "parent_span_id": "0c7a7dea069bf5a6",
706          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
707          "status": "unknown",
708          "data": {},
709          "links": [],
710          "kind": "client"
711        }
712        "###);
713    }
714
715    #[test]
716    fn parse_link() {
717        let json = r#"{
718            "traceId": "3c79f60c11214eb38604f4ae0781bfb2",
719            "spanId": "e342abb1214ca181",
720            "links": [
721                {
722                    "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
723                    "spanId": "fa90fdead5f74052",
724                    "attributes": [
725                        {
726                            "key": "str_key",
727                            "value": {
728                                "stringValue": "str_value"
729                            }
730                        },
731                        {
732                            "key": "bool_key",
733                            "value": {
734                                "boolValue": true
735                            }
736                        },
737                        {
738                            "key": "int_key",
739                            "value": {
740                                "intValue": "123"
741                            }
742                        },
743                        {
744                            "key": "double_key",
745                            "value": {
746                                "doubleValue": 1.23
747                            }
748                        }
749                    ],
750                    "flags": 1
751                }
752            ]
753        }"#;
754        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
755        let event_span: EventSpan = otel_to_sentry_span(otel_span).unwrap();
756        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
757
758        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
759        {
760          "timestamp": 0.0,
761          "start_timestamp": 0.0,
762          "exclusive_time": 0.0,
763          "op": "default",
764          "span_id": "e342abb1214ca181",
765          "trace_id": "3c79f60c11214eb38604f4ae0781bfb2",
766          "status": "unknown",
767          "data": {},
768          "links": [
769            {
770              "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
771              "span_id": "fa90fdead5f74052",
772              "sampled": true,
773              "attributes": {
774                "bool_key": true,
775                "double_key": 1.23,
776                "int_key": 123,
777                "str_key": "str_value"
778              }
779            }
780          ]
781        }
782        "###);
783    }
784
785    #[test]
786    fn parse_span_error_status() {
787        let json = r#"{
788          "traceId": "89143b0763095bd9c9955e8175d1fb23",
789          "spanId": "e342abb1214ca181",
790          "status": {
791            "code": 2,
792            "message": "2 is the error status code"
793          }
794        }"#;
795        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
796        let event_span = otel_to_sentry_span(otel_span).unwrap();
797        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
798        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
799        {
800          "timestamp": 0.0,
801          "start_timestamp": 0.0,
802          "exclusive_time": 0.0,
803          "op": "default",
804          "span_id": "e342abb1214ca181",
805          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
806          "status": "internal_error",
807          "data": {
808            "sentry.status.message": "2 is the error status code"
809          },
810          "links": []
811        }
812        "###);
813    }
814}