relay_event_schema/protocol/
span.rs

1mod convert;
2
3use std::fmt;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use opentelemetry_proto::tonic::trace::v1::span::SpanKind as OtelSpanKind;
8use relay_protocol::{
9    Annotated, Array, Empty, Error, FromValue, Getter, IntoValue, Object, Val, Value,
10};
11
12use crate::processor::ProcessValue;
13use crate::protocol::{
14    EventId, IpAddr, JsonLenientString, LenientString, Measurements, OperationType, OriginType,
15    SpanId, SpanStatus, ThreadId, Timestamp, TraceId,
16};
17
18#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
19#[metastructure(process_func = "process_span", value_type = "Span", trim = false)]
20pub struct Span {
21    /// Timestamp when the span was ended.
22    #[metastructure(required = true)]
23    pub timestamp: Annotated<Timestamp>,
24
25    /// Timestamp when the span started.
26    #[metastructure(required = true)]
27    pub start_timestamp: Annotated<Timestamp>,
28
29    /// The amount of time in milliseconds spent in this span,
30    /// excluding its immediate child spans.
31    pub exclusive_time: Annotated<f64>,
32
33    /// Span type (see `OperationType` docs).
34    #[metastructure(max_chars = 128)]
35    pub op: Annotated<OperationType>,
36
37    /// The Span id.
38    #[metastructure(required = true)]
39    pub span_id: Annotated<SpanId>,
40
41    /// The ID of the span enclosing this span.
42    pub parent_span_id: Annotated<SpanId>,
43
44    /// The ID of the trace the span belongs to.
45    #[metastructure(required = true)]
46    pub trace_id: Annotated<TraceId>,
47
48    /// A unique identifier for a segment within a trace (8 byte hexadecimal string).
49    ///
50    /// For spans embedded in transactions, the `segment_id` is the `span_id` of the containing
51    /// transaction.
52    pub segment_id: Annotated<SpanId>,
53
54    /// Whether or not the current span is the root of the segment.
55    pub is_segment: Annotated<bool>,
56
57    /// Indicates whether a span's parent is remote.
58    ///
59    /// For OpenTelemetry spans, this is derived from span flags bits 8 and 9. See
60    /// `SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK` and `SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK`.
61    ///
62    /// The states are:
63    ///  - `empty`: unknown
64    ///  - `false`: is not remote
65    ///  - `true`: is remote
66    pub is_remote: Annotated<bool>,
67
68    /// The status of a span.
69    pub status: Annotated<SpanStatus>,
70
71    /// Human readable description of a span (e.g. method URL).
72    #[metastructure(pii = "maybe")]
73    pub description: Annotated<String>,
74
75    /// Arbitrary tags on a span, like on the top-level event.
76    #[metastructure(pii = "maybe")]
77    pub tags: Annotated<Object<JsonLenientString>>,
78
79    /// The origin of the span indicates what created the span (see [OriginType] docs).
80    #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
81    pub origin: Annotated<OriginType>,
82
83    /// ID of a profile that can be associated with the span.
84    pub profile_id: Annotated<EventId>,
85
86    /// Arbitrary additional data on a span.
87    ///
88    /// Besides arbitrary user data, this object also contains SDK-provided fields used by the
89    /// product (see <https://develop.sentry.dev/sdk/performance/span-data-conventions/>).
90    #[metastructure(pii = "true")]
91    pub data: Annotated<SpanData>,
92
93    /// Links from this span to other spans
94    #[metastructure(pii = "maybe")]
95    pub links: Annotated<Array<SpanLink>>,
96
97    /// Tags generated by Relay. These tags are a superset of the tags set on span metrics.
98    pub sentry_tags: Annotated<SentryTags>,
99
100    /// Timestamp when the span has been received by Sentry.
101    pub received: Annotated<Timestamp>,
102
103    /// Measurements which holds observed values such as web vitals.
104    #[metastructure(skip_serialization = "empty")]
105    #[metastructure(omit_from_schema)] // we only document error events for now
106    pub measurements: Annotated<Measurements>,
107
108    /// Platform identifier.
109    ///
110    /// See [`Event::platform`](`crate::protocol::Event::platform`).
111    #[metastructure(skip_serialization = "empty")]
112    pub platform: Annotated<String>,
113
114    /// Whether the span is a segment span that was converted from a transaction.
115    #[metastructure(skip_serialization = "empty")]
116    pub was_transaction: Annotated<bool>,
117
118    // Used to clarify the relationship between parents and children, or to distinguish between
119    // spans, e.g. a `server` and `client` span with the same name.
120    //
121    // See <https://opentelemetry.io/docs/specs/otel/trace/api/#spankind>
122    #[metastructure(skip_serialization = "empty", trim = false)]
123    pub kind: Annotated<SpanKind>,
124
125    /// Temporary flag that controls where performance issues are detected.
126    ///
127    /// When the flag is set to true, performance issues will be detected on this span provided it
128    /// is a root (segment) instead of the transaction event.
129    ///
130    /// Only set on root spans extracted from transactions.
131    #[metastructure(skip_serialization = "empty", trim = false)]
132    pub _performance_issues_spans: Annotated<bool>,
133
134    // TODO remove retain when the api stabilizes
135    /// Additional arbitrary fields for forwards compatibility.
136    #[metastructure(additional_properties, retain = true, pii = "maybe")]
137    pub other: Object<Value>,
138}
139
140impl Span {
141    /// Returns the value of an attribute on the span.
142    ///
143    /// This primarily looks up the attribute in the `data` object, but falls back to the `tags`
144    /// object if the attribute is not found.
145    fn attribute(&self, key: &str) -> Option<Val<'_>> {
146        Some(match self.data.value()?.get_value(key) {
147            Some(value) => value,
148            None => self.tags.value()?.get(key)?.as_str()?.into(),
149        })
150    }
151}
152
153impl Getter for Span {
154    fn get_value(&self, path: &str) -> Option<Val<'_>> {
155        let span_prefix = path.strip_prefix("span.");
156        if let Some(span_prefix) = span_prefix {
157            return Some(match span_prefix {
158                "exclusive_time" => self.exclusive_time.value()?.into(),
159                "description" => self.description.as_str()?.into(),
160                "op" => self.op.as_str()?.into(),
161                "span_id" => self.span_id.as_str()?.into(),
162                "parent_span_id" => self.parent_span_id.as_str()?.into(),
163                "trace_id" => self.trace_id.value()?.deref().into(),
164                "status" => self.status.as_str()?.into(),
165                "origin" => self.origin.as_str()?.into(),
166                "duration" => {
167                    let start_timestamp = *self.start_timestamp.value()?;
168                    let timestamp = *self.timestamp.value()?;
169                    relay_common::time::chrono_to_positive_millis(timestamp - start_timestamp)
170                        .into()
171                }
172                "was_transaction" => self.was_transaction.value().unwrap_or(&false).into(),
173                path => {
174                    if let Some(key) = path.strip_prefix("tags.") {
175                        self.tags.value()?.get(key)?.as_str()?.into()
176                    } else if let Some(key) = path.strip_prefix("data.") {
177                        self.attribute(key)?
178                    } else if let Some(key) = path.strip_prefix("sentry_tags.") {
179                        self.sentry_tags.value()?.get_value(key)?
180                    } else if let Some(rest) = path.strip_prefix("measurements.") {
181                        let name = rest.strip_suffix(".value")?;
182                        self.measurements
183                            .value()?
184                            .get(name)?
185                            .value()?
186                            .value
187                            .value()?
188                            .into()
189                    } else {
190                        return None;
191                    }
192                }
193            });
194        }
195
196        // For backward compatibility with event-based rules, we try to support `event.` fields also
197        // for a span.
198        let event_prefix = path.strip_prefix("event.")?;
199        Some(match event_prefix {
200            "release" => self.data.value()?.release.as_str()?.into(),
201            "environment" => self.data.value()?.environment.as_str()?.into(),
202            "transaction" => self.data.value()?.segment_name.as_str()?.into(),
203            "contexts.browser.name" => self.data.value()?.browser_name.as_str()?.into(),
204            // TODO: we might want to add additional fields once they are added to the span.
205            _ => return None,
206        })
207    }
208}
209
210/// Indexable fields added by sentry (server-side).
211#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
212#[metastructure(trim = false)]
213pub struct SentryTags {
214    pub release: Annotated<String>,
215    #[metastructure(pii = "true")]
216    pub user: Annotated<String>,
217    #[metastructure(pii = "true", field = "user.id")]
218    pub user_id: Annotated<String>,
219    #[metastructure(pii = "true", field = "user.ip")]
220    pub user_ip: Annotated<String>,
221    #[metastructure(pii = "true", field = "user.username")]
222    pub user_username: Annotated<String>,
223    #[metastructure(pii = "true", field = "user.email")]
224    pub user_email: Annotated<String>,
225    pub environment: Annotated<String>,
226    pub transaction: Annotated<String>,
227    #[metastructure(field = "transaction.method")]
228    pub transaction_method: Annotated<String>,
229    #[metastructure(field = "transaction.op")]
230    pub transaction_op: Annotated<String>,
231    #[metastructure(field = "browser.name")]
232    pub browser_name: Annotated<String>,
233    #[metastructure(field = "sdk.name")]
234    pub sdk_name: Annotated<String>,
235    #[metastructure(field = "sdk.version")]
236    pub sdk_version: Annotated<String>,
237    pub platform: Annotated<String>,
238    // `"true"` if the transaction was sent by a mobile SDK(String).
239    pub mobile: Annotated<String>,
240    #[metastructure(field = "device.class")]
241    pub device_class: Annotated<String>,
242    #[metastructure(field = "device.family")]
243    pub device_family: Annotated<String>,
244    #[metastructure(field = "device.arch")]
245    pub device_arch: Annotated<String>,
246    #[metastructure(field = "device.battery_level")]
247    pub device_battery_level: Annotated<String>,
248    #[metastructure(field = "device.brand")]
249    pub device_brand: Annotated<String>,
250    #[metastructure(field = "device.charging")]
251    pub device_charging: Annotated<String>,
252    #[metastructure(field = "device.locale")]
253    pub device_locale: Annotated<String>,
254    #[metastructure(field = "device.model_id")]
255    pub device_model_id: Annotated<String>,
256    #[metastructure(field = "device.name")]
257    pub device_name: Annotated<String>,
258    #[metastructure(field = "device.online")]
259    pub device_online: Annotated<String>,
260    #[metastructure(field = "device.orientation")]
261    pub device_orientation: Annotated<String>,
262    #[metastructure(field = "device.screen_density")]
263    pub device_screen_density: Annotated<String>,
264    #[metastructure(field = "device.screen_dpi")]
265    pub device_screen_dpi: Annotated<String>,
266    #[metastructure(field = "device.screen_height_pixels")]
267    pub device_screen_height_pixels: Annotated<String>,
268    #[metastructure(field = "device.screen_width_pixels")]
269    pub device_screen_width_pixels: Annotated<String>,
270    #[metastructure(field = "device.simulator")]
271    pub device_simulator: Annotated<String>,
272    #[metastructure(field = "device.uuid")]
273    pub device_uuid: Annotated<String>,
274    #[metastructure(field = "app.device")]
275    pub app_device: Annotated<String>,
276    #[metastructure(field = "device.model")]
277    pub device_model: Annotated<String>,
278    pub runtime: Annotated<String>,
279    #[metastructure(field = "runtime.name")]
280    pub runtime_name: Annotated<String>,
281    pub browser: Annotated<String>,
282    pub os: Annotated<String>,
283    #[metastructure(field = "os.rooted")]
284    pub os_rooted: Annotated<String>,
285    #[metastructure(field = "gpu.name")]
286    pub gpu_name: Annotated<String>,
287    #[metastructure(field = "gpu.vendor")]
288    pub gpu_vendor: Annotated<String>,
289    #[metastructure(field = "monitor.id")]
290    pub monitor_id: Annotated<String>,
291    #[metastructure(field = "monitor.slug")]
292    pub monitor_slug: Annotated<String>,
293    #[metastructure(field = "request.url")]
294    pub request_url: Annotated<String>,
295    #[metastructure(field = "request.method")]
296    pub request_method: Annotated<String>,
297    // Mobile OS the transaction originated from(String).
298    #[metastructure(field = "os.name")]
299    pub os_name: Annotated<String>,
300    pub action: Annotated<String>,
301    /// The group of the ancestral span with op ai.pipeline.(String)*
302    pub ai_pipeline_group: Annotated<String>,
303    pub category: Annotated<String>,
304    pub description: Annotated<String>,
305    pub domain: Annotated<String>,
306    pub raw_domain: Annotated<String>,
307    pub group: Annotated<String>,
308    #[metastructure(field = "http.decoded_response_content_length")]
309    pub http_decoded_response_content_length: Annotated<String>,
310    #[metastructure(field = "http.response_content_length")]
311    pub http_response_content_length: Annotated<String>,
312    #[metastructure(field = "http.response_transfer_size")]
313    pub http_response_transfer_size: Annotated<String>,
314    #[metastructure(field = "resource.render_blocking_status")]
315    pub resource_render_blocking_status: Annotated<String>,
316    pub op: Annotated<String>,
317    pub status: Annotated<String>,
318    pub status_code: Annotated<String>,
319    pub system: Annotated<String>,
320    /// Contributes to Time-To-Initial-Display(String).
321    pub ttid: Annotated<String>,
322    /// Contributes to Time-To-Full-Display(String).
323    pub ttfd: Annotated<String>,
324    /// File extension for resource spans(String).
325    pub file_extension: Annotated<String>,
326    /// Span started on main thread(String).
327    pub main_thread: Annotated<String>,
328    /// The start type of the application when the span occurred(String).
329    pub app_start_type: Annotated<String>,
330    pub replay_id: Annotated<String>,
331    #[metastructure(field = "cache.hit")]
332    pub cache_hit: Annotated<String>,
333    #[metastructure(field = "cache.key")]
334    pub cache_key: Annotated<String>,
335    #[metastructure(field = "trace.status")]
336    pub trace_status: Annotated<String>,
337    #[metastructure(field = "messaging.destination.name")]
338    pub messaging_destination_name: Annotated<String>,
339    #[metastructure(field = "messaging.message.id")]
340    pub messaging_message_id: Annotated<String>,
341    #[metastructure(field = "messaging.operation.name")]
342    pub messaging_operation_name: Annotated<String>,
343    #[metastructure(field = "messaging.operation.type")]
344    pub messaging_operation_type: Annotated<String>,
345    #[metastructure(field = "thread.name")]
346    pub thread_name: Annotated<String>,
347    #[metastructure(field = "thread.id")]
348    pub thread_id: Annotated<String>,
349    pub profiler_id: Annotated<String>,
350    #[metastructure(field = "user.geo.country_code")]
351    pub user_country_code: Annotated<String>,
352    #[metastructure(field = "user.geo.subregion")]
353    pub user_subregion: Annotated<String>,
354    // no need for an `other` entry here because these fields are added server-side.
355    // If an upstream relay does not recognize a field it will be dropped.
356}
357
358impl Getter for SentryTags {
359    fn get_value(&self, path: &str) -> Option<Val<'_>> {
360        let value = match path {
361            "action" => &self.action,
362            "ai_pipeline_group" => &self.ai_pipeline_group,
363            "app_start_type" => &self.app_start_type,
364            "browser.name" => &self.browser_name,
365            "cache.hit" => &self.cache_hit,
366            "cache.key" => &self.cache_key,
367            "category" => &self.category,
368            "description" => &self.description,
369            "device.class" => &self.device_class,
370            "device.family" => &self.device_family,
371            "device.arch" => &self.device_arch,
372            "device.battery_level" => &self.device_battery_level,
373            "device.brand" => &self.device_brand,
374            "device.charging" => &self.device_charging,
375            "device.locale" => &self.device_locale,
376            "device.model_id" => &self.device_model_id,
377            "device.name" => &self.device_name,
378            "device.online" => &self.device_online,
379            "device.orientation" => &self.device_orientation,
380            "device.screen_density" => &self.device_screen_density,
381            "device.screen_dpi" => &self.device_screen_dpi,
382            "device.screen_height_pixels" => &self.device_screen_height_pixels,
383            "device.screen_width_pixels" => &self.device_screen_width_pixels,
384            "device.simulator" => &self.device_simulator,
385            "device.uuid" => &self.device_uuid,
386            "app.device" => &self.app_device,
387            "device.model" => &self.device_model,
388            "runtime" => &self.runtime,
389            "runtime.name" => &self.runtime_name,
390            "browser" => &self.browser,
391            "os" => &self.os,
392            "os.rooted" => &self.os_rooted,
393            "gpu.name" => &self.gpu_name,
394            "gpu.vendor" => &self.gpu_vendor,
395            "monitor.id" => &self.monitor_id,
396            "monitor.slug" => &self.monitor_slug,
397            "request.url" => &self.request_url,
398            "request.method" => &self.request_method,
399            "domain" => &self.domain,
400            "environment" => &self.environment,
401            "file_extension" => &self.file_extension,
402            "group" => &self.group,
403            "http.decoded_response_content_length" => &self.http_decoded_response_content_length,
404            "http.response_content_length" => &self.http_response_content_length,
405            "http.response_transfer_size" => &self.http_response_transfer_size,
406            "main_thread" => &self.main_thread,
407            "messaging.destination.name" => &self.messaging_destination_name,
408            "messaging.message.id" => &self.messaging_message_id,
409            "messaging.operation.name" => &self.messaging_operation_name,
410            "messaging.operation.type" => &self.messaging_operation_type,
411            "mobile" => &self.mobile,
412            "op" => &self.op,
413            "os.name" => &self.os_name,
414            "platform" => &self.platform,
415            "profiler_id" => &self.profiler_id,
416            "raw_domain" => &self.raw_domain,
417            "release" => &self.release,
418            "replay_id" => &self.replay_id,
419            "resource.render_blocking_status" => &self.resource_render_blocking_status,
420            "sdk.name" => &self.sdk_name,
421            "sdk.version" => &self.sdk_version,
422            "status_code" => &self.status_code,
423            "status" => &self.status,
424            "system" => &self.system,
425            "thread.id" => &self.thread_id,
426            "thread.name" => &self.thread_name,
427            "trace.status" => &self.trace_status,
428            "transaction.method" => &self.transaction_method,
429            "transaction.op" => &self.transaction_op,
430            "transaction" => &self.transaction,
431            "ttfd" => &self.ttfd,
432            "ttid" => &self.ttid,
433            "user.email" => &self.user_email,
434            "user.geo.country_code" => &self.user_country_code,
435            "user.geo.subregion" => &self.user_subregion,
436            "user.id" => &self.user_id,
437            "user.ip" => &self.user_ip,
438            "user.username" => &self.user_username,
439            "user" => &self.user,
440            _ => return None,
441        };
442        Some(value.as_str()?.into())
443    }
444}
445
446/// Arbitrary additional data on a span.
447///
448/// Besides arbitrary user data, this type also contains SDK-provided fields used by the
449/// product (see <https://develop.sentry.dev/sdk/performance/span-data-conventions/>).
450#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
451#[metastructure(trim = false)]
452pub struct SpanData {
453    /// Mobile app start variant.
454    ///
455    /// Can be either "cold" or "warm".
456    #[metastructure(field = "app_start_type")] // TODO: no dot?
457    pub app_start_type: Annotated<Value>,
458
459    /// The total tokens that were used by an LLM call
460    #[metastructure(field = "ai.total_tokens.used")]
461    pub ai_total_tokens_used: Annotated<Value>,
462
463    /// The input tokens used by an LLM call (usually cheaper than output tokens)
464    #[metastructure(field = "ai.prompt_tokens.used")]
465    pub ai_prompt_tokens_used: Annotated<Value>,
466
467    /// The output tokens used by an LLM call (the ones the LLM actually generated)
468    #[metastructure(field = "ai.completion_tokens.used")]
469    pub ai_completion_tokens_used: Annotated<Value>,
470
471    /// The client's browser name.
472    #[metastructure(field = "browser.name")]
473    pub browser_name: Annotated<String>,
474
475    /// The source code file name that identifies the code unit as uniquely as possible.
476    #[metastructure(field = "code.filepath", pii = "maybe")]
477    pub code_filepath: Annotated<Value>,
478    /// The line number in `code.filepath` best representing the operation.
479    #[metastructure(field = "code.lineno", pii = "maybe")]
480    pub code_lineno: Annotated<Value>,
481    /// The method or function name, or equivalent.
482    ///
483    /// Usually rightmost part of the code unit's name.
484    #[metastructure(field = "code.function", pii = "maybe")]
485    pub code_function: Annotated<Value>,
486    /// The "namespace" within which `code.function` is defined.
487    ///
488    /// Usually the qualified class or module name, such that
489    /// `code.namespace + some separator + code.function`
490    /// form a unique identifier for the code unit.
491    #[metastructure(field = "code.namespace", pii = "maybe")]
492    pub code_namespace: Annotated<Value>,
493
494    /// The name of the operation being executed.
495    ///
496    /// E.g. the MongoDB command name such as findAndModify, or the SQL keyword.
497    /// Based on [OpenTelemetry's call level db attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md#call-level-attributes).
498    #[metastructure(field = "db.operation")]
499    pub db_operation: Annotated<Value>,
500
501    /// An identifier for the database management system (DBMS) product being used.
502    ///
503    /// See [OpenTelemetry docs for a list of well-known identifiers](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md#notes-and-well-known-identifiers-for-dbsystem).
504    #[metastructure(field = "db.system")]
505    pub db_system: Annotated<Value>,
506
507    /// The name of a collection (table, container) within the database.
508    ///
509    /// See [OpenTelemetry's database span semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/database-spans/#common-attributes).
510    #[metastructure(
511        field = "db.collection.name",
512        legacy_alias = "db.cassandra.table",
513        legacy_alias = "db.cosmosdb.container",
514        legacy_alias = "db.mongodb.collection",
515        legacy_alias = "db.sql.table"
516    )]
517    pub db_collection_name: Annotated<Value>,
518
519    /// The sentry environment.
520    #[metastructure(field = "sentry.environment", legacy_alias = "environment")]
521    pub environment: Annotated<String>,
522
523    /// The release version of the project.
524    #[metastructure(field = "sentry.release", legacy_alias = "release")]
525    pub release: Annotated<LenientString>,
526
527    /// The decoded body size of the response (in bytes).
528    #[metastructure(field = "http.decoded_response_content_length")]
529    pub http_decoded_response_content_length: Annotated<Value>,
530
531    /// The HTTP method used.
532    #[metastructure(
533        field = "http.request_method",
534        legacy_alias = "http.method",
535        legacy_alias = "method"
536    )]
537    pub http_request_method: Annotated<Value>,
538
539    /// The encoded body size of the response (in bytes).
540    #[metastructure(field = "http.response_content_length")]
541    pub http_response_content_length: Annotated<Value>,
542
543    /// The transfer size of the response (in bytes).
544    #[metastructure(field = "http.response_transfer_size")]
545    pub http_response_transfer_size: Annotated<Value>,
546
547    /// The render blocking status of the resource.
548    #[metastructure(field = "resource.render_blocking_status")]
549    pub resource_render_blocking_status: Annotated<Value>,
550
551    /// Name of the web server host.
552    #[metastructure(field = "server.address")]
553    pub server_address: Annotated<Value>,
554
555    /// Whether cache was hit or miss on a read operation.
556    #[metastructure(field = "cache.hit")]
557    pub cache_hit: Annotated<Value>,
558
559    /// The name of the cache key.
560    #[metastructure(field = "cache.key")]
561    pub cache_key: Annotated<Value>,
562
563    /// The size of the cache item.
564    #[metastructure(field = "cache.item_size")]
565    pub cache_item_size: Annotated<Value>,
566
567    /// The status HTTP response.
568    #[metastructure(field = "http.response.status_code", legacy_alias = "status_code")]
569    pub http_response_status_code: Annotated<Value>,
570
571    /// The 'name' field of the ancestor span with op ai.pipeline.*
572    #[metastructure(field = "ai.pipeline.name")]
573    pub ai_pipeline_name: Annotated<Value>,
574
575    /// The Model ID of an AI pipeline, e.g., gpt-4
576    #[metastructure(field = "ai.model_id")]
577    pub ai_model_id: Annotated<Value>,
578
579    /// The input messages to an AI model call
580    #[metastructure(field = "ai.input_messages")]
581    pub ai_input_messages: Annotated<Value>,
582
583    /// The responses to an AI model call
584    #[metastructure(field = "ai.responses")]
585    pub ai_responses: Annotated<Value>,
586
587    /// Label identifying a thread from where the span originated.
588    #[metastructure(field = "thread.name")]
589    pub thread_name: Annotated<String>,
590
591    /// ID of thread from where the span originated.
592    #[metastructure(field = "thread.id")]
593    pub thread_id: Annotated<ThreadId>,
594
595    /// Name of the segment that this span belongs to (see `segment_id`).
596    ///
597    /// This corresponds to the transaction name in the transaction-based model.
598    ///
599    /// For INP spans, this is the route name where the interaction occurred.
600    #[metastructure(field = "sentry.segment.name", legacy_alias = "transaction")]
601    pub segment_name: Annotated<String>,
602
603    /// Name of the UI component (e.g. React).
604    #[metastructure(field = "ui.component_name")]
605    pub ui_component_name: Annotated<Value>,
606
607    /// The URL scheme, e.g. `"https"`.
608    #[metastructure(field = "url.scheme")]
609    pub url_scheme: Annotated<Value>,
610
611    /// User Display
612    #[metastructure(field = "user")]
613    pub user: Annotated<Value>,
614
615    /// User email address.
616    ///
617    /// <https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/>
618    #[metastructure(field = "user.email")]
619    pub user_email: Annotated<String>,
620
621    /// User’s full name.
622    ///
623    /// <https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/>
624    #[metastructure(field = "user.full_name")]
625    pub user_full_name: Annotated<String>,
626
627    /// Two-letter country code (ISO 3166-1 alpha-2).
628    ///
629    /// This is not an OTel convention (yet).
630    #[metastructure(field = "user.geo.country_code")]
631    pub user_geo_country_code: Annotated<String>,
632
633    /// Human readable city name.
634    ///
635    /// This is not an OTel convention (yet).
636    #[metastructure(field = "user.geo.city")]
637    pub user_geo_city: Annotated<String>,
638
639    /// Human readable subdivision name.
640    ///
641    /// This is not an OTel convention (yet).
642    #[metastructure(field = "user.geo.subdivision")]
643    pub user_geo_subdivision: Annotated<String>,
644
645    /// Human readable region name or code.
646    ///
647    /// This is not an OTel convention (yet).
648    #[metastructure(field = "user.geo.region")]
649    pub user_geo_region: Annotated<String>,
650
651    /// Unique user hash to correlate information for a user in anonymized form.
652    ///
653    /// <https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/>
654    #[metastructure(field = "user.hash")]
655    pub user_hash: Annotated<String>,
656
657    /// Unique identifier of the user.
658    ///
659    /// <https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/>
660    #[metastructure(field = "user.id")]
661    pub user_id: Annotated<String>,
662
663    /// Short name or login/username of the user.
664    ///
665    /// <https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/>
666    #[metastructure(field = "user.name")]
667    pub user_name: Annotated<String>,
668
669    /// Array of user roles at the time of the event.
670    ///
671    /// <https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/>
672    #[metastructure(field = "user.roles")]
673    pub user_roles: Annotated<Array<String>>,
674
675    /// Exclusive Time
676    #[metastructure(field = "sentry.exclusive_time")]
677    pub exclusive_time: Annotated<Value>,
678
679    /// Profile ID
680    #[metastructure(field = "profile_id")]
681    pub profile_id: Annotated<Value>,
682
683    /// Replay ID
684    #[metastructure(field = "sentry.replay.id", legacy_alias = "replay_id")]
685    pub replay_id: Annotated<Value>,
686
687    /// The sentry SDK (see [`crate::protocol::ClientSdkInfo`]).
688    #[metastructure(field = "sentry.sdk.name")]
689    pub sdk_name: Annotated<String>,
690
691    /// The sentry SDK version (see [`crate::protocol::ClientSdkInfo`]).
692    #[metastructure(field = "sentry.sdk.version")]
693    pub sdk_version: Annotated<String>,
694
695    /// Slow Frames
696    #[metastructure(field = "sentry.frames.slow", legacy_alias = "frames.slow")]
697    pub frames_slow: Annotated<Value>,
698
699    /// Frozen Frames
700    #[metastructure(field = "sentry.frames.frozen", legacy_alias = "frames.frozen")]
701    pub frames_frozen: Annotated<Value>,
702
703    /// Total Frames
704    #[metastructure(field = "sentry.frames.total", legacy_alias = "frames.total")]
705    pub frames_total: Annotated<Value>,
706
707    // Frames Delay (in seconds)
708    #[metastructure(field = "frames.delay")]
709    pub frames_delay: Annotated<Value>,
710
711    // Messaging Destination Name
712    #[metastructure(field = "messaging.destination.name")]
713    pub messaging_destination_name: Annotated<String>,
714
715    /// Message Retry Count
716    #[metastructure(field = "messaging.message.retry.count")]
717    pub messaging_message_retry_count: Annotated<Value>,
718
719    /// Message Receive Latency
720    #[metastructure(field = "messaging.message.receive.latency")]
721    pub messaging_message_receive_latency: Annotated<Value>,
722
723    /// Message Body Size
724    #[metastructure(field = "messaging.message.body.size")]
725    pub messaging_message_body_size: Annotated<Value>,
726
727    /// Message ID
728    #[metastructure(field = "messaging.message.id")]
729    pub messaging_message_id: Annotated<String>,
730
731    /// Messaging Operation Name
732    #[metastructure(field = "messaging.operation.name")]
733    pub messaging_operation_name: Annotated<String>,
734
735    /// Messaging Operation Type
736    #[metastructure(field = "messaging.operation.type")]
737    pub messaging_operation_type: Annotated<String>,
738
739    /// Value of the HTTP User-Agent header sent by the client.
740    #[metastructure(field = "user_agent.original")]
741    pub user_agent_original: Annotated<String>,
742
743    /// Absolute URL of a network resource.
744    #[metastructure(field = "url.full")]
745    pub url_full: Annotated<String>,
746
747    /// The client's IP address.
748    #[metastructure(field = "client.address")]
749    pub client_address: Annotated<IpAddr>,
750
751    /// The current route in the application.
752    ///
753    /// Set by React Native SDK.
754    #[metastructure(pii = "maybe", skip_serialization = "empty")]
755    pub route: Annotated<Route>,
756    /// The previous route in the application
757    ///
758    /// Set by React Native SDK.
759    #[metastructure(field = "previousRoute", pii = "maybe", skip_serialization = "empty")]
760    pub previous_route: Annotated<Route>,
761
762    // The dom element responsible for the largest contentful paint.
763    #[metastructure(field = "lcp.element")]
764    pub lcp_element: Annotated<String>,
765
766    // The size of the largest contentful paint element.
767    #[metastructure(field = "lcp.size")]
768    pub lcp_size: Annotated<u64>,
769
770    // The id of the largest contentful paint element.
771    #[metastructure(field = "lcp.id")]
772    pub lcp_id: Annotated<String>,
773
774    // The url of the largest contentful paint element.
775    #[metastructure(field = "lcp.url")]
776    pub lcp_url: Annotated<String>,
777
778    /// Other fields in `span.data`.
779    #[metastructure(
780        additional_properties,
781        pii = "true",
782        retain = true,
783        skip_serialization = "null" // applies to child elements
784    )]
785    pub other: Object<Value>,
786}
787
788impl Getter for SpanData {
789    fn get_value(&self, path: &str) -> Option<Val<'_>> {
790        Some(match path {
791            "app_start_type" => self.app_start_type.value()?.into(),
792            "browser\\.name" => self.browser_name.as_str()?.into(),
793            "code\\.filepath" => self.code_filepath.value()?.into(),
794            "code\\.function" => self.code_function.value()?.into(),
795            "code\\.lineno" => self.code_lineno.value()?.into(),
796            "code\\.namespace" => self.code_namespace.value()?.into(),
797            "db.operation" => self.db_operation.value()?.into(),
798            "db\\.system" => self.db_system.value()?.into(),
799            "environment" => self.environment.as_str()?.into(),
800            "http\\.decoded_response_content_length" => {
801                self.http_decoded_response_content_length.value()?.into()
802            }
803            "http\\.request_method" | "http\\.method" | "method" => {
804                self.http_request_method.value()?.into()
805            }
806            "http\\.response_content_length" => self.http_response_content_length.value()?.into(),
807            "http\\.response_transfer_size" => self.http_response_transfer_size.value()?.into(),
808            "http\\.response.status_code" | "status_code" => {
809                self.http_response_status_code.value()?.into()
810            }
811            "resource\\.render_blocking_status" => {
812                self.resource_render_blocking_status.value()?.into()
813            }
814            "server\\.address" => self.server_address.value()?.into(),
815            "thread\\.name" => self.thread_name.as_str()?.into(),
816            "ui\\.component_name" => self.ui_component_name.value()?.into(),
817            "url\\.scheme" => self.url_scheme.value()?.into(),
818            "user" => self.user.value()?.into(),
819            "user\\.email" => self.user_email.as_str()?.into(),
820            "user\\.full_name" => self.user_full_name.as_str()?.into(),
821            "user\\.geo\\.city" => self.user_geo_city.as_str()?.into(),
822            "user\\.geo\\.country_code" => self.user_geo_country_code.as_str()?.into(),
823            "user\\.geo\\.region" => self.user_geo_region.as_str()?.into(),
824            "user\\.geo\\.subdivision" => self.user_geo_subdivision.as_str()?.into(),
825            "user\\.hash" => self.user_hash.as_str()?.into(),
826            "user\\.id" => self.user_id.as_str()?.into(),
827            "user\\.name" => self.user_name.as_str()?.into(),
828            "transaction" => self.segment_name.as_str()?.into(),
829            "release" => self.release.as_str()?.into(),
830            _ => {
831                let escaped = path.replace("\\.", "\0");
832                let mut path = escaped.split('.').map(|s| s.replace('\0', "."));
833                let root = path.next()?;
834
835                let mut val = self.other.get(&root)?.value()?;
836                for part in path {
837                    // While there is path segments left, `val` has to be an Object.
838                    let relay_protocol::Value::Object(map) = val else {
839                        return None;
840                    };
841                    val = map.get(&part)?.value()?;
842                }
843                val.into()
844            }
845        })
846    }
847}
848
849/// A link from a span to another span.
850#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
851#[metastructure(trim = false)]
852pub struct SpanLink {
853    /// The trace id of the linked span
854    #[metastructure(required = true, trim = false)]
855    pub trace_id: Annotated<TraceId>,
856
857    /// The span id of the linked span
858    #[metastructure(required = true, trim = false)]
859    pub span_id: Annotated<SpanId>,
860
861    /// Whether the linked span was positively/negatively sampled
862    #[metastructure(trim = false)]
863    pub sampled: Annotated<bool>,
864
865    /// Span link attributes, similar to span attributes/data
866    #[metastructure(pii = "maybe", trim = false)]
867    pub attributes: Annotated<Object<Value>>,
868
869    /// Additional arbitrary fields for forwards compatibility.
870    #[metastructure(additional_properties, retain = true, pii = "maybe", trim = false)]
871    pub other: Object<Value>,
872}
873
874/// The route in the application, set by React Native SDK.
875#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
876pub struct Route {
877    /// The name of the route.
878    #[metastructure(pii = "maybe", skip_serialization = "empty")]
879    pub name: Annotated<String>,
880
881    /// Parameters assigned to this route.
882    #[metastructure(
883        pii = "true",
884        skip_serialization = "empty",
885        max_depth = 5,
886        max_bytes = 2048
887    )]
888    pub params: Annotated<Object<Value>>,
889
890    /// Additional arbitrary fields for forwards compatibility.
891    #[metastructure(
892        additional_properties,
893        retain = true,
894        pii = "maybe",
895        skip_serialization = "empty"
896    )]
897    pub other: Object<Value>,
898}
899
900impl FromValue for Route {
901    fn from_value(value: Annotated<Value>) -> Annotated<Self>
902    where
903        Self: Sized,
904    {
905        match value {
906            Annotated(Some(Value::String(name)), meta) => Annotated(
907                Some(Route {
908                    name: Annotated::new(name),
909                    ..Default::default()
910                }),
911                meta,
912            ),
913            Annotated(Some(Value::Object(mut values)), meta) => {
914                let mut route: Route = Default::default();
915                if let Some(Annotated(Some(Value::String(name)), _)) = values.remove("name") {
916                    route.name = Annotated::new(name);
917                }
918                if let Some(Annotated(Some(Value::Object(params)), _)) = values.remove("params") {
919                    route.params = Annotated::new(params);
920                }
921
922                if !values.is_empty() {
923                    route.other = values;
924                }
925
926                Annotated(Some(route), meta)
927            }
928            Annotated(None, meta) => Annotated(None, meta),
929            Annotated(Some(value), mut meta) => {
930                meta.add_error(Error::expected("route expected to be an object"));
931                meta.set_original_value(Some(value));
932                Annotated(None, meta)
933            }
934        }
935    }
936}
937
938#[derive(Clone, Debug, PartialEq, ProcessValue)]
939#[repr(i32)]
940pub enum SpanKind {
941    Unspecified = OtelSpanKind::Unspecified as i32,
942    Internal = OtelSpanKind::Internal as i32,
943    Server = OtelSpanKind::Server as i32,
944    Client = OtelSpanKind::Client as i32,
945    Producer = OtelSpanKind::Producer as i32,
946    Consumer = OtelSpanKind::Consumer as i32,
947}
948
949impl SpanKind {
950    pub fn as_str(&self) -> &'static str {
951        match self {
952            Self::Unspecified => "unspecified",
953            Self::Internal => "internal",
954            Self::Server => "server",
955            Self::Client => "client",
956            Self::Producer => "producer",
957            Self::Consumer => "consumer",
958        }
959    }
960}
961
962impl Empty for SpanKind {
963    fn is_empty(&self) -> bool {
964        false
965    }
966}
967
968#[derive(Debug)]
969pub struct ParseSpanKindError;
970
971impl std::str::FromStr for SpanKind {
972    type Err = ParseSpanKindError;
973
974    fn from_str(s: &str) -> Result<Self, Self::Err> {
975        Ok(match s {
976            "unspecified" => SpanKind::Unspecified,
977            "internal" => SpanKind::Internal,
978            "server" => SpanKind::Server,
979            "client" => SpanKind::Client,
980            "producer" => SpanKind::Producer,
981            "consumer" => SpanKind::Consumer,
982            _ => return Err(ParseSpanKindError),
983        })
984    }
985}
986
987impl Default for SpanKind {
988    fn default() -> Self {
989        Self::Internal
990    }
991}
992
993impl From<OtelSpanKind> for SpanKind {
994    fn from(otel_kind: OtelSpanKind) -> Self {
995        match otel_kind {
996            OtelSpanKind::Unspecified => Self::Unspecified,
997            OtelSpanKind::Internal => Self::Internal,
998            OtelSpanKind::Server => Self::Server,
999            OtelSpanKind::Client => Self::Client,
1000            OtelSpanKind::Producer => Self::Producer,
1001            OtelSpanKind::Consumer => Self::Consumer,
1002        }
1003    }
1004}
1005
1006impl fmt::Display for SpanKind {
1007    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1008        write!(f, "{}", self.as_str())
1009    }
1010}
1011
1012impl FromValue for SpanKind {
1013    fn from_value(value: Annotated<Value>) -> Annotated<Self>
1014    where
1015        Self: Sized,
1016    {
1017        match value {
1018            Annotated(Some(Value::String(s)), meta) => Annotated(SpanKind::from_str(&s).ok(), meta),
1019            Annotated(_, meta) => Annotated(None, meta),
1020        }
1021    }
1022}
1023
1024impl IntoValue for SpanKind {
1025    fn into_value(self) -> Value
1026    where
1027        Self: Sized,
1028    {
1029        Value::String(self.to_string())
1030    }
1031
1032    fn serialize_payload<S>(
1033        &self,
1034        s: S,
1035        _behavior: relay_protocol::SkipSerialization,
1036    ) -> Result<S::Ok, S::Error>
1037    where
1038        Self: Sized,
1039        S: serde::Serializer,
1040    {
1041        s.serialize_str(self.as_str())
1042    }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use crate::protocol::Measurement;
1048    use chrono::{TimeZone, Utc};
1049    use relay_base_schema::metrics::{InformationUnit, MetricUnit};
1050    use relay_protocol::RuleCondition;
1051    use similar_asserts::assert_eq;
1052
1053    use super::*;
1054
1055    #[test]
1056    fn test_span_serialization() {
1057        let json = r#"{
1058  "timestamp": 0.0,
1059  "start_timestamp": -63158400.0,
1060  "exclusive_time": 1.23,
1061  "op": "operation",
1062  "span_id": "fa90fdead5f74052",
1063  "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1064  "status": "ok",
1065  "description": "desc",
1066  "origin": "auto.http",
1067  "links": [
1068    {
1069      "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1070      "span_id": "fa90fdead5f74052",
1071      "sampled": true,
1072      "attributes": {
1073        "boolAttr": true,
1074        "numAttr": 123,
1075        "stringAttr": "foo"
1076      }
1077    }
1078  ],
1079  "measurements": {
1080    "memory": {
1081      "value": 9001.0,
1082      "unit": "byte"
1083    }
1084  },
1085  "kind": "server"
1086}"#;
1087        let mut measurements = Object::new();
1088        measurements.insert(
1089            "memory".into(),
1090            Annotated::new(Measurement {
1091                value: Annotated::new(9001.0),
1092                unit: Annotated::new(MetricUnit::Information(InformationUnit::Byte)),
1093            }),
1094        );
1095
1096        let links = Annotated::new(vec![Annotated::new(SpanLink {
1097            trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1098            span_id: Annotated::new(SpanId("fa90fdead5f74052".into())),
1099            sampled: Annotated::new(true),
1100            attributes: Annotated::new({
1101                let mut map: std::collections::BTreeMap<String, Annotated<Value>> = Object::new();
1102                map.insert(
1103                    "stringAttr".into(),
1104                    Annotated::new(Value::String("foo".into())),
1105                );
1106                map.insert("numAttr".into(), Annotated::new(Value::I64(123)));
1107                map.insert("boolAttr".into(), Value::Bool(true).into());
1108                map
1109            }),
1110            ..Default::default()
1111        })]);
1112
1113        let span = Annotated::new(Span {
1114            timestamp: Annotated::new(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap().into()),
1115            start_timestamp: Annotated::new(
1116                Utc.with_ymd_and_hms(1968, 1, 1, 0, 0, 0).unwrap().into(),
1117            ),
1118            exclusive_time: Annotated::new(1.23),
1119            description: Annotated::new("desc".to_owned()),
1120            op: Annotated::new("operation".to_owned()),
1121            trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1122            span_id: Annotated::new(SpanId("fa90fdead5f74052".into())),
1123            status: Annotated::new(SpanStatus::Ok),
1124            origin: Annotated::new("auto.http".to_owned()),
1125            kind: Annotated::new(SpanKind::Server),
1126            measurements: Annotated::new(Measurements(measurements)),
1127            links,
1128            ..Default::default()
1129        });
1130        assert_eq!(json, span.to_json_pretty().unwrap());
1131
1132        let span_from_string = Annotated::from_json(json).unwrap();
1133        assert_eq!(span, span_from_string);
1134    }
1135
1136    #[test]
1137    fn test_getter_span_data() {
1138        let span = Annotated::<Span>::from_json(
1139            r#"{
1140                "data": {
1141                    "foo": {"bar": 1},
1142                    "foo.bar": 2
1143                },
1144                "measurements": {
1145                    "some": {"value": 100.0}
1146                }
1147            }"#,
1148        )
1149        .unwrap()
1150        .into_value()
1151        .unwrap();
1152
1153        assert_eq!(span.get_value("span.data.foo.bar"), Some(Val::I64(1)));
1154        assert_eq!(span.get_value(r"span.data.foo\.bar"), Some(Val::I64(2)));
1155
1156        assert_eq!(span.get_value("span.data"), None);
1157        assert_eq!(span.get_value("span.data."), None);
1158        assert_eq!(span.get_value("span.data.x"), None);
1159
1160        assert_eq!(
1161            span.get_value("span.measurements.some.value"),
1162            Some(Val::F64(100.0))
1163        );
1164    }
1165
1166    #[test]
1167    fn test_getter_was_transaction() {
1168        let mut span = Span::default();
1169        assert_eq!(
1170            span.get_value("span.was_transaction"),
1171            Some(Val::Bool(false))
1172        );
1173        assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1174        assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1175
1176        span.was_transaction.set_value(Some(false));
1177        assert_eq!(
1178            span.get_value("span.was_transaction"),
1179            Some(Val::Bool(false))
1180        );
1181        assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1182        assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1183
1184        span.was_transaction.set_value(Some(true));
1185        assert_eq!(
1186            span.get_value("span.was_transaction"),
1187            Some(Val::Bool(true))
1188        );
1189        assert!(RuleCondition::eq("span.was_transaction", true).matches(&span));
1190        assert!(!RuleCondition::eq("span.was_transaction", false).matches(&span));
1191    }
1192
1193    #[test]
1194    fn test_span_fields_as_event() {
1195        let span = Annotated::<Span>::from_json(
1196            r#"{
1197                "data": {
1198                    "release": "1.0",
1199                    "environment": "prod",
1200                    "sentry.segment.name": "/api/endpoint"
1201                }
1202            }"#,
1203        )
1204        .unwrap()
1205        .into_value()
1206        .unwrap();
1207
1208        assert_eq!(span.get_value("event.release"), Some(Val::String("1.0")));
1209        assert_eq!(
1210            span.get_value("event.environment"),
1211            Some(Val::String("prod"))
1212        );
1213        assert_eq!(
1214            span.get_value("event.transaction"),
1215            Some(Val::String("/api/endpoint"))
1216        );
1217    }
1218
1219    #[test]
1220    fn test_span_duration() {
1221        let span = Annotated::<Span>::from_json(
1222            r#"{
1223                "start_timestamp": 1694732407.8367,
1224                "timestamp": 1694732408.3145
1225            }"#,
1226        )
1227        .unwrap()
1228        .into_value()
1229        .unwrap();
1230
1231        assert_eq!(span.get_value("span.duration"), Some(Val::F64(477.800131)));
1232    }
1233
1234    #[test]
1235    fn test_span_data() {
1236        let data = r#"{
1237        "foo": 2,
1238        "bar": "3",
1239        "db.system": "mysql",
1240        "code.filepath": "task.py",
1241        "code.lineno": 123,
1242        "code.function": "fn()",
1243        "code.namespace": "ns",
1244        "frames.slow": 1,
1245        "frames.frozen": 2,
1246        "frames.total": 9,
1247        "frames.delay": 100,
1248        "messaging.destination.name": "default",
1249        "messaging.message.retry.count": 3,
1250        "messaging.message.receive.latency": 40,
1251        "messaging.message.body.size": 100,
1252        "messaging.message.id": "abc123",
1253        "messaging.operation.name": "publish",
1254        "messaging.operation.type": "create",
1255        "user_agent.original": "Chrome",
1256        "url.full": "my_url.com",
1257        "client.address": "192.168.0.1"
1258    }"#;
1259        let data = Annotated::<SpanData>::from_json(data)
1260            .unwrap()
1261            .into_value()
1262            .unwrap();
1263        insta::assert_debug_snapshot!(data, @r###"
1264        SpanData {
1265            app_start_type: ~,
1266            ai_total_tokens_used: ~,
1267            ai_prompt_tokens_used: ~,
1268            ai_completion_tokens_used: ~,
1269            browser_name: ~,
1270            code_filepath: String(
1271                "task.py",
1272            ),
1273            code_lineno: I64(
1274                123,
1275            ),
1276            code_function: String(
1277                "fn()",
1278            ),
1279            code_namespace: String(
1280                "ns",
1281            ),
1282            db_operation: ~,
1283            db_system: String(
1284                "mysql",
1285            ),
1286            db_collection_name: ~,
1287            environment: ~,
1288            release: ~,
1289            http_decoded_response_content_length: ~,
1290            http_request_method: ~,
1291            http_response_content_length: ~,
1292            http_response_transfer_size: ~,
1293            resource_render_blocking_status: ~,
1294            server_address: ~,
1295            cache_hit: ~,
1296            cache_key: ~,
1297            cache_item_size: ~,
1298            http_response_status_code: ~,
1299            ai_pipeline_name: ~,
1300            ai_model_id: ~,
1301            ai_input_messages: ~,
1302            ai_responses: ~,
1303            thread_name: ~,
1304            thread_id: ~,
1305            segment_name: ~,
1306            ui_component_name: ~,
1307            url_scheme: ~,
1308            user: ~,
1309            user_email: ~,
1310            user_full_name: ~,
1311            user_geo_country_code: ~,
1312            user_geo_city: ~,
1313            user_geo_subdivision: ~,
1314            user_geo_region: ~,
1315            user_hash: ~,
1316            user_id: ~,
1317            user_name: ~,
1318            user_roles: ~,
1319            exclusive_time: ~,
1320            profile_id: ~,
1321            replay_id: ~,
1322            sdk_name: ~,
1323            sdk_version: ~,
1324            frames_slow: I64(
1325                1,
1326            ),
1327            frames_frozen: I64(
1328                2,
1329            ),
1330            frames_total: I64(
1331                9,
1332            ),
1333            frames_delay: I64(
1334                100,
1335            ),
1336            messaging_destination_name: "default",
1337            messaging_message_retry_count: I64(
1338                3,
1339            ),
1340            messaging_message_receive_latency: I64(
1341                40,
1342            ),
1343            messaging_message_body_size: I64(
1344                100,
1345            ),
1346            messaging_message_id: "abc123",
1347            messaging_operation_name: "publish",
1348            messaging_operation_type: "create",
1349            user_agent_original: "Chrome",
1350            url_full: "my_url.com",
1351            client_address: IpAddr(
1352                "192.168.0.1",
1353            ),
1354            route: ~,
1355            previous_route: ~,
1356            lcp_element: ~,
1357            lcp_size: ~,
1358            lcp_id: ~,
1359            lcp_url: ~,
1360            other: {
1361                "bar": String(
1362                    "3",
1363                ),
1364                "foo": I64(
1365                    2,
1366                ),
1367            },
1368        }
1369        "###);
1370
1371        assert_eq!(data.get_value("foo"), Some(Val::U64(2)));
1372        assert_eq!(data.get_value("bar"), Some(Val::String("3")));
1373        assert_eq!(data.get_value("db\\.system"), Some(Val::String("mysql")));
1374        assert_eq!(data.get_value("code\\.lineno"), Some(Val::U64(123)));
1375        assert_eq!(data.get_value("code\\.function"), Some(Val::String("fn()")));
1376        assert_eq!(data.get_value("code\\.namespace"), Some(Val::String("ns")));
1377        assert_eq!(data.get_value("unknown"), None);
1378    }
1379
1380    #[test]
1381    fn test_span_data_empty_well_known_field() {
1382        let span = r#"{
1383            "data": {
1384                "lcp.url": ""
1385            }
1386        }"#;
1387        let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1388        assert_eq!(span.to_json().unwrap(), r#"{"data":{"lcp.url":""}}"#);
1389    }
1390
1391    #[test]
1392    fn test_span_data_empty_custom_field() {
1393        let span = r#"{
1394            "data": {
1395                "custom_field_empty": ""
1396            }
1397        }"#;
1398        let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1399        assert_eq!(
1400            span.to_json().unwrap(),
1401            r#"{"data":{"custom_field_empty":""}}"#
1402        );
1403    }
1404
1405    #[test]
1406    fn test_span_data_completely_empty() {
1407        let span = r#"{
1408            "data": {}
1409        }"#;
1410        let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1411        assert_eq!(span.to_json().unwrap(), r#"{"data":{}}"#);
1412    }
1413
1414    #[test]
1415    fn test_span_links() {
1416        let span = r#"{
1417            "links": [
1418                {
1419                    "trace_id": "5c79f60c11214eb38604f4ae0781bfb2",
1420                    "span_id": "ab90fdead5f74052",
1421                    "sampled": true,
1422                    "attributes": {
1423                        "sentry.link.type": "previous_trace"
1424                    }
1425                },
1426                {
1427                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1428                    "span_id": "fa90fdead5f74052",
1429                    "sampled": true,
1430                    "attributes": {
1431                        "sentry.link.type": "next_trace"
1432                    }
1433                }
1434            ]
1435        }"#;
1436
1437        let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1438        assert_eq!(
1439            span.to_json().unwrap(),
1440            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"}}]}"#
1441        );
1442    }
1443}