relay_spans/
v2_to_v1.rs

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