relay_event_schema/protocol/
span.rs

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