Skip to main content

relay_event_schema/protocol/span/
convert.rs

1//! This module defines bidirectional field mappings between spans and transactions.
2
3use relay_conventions::attributes::{
4    BROWSER__NAME, HTTP__QUERY, SENTRY__ENVIRONMENT, SENTRY__RELEASE, SENTRY__SDK__NAME,
5    SENTRY__SDK__VERSION, SENTRY__SEGMENT__NAME, URL__QUERY,
6};
7use relay_protocol::IntoValue;
8
9use crate::protocol::{BrowserContext, Event, ProfileContext, Span, SpanData, TraceContext};
10
11impl From<&Event> for Span {
12    fn from(event: &Event) -> Self {
13        let Event {
14            transaction,
15
16            platform,
17            timestamp,
18            start_timestamp,
19            received,
20            release,
21            environment,
22            tags,
23
24            measurements,
25            _metrics,
26            ..
27        } = event;
28
29        let trace = event.context::<TraceContext>();
30
31        // Fill data from trace context:
32        let mut data = trace
33            .map(|c| c.data.clone().map_value(SpanData::from))
34            .unwrap_or_default();
35
36        // Overwrite specific fields:
37        let span_data = data.get_or_insert_with(Default::default);
38        span_data.insert(
39            SENTRY__SEGMENT__NAME,
40            transaction.clone().map_value(IntoValue::into_value),
41        );
42        // For root spans, the name should just be the transaction name.
43        span_data.insert(
44            "sentry.name",
45            transaction.clone().map_value(IntoValue::into_value),
46        );
47        span_data.insert(
48            SENTRY__RELEASE,
49            release.clone().map_value(IntoValue::into_value),
50        );
51        span_data.insert(
52            SENTRY__ENVIRONMENT,
53            environment.clone().map_value(IntoValue::into_value),
54        );
55        if let Some(browser) = event.context::<BrowserContext>() {
56            span_data.insert(
57                BROWSER__NAME,
58                browser.name.clone().map_value(IntoValue::into_value),
59            );
60        }
61        if let Some(client_sdk) = event.client_sdk.value() {
62            span_data.insert(
63                SENTRY__SDK__NAME,
64                client_sdk.name.clone().map_value(IntoValue::into_value),
65            );
66            span_data.insert(
67                SENTRY__SDK__VERSION,
68                client_sdk.version.clone().map_value(IntoValue::into_value),
69            );
70        }
71        if let Some(request) = event.request.value()
72            && let Some(query) = request.query_string.value()
73            && let Some(qs) = query.to_query_string()
74        {
75            span_data.insert_value(HTTP__QUERY, format!("?{qs}"));
76            span_data.insert_value(URL__QUERY, qs);
77        }
78
79        Self {
80            timestamp: timestamp.clone(),
81            start_timestamp: start_timestamp.clone(),
82            exclusive_time: trace.map(|c| c.exclusive_time.clone()).unwrap_or_default(),
83            op: trace.map(|c| c.op.clone()).unwrap_or_default(),
84            span_id: trace.map(|c| c.span_id.clone()).unwrap_or_default(),
85            parent_span_id: trace.map(|c| c.parent_span_id.clone()).unwrap_or_default(),
86            trace_id: trace.map(|c| c.trace_id.clone()).unwrap_or_default(),
87            segment_id: trace.map(|c| c.span_id.clone()).unwrap_or_default(),
88            is_segment: true.into(),
89            // NB: Technically, this span may not be an actual remote span if this is a child
90            // transaction created within the same service as its parent. We still set `is_remote`
91            // as the best proxy to ensure this span will be detected as a segment by the spans
92            // pipeline.
93            is_remote: true.into(),
94            status: trace.map(|c| c.status.clone()).unwrap_or_default(),
95            description: transaction.clone(),
96            tags: tags.clone().map_value(|t| t.into()),
97            origin: trace.map(|c| c.origin.clone()).unwrap_or_default(),
98            profile_id: event
99                .context::<ProfileContext>()
100                .map(|c| c.profile_id.clone())
101                .unwrap_or_default(),
102            data,
103            links: trace.map(|c| c.links.clone()).unwrap_or_default(),
104            sentry_tags: Default::default(),
105            received: received.clone(),
106            measurements: measurements.clone(),
107            platform: platform.clone(),
108            was_transaction: true.into(),
109            kind: Default::default(),
110            other: Default::default(),
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use relay_protocol::Annotated;
118
119    use super::*;
120
121    #[test]
122    fn convert() {
123        let event = Annotated::<Event>::from_json(
124            r#"{
125                "type": "transaction",
126                "platform": "php",
127                "sdk": {"name": "sentry.php", "version": "1.2.3"},
128                "release": "myapp@1.0.0",
129                "environment": "prod",
130                "transaction": "my 1st transaction",
131                "contexts": {
132                    "browser": {"name": "Chrome"},
133                    "profile": {"profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab"},
134                    "trace": {
135                        "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
136                        "span_id": "FA90FDEAD5F74052",
137                        "type": "trace",
138                        "origin": "manual",
139                        "op": "myop",
140                        "status": "ok",
141                        "exclusive_time": 123.4,
142                        "parent_span_id": "FA90FDEAD5F74051",
143                        "data": {
144                            "custom_attribute": 42
145                        },
146                        "links": [
147                            {
148                                "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
149                                "span_id": "fa90fdead5f74052",
150                                "sampled": true,
151                                "attributes": {
152                                    "sentry.link.type": "previous_trace"
153                                }
154                            }
155                        ]
156                    }
157                },
158                "request": {
159                    "url": "http://example.com/api/0/organizations/",
160                    "method": "GET",
161                    "query_string": "project=1&sort=date"
162                },
163                "measurements": {
164                    "memory": {
165                        "value": 9001.0,
166                        "unit": "byte"
167                    }
168                }
169            }"#,
170        )
171        .unwrap()
172        .into_value()
173        .unwrap();
174
175        let span_from_event = Span::from(&event);
176        insta::assert_debug_snapshot!(span_from_event, @r#"
177        Span {
178            timestamp: ~,
179            start_timestamp: ~,
180            exclusive_time: 123.4,
181            op: "myop",
182            span_id: SpanId("fa90fdead5f74052"),
183            parent_span_id: SpanId("fa90fdead5f74051"),
184            trace_id: TraceId("4c79f60c11214eb38604f4ae0781bfb2"),
185            segment_id: SpanId("fa90fdead5f74052"),
186            is_segment: true,
187            is_remote: true,
188            status: Ok,
189            description: "my 1st transaction",
190            tags: ~,
191            origin: "manual",
192            profile_id: EventId(
193                a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab,
194            ),
195            data: SpanData {
196                other: {
197                    "browser.name": String(
198                        "Chrome",
199                    ),
200                    "custom_attribute": I64(
201                        42,
202                    ),
203                    "http.query": String(
204                        "?project=1&sort=date",
205                    ),
206                    "sentry.environment": String(
207                        "prod",
208                    ),
209                    "sentry.name": String(
210                        "my 1st transaction",
211                    ),
212                    "sentry.release": String(
213                        "myapp@1.0.0",
214                    ),
215                    "sentry.sdk.name": String(
216                        "sentry.php",
217                    ),
218                    "sentry.sdk.version": String(
219                        "1.2.3",
220                    ),
221                    "sentry.segment.name": String(
222                        "my 1st transaction",
223                    ),
224                    "url.query": String(
225                        "project=1&sort=date",
226                    ),
227                },
228            },
229            links: [
230                SpanLink {
231                    trace_id: TraceId("4c79f60c11214eb38604f4ae0781bfb2"),
232                    span_id: SpanId("fa90fdead5f74052"),
233                    sampled: true,
234                    attributes: {
235                        "sentry.link.type": String(
236                            "previous_trace",
237                        ),
238                    },
239                    other: {},
240                },
241            ],
242            sentry_tags: ~,
243            received: ~,
244            measurements: Measurements(
245                {
246                    "memory": Measurement {
247                        value: 9001.0,
248                        unit: Information(
249                            Byte,
250                        ),
251                    },
252                },
253            ),
254            platform: "php",
255            was_transaction: true,
256            kind: ~,
257            other: {},
258        }
259        "#);
260    }
261}