1use relay_conventions::attributes::{
4 BROWSER__NAME, HTTP__QUERY, SENTRY__ENVIRONMENT, SENTRY__EVENT__SERIALIZED_BREADCRUMBS,
5 SENTRY__EVENT__SERIALIZED_CONTEXTS, SENTRY__EVENT__SERIALIZED_EXTRA, SENTRY__RELEASE,
6 SENTRY__SDK__NAME, SENTRY__SDK__VERSION, SENTRY__SEGMENT__NAME, URL__QUERY,
7};
8use relay_protocol::{IntoValue, Object, SerializePayload, SkipSerialization};
9use serde::ser::SerializeMap;
10use serde::{Serialize, Serializer};
11
12use crate::protocol::{
13 BrowserContext, ContextInner, DefaultContext, Event, ProfileContext, Span, SpanData,
14 TraceContext,
15};
16
17struct ContextsWithout<'a> {
19 contexts: &'a Object<ContextInner>,
20 skip_key: &'a str,
21}
22
23impl Serialize for ContextsWithout<'_> {
24 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
25 where
26 S: Serializer,
27 {
28 let behavior = SkipSerialization::default();
29 let mut map = serializer.serialize_map(None)?;
30 for (key, value) in self.contexts {
31 if key == self.skip_key || value.skip_serialization(behavior) {
32 continue;
33 }
34 map.serialize_entry(key, &SerializePayload(value, behavior))?;
35 }
36 map.end()
37 }
38}
39
40impl From<&Event> for Span {
41 fn from(event: &Event) -> Self {
42 let Event {
43 transaction,
44
45 platform,
46 timestamp,
47 start_timestamp,
48 received,
49 release,
50 environment,
51 tags,
52 contexts,
53 breadcrumbs,
54 extra,
55 measurements,
56 _metrics,
57 ..
58 } = event;
59
60 let trace = event.context::<TraceContext>();
61
62 let mut data = trace
64 .map(|c| c.data.clone().map_value(SpanData::from))
65 .unwrap_or_default();
66
67 let span_data = data.get_or_insert_with(Default::default);
69 span_data.insert(
70 SENTRY__SEGMENT__NAME,
71 transaction.clone().map_value(IntoValue::into_value),
72 );
73 span_data.insert(
75 "sentry.name",
76 transaction.clone().map_value(IntoValue::into_value),
77 );
78 span_data.insert(
79 SENTRY__RELEASE,
80 release.clone().map_value(IntoValue::into_value),
81 );
82 span_data.insert(
83 SENTRY__ENVIRONMENT,
84 environment.clone().map_value(IntoValue::into_value),
85 );
86 if let Some(browser) = event.context::<BrowserContext>() {
87 span_data.insert(
88 BROWSER__NAME,
89 browser.name.clone().map_value(IntoValue::into_value),
90 );
91 }
92 if let Some(client_sdk) = event.client_sdk.value() {
93 span_data.insert(
94 SENTRY__SDK__NAME,
95 client_sdk.name.clone().map_value(IntoValue::into_value),
96 );
97 span_data.insert(
98 SENTRY__SDK__VERSION,
99 client_sdk.version.clone().map_value(IntoValue::into_value),
100 );
101 }
102 if let Some(request) = event.request.value()
103 && let Some(query) = request.query_string.value()
104 && let Some(qs) = query.to_query_string()
105 {
106 span_data.insert_value(HTTP__QUERY, format!("?{qs}"));
107 span_data.insert_value(URL__QUERY, qs);
108 }
109
110 if let Some(contexts) = contexts.value() {
111 let has_other = contexts.0.iter().any(|(key, value)| {
112 key != TraceContext::default_key()
113 && !value.skip_serialization(SkipSerialization::default())
114 });
115 if has_other {
116 let payload = ContextsWithout {
117 contexts: &contexts.0,
118 skip_key: TraceContext::default_key(),
119 };
120 if let Ok(json) = serde_json::to_string(&payload) {
121 span_data.insert_value(SENTRY__EVENT__SERIALIZED_CONTEXTS, json);
122 }
123 }
124 }
125 if breadcrumbs
126 .value()
127 .and_then(|b| b.values.value())
128 .is_some_and(|v| !v.is_empty())
129 && let Ok(json) = breadcrumbs.payload_to_json()
130 {
131 span_data.insert_value(SENTRY__EVENT__SERIALIZED_BREADCRUMBS, json);
132 }
133 if extra.value().is_some_and(|e| !e.is_empty())
134 && let Ok(json) = extra.payload_to_json()
135 {
136 span_data.insert_value(SENTRY__EVENT__SERIALIZED_EXTRA, json);
137 }
138
139 Self {
140 timestamp: timestamp.clone(),
141 start_timestamp: start_timestamp.clone(),
142 exclusive_time: trace.map(|c| c.exclusive_time.clone()).unwrap_or_default(),
143 op: trace.map(|c| c.op.clone()).unwrap_or_default(),
144 span_id: trace.map(|c| c.span_id.clone()).unwrap_or_default(),
145 parent_span_id: trace.map(|c| c.parent_span_id.clone()).unwrap_or_default(),
146 trace_id: trace.map(|c| c.trace_id.clone()).unwrap_or_default(),
147 segment_id: trace.map(|c| c.span_id.clone()).unwrap_or_default(),
148 is_segment: true.into(),
149 is_remote: true.into(),
154 status: trace.map(|c| c.status.clone()).unwrap_or_default(),
155 description: transaction.clone(),
156 tags: tags.clone().map_value(|t| t.into()),
157 origin: trace.map(|c| c.origin.clone()).unwrap_or_default(),
158 profile_id: event
159 .context::<ProfileContext>()
160 .map(|c| c.profile_id.clone())
161 .unwrap_or_default(),
162 data,
163 links: trace.map(|c| c.links.clone()).unwrap_or_default(),
164 sentry_tags: Default::default(),
165 received: received.clone(),
166 measurements: measurements.clone(),
167 platform: platform.clone(),
168 was_transaction: true.into(),
169 kind: Default::default(),
170 other: Default::default(),
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use relay_protocol::Annotated;
178
179 use super::*;
180
181 #[test]
182 fn convert() {
183 let event = Annotated::<Event>::from_json(
184 r#"{
185 "type": "transaction",
186 "platform": "php",
187 "sdk": {"name": "sentry.php", "version": "1.2.3"},
188 "release": "myapp@1.0.0",
189 "environment": "prod",
190 "transaction": "my 1st transaction",
191 "contexts": {
192 "browser": {"name": "Chrome"},
193 "profile": {"profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab"},
194 "trace": {
195 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
196 "span_id": "FA90FDEAD5F74052",
197 "type": "trace",
198 "origin": "manual",
199 "op": "myop",
200 "status": "ok",
201 "exclusive_time": 123.4,
202 "parent_span_id": "FA90FDEAD5F74051",
203 "data": {
204 "custom_attribute": 42
205 },
206 "links": [
207 {
208 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
209 "span_id": "fa90fdead5f74052",
210 "sampled": true,
211 "attributes": {
212 "sentry.link.type": "previous_trace"
213 }
214 }
215 ]
216 }
217 },
218 "breadcrumbs": [
219 {"type": "default", "category": "auth", "message": "login"}
220 ],
221 "extra": {
222 "my_key": 1,
223 "some_other_value": "foo bar"
224 },
225 "request": {
226 "url": "http://example.com/api/0/organizations/",
227 "method": "GET",
228 "query_string": "project=1&sort=date"
229 },
230 "measurements": {
231 "memory": {
232 "value": 9001.0,
233 "unit": "byte"
234 }
235 }
236 }"#,
237 )
238 .unwrap()
239 .into_value()
240 .unwrap();
241
242 let span_from_event = Span::from(&event);
243 insta::assert_debug_snapshot!(span_from_event, @r#"
244 Span {
245 timestamp: ~,
246 start_timestamp: ~,
247 exclusive_time: 123.4,
248 op: "myop",
249 span_id: SpanId("fa90fdead5f74052"),
250 parent_span_id: SpanId("fa90fdead5f74051"),
251 trace_id: TraceId("4c79f60c11214eb38604f4ae0781bfb2"),
252 segment_id: SpanId("fa90fdead5f74052"),
253 is_segment: true,
254 is_remote: true,
255 status: Ok,
256 description: "my 1st transaction",
257 tags: ~,
258 origin: "manual",
259 profile_id: EventId(
260 a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab,
261 ),
262 data: SpanData {
263 other: {
264 "browser.name": String(
265 "Chrome",
266 ),
267 "custom_attribute": I64(
268 42,
269 ),
270 "http.query": String(
271 "?project=1&sort=date",
272 ),
273 "sentry.environment": String(
274 "prod",
275 ),
276 "sentry.event.serialized_breadcrumbs": String(
277 "{\"values\":[{\"type\":\"default\",\"category\":\"auth\",\"message\":\"login\"}]}",
278 ),
279 "sentry.event.serialized_contexts": String(
280 "{\"browser\":{\"name\":\"Chrome\",\"type\":\"browser\"},\"profile\":{\"profile_id\":\"a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab\",\"type\":\"profile\"}}",
281 ),
282 "sentry.event.serialized_extra": String(
283 "{\"my_key\":1,\"some_other_value\":\"foo bar\"}",
284 ),
285 "sentry.name": String(
286 "my 1st transaction",
287 ),
288 "sentry.release": String(
289 "myapp@1.0.0",
290 ),
291 "sentry.sdk.name": String(
292 "sentry.php",
293 ),
294 "sentry.sdk.version": String(
295 "1.2.3",
296 ),
297 "sentry.segment.name": String(
298 "my 1st transaction",
299 ),
300 "url.query": String(
301 "project=1&sort=date",
302 ),
303 },
304 },
305 links: [
306 SpanLink {
307 trace_id: TraceId("4c79f60c11214eb38604f4ae0781bfb2"),
308 span_id: SpanId("fa90fdead5f74052"),
309 sampled: true,
310 attributes: {
311 "sentry.link.type": String(
312 "previous_trace",
313 ),
314 },
315 other: {},
316 },
317 ],
318 sentry_tags: ~,
319 received: ~,
320 measurements: Measurements(
321 {
322 "memory": Measurement {
323 value: 9001.0,
324 unit: Information(
325 Byte,
326 ),
327 },
328 },
329 ),
330 platform: "php",
331 was_transaction: true,
332 kind: ~,
333 other: {},
334 }
335 "#);
336 }
337
338 #[test]
339 fn convert_preserves_contexts_breadcrumbs_extra() {
340 let event = Annotated::<Event>::from_json(
341 r#"{
342 "type": "transaction",
343 "transaction": "my transaction",
344 "contexts": {
345 "browser": {"name": "Chrome"},
346 "trace": {
347 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
348 "span_id": "fa90fdead5f74052"
349 }
350 },
351 "breadcrumbs": [
352 {"type": "default", "category": "auth", "message": "login"}
353 ],
354 "extra": {
355 "my_key": 1,
356 "some_other_value": "foo bar"
357 }
358 }"#,
359 )
360 .unwrap()
361 .into_value()
362 .unwrap();
363
364 let span = Span::from(&event);
365 let data = span.data.value().unwrap();
366
367 assert_eq!(
368 data.get_str(SENTRY__EVENT__SERIALIZED_CONTEXTS),
369 Some(r#"{"browser":{"name":"Chrome","type":"browser"}}"#)
370 );
371 assert_eq!(
372 data.get_str(SENTRY__EVENT__SERIALIZED_BREADCRUMBS),
373 Some(r#"{"values":[{"type":"default","category":"auth","message":"login"}]}"#)
374 );
375 assert_eq!(
376 data.get_str(SENTRY__EVENT__SERIALIZED_EXTRA),
377 Some(r#"{"my_key":1,"some_other_value":"foo bar"}"#)
378 );
379 }
380
381 #[test]
382 fn convert_omits_absent_contexts_breadcrumbs_extra() {
383 let event = Annotated::<Event>::from_json(
384 r#"{
385 "type": "transaction",
386 "transaction": "my transaction"
387 }"#,
388 )
389 .unwrap()
390 .into_value()
391 .unwrap();
392
393 let span = Span::from(&event);
394
395 if let Some(data) = span.data.value() {
396 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_CONTEXTS));
397 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_BREADCRUMBS));
398 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_EXTRA));
399 }
400 }
401
402 #[test]
403 fn convert_omits_empty_contexts_breadcrumbs_extra() {
404 let event = Annotated::<Event>::from_json(
405 r#"{
406 "type": "transaction",
407 "transaction": "my transaction",
408 "contexts": {},
409 "breadcrumbs": [],
410 "extra": {}
411 }"#,
412 )
413 .unwrap()
414 .into_value()
415 .unwrap();
416
417 let span = Span::from(&event);
418
419 if let Some(data) = span.data.value() {
420 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_CONTEXTS));
421 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_BREADCRUMBS));
422 assert!(!data.contains(SENTRY__EVENT__SERIALIZED_EXTRA));
423 }
424 }
425}