Skip to main content

relay_event_schema/protocol/
span.rs

1mod convert;
2
3use std::fmt;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use relay_conventions::attributes::{
8    BROWSER__NAME, SENTRY__ENVIRONMENT, SENTRY__RELEASE, SENTRY__SEGMENT__NAME,
9};
10use relay_protocol::{
11    Annotated, Array, Empty, Error, FromValue, Getter, IntoValue, Object, Val, Value,
12};
13
14use crate::processor::{Pii, ProcessValue, ProcessingState};
15use crate::protocol::{
16    EventId, JsonLenientString, Measurements, OperationType, OriginType, SpanId, SpanStatus,
17    Timestamp, TraceId,
18};
19
20#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
21#[metastructure(process_func = "process_span", value_type = "Span", trim = false)]
22pub struct Span {
23    /// Timestamp when the span was ended.
24    #[metastructure(required = true)]
25    pub timestamp: Annotated<Timestamp>,
26
27    /// Timestamp when the span started.
28    #[metastructure(required = true)]
29    pub start_timestamp: Annotated<Timestamp>,
30
31    /// The amount of time in milliseconds spent in this span,
32    /// excluding its immediate child spans.
33    pub exclusive_time: Annotated<f64>,
34
35    /// Span type (see `OperationType` docs).
36    #[metastructure(max_chars = 128)]
37    pub op: Annotated<OperationType>,
38
39    /// The Span id.
40    #[metastructure(required = true)]
41    pub span_id: Annotated<SpanId>,
42
43    /// The ID of the span enclosing this span.
44    pub parent_span_id: Annotated<SpanId>,
45
46    /// The ID of the trace the span belongs to.
47    #[metastructure(required = true)]
48    pub trace_id: Annotated<TraceId>,
49
50    /// A unique identifier for a segment within a trace (8 byte hexadecimal string).
51    ///
52    /// For spans embedded in transactions, the `segment_id` is the `span_id` of the containing
53    /// transaction.
54    pub segment_id: Annotated<SpanId>,
55
56    /// Whether or not the current span is the root of the segment.
57    pub is_segment: Annotated<bool>,
58
59    /// Indicates whether a span's parent is remote.
60    ///
61    /// For OpenTelemetry spans, this is derived from span flags bits 8 and 9. See
62    /// `SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK` and `SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK`.
63    ///
64    /// The states are:
65    ///  - `empty`: unknown
66    ///  - `false`: is not remote
67    ///  - `true`: is remote
68    pub is_remote: Annotated<bool>,
69
70    /// The status of a span.
71    pub status: Annotated<SpanStatus>,
72
73    /// Human readable description of a span (e.g. method URL).
74    #[metastructure(pii = "maybe")]
75    pub description: Annotated<String>,
76
77    /// Arbitrary tags on a span, like on the top-level event.
78    #[metastructure(pii = "maybe")]
79    pub tags: Annotated<Object<JsonLenientString>>,
80
81    /// The origin of the span indicates what created the span (see [OriginType] docs).
82    #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
83    pub origin: Annotated<OriginType>,
84
85    /// ID of a profile that can be associated with the span.
86    pub profile_id: Annotated<EventId>,
87
88    /// Arbitrary additional data on a span.
89    ///
90    /// Besides arbitrary user data, this object also contains SDK-provided fields used by the
91    /// product (see <https://develop.sentry.dev/sdk/performance/span-data-conventions/>).
92    #[metastructure(pii = "true")]
93    pub data: Annotated<SpanData>,
94
95    /// Links from this span to other spans
96    #[metastructure(pii = "maybe")]
97    pub links: Annotated<Array<SpanLink>>,
98
99    /// Tags generated by Relay. These tags are a superset of the tags set on span metrics.
100    pub sentry_tags: Annotated<SentryTags>,
101
102    /// Timestamp when the span has been received by Sentry.
103    pub received: Annotated<Timestamp>,
104
105    /// Measurements which holds observed values such as web vitals.
106    #[metastructure(skip_serialization = "empty")]
107    #[metastructure(omit_from_schema)] // we only document error events for now
108    pub measurements: Annotated<Measurements>,
109
110    /// Platform identifier.
111    ///
112    /// See [`Event::platform`](`crate::protocol::Event::platform`).
113    #[metastructure(skip_serialization = "empty")]
114    pub platform: Annotated<String>,
115
116    /// Whether the span is a segment span that was converted from a transaction.
117    #[metastructure(skip_serialization = "empty")]
118    pub was_transaction: Annotated<bool>,
119
120    // Used to clarify the relationship between parents and children, or to distinguish between
121    // spans, e.g. a `server` and `client` span with the same name.
122    //
123    // See <https://opentelemetry.io/docs/specs/otel/trace/api/#spankind>
124    #[metastructure(skip_serialization = "empty", trim = false)]
125    pub kind: Annotated<SpanKind>,
126
127    /// Additional arbitrary fields for forwards compatibility.
128    #[metastructure(additional_properties, pii = "maybe")]
129    pub other: Object<Value>,
130}
131
132impl Span {
133    /// Returns the value of an attribute on the span.
134    ///
135    /// This primarily looks up the attribute in the `data` object, but falls back to the `tags`
136    /// object if the attribute is not found.
137    fn attribute(&self, key: &str) -> Option<Val<'_>> {
138        Some(match Getter::get_value(self.data.value()?, key) {
139            Some(value) => value,
140            None => self.tags.value()?.get(key)?.as_str()?.into(),
141        })
142    }
143}
144
145impl Getter for Span {
146    fn get_value(&self, path: &str) -> Option<Val<'_>> {
147        let span_prefix = path.strip_prefix("span.");
148        if let Some(span_prefix) = span_prefix {
149            return Some(match span_prefix {
150                "exclusive_time" => self.exclusive_time.value()?.into(),
151                "description" => self.description.as_str()?.into(),
152                "op" => self.op.as_str()?.into(),
153                "span_id" => self.span_id.value()?.into(),
154                "parent_span_id" => self.parent_span_id.value()?.into(),
155                "trace_id" => self.trace_id.value()?.deref().into(),
156                "status" => self.status.as_str()?.into(),
157                "is_segment" => self.is_segment.value()?.into(),
158                "origin" => self.origin.as_str()?.into(),
159                "duration" => {
160                    let start_timestamp = *self.start_timestamp.value()?;
161                    let timestamp = *self.timestamp.value()?;
162                    relay_common::time::chrono_to_positive_millis(timestamp - start_timestamp)
163                        .into()
164                }
165                "was_transaction" => self.was_transaction.value().unwrap_or(&false).into(),
166                path => {
167                    if let Some(key) = path.strip_prefix("tags.") {
168                        self.tags.value()?.get(key)?.as_str()?.into()
169                    } else if let Some(key) = path.strip_prefix("data.") {
170                        self.attribute(key)?
171                    } else if let Some(key) = path.strip_prefix("sentry_tags.") {
172                        self.sentry_tags.value()?.get_value(key)?
173                    } else {
174                        let rest = path.strip_prefix("measurements.")?;
175                        let name = rest.strip_suffix(".value")?;
176                        self.measurements
177                            .value()?
178                            .get(name)?
179                            .value()?
180                            .value
181                            .value()?
182                            .into()
183                    }
184                }
185            });
186        }
187
188        // For backward compatibility with event-based rules, we try to support `event.` fields also
189        // for a span.
190        let event_prefix = path.strip_prefix("event.")?;
191        Some(match event_prefix {
192            "release" => self.data.value()?.get_str(SENTRY__RELEASE)?.into(),
193            "environment" => self.data.value()?.get_str(SENTRY__ENVIRONMENT)?.into(),
194            "transaction" => self.data.value()?.get_str(SENTRY__SEGMENT__NAME)?.into(),
195            "contexts.browser.name" => self.data.value()?.get_str(BROWSER__NAME)?.into(),
196            // TODO: we might want to add additional fields once they are added to the span.
197            _ => return None,
198        })
199    }
200}
201
202/// Indexable fields added by sentry (server-side).
203#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
204#[metastructure(trim = false, pii = "maybe")]
205pub struct SentryTags {
206    pub release: Annotated<String>,
207    #[metastructure(pii = "true")]
208    pub user: Annotated<String>,
209    #[metastructure(pii = "true", field = "user.id")]
210    pub user_id: Annotated<String>,
211    #[metastructure(pii = "true", field = "user.ip")]
212    pub user_ip: Annotated<String>,
213    #[metastructure(pii = "true", field = "user.username")]
214    pub user_username: Annotated<String>,
215    #[metastructure(pii = "true", field = "user.email")]
216    pub user_email: Annotated<String>,
217    pub environment: Annotated<String>,
218    #[metastructure(field = "transaction.method")]
219    pub transaction_method: Annotated<String>,
220    #[metastructure(field = "transaction.op")]
221    pub transaction_op: Annotated<String>,
222    #[metastructure(field = "browser.name")]
223    pub browser_name: Annotated<String>,
224    #[metastructure(field = "sdk.name")]
225    pub sdk_name: Annotated<String>,
226    #[metastructure(field = "sdk.version")]
227    pub sdk_version: Annotated<String>,
228    pub platform: Annotated<String>,
229    // `"true"` if the transaction was sent by a mobile SDK(String).
230    pub mobile: Annotated<String>,
231    #[metastructure(field = "device.class")]
232    pub device_class: Annotated<String>,
233    #[metastructure(field = "device.family")]
234    pub device_family: Annotated<String>,
235    #[metastructure(field = "device.arch")]
236    pub device_arch: Annotated<String>,
237    #[metastructure(field = "device.battery_level")]
238    pub device_battery_level: Annotated<String>,
239    #[metastructure(field = "device.brand")]
240    pub device_brand: Annotated<String>,
241    #[metastructure(field = "device.charging")]
242    pub device_charging: Annotated<String>,
243    #[metastructure(field = "device.locale")]
244    pub device_locale: Annotated<String>,
245    #[metastructure(field = "device.model_id")]
246    pub device_model_id: Annotated<String>,
247    #[metastructure(field = "device.name")]
248    pub device_name: Annotated<String>,
249    #[metastructure(field = "device.online")]
250    pub device_online: Annotated<String>,
251    #[metastructure(field = "device.orientation")]
252    pub device_orientation: Annotated<String>,
253    #[metastructure(field = "device.screen_density")]
254    pub device_screen_density: Annotated<String>,
255    #[metastructure(field = "device.screen_dpi")]
256    pub device_screen_dpi: Annotated<String>,
257    #[metastructure(field = "device.screen_height_pixels")]
258    pub device_screen_height_pixels: Annotated<String>,
259    #[metastructure(field = "device.screen_width_pixels")]
260    pub device_screen_width_pixels: Annotated<String>,
261    #[metastructure(field = "device.simulator")]
262    pub device_simulator: Annotated<String>,
263    #[metastructure(field = "device.uuid")]
264    pub device_uuid: Annotated<String>,
265    #[metastructure(field = "app.device")]
266    pub app_device: Annotated<String>,
267    #[metastructure(field = "device.model")]
268    pub device_model: Annotated<String>,
269    pub runtime: Annotated<String>,
270    #[metastructure(field = "runtime.name")]
271    pub runtime_name: Annotated<String>,
272    pub browser: Annotated<String>,
273    pub os: Annotated<String>,
274    #[metastructure(field = "os.rooted")]
275    pub os_rooted: Annotated<String>,
276    #[metastructure(field = "gpu.name")]
277    pub gpu_name: Annotated<String>,
278    #[metastructure(field = "gpu.vendor")]
279    pub gpu_vendor: Annotated<String>,
280    #[metastructure(field = "monitor.id")]
281    pub monitor_id: Annotated<String>,
282    #[metastructure(field = "monitor.slug")]
283    pub monitor_slug: Annotated<String>,
284    #[metastructure(field = "request.url")]
285    pub request_url: Annotated<String>,
286    #[metastructure(field = "request.method")]
287    pub request_method: Annotated<String>,
288    // Mobile OS the transaction originated from(String).
289    #[metastructure(field = "os.name")]
290    pub os_name: Annotated<String>,
291    pub action: Annotated<String>,
292    pub category: Annotated<String>,
293    pub description: Annotated<String>,
294    pub domain: Annotated<String>,
295    pub raw_domain: Annotated<String>,
296    pub group: Annotated<String>,
297    #[metastructure(field = "http.decoded_response_content_length")]
298    pub http_decoded_response_content_length: Annotated<String>,
299    #[metastructure(field = "http.response_content_length")]
300    pub http_response_content_length: Annotated<String>,
301    #[metastructure(field = "http.response_transfer_size")]
302    pub http_response_transfer_size: Annotated<String>,
303    #[metastructure(field = "resource.render_blocking_status")]
304    pub resource_render_blocking_status: Annotated<String>,
305    pub op: Annotated<String>,
306    pub status: Annotated<String>,
307    pub status_code: Annotated<String>,
308    pub system: Annotated<String>,
309    /// Contributes to Time-To-Initial-Display(String).
310    pub ttid: Annotated<String>,
311    /// Contributes to Time-To-Full-Display(String).
312    pub ttfd: Annotated<String>,
313    /// File extension for resource spans(String).
314    pub file_extension: Annotated<String>,
315    /// Span started on main thread(String).
316    pub main_thread: Annotated<String>,
317    /// The start type of the application when the span occurred(String).
318    pub app_start_type: Annotated<String>,
319    pub replay_id: Annotated<String>,
320    #[metastructure(field = "cache.hit")]
321    pub cache_hit: Annotated<String>,
322    #[metastructure(field = "cache.key")]
323    pub cache_key: Annotated<String>,
324    #[metastructure(field = "trace.status")]
325    pub trace_status: Annotated<String>,
326    #[metastructure(field = "messaging.destination.name")]
327    pub messaging_destination_name: Annotated<String>,
328    #[metastructure(field = "messaging.message.id")]
329    pub messaging_message_id: Annotated<String>,
330    #[metastructure(field = "messaging.operation.name")]
331    pub messaging_operation_name: Annotated<String>,
332    #[metastructure(field = "messaging.operation.type")]
333    pub messaging_operation_type: Annotated<String>,
334    #[metastructure(field = "thread.name")]
335    pub thread_name: Annotated<String>,
336    #[metastructure(field = "thread.id")]
337    pub thread_id: Annotated<String>,
338    pub profiler_id: Annotated<String>,
339    #[metastructure(field = "user.geo.city")]
340    pub user_city: Annotated<String>,
341    #[metastructure(field = "user.geo.country_code")]
342    pub user_country_code: Annotated<String>,
343    #[metastructure(field = "user.geo.region")]
344    pub user_region: Annotated<String>,
345    #[metastructure(field = "user.geo.subdivision")]
346    pub user_subdivision: Annotated<String>,
347    #[metastructure(field = "user.geo.subregion")]
348    pub user_subregion: Annotated<String>,
349    pub name: Annotated<String>,
350    // no need for an `other` entry here because these fields are added server-side.
351    // If an upstream relay does not recognize a field it will be dropped.
352}
353
354impl Getter for SentryTags {
355    fn get_value(&self, path: &str) -> Option<Val<'_>> {
356        let value = match path {
357            "action" => &self.action,
358            "app_start_type" => &self.app_start_type,
359            "browser.name" => &self.browser_name,
360            "cache.hit" => &self.cache_hit,
361            "cache.key" => &self.cache_key,
362            "category" => &self.category,
363            "description" => &self.description,
364            "device.class" => &self.device_class,
365            "device.family" => &self.device_family,
366            "device.arch" => &self.device_arch,
367            "device.battery_level" => &self.device_battery_level,
368            "device.brand" => &self.device_brand,
369            "device.charging" => &self.device_charging,
370            "device.locale" => &self.device_locale,
371            "device.model_id" => &self.device_model_id,
372            "device.name" => &self.device_name,
373            "device.online" => &self.device_online,
374            "device.orientation" => &self.device_orientation,
375            "device.screen_density" => &self.device_screen_density,
376            "device.screen_dpi" => &self.device_screen_dpi,
377            "device.screen_height_pixels" => &self.device_screen_height_pixels,
378            "device.screen_width_pixels" => &self.device_screen_width_pixels,
379            "device.simulator" => &self.device_simulator,
380            "device.uuid" => &self.device_uuid,
381            "app.device" => &self.app_device,
382            "device.model" => &self.device_model,
383            "runtime" => &self.runtime,
384            "runtime.name" => &self.runtime_name,
385            "browser" => &self.browser,
386            "os" => &self.os,
387            "os.rooted" => &self.os_rooted,
388            "gpu.name" => &self.gpu_name,
389            "gpu.vendor" => &self.gpu_vendor,
390            "monitor.id" => &self.monitor_id,
391            "monitor.slug" => &self.monitor_slug,
392            "request.url" => &self.request_url,
393            "request.method" => &self.request_method,
394            "domain" => &self.domain,
395            "environment" => &self.environment,
396            "file_extension" => &self.file_extension,
397            "group" => &self.group,
398            "http.decoded_response_content_length" => &self.http_decoded_response_content_length,
399            "http.response_content_length" => &self.http_response_content_length,
400            "http.response_transfer_size" => &self.http_response_transfer_size,
401            "main_thread" => &self.main_thread,
402            "messaging.destination.name" => &self.messaging_destination_name,
403            "messaging.message.id" => &self.messaging_message_id,
404            "messaging.operation.name" => &self.messaging_operation_name,
405            "messaging.operation.type" => &self.messaging_operation_type,
406            "mobile" => &self.mobile,
407            "name" => &self.name,
408            "op" => &self.op,
409            "os.name" => &self.os_name,
410            "platform" => &self.platform,
411            "profiler_id" => &self.profiler_id,
412            "raw_domain" => &self.raw_domain,
413            "release" => &self.release,
414            "replay_id" => &self.replay_id,
415            "resource.render_blocking_status" => &self.resource_render_blocking_status,
416            "sdk.name" => &self.sdk_name,
417            "sdk.version" => &self.sdk_version,
418            "status_code" => &self.status_code,
419            "status" => &self.status,
420            "system" => &self.system,
421            "thread.id" => &self.thread_id,
422            "thread.name" => &self.thread_name,
423            "trace.status" => &self.trace_status,
424            "transaction.method" => &self.transaction_method,
425            "transaction.op" => &self.transaction_op,
426            "ttfd" => &self.ttfd,
427            "ttid" => &self.ttid,
428            "user.email" => &self.user_email,
429            "user.geo.city" => &self.user_city,
430            "user.geo.country_code" => &self.user_country_code,
431            "user.geo.region" => &self.user_region,
432            "user.geo.subdivision" => &self.user_subdivision,
433            "user.geo.subregion" => &self.user_subregion,
434            "user.id" => &self.user_id,
435            "user.ip" => &self.user_ip,
436            "user.username" => &self.user_username,
437            "user" => &self.user,
438            _ => return None,
439        };
440        Some(value.as_str()?.into())
441    }
442}
443
444/// Determines the `Pii` value for a field of [`SpanData`] by looking it up in `relay-conventions`.
445///
446/// If the field is not found in the conventions, this returns `Pii::True`
447/// as a precaution.
448fn span_data_pii_from_conventions(state: &ProcessingState) -> Pii {
449    fn inner(state: &ProcessingState) -> Option<Pii> {
450        // `state.keys().next()` is the _last_ segment in the state's
451        // path, i.e. the field name.
452        let key = state.keys().next()?;
453
454        match relay_conventions::attribute_info(key)?.apply_scrubbing {
455            relay_conventions::ApplyScrubbing::Auto => Some(Pii::True),
456            relay_conventions::ApplyScrubbing::Never => Some(Pii::False),
457            relay_conventions::ApplyScrubbing::Manual => Some(Pii::Maybe),
458        }
459    }
460
461    inner(state).unwrap_or(Pii::True)
462}
463
464/// Arbitrary additional data on a span.
465///
466/// Besides arbitrary user data, this type also contains SDK-provided fields used by the
467/// product (see <https://develop.sentry.dev/sdk/performance/span-data-conventions/>).
468#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
469#[metastructure(trim = false, pii = "span_data_pii_from_conventions")]
470pub struct SpanData {
471    /// Other fields in `span.data`.
472    #[metastructure(
473        additional_properties,
474        retain = true,
475        skip_serialization = "null" // applies to child elements
476    )]
477    pub other: Object<Value>,
478}
479
480impl SpanData {
481    /// Returns an annotated attribute from span data.
482    pub fn get(&self, key: &str) -> Option<&Annotated<Value>> {
483        self.other.get(key)
484    }
485
486    /// Returns an attribute value from span data.
487    pub fn get_value(&self, key: &str) -> Option<&Value> {
488        self.get(key).and_then(Annotated::value)
489    }
490
491    /// Returns a string attribute from span data.
492    pub fn get_str(&self, key: &str) -> Option<&str> {
493        self.get_value(key)?.as_str()
494    }
495
496    /// Returns whether span data contains an attribute.
497    pub fn contains(&self, key: &str) -> bool {
498        self.other.contains_key(key)
499    }
500
501    /// Inserts an annotated attribute into span data.
502    pub fn insert(&mut self, key: impl Into<String>, value: Annotated<Value>) {
503        self.other.insert(key.into(), value);
504    }
505
506    /// Inserts an attribute into span data.
507    pub fn insert_value<T>(&mut self, key: impl Into<String>, value: T)
508    where
509        T: IntoValue,
510    {
511        self.insert(key, Annotated::new(value.into_value()));
512    }
513
514    /// Removes an attribute from span data.
515    pub fn remove(&mut self, key: &str) -> Option<Annotated<Value>> {
516        self.other.remove(key)
517    }
518}
519
520impl Getter for SpanData {
521    fn get_value(&self, path: &str) -> Option<Val<'_>> {
522        let escaped = path.replace("\\.", "\0");
523        let mut path = escaped.split('.').map(|s| s.replace('\0', "."));
524        let root = path.next()?;
525
526        let mut val = self.get(&root)?.value()?;
527        for part in path {
528            // While there is path segments left, `val` has to be an Object.
529            let relay_protocol::Value::Object(map) = val else {
530                return None;
531            };
532            val = map.get(&part)?.value()?;
533        }
534        Some(val.into())
535    }
536}
537
538impl From<Object<Value>> for SpanData {
539    fn from(other: Object<Value>) -> Self {
540        Self { other }
541    }
542}
543
544impl<const N: usize> From<[(String, Annotated<Value>); N]> for SpanData {
545    fn from(value: [(String, Annotated<Value>); N]) -> Self {
546        Self::from(Object::from(value))
547    }
548}
549
550/// A link from a span to another span.
551#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
552#[metastructure(trim = false)]
553pub struct SpanLink {
554    /// The trace id of the linked span
555    #[metastructure(required = true, trim = false)]
556    pub trace_id: Annotated<TraceId>,
557
558    /// The span id of the linked span
559    #[metastructure(required = true, trim = false)]
560    pub span_id: Annotated<SpanId>,
561
562    /// Whether the linked span was positively/negatively sampled
563    #[metastructure(trim = false)]
564    pub sampled: Annotated<bool>,
565
566    /// Span link attributes, similar to span attributes/data
567    #[metastructure(pii = "maybe", trim = false)]
568    pub attributes: Annotated<Object<Value>>,
569
570    /// Additional arbitrary fields for forwards compatibility.
571    #[metastructure(additional_properties, retain = true, pii = "maybe", trim = false)]
572    pub other: Object<Value>,
573}
574
575/// The route in the application, set by React Native SDK.
576#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
577pub struct Route {
578    /// The name of the route.
579    #[metastructure(pii = "maybe", skip_serialization = "empty")]
580    pub name: Annotated<String>,
581
582    /// Parameters assigned to this route.
583    #[metastructure(
584        pii = "true",
585        skip_serialization = "empty",
586        max_depth = 5,
587        max_bytes = 2048
588    )]
589    pub params: Annotated<Object<Value>>,
590
591    /// Additional arbitrary fields for forwards compatibility.
592    #[metastructure(
593        additional_properties,
594        retain = true,
595        pii = "maybe",
596        skip_serialization = "empty"
597    )]
598    pub other: Object<Value>,
599}
600
601impl FromValue for Route {
602    fn from_value(value: Annotated<Value>) -> Annotated<Self>
603    where
604        Self: Sized,
605    {
606        match value {
607            Annotated(Some(Value::String(name)), meta) => Annotated(
608                Some(Route {
609                    name: Annotated::new(name),
610                    ..Default::default()
611                }),
612                meta,
613            ),
614            Annotated(Some(Value::Object(mut values)), meta) => {
615                let mut route: Route = Default::default();
616                if let Some(Annotated(Some(Value::String(name)), _)) = values.remove("name") {
617                    route.name = Annotated::new(name);
618                }
619                if let Some(Annotated(Some(Value::Object(params)), _)) = values.remove("params") {
620                    route.params = Annotated::new(params);
621                }
622
623                if !values.is_empty() {
624                    route.other = values;
625                }
626
627                Annotated(Some(route), meta)
628            }
629            Annotated(None, meta) => Annotated(None, meta),
630            Annotated(Some(value), mut meta) => {
631                meta.add_error(Error::expected("route expected to be an object"));
632                meta.set_original_value(Some(value));
633                Annotated(None, meta)
634            }
635        }
636    }
637}
638
639/// The kind of a span.
640///
641/// This corresponds to OTEL's kind enum, plus a
642/// catchall variant for forward compatibility.
643#[derive(Clone, Debug, PartialEq, ProcessValue, Default)]
644pub enum SpanKind {
645    /// An operation internal to an application.
646    #[default]
647    Internal,
648    /// Server-side processing requested by a client.
649    Server,
650    /// A request from a client to a server.
651    Client,
652    /// Scheduling of an operation.
653    Producer,
654    /// Processing of a scheduled operation.
655    Consumer,
656    /// Unknown kind, for forward compatibility.
657    Unknown(String),
658}
659
660impl SpanKind {
661    pub fn as_str(&self) -> &str {
662        match self {
663            Self::Internal => "internal",
664            Self::Server => "server",
665            Self::Client => "client",
666            Self::Producer => "producer",
667            Self::Consumer => "consumer",
668            Self::Unknown(s) => s.as_str(),
669        }
670    }
671}
672
673impl Empty for SpanKind {
674    fn is_empty(&self) -> bool {
675        false
676    }
677}
678
679#[derive(Debug)]
680pub struct ParseSpanKindError;
681
682impl std::str::FromStr for SpanKind {
683    type Err = ParseSpanKindError;
684
685    fn from_str(s: &str) -> Result<Self, Self::Err> {
686        Ok(match s {
687            "internal" => SpanKind::Internal,
688            "server" => SpanKind::Server,
689            "client" => SpanKind::Client,
690            "producer" => SpanKind::Producer,
691            "consumer" => SpanKind::Consumer,
692            other => SpanKind::Unknown(other.to_owned()),
693        })
694    }
695}
696
697impl fmt::Display for SpanKind {
698    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699        write!(f, "{}", self.as_str())
700    }
701}
702
703impl FromValue for SpanKind {
704    fn from_value(value: Annotated<Value>) -> Annotated<Self>
705    where
706        Self: Sized,
707    {
708        match value {
709            Annotated(Some(Value::String(s)), meta) => Annotated(SpanKind::from_str(&s).ok(), meta),
710            Annotated(_, meta) => Annotated(None, meta),
711        }
712    }
713}
714
715impl IntoValue for SpanKind {
716    fn into_value(self) -> Value
717    where
718        Self: Sized,
719    {
720        Value::String(self.to_string())
721    }
722
723    fn serialize_payload<S>(
724        &self,
725        s: S,
726        _behavior: relay_protocol::SkipSerialization,
727    ) -> Result<S::Ok, S::Error>
728    where
729        Self: Sized,
730        S: serde::Serializer,
731    {
732        s.serialize_str(self.as_str())
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use crate::protocol::Measurement;
739    use chrono::{TimeZone, Utc};
740    use relay_base_schema::metrics::{InformationUnit, MetricUnit};
741    use relay_conventions::attributes::*;
742    use relay_protocol::RuleCondition;
743    use similar_asserts::assert_eq;
744
745    use super::*;
746
747    /// Test that span data attributes expected to follow sentry conventions actually do so. This
748    /// is achieved by 1) creating a json which uses sentry conventions constants, 2) creating a
749    /// `SpanData` object from the json, and 3) verifying that the json values end up in the
750    /// expected `SpanData` fields (which wouldn't happen if the sentry conventions constants don't
751    /// match the declared field names).
752    #[test]
753    fn test_span_data_attributes_follow_sentry_conventions() {
754        let my_trace = &"my_trace".to_owned();
755        let my_transaction = &"my_transaction".to_owned();
756        let my_project_id = &"my_project_id".to_owned();
757        let json = format!(
758            r#"{{
759                "{SENTRY__DSC__TRACE_ID}": "{my_trace}",
760                "{SENTRY__DSC__TRANSACTION}": "{my_transaction}",
761                "{SENTRY__DSC__PROJECT_ID}": "{my_project_id}"
762            }}"#,
763        );
764        let data = Annotated::<SpanData>::from_json(&json).unwrap();
765        let data = data.value().unwrap();
766        assert_eq!(data.get_str(SENTRY__DSC__TRACE_ID), Some(my_trace.as_str()));
767        assert_eq!(
768            data.get_str(SENTRY__DSC__TRANSACTION),
769            Some(my_transaction.as_str())
770        );
771        assert_eq!(
772            data.get_str(SENTRY__DSC__PROJECT_ID),
773            Some(my_project_id.as_str())
774        );
775    }
776
777    #[test]
778    fn test_span_serialization() {
779        let json = r#"{
780  "timestamp": 0.0,
781  "start_timestamp": -63158400.0,
782  "exclusive_time": 1.23,
783  "op": "operation",
784  "span_id": "fa90fdead5f74052",
785  "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
786  "status": "ok",
787  "description": "desc",
788  "origin": "auto.http",
789  "links": [
790    {
791      "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
792      "span_id": "fa90fdead5f74052",
793      "sampled": true,
794      "attributes": {
795        "boolAttr": true,
796        "numAttr": 123,
797        "stringAttr": "foo"
798      }
799    }
800  ],
801  "measurements": {
802    "memory": {
803      "value": 9001.0,
804      "unit": "byte"
805    }
806  },
807  "kind": "server"
808}"#;
809        let mut measurements = Object::new();
810        measurements.insert(
811            "memory".into(),
812            Annotated::new(Measurement {
813                value: Annotated::new(9001.0.try_into().unwrap()),
814                unit: Annotated::new(MetricUnit::Information(InformationUnit::Byte)),
815            }),
816        );
817
818        let links = Annotated::new(vec![Annotated::new(SpanLink {
819            trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
820            span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
821            sampled: Annotated::new(true),
822            attributes: Annotated::new({
823                let mut map: std::collections::BTreeMap<String, Annotated<Value>> = Object::new();
824                map.insert(
825                    "stringAttr".into(),
826                    Annotated::new(Value::String("foo".into())),
827                );
828                map.insert("numAttr".into(), Annotated::new(Value::I64(123)));
829                map.insert("boolAttr".into(), Value::Bool(true).into());
830                map
831            }),
832            ..Default::default()
833        })]);
834
835        let span = Annotated::new(Span {
836            timestamp: Annotated::new(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap().into()),
837            start_timestamp: Annotated::new(
838                Utc.with_ymd_and_hms(1968, 1, 1, 0, 0, 0).unwrap().into(),
839            ),
840            exclusive_time: Annotated::new(1.23),
841            description: Annotated::new("desc".to_owned()),
842            op: Annotated::new("operation".to_owned()),
843            trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
844            span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
845            status: Annotated::new(SpanStatus::Ok),
846            origin: Annotated::new("auto.http".to_owned()),
847            kind: Annotated::new(SpanKind::Server),
848            measurements: Annotated::new(Measurements(measurements)),
849            links,
850            ..Default::default()
851        });
852        assert_eq!(json, span.to_json_pretty().unwrap());
853
854        let span_from_string = Annotated::from_json(json).unwrap();
855        assert_eq!(span, span_from_string);
856    }
857
858    #[test]
859    fn test_getter_span_data() {
860        let span = Annotated::<Span>::from_json(
861            r#"{
862                "data": {
863                    "foo": {"bar": 1},
864                    "foo.bar": 2
865                },
866                "measurements": {
867                    "some": {"value": 100.0}
868                }
869            }"#,
870        )
871        .unwrap()
872        .into_value()
873        .unwrap();
874
875        assert_eq!(span.get_value("span.data.foo.bar"), Some(Val::I64(1)));
876        assert_eq!(span.get_value(r"span.data.foo\.bar"), Some(Val::I64(2)));
877
878        assert_eq!(span.get_value("span.data"), None);
879        assert_eq!(span.get_value("span.data."), None);
880        assert_eq!(span.get_value("span.data.x"), None);
881
882        assert_eq!(
883            span.get_value("span.measurements.some.value"),
884            Some(Val::F64(100.0))
885        );
886    }
887
888    #[test]
889    fn test_getter_was_transaction() {
890        let mut span = Span::default();
891        assert_eq!(
892            span.get_value("span.was_transaction"),
893            Some(Val::Bool(false))
894        );
895        assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
896        assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
897
898        span.was_transaction.set_value(Some(false));
899        assert_eq!(
900            span.get_value("span.was_transaction"),
901            Some(Val::Bool(false))
902        );
903        assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
904        assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
905
906        span.was_transaction.set_value(Some(true));
907        assert_eq!(
908            span.get_value("span.was_transaction"),
909            Some(Val::Bool(true))
910        );
911        assert!(RuleCondition::eq("span.was_transaction", true).matches(&span));
912        assert!(!RuleCondition::eq("span.was_transaction", false).matches(&span));
913    }
914
915    #[test]
916    fn test_span_fields_as_event() {
917        let span = Annotated::<Span>::from_json(
918            r#"{
919                "data": {
920                    "sentry.release": "1.0",
921                    "sentry.environment": "prod",
922                    "sentry.segment.name": "/api/endpoint"
923                }
924            }"#,
925        )
926        .unwrap()
927        .into_value()
928        .unwrap();
929
930        assert_eq!(span.get_value("event.release"), Some(Val::String("1.0")));
931        assert_eq!(
932            span.get_value("event.environment"),
933            Some(Val::String("prod"))
934        );
935        assert_eq!(
936            span.get_value("event.transaction"),
937            Some(Val::String("/api/endpoint"))
938        );
939    }
940
941    #[test]
942    fn test_span_duration() {
943        let span = Annotated::<Span>::from_json(
944            r#"{
945                "start_timestamp": 1694732407.8367,
946                "timestamp": 1694732408.31451233
947            }"#,
948        )
949        .unwrap()
950        .into_value()
951        .unwrap();
952
953        assert_eq!(span.get_value("span.duration"), Some(Val::F64(477.812)));
954    }
955
956    #[test]
957    fn test_span_data() {
958        let data = r#"{
959        "foo": 2,
960        "bar": "3",
961        "db.system.name": "mysql",
962        "code.filepath": "task.py",
963        "code.lineno": 123,
964        "code.function": "fn()",
965        "code.namespace": "ns",
966        "frames.slow": 1,
967        "frames.frozen": 2,
968        "frames.total": 9,
969        "frames.delay": 100,
970        "messaging.destination.name": "default",
971        "messaging.message.retry.count": 3,
972        "messaging.message.receive.latency": 40,
973        "messaging.message.body.size": 100,
974        "messaging.message.id": "abc123",
975        "messaging.operation.name": "publish",
976        "messaging.operation.type": "create",
977        "user_agent.original": "Chrome",
978        "url.full": "my_url.com",
979        "client.address": "192.168.0.1"
980    }"#;
981        let mut data = Annotated::<SpanData>::from_json(data)
982            .unwrap()
983            .into_value()
984            .unwrap();
985        insta::assert_debug_snapshot!(data, @r###"
986        SpanData {
987            other: {
988                "bar": String(
989                    "3",
990                ),
991                "client.address": String(
992                    "192.168.0.1",
993                ),
994                "code.filepath": String(
995                    "task.py",
996                ),
997                "code.function": String(
998                    "fn()",
999                ),
1000                "code.lineno": I64(
1001                    123,
1002                ),
1003                "code.namespace": String(
1004                    "ns",
1005                ),
1006                "db.system.name": String(
1007                    "mysql",
1008                ),
1009                "foo": I64(
1010                    2,
1011                ),
1012                "frames.delay": I64(
1013                    100,
1014                ),
1015                "frames.frozen": I64(
1016                    2,
1017                ),
1018                "frames.slow": I64(
1019                    1,
1020                ),
1021                "frames.total": I64(
1022                    9,
1023                ),
1024                "messaging.destination.name": String(
1025                    "default",
1026                ),
1027                "messaging.message.body.size": I64(
1028                    100,
1029                ),
1030                "messaging.message.id": String(
1031                    "abc123",
1032                ),
1033                "messaging.message.receive.latency": I64(
1034                    40,
1035                ),
1036                "messaging.message.retry.count": I64(
1037                    3,
1038                ),
1039                "messaging.operation.name": String(
1040                    "publish",
1041                ),
1042                "messaging.operation.type": String(
1043                    "create",
1044                ),
1045                "url.full": String(
1046                    "my_url.com",
1047                ),
1048                "user_agent.original": String(
1049                    "Chrome",
1050                ),
1051            },
1052        }
1053        "###);
1054
1055        assert_eq!(
1056            data.get("foo").and_then(Annotated::value),
1057            Some(&Value::I64(2))
1058        );
1059        assert_eq!(data.get_value("foo"), Some(&Value::I64(2)));
1060        assert_eq!(data.get_str("bar"), Some("3"));
1061        assert_eq!(data.get_str("foo"), None);
1062        data.insert("bool", Annotated::new(Value::Bool(true)));
1063        assert_eq!(data.get_value("bool"), Some(&Value::Bool(true)));
1064        data.insert_value("string", "value".to_owned());
1065        assert_eq!(data.get_str("string"), Some("value"));
1066        assert!(data.contains("string"));
1067        assert_eq!(
1068            data.remove("string").and_then(Annotated::into_value),
1069            Some(Value::String("value".to_owned()))
1070        );
1071        assert!(!data.contains("string"));
1072        assert_eq!(
1073            Getter::get_value(&data, "db\\.system\\.name"),
1074            Some(Val::String("mysql"))
1075        );
1076        assert_eq!(
1077            Getter::get_value(&data, "code\\.lineno"),
1078            Some(Val::U64(123))
1079        );
1080        assert_eq!(
1081            Getter::get_value(&data, "code\\.function"),
1082            Some(Val::String("fn()"))
1083        );
1084        assert_eq!(
1085            Getter::get_value(&data, "code\\.namespace"),
1086            Some(Val::String("ns"))
1087        );
1088        assert_eq!(data.get_value("unknown"), None);
1089    }
1090
1091    #[test]
1092    fn test_span_data_empty_well_known_field() {
1093        let span = r#"{
1094            "data": {
1095                "lcp.url": ""
1096            }
1097        }"#;
1098        let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1099        assert_eq!(span.to_json().unwrap(), r#"{"data":{"lcp.url":""}}"#);
1100    }
1101
1102    #[test]
1103    fn test_span_data_empty_custom_field() {
1104        let span = r#"{
1105            "data": {
1106                "custom_field_empty": ""
1107            }
1108        }"#;
1109        let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1110        assert_eq!(
1111            span.to_json().unwrap(),
1112            r#"{"data":{"custom_field_empty":""}}"#
1113        );
1114    }
1115
1116    #[test]
1117    fn test_span_data_completely_empty() {
1118        let span = r#"{
1119            "data": {}
1120        }"#;
1121        let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1122        assert_eq!(span.to_json().unwrap(), r#"{"data":{}}"#);
1123    }
1124
1125    #[test]
1126    fn test_span_links() {
1127        let span = r#"{
1128            "links": [
1129                {
1130                    "trace_id": "5c79f60c11214eb38604f4ae0781bfb2",
1131                    "span_id": "ab90fdead5f74052",
1132                    "sampled": true,
1133                    "attributes": {
1134                        "sentry.link.type": "previous_trace"
1135                    }
1136                },
1137                {
1138                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1139                    "span_id": "fa90fdead5f74052",
1140                    "sampled": true,
1141                    "attributes": {
1142                        "sentry.link.type": "next_trace"
1143                    }
1144                }
1145            ]
1146        }"#;
1147
1148        let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1149        assert_eq!(
1150            span.to_json().unwrap(),
1151            r#"{"links":[{"trace_id":"5c79f60c11214eb38604f4ae0781bfb2","span_id":"ab90fdead5f74052","sampled":true,"attributes":{"sentry.link.type":"previous_trace"}},{"trace_id":"4c79f60c11214eb38604f4ae0781bfb2","span_id":"fa90fdead5f74052","sampled":true,"attributes":{"sentry.link.type":"next_trace"}}]}"#
1152        );
1153    }
1154
1155    #[test]
1156    fn test_span_kind() {
1157        let span = Annotated::<Span>::from_json(
1158            r#"{
1159                "kind": "???"
1160            }"#,
1161        )
1162        .unwrap()
1163        .into_value()
1164        .unwrap();
1165        assert_eq!(
1166            span.kind.value().unwrap(),
1167            &SpanKind::Unknown("???".to_owned())
1168        );
1169    }
1170}