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