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
13pub 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 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 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 let op = op.or_else(|| Annotated::from(inferred_op));
135
136 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
229fn derive_op_for_v2_span(span: &SpanV2) -> String {
236 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
283fn derive_description_for_v2_span(span: &SpanV2) -> Option<String> {
289 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 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 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 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 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 attributes
354 .get_value("db.system")
355 .or_else(|| attributes.get_value("db.system.name"))
356 .and_then(|v| v.as_str())?;
357
358 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 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 if let Some(route) = attributes.get_value("http.route").and_then(|v| v.as_str()) {
382 return Some(route.to_owned());
383 }
384
385 if let Some(path) = attributes.get_value("url.path").and_then(|v| v.as_str()) {
387 return Some(path.to_owned());
388 }
389
390 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}