1use relay_conventions::attributes::{
4 BROWSER__NAME, HTTP__QUERY, SENTRY__ENVIRONMENT, SENTRY__EVENT__SERIALIZED_BREADCRUMBS,
5 SENTRY__EVENT__SERIALIZED_CONTEXTS, SENTRY__EVENT__SERIALIZED_EXTRA,
6 SENTRY__EVENT__SERIALIZED_META, SENTRY__EVENT__SERIALIZED_REQUEST, SENTRY__RELEASE,
7 SENTRY__SDK__NAME, SENTRY__SDK__VERSION, SENTRY__SEGMENT__NAME, URL__QUERY,
8};
9use relay_protocol::{Empty, IntoValue, MetaTree};
10
11use crate::protocol::{BrowserContext, Event, ProfileContext, Span, SpanData, TraceContext};
12
13impl From<&Event> for Span {
14 fn from(event: &Event) -> Self {
15 let Event {
16 transaction,
17
18 platform,
19 timestamp,
20 start_timestamp,
21 received,
22 release,
23 environment,
24 tags,
25 contexts,
26 breadcrumbs,
27 extra,
28 request,
29 measurements,
30 _metrics,
31 ..
32 } = event;
33
34 let trace = event.context::<TraceContext>();
35
36 let mut data = trace
38 .map(|c| c.data.clone().map_value(SpanData::from))
39 .unwrap_or_default();
40
41 let span_data = data.get_or_insert_with(Default::default);
43 span_data.insert(
44 SENTRY__SEGMENT__NAME,
45 transaction.clone().map_value(IntoValue::into_value),
46 );
47 span_data.insert(
49 "sentry.name",
50 transaction.clone().map_value(IntoValue::into_value),
51 );
52 span_data.insert(
53 SENTRY__RELEASE,
54 release.clone().map_value(IntoValue::into_value),
55 );
56 span_data.insert(
57 SENTRY__ENVIRONMENT,
58 environment.clone().map_value(IntoValue::into_value),
59 );
60 if let Some(browser) = event.context::<BrowserContext>() {
61 span_data.insert(
62 BROWSER__NAME,
63 browser.name.clone().map_value(IntoValue::into_value),
64 );
65 }
66 if let Some(client_sdk) = event.client_sdk.value() {
67 span_data.insert(
68 SENTRY__SDK__NAME,
69 client_sdk.name.clone().map_value(IntoValue::into_value),
70 );
71 span_data.insert(
72 SENTRY__SDK__VERSION,
73 client_sdk.version.clone().map_value(IntoValue::into_value),
74 );
75 }
76 if let Some(request) = request.value()
77 && let Some(query) = request.query_string.value()
78 && let Some(qs) = query.to_query_string()
79 {
80 span_data.insert_value(HTTP__QUERY, format!("?{qs}"));
81 span_data.insert_value(URL__QUERY, qs);
82 }
83
84 if contexts.value().is_some_and(|c| !c.0.is_empty())
85 && let Ok(json) = contexts.payload_to_json()
86 {
87 span_data.insert_value(SENTRY__EVENT__SERIALIZED_CONTEXTS, json);
88 }
89 if breadcrumbs
90 .value()
91 .and_then(|b| b.values.value())
92 .is_some_and(|v| !v.is_empty())
93 && let Ok(json) = breadcrumbs.payload_to_json()
94 {
95 span_data.insert_value(SENTRY__EVENT__SERIALIZED_BREADCRUMBS, json);
96 }
97 if extra.value().is_some_and(|e| !e.is_empty())
98 && let Ok(json) = extra.payload_to_json()
99 {
100 span_data.insert_value(SENTRY__EVENT__SERIALIZED_EXTRA, json);
101 }
102 if request.value().is_some_and(|r| !r.is_empty())
103 && let Ok(json) = request.payload_to_json()
104 {
105 span_data.insert_value(SENTRY__EVENT__SERIALIZED_REQUEST, json);
106 }
107
108 let mut meta = MetaTree::default();
109 let contexts_meta = IntoValue::extract_meta_tree(contexts);
110 if !contexts_meta.is_empty() {
111 meta.children.insert("contexts".to_owned(), contexts_meta);
112 }
113 let breadcrumbs_meta = IntoValue::extract_meta_tree(breadcrumbs);
114 if !breadcrumbs_meta.is_empty() {
115 meta.children
116 .insert("breadcrumbs".to_owned(), breadcrumbs_meta);
117 }
118 let extra_meta = IntoValue::extract_meta_tree(extra);
119 if !extra_meta.is_empty() {
120 meta.children.insert("extra".to_owned(), extra_meta);
121 }
122 let request_meta = IntoValue::extract_meta_tree(request);
123 if !request_meta.is_empty() {
124 meta.children.insert("request".to_owned(), request_meta);
125 }
126 if !meta.is_empty()
127 && let Ok(json) = serde_json::to_string(&meta)
128 {
129 span_data.insert_value(SENTRY__EVENT__SERIALIZED_META, json);
130 }
131
132 Self {
133 timestamp: timestamp.clone(),
134 start_timestamp: start_timestamp.clone(),
135 exclusive_time: trace.map(|c| c.exclusive_time.clone()).unwrap_or_default(),
136 op: trace.map(|c| c.op.clone()).unwrap_or_default(),
137 span_id: trace.map(|c| c.span_id.clone()).unwrap_or_default(),
138 parent_span_id: trace.map(|c| c.parent_span_id.clone()).unwrap_or_default(),
139 trace_id: trace.map(|c| c.trace_id.clone()).unwrap_or_default(),
140 segment_id: trace.map(|c| c.span_id.clone()).unwrap_or_default(),
141 is_segment: true.into(),
142 is_remote: true.into(),
147 status: trace.map(|c| c.status.clone()).unwrap_or_default(),
148 description: transaction.clone(),
149 tags: tags.clone().map_value(|t| t.into()),
150 origin: trace.map(|c| c.origin.clone()).unwrap_or_default(),
151 profile_id: event
152 .context::<ProfileContext>()
153 .map(|c| c.profile_id.clone())
154 .unwrap_or_default(),
155 data,
156 links: trace.map(|c| c.links.clone()).unwrap_or_default(),
157 sentry_tags: Default::default(),
158 received: received.clone(),
159 measurements: measurements.clone(),
160 platform: platform.clone(),
161 was_transaction: true.into(),
162 kind: Default::default(),
163 other: Default::default(),
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use relay_protocol::Annotated;
171
172 use super::*;
173
174 #[test]
175 fn convert() {
176 let event = Annotated::<Event>::from_json(
177 r#"{
178 "type": "transaction",
179 "platform": "php",
180 "sdk": {"name": "sentry.php", "version": "1.2.3"},
181 "release": "myapp@1.0.0",
182 "environment": "prod",
183 "transaction": "my 1st transaction",
184 "contexts": {
185 "browser": {"name": "Chrome"},
186 "profile": {"profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab"},
187 "trace": {
188 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
189 "span_id": "FA90FDEAD5F74052",
190 "type": "trace",
191 "origin": "manual",
192 "op": "myop",
193 "status": "ok",
194 "exclusive_time": 123.4,
195 "parent_span_id": "FA90FDEAD5F74051",
196 "data": {
197 "custom_attribute": 42
198 },
199 "links": [
200 {
201 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
202 "span_id": "fa90fdead5f74052",
203 "sampled": true,
204 "attributes": {
205 "sentry.link.type": "previous_trace"
206 }
207 }
208 ]
209 }
210 },
211 "breadcrumbs": [
212 {"type": "default", "category": "auth", "message": "login"}
213 ],
214 "extra": {
215 "my_key": 1,
216 "some_other_value": "foo bar"
217 },
218 "request": {
219 "url": "http://example.com/api/0/organizations/",
220 "method": "GET",
221 "query_string": "project=1&sort=date"
222 },
223 "measurements": {
224 "memory": {
225 "value": 9001.0,
226 "unit": "byte"
227 }
228 }
229 }"#,
230 )
231 .unwrap()
232 .into_value()
233 .unwrap();
234
235 let span_from_event = Span::from(&event);
236 insta::assert_debug_snapshot!(span_from_event, @r#"
237 Span {
238 timestamp: ~,
239 start_timestamp: ~,
240 exclusive_time: 123.4,
241 op: "myop",
242 span_id: SpanId("fa90fdead5f74052"),
243 parent_span_id: SpanId("fa90fdead5f74051"),
244 trace_id: TraceId("4c79f60c11214eb38604f4ae0781bfb2"),
245 segment_id: SpanId("fa90fdead5f74052"),
246 is_segment: true,
247 is_remote: true,
248 status: Ok,
249 description: "my 1st transaction",
250 tags: ~,
251 origin: "manual",
252 profile_id: EventId(
253 a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab,
254 ),
255 data: SpanData {
256 other: {
257 "browser.name": String(
258 "Chrome",
259 ),
260 "custom_attribute": I64(
261 42,
262 ),
263 "http.query": String(
264 "?project=1&sort=date",
265 ),
266 "sentry.environment": String(
267 "prod",
268 ),
269 "sentry.event.serialized_breadcrumbs": String(
270 "{\"values\":[{\"type\":\"default\",\"category\":\"auth\",\"message\":\"login\"}]}",
271 ),
272 "sentry.event.serialized_contexts": String(
273 "{\"browser\":{\"name\":\"Chrome\",\"type\":\"browser\"},\"profile\":{\"profile_id\":\"a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab\",\"type\":\"profile\"},\"trace\":{\"trace_id\":\"4c79f60c11214eb38604f4ae0781bfb2\",\"span_id\":\"fa90fdead5f74052\",\"parent_span_id\":\"fa90fdead5f74051\",\"op\":\"myop\",\"status\":\"ok\",\"exclusive_time\":123.4,\"origin\":\"manual\",\"data\":{\"custom_attribute\":42},\"links\":[{\"trace_id\":\"4c79f60c11214eb38604f4ae0781bfb2\",\"span_id\":\"fa90fdead5f74052\",\"sampled\":true,\"attributes\":{\"sentry.link.type\":\"previous_trace\"}}],\"type\":\"trace\"}}",
274 ),
275 "sentry.event.serialized_extra": String(
276 "{\"my_key\":1,\"some_other_value\":\"foo bar\"}",
277 ),
278 "sentry.event.serialized_request": String(
279 "{\"url\":\"http://example.com/api/0/organizations/\",\"method\":\"GET\",\"query_string\":[[\"project\",\"1\"],[\"sort\",\"date\"]]}",
280 ),
281 "sentry.name": String(
282 "my 1st transaction",
283 ),
284 "sentry.release": String(
285 "myapp@1.0.0",
286 ),
287 "sentry.sdk.name": String(
288 "sentry.php",
289 ),
290 "sentry.sdk.version": String(
291 "1.2.3",
292 ),
293 "sentry.segment.name": String(
294 "my 1st transaction",
295 ),
296 "url.query": String(
297 "project=1&sort=date",
298 ),
299 },
300 },
301 links: [
302 SpanLink {
303 trace_id: TraceId("4c79f60c11214eb38604f4ae0781bfb2"),
304 span_id: SpanId("fa90fdead5f74052"),
305 sampled: true,
306 attributes: {
307 "sentry.link.type": String(
308 "previous_trace",
309 ),
310 },
311 other: {},
312 },
313 ],
314 sentry_tags: ~,
315 received: ~,
316 measurements: Measurements(
317 {
318 "memory": Measurement {
319 value: 9001.0,
320 unit: Information(
321 Byte,
322 ),
323 },
324 },
325 ),
326 platform: "php",
327 was_transaction: true,
328 kind: ~,
329 other: {},
330 }
331 "#);
332 }
333
334 #[test]
335 fn convert_preserves_contexts_breadcrumbs_extra() {
336 let event = Annotated::<Event>::from_json(
337 r#"{
338 "type": "transaction",
339 "transaction": "my transaction",
340 "contexts": {
341 "browser": {"name": "Chrome"},
342 "trace": {
343 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
344 "span_id": "fa90fdead5f74052"
345 }
346 },
347 "breadcrumbs": [
348 {"type": "default", "category": "auth", "message": "login"}
349 ],
350 "extra": {
351 "my_key": 1,
352 "some_other_value": "foo bar"
353 }
354 }"#,
355 )
356 .unwrap()
357 .into_value()
358 .unwrap();
359
360 let span = Span::from(&event);
361 let data = span.data.value().unwrap();
362
363 assert_eq!(
364 data.get_str(SENTRY__EVENT__SERIALIZED_CONTEXTS),
365 Some(
366 r#"{"browser":{"name":"Chrome","type":"browser"},"trace":{"trace_id":"4c79f60c11214eb38604f4ae0781bfb2","span_id":"fa90fdead5f74052","type":"trace"}}"#
367 )
368 );
369 assert_eq!(
370 data.get_str(SENTRY__EVENT__SERIALIZED_BREADCRUMBS),
371 Some(r#"{"values":[{"type":"default","category":"auth","message":"login"}]}"#)
372 );
373 assert_eq!(
374 data.get_str(SENTRY__EVENT__SERIALIZED_EXTRA),
375 Some(r#"{"my_key":1,"some_other_value":"foo bar"}"#)
376 );
377 }
378
379 #[test]
380 fn convert_preserves_request() {
381 let event = Annotated::<Event>::from_json(
382 r#"{
383 "type": "transaction",
384 "request": {
385 "url": "https://example.com/api/",
386 "method": "POST",
387 "data": {"answer": 42},
388 "query_string": "project=1",
389 "headers": {"content-type": "application/json"},
390 "custom": "value"
391 },
392 "_meta": {
393 "request": {"url": {"": {"len": 100}}}
394 }
395 }"#,
396 )
397 .unwrap()
398 .into_value()
399 .unwrap();
400
401 let span = Span::from(&event);
402 let data = span.data.value().unwrap();
403
404 assert_eq!(
405 data.get_str(SENTRY__EVENT__SERIALIZED_REQUEST),
406 Some(
407 r#"{"url":"https://example.com/api/","method":"POST","data":{"answer":42},"query_string":[["project","1"]],"headers":[["Content-Type","application/json"]],"custom":"value"}"#
408 )
409 );
410 assert_eq!(
411 data.get_str(SENTRY__EVENT__SERIALIZED_META),
412 Some(r#"{"request":{"url":{"":{"len":100}}}}"#)
413 );
414 }
415
416 #[test]
417 fn convert_omits_absent_or_empty_request() {
418 for json in [
419 r#"{"type":"transaction"}"#,
420 r#"{"type":"transaction","request":null}"#,
421 r#"{"type":"transaction","request":{}}"#,
422 ] {
423 let event = Annotated::<Event>::from_json(json)
424 .unwrap()
425 .into_value()
426 .unwrap();
427
428 let span = Span::from(&event);
429 let data = span.data.value().unwrap();
430
431 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_REQUEST));
432 }
433 }
434
435 #[test]
436 fn convert_preserves_trace_only_contexts() {
437 let event = Annotated::<Event>::from_json(
438 r#"{
439 "type": "transaction",
440 "contexts": {
441 "trace": {
442 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
443 "span_id": "fa90fdead5f74052"
444 }
445 }
446 }"#,
447 )
448 .unwrap()
449 .into_value()
450 .unwrap();
451
452 let span = Span::from(&event);
453 let data = span.data.value().unwrap();
454
455 assert_eq!(
456 data.get_str(SENTRY__EVENT__SERIALIZED_CONTEXTS),
457 Some(
458 r#"{"trace":{"trace_id":"4c79f60c11214eb38604f4ae0781bfb2","span_id":"fa90fdead5f74052","type":"trace"}}"#
459 )
460 );
461 }
462
463 #[test]
464 fn convert_omits_absent_contexts_breadcrumbs_extra() {
465 let event = Annotated::<Event>::from_json(
466 r#"{
467 "type": "transaction",
468 "transaction": "my transaction"
469 }"#,
470 )
471 .unwrap()
472 .into_value()
473 .unwrap();
474
475 let span = Span::from(&event);
476
477 if let Some(data) = span.data.value() {
478 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_CONTEXTS));
479 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_BREADCRUMBS));
480 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_EXTRA));
481 }
482 }
483
484 #[test]
485 fn convert_omits_empty_contexts_breadcrumbs_extra() {
486 let event = Annotated::<Event>::from_json(
487 r#"{
488 "type": "transaction",
489 "transaction": "my transaction",
490 "contexts": {},
491 "breadcrumbs": [],
492 "extra": {}
493 }"#,
494 )
495 .unwrap()
496 .into_value()
497 .unwrap();
498
499 let span = Span::from(&event);
500
501 if let Some(data) = span.data.value() {
502 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_CONTEXTS));
503 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_BREADCRUMBS));
504 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_EXTRA));
505 }
506 }
507
508 #[test]
509 fn convert_preserves_meta() {
510 let event = Annotated::<Event>::from_json(
511 r#"{
512 "type": "transaction",
513 "transaction": "my transaction",
514 "contexts": {
515 "browser": {"name": "Chrome"},
516 "trace": {
517 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
518 "span_id": "fa90fdead5f74052"
519 }
520 },
521 "breadcrumbs": [
522 {"type": "default", "category": "auth", "message": "login"}
523 ],
524 "extra": {
525 "my_key": "[Filtered]"
526 },
527 "request": {
528 "data": {"note": "[Filtered]"}
529 },
530 "_meta": {
531 "contexts": {
532 "browser": {
533 "name": {"": {"rem": [["browser_rule", "s"]]}}
534 },
535 "trace": {
536 "trace_id": {"": {"rem": [["trace_rule", "s"]]}}
537 }
538 },
539 "breadcrumbs": {
540 "0": {
541 "message": {"": {"rem": [["breadcrumb_rule", "s"]]}}
542 }
543 },
544 "extra": {
545 "my_key": {"": {"rem": [["extra_rule", "s", 0, 10]]}}
546 },
547 "request": {
548 "data": {
549 "note": {"": {"rem": [["request_rule", "s"]]}}
550 }
551 }
552 }
553 }"#,
554 )
555 .unwrap()
556 .into_value()
557 .unwrap();
558
559 let span = Span::from(&event);
560 let data = span.data.value().unwrap();
561
562 let meta = data.get_str(SENTRY__EVENT__SERIALIZED_META).unwrap();
563
564 assert_eq!(
565 meta,
566 r#"{"breadcrumbs":{"values":{"0":{"message":{"":{"rem":[["breadcrumb_rule","s"]]}}}}},"contexts":{"browser":{"name":{"":{"rem":[["browser_rule","s"]]}}},"trace":{"trace_id":{"":{"rem":[["trace_rule","s"]]}}}},"extra":{"my_key":{"":{"rem":[["extra_rule","s",0,10]]}}},"request":{"data":{"note":{"":{"rem":[["request_rule","s"]]}}}}}"#
567 );
568 }
569
570 #[test]
571 fn convert_omits_meta_when_absent() {
572 let event = Annotated::<Event>::from_json(
573 r#"{
574 "type": "transaction",
575 "transaction": "my transaction",
576 "contexts": {
577 "browser": {"name": "Chrome"}
578 },
579 "extra": {
580 "my_key": 1
581 }
582 }"#,
583 )
584 .unwrap()
585 .into_value()
586 .unwrap();
587
588 let span = Span::from(&event);
589
590 if let Some(data) = span.data.value() {
591 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_META));
592 }
593 }
594
595 #[test]
596 fn convert_preserves_meta_when_only_trace_context_has_meta() {
597 let event = Annotated::<Event>::from_json(
598 r#"{
599 "type": "transaction",
600 "transaction": "my transaction",
601 "contexts": {
602 "trace": {
603 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
604 "span_id": "fa90fdead5f74052"
605 }
606 },
607 "_meta": {
608 "contexts": {
609 "trace": {
610 "trace_id": {"": {"rem": [["trace_rule", "s"]]}}
611 }
612 }
613 }
614 }"#,
615 )
616 .unwrap()
617 .into_value()
618 .unwrap();
619
620 let span = Span::from(&event);
621 let data = span.data.value().unwrap();
622
623 assert_eq!(
624 data.get_str(SENTRY__EVENT__SERIALIZED_META),
625 Some(r#"{"contexts":{"trace":{"trace_id":{"":{"rem":[["trace_rule","s"]]}}}}}"#)
626 );
627 }
628}