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
19pub 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 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 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 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 let op = op.or_else(|| Annotated::from(inferred_op));
139
140 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
222fn derive_op_for_v2_span(span: &SpanV2) -> String {
229 let op = String::from("default");
231
232 let Some(attributes) = span.attributes.value() else {
233 return op;
234 };
235
236 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 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
275fn derive_description_for_v2_span(span: &SpanV2) -> Option<String> {
281 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 let http_method = attributes
302 .get_value(HTTP_REQUEST_METHOD)
303 .or_else(|| attributes.get_value("http.method"))
305 .and_then(|v| v.as_str())?;
306
307 let description = http_method.to_owned();
308
309 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 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 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 attributes
347 .get_value(DB_SYSTEM_NAME)
348 .or_else(|| attributes.get_value("db.system"))
350 .and_then(|v| v.as_str())?;
351
352 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 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 if let Some(route) = attributes.get_value(HTTP_ROUTE).and_then(|v| v.as_str()) {
370 return Some(route.to_owned());
371 }
372
373 if let Some(path) = attributes.get_value(URL_PATH).and_then(|v| v.as_str()) {
375 return Some(path.to_owned());
376 }
377
378 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 .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}