Skip to main content

relay_event_schema/protocol/span/
convert.rs

1//! This module defines bidirectional field mappings between spans and transactions.
2
3use crate::protocol::{BrowserContext, Event, ProfileContext, Span, SpanData, TraceContext};
4
5impl From<&Event> for Span {
6    fn from(event: &Event) -> Self {
7        let Event {
8            transaction,
9
10            platform,
11            timestamp,
12            start_timestamp,
13            received,
14            release,
15            environment,
16            tags,
17
18            measurements,
19            _metrics,
20            ..
21        } = event;
22
23        let trace = event.context::<TraceContext>();
24
25        // Fill data from trace context:
26        let mut data = trace
27            .map(|c| c.data.clone().map_value(SpanData::from))
28            .unwrap_or_default();
29
30        // Overwrite specific fields:
31        let span_data = data.get_or_insert_with(Default::default);
32        span_data.segment_name = transaction.clone();
33        // For root spans, the name should just be the transaction name.
34        span_data.span_name = transaction.clone();
35        span_data.release = release.clone();
36        span_data.environment = environment.clone();
37        if let Some(browser) = event.context::<BrowserContext>() {
38            span_data.browser_name = browser.name.clone();
39        }
40        if let Some(client_sdk) = event.client_sdk.value() {
41            span_data.sdk_name = client_sdk.name.clone();
42            span_data.sdk_version = client_sdk.version.clone();
43        }
44        if let Some(request) = event.request.value()
45            && let Some(query) = request.query_string.value()
46            && let Some(qs) = query.to_query_string()
47        {
48            span_data.http_query = format!("?{qs}").into();
49            span_data.url_query = qs.into();
50        }
51
52        Self {
53            timestamp: timestamp.clone(),
54            start_timestamp: start_timestamp.clone(),
55            exclusive_time: trace.map(|c| c.exclusive_time.clone()).unwrap_or_default(),
56            op: trace.map(|c| c.op.clone()).unwrap_or_default(),
57            span_id: trace.map(|c| c.span_id.clone()).unwrap_or_default(),
58            parent_span_id: trace.map(|c| c.parent_span_id.clone()).unwrap_or_default(),
59            trace_id: trace.map(|c| c.trace_id.clone()).unwrap_or_default(),
60            segment_id: trace.map(|c| c.span_id.clone()).unwrap_or_default(),
61            is_segment: true.into(),
62            // NB: Technically, this span may not be an actual remote span if this is a child
63            // transaction created within the same service as its parent. We still set `is_remote`
64            // as the best proxy to ensure this span will be detected as a segment by the spans
65            // pipeline.
66            is_remote: true.into(),
67            status: trace.map(|c| c.status.clone()).unwrap_or_default(),
68            description: transaction.clone(),
69            tags: tags.clone().map_value(|t| t.into()),
70            origin: trace.map(|c| c.origin.clone()).unwrap_or_default(),
71            profile_id: event
72                .context::<ProfileContext>()
73                .map(|c| c.profile_id.clone())
74                .unwrap_or_default(),
75            data,
76            links: trace.map(|c| c.links.clone()).unwrap_or_default(),
77            sentry_tags: Default::default(),
78            received: received.clone(),
79            measurements: measurements.clone(),
80            platform: platform.clone(),
81            was_transaction: true.into(),
82            kind: Default::default(),
83            other: Default::default(),
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use relay_protocol::Annotated;
91
92    use super::*;
93
94    #[test]
95    fn convert() {
96        let event = Annotated::<Event>::from_json(
97            r#"{
98                "type": "transaction",
99                "platform": "php",
100                "sdk": {"name": "sentry.php", "version": "1.2.3"},
101                "release": "myapp@1.0.0",
102                "environment": "prod",
103                "transaction": "my 1st transaction",
104                "contexts": {
105                    "browser": {"name": "Chrome"},
106                    "profile": {"profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab"},
107                    "trace": {
108                        "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
109                        "span_id": "FA90FDEAD5F74052",
110                        "type": "trace",
111                        "origin": "manual",
112                        "op": "myop",
113                        "status": "ok",
114                        "exclusive_time": 123.4,
115                        "parent_span_id": "FA90FDEAD5F74051",
116                        "data": {
117                            "custom_attribute": 42
118                        },
119                        "links": [
120                            {
121                                "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
122                                "span_id": "fa90fdead5f74052",
123                                "sampled": true,
124                                "attributes": {
125                                    "sentry.link.type": "previous_trace"
126                                }
127                            }
128                        ]
129                    }
130                },
131                "request": {
132                    "url": "http://example.com/api/0/organizations/",
133                    "method": "GET",
134                    "query_string": "project=1&sort=date"
135                },
136                "measurements": {
137                    "memory": {
138                        "value": 9001.0,
139                        "unit": "byte"
140                    }
141                }
142            }"#,
143        )
144        .unwrap()
145        .into_value()
146        .unwrap();
147
148        let span_from_event = Span::from(&event);
149        insta::assert_debug_snapshot!(span_from_event, @r#"
150        Span {
151            timestamp: ~,
152            start_timestamp: ~,
153            exclusive_time: 123.4,
154            op: "myop",
155            span_id: SpanId("fa90fdead5f74052"),
156            parent_span_id: SpanId("fa90fdead5f74051"),
157            trace_id: TraceId("4c79f60c11214eb38604f4ae0781bfb2"),
158            segment_id: SpanId("fa90fdead5f74052"),
159            is_segment: true,
160            is_remote: true,
161            status: Ok,
162            description: "my 1st transaction",
163            tags: ~,
164            origin: "manual",
165            profile_id: EventId(
166                a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab,
167            ),
168            data: SpanData {
169                app_start_type: ~,
170                gen_ai_pipeline_name: ~,
171                gen_ai_usage_total_tokens: ~,
172                gen_ai_usage_input_tokens: ~,
173                gen_ai_usage_input_tokens_cached: ~,
174                gen_ai_usage_input_tokens_cache_write: ~,
175                gen_ai_usage_output_tokens: ~,
176                gen_ai_usage_output_tokens_reasoning: ~,
177                gen_ai_response_model: ~,
178                gen_ai_request_model: ~,
179                gen_ai_context_window_size: ~,
180                gen_ai_context_utilization: ~,
181                gen_ai_cost_total_tokens: ~,
182                gen_ai_cost_input_tokens: ~,
183                gen_ai_cost_output_tokens: ~,
184                gen_ai_input_messages: ~,
185                gen_ai_tool_call_arguments: ~,
186                gen_ai_tool_call_result: ~,
187                gen_ai_output_messages: ~,
188                gen_ai_response_streaming: ~,
189                gen_ai_response_tokens_per_second: ~,
190                gen_ai_tool_definitions: ~,
191                gen_ai_request_frequency_penalty: ~,
192                gen_ai_request_presence_penalty: ~,
193                gen_ai_request_seed: ~,
194                gen_ai_request_temperature: ~,
195                gen_ai_request_top_k: ~,
196                gen_ai_request_top_p: ~,
197                gen_ai_response_finish_reasons: ~,
198                gen_ai_response_id: ~,
199                gen_ai_provider_name: ~,
200                gen_ai_system_instructions: ~,
201                gen_ai_tool_name: ~,
202                gen_ai_operation_name: ~,
203                gen_ai_operation_type: ~,
204                gen_ai_agent_name: ~,
205                gen_ai_function_id: ~,
206                browser_name: "Chrome",
207                db_operation: ~,
208                db_system: ~,
209                db_collection_name: ~,
210                environment: "prod",
211                release: LenientString(
212                    "myapp@1.0.0",
213                ),
214                http_decoded_response_content_length: ~,
215                http_request_method: ~,
216                http_response_content_length: ~,
217                http_response_transfer_size: ~,
218                resource_render_blocking_status: ~,
219                server_address: ~,
220                cache_hit: ~,
221                cache_key: ~,
222                cache_item_size: ~,
223                http_response_status_code: ~,
224                thread_name: ~,
225                thread_id: ~,
226                segment_name: "my 1st transaction",
227                ui_component_name: ~,
228                url_scheme: ~,
229                user: ~,
230                user_geo_country_code: ~,
231                user_geo_city: ~,
232                user_geo_subdivision: ~,
233                user_geo_region: ~,
234                exclusive_time: ~,
235                profile_id: ~,
236                replay_id: ~,
237                sdk_name: "sentry.php",
238                sdk_version: "1.2.3",
239                frames_slow: ~,
240                frames_frozen: ~,
241                frames_total: ~,
242                frames_delay: ~,
243                messaging_destination_name: ~,
244                messaging_message_retry_count: ~,
245                messaging_message_receive_latency: ~,
246                messaging_message_body_size: ~,
247                messaging_message_id: ~,
248                messaging_operation_name: ~,
249                messaging_operation_type: ~,
250                user_agent_original: ~,
251                url_full: ~,
252                url_query: "project=1&sort=date",
253                http_query: "?project=1&sort=date",
254                client_address: ~,
255                route: ~,
256                previous_route: ~,
257                lcp_element: ~,
258                lcp_size: ~,
259                lcp_id: ~,
260                lcp_url: ~,
261                sentry_dsc_trace_id: ~,
262                sentry_dsc_transaction: ~,
263                sentry_dsc_project_id: ~,
264                span_name: "my 1st transaction",
265                other: {
266                    "custom_attribute": I64(
267                        42,
268                    ),
269                },
270            },
271            links: [
272                SpanLink {
273                    trace_id: TraceId("4c79f60c11214eb38604f4ae0781bfb2"),
274                    span_id: SpanId("fa90fdead5f74052"),
275                    sampled: true,
276                    attributes: {
277                        "sentry.link.type": String(
278                            "previous_trace",
279                        ),
280                    },
281                    other: {},
282                },
283            ],
284            sentry_tags: ~,
285            received: ~,
286            measurements: Measurements(
287                {
288                    "memory": Measurement {
289                        value: 9001.0,
290                        unit: Information(
291                            Byte,
292                        ),
293                    },
294                },
295            ),
296            platform: "php",
297            was_transaction: true,
298            kind: ~,
299            other: {},
300        }
301        "#);
302    }
303}