relay_event_schema/protocol/
span.rs

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