Skip to main content

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