relay_spans/
v2_to_v1.rs

1use std::str::FromStr;
2
3use relay_event_schema::protocol::{SpanKind, SpanV2Link};
4
5use crate::status_codes;
6use relay_event_schema::protocol::{
7    Attributes, EventId, Span as SpanV1, SpanData, SpanId, SpanLink, SpanStatus, SpanV2,
8    SpanV2Kind, SpanV2Status,
9};
10use relay_protocol::{Annotated, FromValue, Object, Value};
11use url::Url;
12
13/// Transforms a Sentry span V2 to a Sentry span V1.
14///
15/// This uses attributes in the V2 span to populate various fields in the V1 span.
16/// * The V1 span's `op` field will be set based on the V2 span's `sentry.op` attribute, or
17///   inferred from other attributes if the `sentry.op` attribute is not set.
18/// * The V1 span's `description` field will be set based on the V2 span's `sentry.description`
19///   attribute, or inferred from other attributes if the `sentry.description` attribute is not set.
20/// * The V1 span's `description` field is set based on the V2 span's `sentry.description` attribute.
21/// * The V1 span's `status` field is set based on the V2 span's `status` field and
22///   `http.status_code` and `rpc.grpc.status_code` attributes.
23/// * The V1 span's `exclusive_time` field is set based on the V2 span's `exclusive_time_nano`
24///   attribute, or the difference between the start and end timestamp if that attribute is not set.
25/// * The V1 span's `platform` field is set based on the V2 span's `sentry.platform` attribute.
26/// * The V1 span's `profile_id` field is set based on the V2 span's `sentry.profile_id` attribute.
27/// * The V1 span's `segment_id` field is set based on the V2 span's `sentry.segment.id` attribute.
28///
29/// All other attributes are carried over from the V2 span to the V1 span's `data`.
30pub fn span_v2_to_span_v1(span_v2: SpanV2) -> SpanV1 {
31    let mut exclusive_time_ms = 0f64;
32    let mut data = Object::new();
33
34    let inferred_op = derive_op_for_v2_span(&span_v2);
35    // NOTE: Inferring the description should happen after inferring the op, since the op may affect
36    // how we infer the description.
37    let inferred_description = derive_description_for_v2_span(&span_v2);
38
39    let SpanV2 {
40        start_timestamp,
41        end_timestamp,
42        trace_id,
43        span_id,
44        parent_span_id,
45        name,
46        kind,
47        links,
48        attributes,
49        status,
50        is_remote,
51        other: _other,
52    } = span_v2;
53
54    let mut description = Annotated::empty();
55    let mut op = Annotated::empty();
56    let mut http_status_code = Annotated::empty();
57    let mut grpc_status_code = Annotated::empty();
58    let mut platform = Annotated::empty();
59    let mut segment_id = Annotated::empty();
60    let mut profile_id = Annotated::empty();
61
62    for (key, value) in attributes.into_value().into_iter().flat_map(|attributes| {
63        attributes.into_iter().flat_map(|(key, attribute)| {
64            let attribute = attribute.into_value()?;
65            Some((key, attribute.value.value))
66        })
67    }) {
68        match key.as_str() {
69            "sentry.description" => {
70                description = String::from_value(value);
71            }
72            "sentry.op" => {
73                op = String::from_value(value);
74            }
75            key if key.contains("exclusive_time_nano") => {
76                let value = match value.value() {
77                    Some(Value::I64(v)) => *v as f64,
78                    Some(Value::U64(v)) => *v as f64,
79                    Some(Value::F64(v)) => *v,
80                    Some(Value::String(v)) => v.parse::<f64>().unwrap_or_default(),
81                    _ => 0f64,
82                };
83                exclusive_time_ms = value / 1e6f64;
84            }
85            "http.status_code" => {
86                http_status_code = i64::from_value(value.clone());
87                data.insert(key.to_owned(), value);
88            }
89            "rpc.grpc.status_code" => {
90                grpc_status_code = i64::from_value(value.clone());
91                data.insert(key.to_owned(), value);
92            }
93            "sentry.platform" => {
94                platform = String::from_value(value);
95            }
96            "sentry.segment.id" => {
97                segment_id = SpanId::from_value(value);
98            }
99            "sentry.profile_id" => {
100                profile_id = EventId::from_value(value);
101            }
102            _ => {
103                data.insert(key.to_owned(), value);
104            }
105        }
106    }
107
108    // Write the incoming `name` field to a the `sentry.name` attribute, since the V1
109    // Span schema doesn't have a top-level `name` field.
110    if let Some(name) = name.value() {
111        data.insert(
112            "sentry.name".to_owned(),
113            Annotated::new(Value::String(name.to_owned())),
114        );
115    }
116
117    if exclusive_time_ms == 0f64
118        && let (Some(start), Some(end)) = (start_timestamp.value(), end_timestamp.value())
119        && let Some(nanos) = (end.0 - start.0).num_nanoseconds()
120    {
121        exclusive_time_ms = nanos as f64 / 1e6f64;
122    }
123
124    let links = links.map_value(|links| {
125        links
126            .into_iter()
127            .map(|link| link.map_value(span_v2_link_to_span_v1_link))
128            .collect()
129    });
130
131    let status = span_v2_status_to_span_v1_status(status, http_status_code, grpc_status_code);
132
133    // If the SDK sent in a `sentry.op` attribute, use it. If not, derive it from the span attributes.
134    let op = op.or_else(|| Annotated::from(inferred_op));
135
136    // If the SDK sent in a `sentry.description` attribute, use it. If not, derive it from the span attributes.
137    let description = description.or_else(|| Annotated::from(inferred_description));
138
139    SpanV1 {
140        op,
141        description,
142        data: SpanData::from_value(Annotated::new(data.into())),
143        exclusive_time: exclusive_time_ms.into(),
144        parent_span_id,
145        segment_id,
146        span_id,
147        is_remote,
148        profile_id,
149        start_timestamp,
150        status,
151        timestamp: end_timestamp,
152        trace_id,
153        platform,
154        kind: kind.map_value(span_v2_kind_to_span_v1_kind),
155        links,
156        ..Default::default()
157    }
158}
159
160fn span_v2_status_to_span_v1_status(
161    status: Annotated<SpanV2Status>,
162    http_status_code: Annotated<i64>,
163    grpc_status_code: Annotated<i64>,
164) -> Annotated<SpanStatus> {
165    status
166        .clone()
167        .and_then(|status| (status == SpanV2Status::Ok).then_some(SpanStatus::Ok))
168        .or_else(|| {
169            http_status_code.and_then(|http_status_code| {
170                status_codes::HTTP
171                    .get(&http_status_code)
172                    .and_then(|sentry_status| SpanStatus::from_str(sentry_status).ok())
173            })
174        })
175        .or_else(|| {
176            grpc_status_code.and_then(|grpc_status_code| {
177                status_codes::GRPC
178                    .get(&grpc_status_code)
179                    .and_then(|sentry_status| SpanStatus::from_str(sentry_status).ok())
180            })
181        })
182        .or_else(|| {
183            status.and_then(|status| {
184                (status == SpanV2Status::Error).then_some(SpanStatus::InternalError)
185            })
186        })
187        .or_else(|| Annotated::new(SpanStatus::Unknown))
188}
189
190fn span_v2_kind_to_span_v1_kind(kind: SpanV2Kind) -> SpanKind {
191    match kind {
192        SpanV2Kind::Internal => SpanKind::Internal,
193        SpanV2Kind::Server => SpanKind::Server,
194        SpanV2Kind::Client => SpanKind::Client,
195        SpanV2Kind::Producer => SpanKind::Producer,
196        SpanV2Kind::Consumer => SpanKind::Consumer,
197    }
198}
199
200fn span_v2_link_to_span_v1_link(link: SpanV2Link) -> SpanLink {
201    let SpanV2Link {
202        trace_id,
203        span_id,
204        sampled,
205        attributes,
206        other,
207    } = link;
208
209    let attributes = attributes.map_value(|attributes| {
210        attributes
211            .into_iter()
212            .map(|(key, attribute)| {
213                (
214                    key,
215                    attribute.and_then(|attribute| attribute.value.value.into_value()),
216                )
217            })
218            .collect()
219    });
220    SpanLink {
221        trace_id,
222        span_id,
223        sampled,
224        attributes,
225        other,
226    }
227}
228
229/// Generates a `sentry.op` attribute for V2 span, if possible.
230///
231/// This uses attributes of the span to figure out an appropriate operation name, inferring what the
232/// SDK might have sent. Reliably infers an op for well-known OTel span kinds like database
233/// operations. Does not infer an op for frontend and mobile spans sent by Sentry SDKs that don't
234/// have an OTel equivalent (e.g., resource loads).
235fn derive_op_for_v2_span(span: &SpanV2) -> String {
236    // NOTE: `op` is not a required field in the SDK, so the fallback is an empty string.
237    let op = String::from("default");
238
239    let Some(attributes) = span.attributes.value() else {
240        return op;
241    };
242
243    if attributes.contains_key("http.request.method") || attributes.contains_key("http.method") {
244        return match span.kind.value() {
245            Some(SpanV2Kind::Client) => String::from("http.client"),
246            Some(SpanV2Kind::Server) => String::from("http.server"),
247            _ => {
248                if attributes.contains_key("sentry.http.prefetch") {
249                    String::from("http.prefetch")
250                } else {
251                    String::from("http")
252                }
253            }
254        };
255    }
256
257    if attributes.contains_key("db.system") || attributes.contains_key("db.system.name") {
258        return String::from("db");
259    }
260
261    if attributes.contains_key("gen_ai.system") {
262        return String::from("gen_ai");
263    }
264
265    if attributes.contains_key("rpc.service") {
266        return String::from("rpc");
267    }
268
269    if attributes.contains_key("messaging.system") {
270        return String::from("message");
271    }
272
273    if let Some(faas_trigger) = attributes
274        .get_value("faas.trigger")
275        .and_then(|v| v.as_str())
276    {
277        return faas_trigger.to_owned();
278    }
279
280    op
281}
282
283/// Generates a `sentry.description` attribute for V2 span, if possible.
284///
285/// This uses attributes of the span to figure out an appropriate description, trying to match what
286/// the SDK might have sent. This works well for HTTP and database spans, but doesn't have a
287/// thorough implementation for other types of spans for now.
288fn derive_description_for_v2_span(span: &SpanV2) -> Option<String> {
289    // `name` is a low-cardinality description of the span, so it makes for a good fallback.
290    let description = span.name.value().map(|v| v.to_owned());
291
292    let Some(attributes) = span.attributes.value() else {
293        return description;
294    };
295
296    if let Some(http_description) = derive_http_description(attributes, &span.kind.value()) {
297        return Some(http_description);
298    }
299
300    if let Some(database_description) = derive_db_description(attributes) {
301        return Some(database_description);
302    }
303
304    description
305}
306
307fn derive_http_description(attributes: &Attributes, kind: &Option<&SpanV2Kind>) -> Option<String> {
308    // Get HTTP method
309    let http_method = attributes
310        .get_value("http.request.method")
311        .or_else(|| attributes.get_value("http.method"))
312        .and_then(|v| v.as_str())?;
313
314    let description = http_method.to_owned();
315
316    // Get URL path information
317    let url_path = match kind {
318        Some(SpanV2Kind::Server) => get_server_url_path(attributes),
319        Some(SpanV2Kind::Client) => get_client_url_path(attributes),
320        _ => None,
321    };
322
323    let Some(url_path) = url_path else {
324        return Some(description);
325    };
326    let base_description = format!("{http_method} {url_path}");
327
328    // Check for GraphQL operations
329    if let Some(graphql_ops) = attributes
330        .get_value("sentry.graphql.operation")
331        .and_then(|v| v.as_str())
332    {
333        return Some(format!("{base_description} ({graphql_ops})"));
334    }
335
336    Some(base_description)
337}
338
339fn derive_db_description(attributes: &Attributes) -> Option<String> {
340    // Check if this is a cache operation. Cache operations look very similar to database
341    // operations, since they have a `db.system` attribute, but should be treated differently, since
342    // we don't want their statements to end up in description for now.
343    if attributes
344        .get_value("sentry.op")
345        .and_then(|v| v.as_str())
346        .is_some_and(|op| op.starts_with("cache."))
347    {
348        return None;
349    }
350
351    // Check the `db.system` attribute. It's mandatory, so if it's missing, return `None` right
352    // away, since there's not much point trying to derive a description.
353    attributes
354        .get_value("db.system")
355        .or_else(|| attributes.get_value("db.system.name"))
356        .and_then(|v| v.as_str())?;
357
358    // `db.query.text` is a recommended attribute, and it contains the full query text if available.
359    // This is the ideal description.
360    if let Some(query_text) = attributes
361        .get_value("db.query.text")
362        .and_then(|v| v.as_str())
363    {
364        return Some(query_text.to_owned());
365    }
366
367    // Other SDKs check for `db.statement`, it's a legacy OTel attribute, useful as a fallback in some cases.
368    if let Some(statement) = attributes
369        .get_value("db.statement")
370        .and_then(|v| v.as_str())
371    {
372        return Some(statement.to_owned());
373    }
374
375    None
376}
377
378fn get_server_url_path(attributes: &Attributes) -> Option<String> {
379    // `http.route` takes precedence. If available, this is the matched route of the server
380    // framework for server spans. Not always available, even for server spans.
381    if let Some(route) = attributes.get_value("http.route").and_then(|v| v.as_str()) {
382        return Some(route.to_owned());
383    }
384
385    // `url.path` is the path of the HTTP request for server spans. This is required for server spans.
386    if let Some(path) = attributes.get_value("url.path").and_then(|v| v.as_str()) {
387        return Some(path.to_owned());
388    }
389
390    // `http.target` is deprecated, but might be present in older data. Here as a fallback
391    if let Some(target) = attributes.get_value("http.target").and_then(|v| v.as_str()) {
392        return Some(strip_url_query_and_fragment(target));
393    }
394
395    None
396}
397
398fn strip_url_query_and_fragment(url: &str) -> String {
399    url.split(&['?', '#']).next().unwrap_or(url).to_owned()
400}
401
402fn get_client_url_path(attributes: &Attributes) -> Option<String> {
403    let url = attributes
404        .get_value("url.full")
405        .or_else(|| attributes.get_value("http.url"))?
406        .as_str()?;
407
408    let parsed_url = Url::parse(url).ok()?;
409
410    Some(format!(
411        "{}://{}{}",
412        parsed_url.scheme(),
413        parsed_url.domain().unwrap_or(""),
414        parsed_url.path()
415    ))
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use relay_protocol::SerializableAnnotated;
422
423    #[test]
424    fn parse_span() {
425        let json = r#"{
426            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
427            "span_id": "e342abb1214ca181",
428            "parent_span_id": "0c7a7dea069bf5a6",
429            "name": "middleware - fastify -> @fastify/multipart",
430            "kind": "internal",
431            "start_timestamp": "2023-10-18T09:14:14.980Z",
432            "end_timestamp": "2023-10-18T09:14:14.980078800Z",
433            "links": [],
434            "attributes": {
435                "sentry.environment": {
436                    "value": "test",
437                    "type": "string"
438                },
439                "fastify.type": {
440                    "value": "middleware",
441                    "type": "string"
442                },
443                "plugin.name": {
444                    "value": "fastify -> @fastify/multipart",
445                    "type": "string"
446                },
447                "hook.name": {
448                    "value": "onResponse",
449                    "type": "string"
450                },
451                "sentry.sample_rate": {
452                    "value": 1,
453                    "type": "u64"
454                },
455                "sentry.parentSampled": {
456                    "value": true,
457                    "type": "boolean"
458                },
459                "sentry.exclusive_time_nano": {
460                    "value": "1000000000",
461                    "type": "u64"
462                }
463            },
464            "status": "ok",
465            "links": []
466        }"#;
467        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
468        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
469        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
470        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
471        {
472          "timestamp": 1697620454.980079,
473          "start_timestamp": 1697620454.98,
474          "exclusive_time": 1000.0,
475          "op": "default",
476          "span_id": "e342abb1214ca181",
477          "parent_span_id": "0c7a7dea069bf5a6",
478          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
479          "status": "ok",
480          "description": "middleware - fastify -> @fastify/multipart",
481          "data": {
482            "sentry.environment": "test",
483            "fastify.type": "middleware",
484            "hook.name": "onResponse",
485            "plugin.name": "fastify -> @fastify/multipart",
486            "sentry.name": "middleware - fastify -> @fastify/multipart",
487            "sentry.parentSampled": true,
488            "sentry.sample_rate": 1
489          },
490          "links": [],
491          "kind": "internal"
492        }
493        "###);
494    }
495
496    #[test]
497    fn parse_span_with_exclusive_time_nano_attribute() {
498        let json = r#"{
499            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
500            "span_id": "e342abb1214ca181",
501            "parent_span_id": "0c7a7dea069bf5a6",
502            "name": "middleware - fastify -> @fastify/multipart",
503            "kind": "internal",
504            "start_timestamp": "2023-10-18T09:14:14.980Z",
505            "end_timestamp": "2023-10-18T09:14:14.980078800Z",
506            "links": [],
507            "attributes": {
508                "sentry.exclusive_time_nano": {
509                    "value": 3200000000,
510                    "type": "u64"
511                }
512            }
513        }"#;
514        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
515        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
516        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
517        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
518        {
519          "timestamp": 1697620454.980079,
520          "start_timestamp": 1697620454.98,
521          "exclusive_time": 3200.0,
522          "op": "default",
523          "span_id": "e342abb1214ca181",
524          "parent_span_id": "0c7a7dea069bf5a6",
525          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
526          "status": "unknown",
527          "description": "middleware - fastify -> @fastify/multipart",
528          "data": {
529            "sentry.name": "middleware - fastify -> @fastify/multipart"
530          },
531          "links": [],
532          "kind": "internal"
533        }
534        "###);
535    }
536
537    #[test]
538    fn parse_span_no_exclusive_time_nano_attribute() {
539        let json = r#"{
540            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
541            "span_id": "e342abb1214ca181",
542            "parent_span_id": "0c7a7dea069bf5a6",
543            "name": "middleware - fastify -> @fastify/multipart",
544            "kind": "internal",
545            "start_timestamp": "2023-10-18T09:14:14.980Z",
546            "end_timestamp": "2023-10-18T09:14:14.980078800Z",
547            "links": []
548        }"#;
549        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
550        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
551        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
552        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
553        {
554          "timestamp": 1697620454.980079,
555          "start_timestamp": 1697620454.98,
556          "exclusive_time": 0.0788,
557          "op": "default",
558          "span_id": "e342abb1214ca181",
559          "parent_span_id": "0c7a7dea069bf5a6",
560          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
561          "status": "unknown",
562          "description": "middleware - fastify -> @fastify/multipart",
563          "data": {
564            "sentry.name": "middleware - fastify -> @fastify/multipart"
565          },
566          "links": [],
567          "kind": "internal"
568        }
569        "###);
570    }
571
572    #[test]
573    fn parse_sentry_attributes() {
574        let json = r#"{
575            "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
576            "span_id": "fa90fdead5f74052",
577            "parent_span_id": "fa90fdead5f74051",
578            "start_timestamp": 123,
579            "end_timestamp": 123.5,
580            "name": "myname",
581            "status": "ok",
582            "links": [],
583            "attributes": {
584                "browser.name": {
585                    "value": "Chrome",
586                    "type": "string"
587                },
588                "sentry.description": {
589                    "value": "mydescription",
590                    "type": "string"
591                },
592                "sentry.environment": {
593                    "value": "prod",
594                    "type": "string"
595                },
596                "sentry.op": {
597                    "value": "myop",
598                    "type": "string"
599                },
600                "sentry.platform": {
601                    "value": "php",
602                    "type": "string"
603                },
604                "sentry.profile_id": {
605                    "value": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab",
606                    "type": "string"
607                },
608                "sentry.release": {
609                    "value": "myapp@1.0.0",
610                    "type": "string"
611                },
612                "sentry.sdk.name": {
613                    "value": "sentry.php",
614                    "type": "string"
615                },
616                "sentry.segment.id": {
617                    "value": "FA90FDEAD5F74052",
618                    "type": "string"
619                },
620                "sentry.segment.name": {
621                    "value": "my 1st transaction",
622                    "type": "string"
623                }
624            }
625        }"#;
626
627        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
628        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
629
630        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
631        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
632        {
633          "timestamp": 123.5,
634          "start_timestamp": 123.0,
635          "exclusive_time": 500.0,
636          "op": "myop",
637          "span_id": "fa90fdead5f74052",
638          "parent_span_id": "fa90fdead5f74051",
639          "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
640          "segment_id": "fa90fdead5f74052",
641          "status": "ok",
642          "description": "mydescription",
643          "profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
644          "data": {
645            "browser.name": "Chrome",
646            "sentry.environment": "prod",
647            "sentry.release": "myapp@1.0.0",
648            "sentry.segment.name": "my 1st transaction",
649            "sentry.sdk.name": "sentry.php",
650            "sentry.name": "myname"
651          },
652          "links": [],
653          "platform": "php"
654        }
655        "###);
656    }
657
658    #[test]
659    fn parse_span_is_remote() {
660        let json = r#"{
661            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
662            "span_id": "e342abb1214ca181",
663            "parent_span_id": "0c7a7dea069bf5a6",
664            "start_timestamp": 123,
665            "end_timestamp": 123.5,
666            "is_remote": true,
667            "links": []
668        }"#;
669        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
670        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
671        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
672        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
673        {
674          "timestamp": 123.5,
675          "start_timestamp": 123.0,
676          "exclusive_time": 500.0,
677          "op": "default",
678          "span_id": "e342abb1214ca181",
679          "parent_span_id": "0c7a7dea069bf5a6",
680          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
681          "is_remote": true,
682          "status": "unknown",
683          "data": {},
684          "links": []
685        }
686        "###);
687    }
688
689    #[test]
690    fn parse_span_is_not_remote() {
691        let json = r#"{
692            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
693            "span_id": "e342abb1214ca181",
694            "parent_span_id": "0c7a7dea069bf5a6",
695            "start_timestamp": 123,
696            "end_timestamp": 123.5,
697            "is_remote": false,
698            "links": []
699        }"#;
700        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
701        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
702        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
703        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
704        {
705          "timestamp": 123.5,
706          "start_timestamp": 123.0,
707          "exclusive_time": 500.0,
708          "op": "default",
709          "span_id": "e342abb1214ca181",
710          "parent_span_id": "0c7a7dea069bf5a6",
711          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
712          "is_remote": false,
713          "status": "unknown",
714          "data": {},
715          "links": []
716        }
717        "###);
718    }
719
720    #[test]
721    fn parse_http_client_span_only_method() {
722        let json = r#"{
723            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
724            "span_id": "e342abb1214ca181",
725            "parent_span_id": "0c7a7dea069bf5a6",
726            "start_timestamp": 123,
727            "end_timestamp": 123.5,
728            "kind": "client",
729            "attributes": {
730                "http.method": {
731                    "value": "GET",
732                    "type": "string"
733                }
734            }
735        }"#;
736        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
737        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
738        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
739        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
740        {
741          "timestamp": 123.5,
742          "start_timestamp": 123.0,
743          "exclusive_time": 500.0,
744          "op": "http.client",
745          "span_id": "e342abb1214ca181",
746          "parent_span_id": "0c7a7dea069bf5a6",
747          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
748          "status": "unknown",
749          "description": "GET",
750          "data": {
751            "http.request_method": "GET"
752          },
753          "kind": "client"
754        }
755        "###);
756    }
757
758    #[test]
759    fn parse_semantic_http_client_span() {
760        let json = r#"{
761            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
762            "span_id": "e342abb1214ca181",
763            "parent_span_id": "0c7a7dea069bf5a6",
764            "start_timestamp": 123,
765            "end_timestamp": 123.5,
766            "kind": "client",
767            "attributes": {
768                "server.address": {
769                    "value": "github.com",
770                    "type": "string"
771                },
772                "server.port": {
773                    "value": 443,
774                    "type": "integer"
775                },
776                "http.request.method": {
777                    "value": "GET",
778                    "type": "string"
779                },
780                "url.full": {
781                    "value": "https://github.com/rust-lang/rust/issues?labels=E-easy&state=open",
782                    "type": "string"
783                }
784            }
785        }"#;
786        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
787        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
788        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
789        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
790        {
791          "timestamp": 123.5,
792          "start_timestamp": 123.0,
793          "exclusive_time": 500.0,
794          "op": "http.client",
795          "span_id": "e342abb1214ca181",
796          "parent_span_id": "0c7a7dea069bf5a6",
797          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
798          "status": "unknown",
799          "description": "GET https://github.com/rust-lang/rust/issues",
800          "data": {
801            "server.address": "github.com",
802            "url.full": "https://github.com/rust-lang/rust/issues?labels=E-easy&state=open",
803            "http.request.method": "GET",
804            "server.port": 443
805          },
806          "kind": "client"
807        }
808        "###);
809    }
810
811    #[test]
812    fn parse_http_server_span_only_method() {
813        let json = r#"{
814            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
815            "span_id": "e342abb1214ca181",
816            "parent_span_id": "0c7a7dea069bf5a6",
817            "start_timestamp": 123,
818            "end_timestamp": 123.5,
819            "kind": "server",
820            "attributes": {
821                "http.method": {
822                    "value": "GET",
823                    "type": "string"
824                }
825            }
826        }"#;
827        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
828        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
829        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
830        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
831        {
832          "timestamp": 123.5,
833          "start_timestamp": 123.0,
834          "exclusive_time": 500.0,
835          "op": "http.server",
836          "span_id": "e342abb1214ca181",
837          "parent_span_id": "0c7a7dea069bf5a6",
838          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
839          "status": "unknown",
840          "description": "GET",
841          "data": {
842            "http.request_method": "GET"
843          },
844          "kind": "server"
845        }
846        "###);
847    }
848
849    #[test]
850    fn parse_semantic_http_server_span() {
851        let json = r#"{
852            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
853            "span_id": "e342abb1214ca181",
854            "parent_span_id": "0c7a7dea069bf5a6",
855            "start_timestamp": 123,
856            "end_timestamp": 123.5,
857            "kind": "server",
858            "attributes": {
859                "http.request.method": {
860                    "value": "GET",
861                    "type": "string"
862                },
863                "url.path": {
864                    "value": "/users",
865                    "type": "string"
866                },
867                "url.scheme": {
868                    "value": "GET",
869                    "type": "string"
870                }
871            }
872        }"#;
873        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
874        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
875        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
876        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
877        {
878          "timestamp": 123.5,
879          "start_timestamp": 123.0,
880          "exclusive_time": 500.0,
881          "op": "http.server",
882          "span_id": "e342abb1214ca181",
883          "parent_span_id": "0c7a7dea069bf5a6",
884          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
885          "status": "unknown",
886          "description": "GET /users",
887          "data": {
888            "url.scheme": "GET",
889            "http.request.method": "GET",
890            "url.path": "/users"
891          },
892          "kind": "server"
893        }
894        "###);
895    }
896
897    #[test]
898    fn parse_database_span_only_system() {
899        let json = r#"{
900            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
901            "span_id": "e342abb1214ca181",
902            "parent_span_id": "0c7a7dea069bf5a6",
903            "start_timestamp": 123,
904            "name": "SELECT users",
905            "end_timestamp": 123.5,
906            "kind": "client",
907            "attributes": {
908                "db.system": {
909                    "value": "postgres",
910                    "type": "string"
911                }
912            }
913        }"#;
914        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
915        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
916        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
917        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
918        {
919          "timestamp": 123.5,
920          "start_timestamp": 123.0,
921          "exclusive_time": 500.0,
922          "op": "db",
923          "span_id": "e342abb1214ca181",
924          "parent_span_id": "0c7a7dea069bf5a6",
925          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
926          "status": "unknown",
927          "description": "SELECT users",
928          "data": {
929            "db.system": "postgres",
930            "sentry.name": "SELECT users"
931          },
932          "kind": "client"
933        }
934        "###);
935    }
936
937    #[test]
938    fn parse_cache_span() {
939        let json = r#"{
940            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
941            "span_id": "e342abb1214ca181",
942            "parent_span_id": "0c7a7dea069bf5a6",
943            "start_timestamp": 123,
944            "name": "CACHE HIT",
945            "end_timestamp": 123.1,
946            "kind": "client",
947            "attributes": {
948                "db.system": {
949                    "value": "redis",
950                    "type": "string"
951                },
952                "db.statement": {
953                    "value": "GET s:user:123",
954                    "type": "string"
955                },
956                "sentry.op": {
957                    "value": "cache.hit",
958                    "type": "string"
959                }
960            }
961        }"#;
962        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
963        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
964        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
965        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
966        {
967          "timestamp": 123.1,
968          "start_timestamp": 123.0,
969          "exclusive_time": 100.0,
970          "op": "cache.hit",
971          "span_id": "e342abb1214ca181",
972          "parent_span_id": "0c7a7dea069bf5a6",
973          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
974          "status": "unknown",
975          "description": "CACHE HIT",
976          "data": {
977            "db.system": "redis",
978            "db.statement": "GET s:user:123",
979            "sentry.name": "CACHE HIT"
980          },
981          "kind": "client"
982        }
983        "###);
984    }
985
986    #[test]
987    fn parse_semantic_database_span() {
988        let json = r#"{
989            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
990            "span_id": "e342abb1214ca181",
991            "parent_span_id": "0c7a7dea069bf5a6",
992            "start_timestamp": 123,
993            "end_timestamp": 123.5,
994            "kind": "client",
995            "attributes": {
996                "db.system": {
997                    "value": "postgres",
998                    "type": "string"
999                },
1000                "db.statement": {
1001                    "value": "SELECT * FROM users",
1002                    "type": "string"
1003                }
1004            }
1005        }"#;
1006        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1007        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1008        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1009        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1010        {
1011          "timestamp": 123.5,
1012          "start_timestamp": 123.0,
1013          "exclusive_time": 500.0,
1014          "op": "db",
1015          "span_id": "e342abb1214ca181",
1016          "parent_span_id": "0c7a7dea069bf5a6",
1017          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1018          "status": "unknown",
1019          "description": "SELECT * FROM users",
1020          "data": {
1021            "db.system": "postgres",
1022            "db.statement": "SELECT * FROM users"
1023          },
1024          "kind": "client"
1025        }
1026        "###);
1027    }
1028
1029    #[test]
1030    fn parse_gen_ai_span() {
1031        let json = r#"{
1032            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1033            "span_id": "e342abb1214ca181",
1034            "parent_span_id": "0c7a7dea069bf5a6",
1035            "start_timestamp": 123,
1036            "end_timestamp": 123.5,
1037            "kind": "client",
1038            "attributes": {
1039                "gen_ai.system": {
1040                    "value": "openai",
1041                    "type": "string"
1042                },
1043                "gen_ai.agent.name": {
1044                    "value": "Seer",
1045                    "type": "string"
1046                }
1047            }
1048        }"#;
1049        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1050        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1051        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1052        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1053        {
1054          "timestamp": 123.5,
1055          "start_timestamp": 123.0,
1056          "exclusive_time": 500.0,
1057          "op": "gen_ai",
1058          "span_id": "e342abb1214ca181",
1059          "parent_span_id": "0c7a7dea069bf5a6",
1060          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1061          "status": "unknown",
1062          "data": {
1063            "gen_ai.system": "openai",
1064            "gen_ai.agent.name": "Seer"
1065          },
1066          "kind": "client"
1067        }
1068        "###);
1069    }
1070
1071    #[test]
1072    fn parse_span_with_sentry_op() {
1073        let json = r#"{
1074            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1075            "span_id": "e342abb1214ca181",
1076            "parent_span_id": "0c7a7dea069bf5a6",
1077            "start_timestamp": 123,
1078            "end_timestamp": 123.5,
1079            "kind": "client",
1080            "attributes": {
1081                "db.system": {
1082                    "value": "postgres",
1083                    "type": "string"
1084                },
1085                "sentry.op": {
1086                    "value": "function",
1087                    "type": "string"
1088                }
1089            }
1090        }"#;
1091        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1092        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1093        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1094        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1095        {
1096          "timestamp": 123.5,
1097          "start_timestamp": 123.0,
1098          "exclusive_time": 500.0,
1099          "op": "function",
1100          "span_id": "e342abb1214ca181",
1101          "parent_span_id": "0c7a7dea069bf5a6",
1102          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1103          "status": "unknown",
1104          "data": {
1105            "db.system": "postgres"
1106          },
1107          "kind": "client"
1108        }
1109        "###);
1110    }
1111
1112    #[test]
1113    fn extract_span_kind() {
1114        let json = r#"{
1115            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1116            "span_id": "e342abb1214ca181",
1117            "parent_span_id": "0c7a7dea069bf5a6",
1118            "start_timestamp": 123,
1119            "end_timestamp": 123.5,
1120            "kind": "client",
1121            "links": []
1122        }"#;
1123        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1124        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1125        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1126        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1127        {
1128          "timestamp": 123.5,
1129          "start_timestamp": 123.0,
1130          "exclusive_time": 500.0,
1131          "op": "default",
1132          "span_id": "e342abb1214ca181",
1133          "parent_span_id": "0c7a7dea069bf5a6",
1134          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1135          "status": "unknown",
1136          "data": {},
1137          "links": [],
1138          "kind": "client"
1139        }
1140        "###);
1141    }
1142
1143    #[test]
1144    fn parse_link() {
1145        let json = r#"{
1146            "trace_id": "3c79f60c11214eb38604f4ae0781bfb2",
1147            "links": [
1148                {
1149                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1150                    "span_id": "fa90fdead5f74052",
1151                    "sampled": true,
1152                    "attributes": {
1153                        "str_key": {
1154                            "value": "str_value",
1155                            "type": "string"
1156                        },
1157                        "bool_key": {
1158                            "value": true,
1159                            "type": "boolean"
1160                        },
1161                        "int_key": {
1162                            "value": 123,
1163                            "type": "i64"
1164                        },
1165                        "double_key": {
1166                            "value": 1.23,
1167                            "type": "f64"
1168                        }
1169                    }
1170                }
1171            ]
1172        }"#;
1173        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1174        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1175        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1176
1177        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1178        {
1179          "exclusive_time": 0.0,
1180          "op": "default",
1181          "trace_id": "3c79f60c11214eb38604f4ae0781bfb2",
1182          "status": "unknown",
1183          "data": {},
1184          "links": [
1185            {
1186              "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1187              "span_id": "fa90fdead5f74052",
1188              "sampled": true,
1189              "attributes": {
1190                "bool_key": true,
1191                "double_key": 1.23,
1192                "int_key": 123,
1193                "str_key": "str_value"
1194              }
1195            }
1196          ]
1197        }
1198        "###);
1199    }
1200
1201    #[test]
1202    fn parse_faas_trigger_span() {
1203        let json = r#"{
1204            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1205            "span_id": "e342abb1214ca181",
1206            "parent_span_id": "0c7a7dea069bf5a6",
1207            "name": "FAAS",
1208            "attributes": {
1209                "faas.trigger": {
1210                    "value": "http",
1211                    "type": "string"
1212                }
1213            }
1214        }"#;
1215        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1216        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1217        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1218        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1219        {
1220          "exclusive_time": 0.0,
1221          "op": "http",
1222          "span_id": "e342abb1214ca181",
1223          "parent_span_id": "0c7a7dea069bf5a6",
1224          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1225          "status": "unknown",
1226          "description": "FAAS",
1227          "data": {
1228            "faas.trigger": "http",
1229            "sentry.name": "FAAS"
1230          }
1231        }
1232        "###);
1233    }
1234
1235    #[test]
1236    fn parse_http_span_with_route() {
1237        let json = r#"{
1238            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1239            "span_id": "e342abb1214ca181",
1240            "parent_span_id": "0c7a7dea069bf5a6",
1241            "start_timestamp": 123,
1242            "end_timestamp": 123.5,
1243            "name": "GET /api/users",
1244            "kind": "server",
1245            "attributes": {
1246                "http.method": {
1247                    "value": "GET",
1248                    "type": "string"
1249                },
1250                "http.route": {
1251                    "value": "/api/users",
1252                    "type": "string"
1253                }
1254            }
1255        }"#;
1256        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1257        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1258        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1259        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1260        {
1261          "timestamp": 123.5,
1262          "start_timestamp": 123.0,
1263          "exclusive_time": 500.0,
1264          "op": "http.server",
1265          "span_id": "e342abb1214ca181",
1266          "parent_span_id": "0c7a7dea069bf5a6",
1267          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1268          "status": "unknown",
1269          "description": "GET /api/users",
1270          "data": {
1271            "http.request_method": "GET",
1272            "http.route": "/api/users",
1273            "sentry.name": "GET /api/users"
1274          },
1275          "kind": "server"
1276        }
1277        "###);
1278    }
1279
1280    #[test]
1281    fn parse_db_span_with_statement() {
1282        let json = r#"{
1283            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1284            "span_id": "e342abb1214ca181",
1285            "parent_span_id": "0c7a7dea069bf5a6",
1286            "start_timestamp": 123,
1287            "end_timestamp": 123.5,
1288            "name": "SELECT users",
1289            "kind": "client",
1290            "attributes": {
1291                "db.system": {
1292                    "value": "postgres",
1293                    "type": "string"
1294                },
1295                "db.statement": {
1296                    "value": "SELECT * FROM users WHERE id = $1",
1297                    "type": "string"
1298                }
1299            }
1300        }"#;
1301        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1302        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1303        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1304        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1305        {
1306          "timestamp": 123.5,
1307          "start_timestamp": 123.0,
1308          "exclusive_time": 500.0,
1309          "op": "db",
1310          "span_id": "e342abb1214ca181",
1311          "parent_span_id": "0c7a7dea069bf5a6",
1312          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1313          "status": "unknown",
1314          "description": "SELECT * FROM users WHERE id = $1",
1315          "data": {
1316            "db.system": "postgres",
1317            "db.statement": "SELECT * FROM users WHERE id = $1",
1318            "sentry.name": "SELECT users"
1319          },
1320          "kind": "client"
1321        }
1322        "###);
1323    }
1324
1325    #[test]
1326    fn parse_http_span_with_graphql() {
1327        let json = r#"{
1328            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1329            "span_id": "e342abb1214ca181",
1330            "parent_span_id": "0c7a7dea069bf5a6",
1331            "start_timestamp": 123,
1332            "end_timestamp": 123.5,
1333            "name": "POST /graphql",
1334            "kind": "server",
1335            "attributes": {
1336                "http.method": {
1337                    "value": "POST",
1338                    "type": "string"
1339                },
1340                "http.route": {
1341                    "value": "/graphql",
1342                    "type": "string"
1343                },
1344                "sentry.graphql.operation": {
1345                    "value": "getUserById",
1346                    "type": "string"
1347                }
1348            }
1349        }"#;
1350        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1351        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1352        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1353        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1354        {
1355          "timestamp": 123.5,
1356          "start_timestamp": 123.0,
1357          "exclusive_time": 500.0,
1358          "op": "http.server",
1359          "span_id": "e342abb1214ca181",
1360          "parent_span_id": "0c7a7dea069bf5a6",
1361          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1362          "status": "unknown",
1363          "description": "POST /graphql (getUserById)",
1364          "data": {
1365            "http.request_method": "POST",
1366            "http.route": "/graphql",
1367            "sentry.graphql.operation": "getUserById",
1368            "sentry.name": "POST /graphql"
1369          },
1370          "kind": "server"
1371        }
1372        "###);
1373    }
1374
1375    #[test]
1376    fn parse_error_status() {
1377        let json = r#"{
1378            "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1379            "span_id": "e342abb1214ca181",
1380            "parent_span_id": "0c7a7dea069bf5a6",
1381            "start_timestamp": 123,
1382            "end_timestamp": 123.5,
1383            "status": "error"
1384        }"#;
1385        let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1386        let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1387        let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1388        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1389        {
1390          "timestamp": 123.5,
1391          "start_timestamp": 123.0,
1392          "exclusive_time": 500.0,
1393          "op": "default",
1394          "span_id": "e342abb1214ca181",
1395          "parent_span_id": "0c7a7dea069bf5a6",
1396          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1397          "status": "internal_error",
1398          "data": {}
1399        }
1400        "###);
1401    }
1402}