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