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