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 #[metastructure(required = true)]
22 pub timestamp: Annotated<Timestamp>,
23
24 #[metastructure(required = true)]
26 pub start_timestamp: Annotated<Timestamp>,
27
28 pub exclusive_time: Annotated<f64>,
31
32 #[metastructure(max_chars = 128)]
34 pub op: Annotated<OperationType>,
35
36 #[metastructure(required = true)]
38 pub span_id: Annotated<SpanId>,
39
40 pub parent_span_id: Annotated<SpanId>,
42
43 #[metastructure(required = true)]
45 pub trace_id: Annotated<TraceId>,
46
47 pub segment_id: Annotated<SpanId>,
52
53 pub is_segment: Annotated<bool>,
55
56 pub is_remote: Annotated<bool>,
66
67 pub status: Annotated<SpanStatus>,
69
70 #[metastructure(pii = "maybe")]
72 pub description: Annotated<String>,
73
74 #[metastructure(pii = "maybe")]
76 pub tags: Annotated<Object<JsonLenientString>>,
77
78 #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
80 pub origin: Annotated<OriginType>,
81
82 pub profile_id: Annotated<EventId>,
84
85 #[metastructure(pii = "true")]
90 pub data: Annotated<SpanData>,
91
92 #[metastructure(pii = "maybe")]
94 pub links: Annotated<Array<SpanLink>>,
95
96 pub sentry_tags: Annotated<SentryTags>,
98
99 pub received: Annotated<Timestamp>,
101
102 #[metastructure(skip_serialization = "empty")]
104 #[metastructure(omit_from_schema)] pub measurements: Annotated<Measurements>,
106
107 #[metastructure(skip_serialization = "empty")]
111 pub platform: Annotated<String>,
112
113 #[metastructure(skip_serialization = "empty")]
115 pub was_transaction: Annotated<bool>,
116
117 #[metastructure(skip_serialization = "empty", trim = false)]
122 pub kind: Annotated<SpanKind>,
123
124 #[metastructure(
131 field = "_performance_issues_spans",
132 skip_serialization = "empty",
133 trim = false
134 )]
135 pub performance_issues_spans: Annotated<bool>,
136
137 #[metastructure(additional_properties, pii = "maybe")]
139 pub other: Object<Value>,
140}
141
142impl Span {
143 fn attribute(&self, key: &str) -> Option<Val<'_>> {
148 Some(match self.data.value()?.get_value(key) {
149 Some(value) => value,
150 None => self.tags.value()?.get(key)?.as_str()?.into(),
151 })
152 }
153}
154
155impl Getter for Span {
156 fn get_value(&self, path: &str) -> Option<Val<'_>> {
157 let span_prefix = path.strip_prefix("span.");
158 if let Some(span_prefix) = span_prefix {
159 return Some(match span_prefix {
160 "exclusive_time" => self.exclusive_time.value()?.into(),
161 "description" => self.description.as_str()?.into(),
162 "op" => self.op.as_str()?.into(),
163 "span_id" => self.span_id.value()?.into(),
164 "parent_span_id" => self.parent_span_id.value()?.into(),
165 "trace_id" => self.trace_id.value()?.deref().into(),
166 "status" => self.status.as_str()?.into(),
167 "is_segment" => self.is_segment.value()?.into(),
168 "origin" => self.origin.as_str()?.into(),
169 "duration" => {
170 let start_timestamp = *self.start_timestamp.value()?;
171 let timestamp = *self.timestamp.value()?;
172 relay_common::time::chrono_to_positive_millis(timestamp - start_timestamp)
173 .into()
174 }
175 "was_transaction" => self.was_transaction.value().unwrap_or(&false).into(),
176 path => {
177 if let Some(key) = path.strip_prefix("tags.") {
178 self.tags.value()?.get(key)?.as_str()?.into()
179 } else if let Some(key) = path.strip_prefix("data.") {
180 self.attribute(key)?
181 } else if let Some(key) = path.strip_prefix("sentry_tags.") {
182 self.sentry_tags.value()?.get_value(key)?
183 } else if let Some(rest) = path.strip_prefix("measurements.") {
184 let name = rest.strip_suffix(".value")?;
185 self.measurements
186 .value()?
187 .get(name)?
188 .value()?
189 .value
190 .value()?
191 .into()
192 } else {
193 return None;
194 }
195 }
196 });
197 }
198
199 let event_prefix = path.strip_prefix("event.")?;
202 Some(match event_prefix {
203 "release" => self.data.value()?.release.as_str()?.into(),
204 "environment" => self.data.value()?.environment.as_str()?.into(),
205 "transaction" => self.data.value()?.segment_name.as_str()?.into(),
206 "contexts.browser.name" => self.data.value()?.browser_name.as_str()?.into(),
207 _ => return None,
209 })
210 }
211}
212
213#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
215#[metastructure(trim = false, pii = "maybe")]
216pub struct SentryTags {
217 pub release: Annotated<String>,
218 #[metastructure(pii = "true")]
219 pub user: Annotated<String>,
220 #[metastructure(pii = "true", field = "user.id")]
221 pub user_id: Annotated<String>,
222 #[metastructure(pii = "true", field = "user.ip")]
223 pub user_ip: Annotated<String>,
224 #[metastructure(pii = "true", field = "user.username")]
225 pub user_username: Annotated<String>,
226 #[metastructure(pii = "true", field = "user.email")]
227 pub user_email: Annotated<String>,
228 pub environment: Annotated<String>,
229 pub transaction: Annotated<String>,
230 #[metastructure(field = "transaction.method")]
231 pub transaction_method: Annotated<String>,
232 #[metastructure(field = "transaction.op")]
233 pub transaction_op: Annotated<String>,
234 #[metastructure(field = "browser.name")]
235 pub browser_name: Annotated<String>,
236 #[metastructure(field = "sdk.name")]
237 pub sdk_name: Annotated<String>,
238 #[metastructure(field = "sdk.version")]
239 pub sdk_version: Annotated<String>,
240 pub platform: Annotated<String>,
241 pub mobile: Annotated<String>,
243 #[metastructure(field = "device.class")]
244 pub device_class: Annotated<String>,
245 #[metastructure(field = "device.family")]
246 pub device_family: Annotated<String>,
247 #[metastructure(field = "device.arch")]
248 pub device_arch: Annotated<String>,
249 #[metastructure(field = "device.battery_level")]
250 pub device_battery_level: Annotated<String>,
251 #[metastructure(field = "device.brand")]
252 pub device_brand: Annotated<String>,
253 #[metastructure(field = "device.charging")]
254 pub device_charging: Annotated<String>,
255 #[metastructure(field = "device.locale")]
256 pub device_locale: Annotated<String>,
257 #[metastructure(field = "device.model_id")]
258 pub device_model_id: Annotated<String>,
259 #[metastructure(field = "device.name")]
260 pub device_name: Annotated<String>,
261 #[metastructure(field = "device.online")]
262 pub device_online: Annotated<String>,
263 #[metastructure(field = "device.orientation")]
264 pub device_orientation: Annotated<String>,
265 #[metastructure(field = "device.screen_density")]
266 pub device_screen_density: Annotated<String>,
267 #[metastructure(field = "device.screen_dpi")]
268 pub device_screen_dpi: Annotated<String>,
269 #[metastructure(field = "device.screen_height_pixels")]
270 pub device_screen_height_pixels: Annotated<String>,
271 #[metastructure(field = "device.screen_width_pixels")]
272 pub device_screen_width_pixels: Annotated<String>,
273 #[metastructure(field = "device.simulator")]
274 pub device_simulator: Annotated<String>,
275 #[metastructure(field = "device.uuid")]
276 pub device_uuid: Annotated<String>,
277 #[metastructure(field = "app.device")]
278 pub app_device: Annotated<String>,
279 #[metastructure(field = "device.model")]
280 pub device_model: Annotated<String>,
281 pub runtime: Annotated<String>,
282 #[metastructure(field = "runtime.name")]
283 pub runtime_name: Annotated<String>,
284 pub browser: Annotated<String>,
285 pub os: Annotated<String>,
286 #[metastructure(field = "os.rooted")]
287 pub os_rooted: Annotated<String>,
288 #[metastructure(field = "gpu.name")]
289 pub gpu_name: Annotated<String>,
290 #[metastructure(field = "gpu.vendor")]
291 pub gpu_vendor: Annotated<String>,
292 #[metastructure(field = "monitor.id")]
293 pub monitor_id: Annotated<String>,
294 #[metastructure(field = "monitor.slug")]
295 pub monitor_slug: Annotated<String>,
296 #[metastructure(field = "request.url")]
297 pub request_url: Annotated<String>,
298 #[metastructure(field = "request.method")]
299 pub request_method: Annotated<String>,
300 #[metastructure(field = "os.name")]
302 pub os_name: Annotated<String>,
303 pub action: Annotated<String>,
304 pub category: Annotated<String>,
305 pub description: Annotated<String>,
306 pub domain: Annotated<String>,
307 pub raw_domain: Annotated<String>,
308 pub group: Annotated<String>,
309 #[metastructure(field = "http.decoded_response_content_length")]
310 pub http_decoded_response_content_length: Annotated<String>,
311 #[metastructure(field = "http.response_content_length")]
312 pub http_response_content_length: Annotated<String>,
313 #[metastructure(field = "http.response_transfer_size")]
314 pub http_response_transfer_size: Annotated<String>,
315 #[metastructure(field = "resource.render_blocking_status")]
316 pub resource_render_blocking_status: Annotated<String>,
317 pub op: Annotated<String>,
318 pub status: Annotated<String>,
319 pub status_code: Annotated<String>,
320 pub system: Annotated<String>,
321 pub ttid: Annotated<String>,
323 pub ttfd: Annotated<String>,
325 pub file_extension: Annotated<String>,
327 pub main_thread: Annotated<String>,
329 pub app_start_type: Annotated<String>,
331 pub replay_id: Annotated<String>,
332 #[metastructure(field = "cache.hit")]
333 pub cache_hit: Annotated<String>,
334 #[metastructure(field = "cache.key")]
335 pub cache_key: Annotated<String>,
336 #[metastructure(field = "trace.status")]
337 pub trace_status: Annotated<String>,
338 #[metastructure(field = "messaging.destination.name")]
339 pub messaging_destination_name: Annotated<String>,
340 #[metastructure(field = "messaging.message.id")]
341 pub messaging_message_id: Annotated<String>,
342 #[metastructure(field = "messaging.operation.name")]
343 pub messaging_operation_name: Annotated<String>,
344 #[metastructure(field = "messaging.operation.type")]
345 pub messaging_operation_type: Annotated<String>,
346 #[metastructure(field = "thread.name")]
347 pub thread_name: Annotated<String>,
348 #[metastructure(field = "thread.id")]
349 pub thread_id: Annotated<String>,
350 pub profiler_id: Annotated<String>,
351 #[metastructure(field = "user.geo.city")]
352 pub user_city: Annotated<String>,
353 #[metastructure(field = "user.geo.country_code")]
354 pub user_country_code: Annotated<String>,
355 #[metastructure(field = "user.geo.region")]
356 pub user_region: Annotated<String>,
357 #[metastructure(field = "user.geo.subdivision")]
358 pub user_subdivision: Annotated<String>,
359 #[metastructure(field = "user.geo.subregion")]
360 pub user_subregion: Annotated<String>,
361 pub name: Annotated<String>,
362 }
365
366impl Getter for SentryTags {
367 fn get_value(&self, path: &str) -> Option<Val<'_>> {
368 let value = match path {
369 "action" => &self.action,
370 "app_start_type" => &self.app_start_type,
371 "browser.name" => &self.browser_name,
372 "cache.hit" => &self.cache_hit,
373 "cache.key" => &self.cache_key,
374 "category" => &self.category,
375 "description" => &self.description,
376 "device.class" => &self.device_class,
377 "device.family" => &self.device_family,
378 "device.arch" => &self.device_arch,
379 "device.battery_level" => &self.device_battery_level,
380 "device.brand" => &self.device_brand,
381 "device.charging" => &self.device_charging,
382 "device.locale" => &self.device_locale,
383 "device.model_id" => &self.device_model_id,
384 "device.name" => &self.device_name,
385 "device.online" => &self.device_online,
386 "device.orientation" => &self.device_orientation,
387 "device.screen_density" => &self.device_screen_density,
388 "device.screen_dpi" => &self.device_screen_dpi,
389 "device.screen_height_pixels" => &self.device_screen_height_pixels,
390 "device.screen_width_pixels" => &self.device_screen_width_pixels,
391 "device.simulator" => &self.device_simulator,
392 "device.uuid" => &self.device_uuid,
393 "app.device" => &self.app_device,
394 "device.model" => &self.device_model,
395 "runtime" => &self.runtime,
396 "runtime.name" => &self.runtime_name,
397 "browser" => &self.browser,
398 "os" => &self.os,
399 "os.rooted" => &self.os_rooted,
400 "gpu.name" => &self.gpu_name,
401 "gpu.vendor" => &self.gpu_vendor,
402 "monitor.id" => &self.monitor_id,
403 "monitor.slug" => &self.monitor_slug,
404 "request.url" => &self.request_url,
405 "request.method" => &self.request_method,
406 "domain" => &self.domain,
407 "environment" => &self.environment,
408 "file_extension" => &self.file_extension,
409 "group" => &self.group,
410 "http.decoded_response_content_length" => &self.http_decoded_response_content_length,
411 "http.response_content_length" => &self.http_response_content_length,
412 "http.response_transfer_size" => &self.http_response_transfer_size,
413 "main_thread" => &self.main_thread,
414 "messaging.destination.name" => &self.messaging_destination_name,
415 "messaging.message.id" => &self.messaging_message_id,
416 "messaging.operation.name" => &self.messaging_operation_name,
417 "messaging.operation.type" => &self.messaging_operation_type,
418 "mobile" => &self.mobile,
419 "name" => &self.name,
420 "op" => &self.op,
421 "os.name" => &self.os_name,
422 "platform" => &self.platform,
423 "profiler_id" => &self.profiler_id,
424 "raw_domain" => &self.raw_domain,
425 "release" => &self.release,
426 "replay_id" => &self.replay_id,
427 "resource.render_blocking_status" => &self.resource_render_blocking_status,
428 "sdk.name" => &self.sdk_name,
429 "sdk.version" => &self.sdk_version,
430 "status_code" => &self.status_code,
431 "status" => &self.status,
432 "system" => &self.system,
433 "thread.id" => &self.thread_id,
434 "thread.name" => &self.thread_name,
435 "trace.status" => &self.trace_status,
436 "transaction.method" => &self.transaction_method,
437 "transaction.op" => &self.transaction_op,
438 "transaction" => &self.transaction,
439 "ttfd" => &self.ttfd,
440 "ttid" => &self.ttid,
441 "user.email" => &self.user_email,
442 "user.geo.city" => &self.user_city,
443 "user.geo.country_code" => &self.user_country_code,
444 "user.geo.region" => &self.user_region,
445 "user.geo.subdivision" => &self.user_subdivision,
446 "user.geo.subregion" => &self.user_subregion,
447 "user.id" => &self.user_id,
448 "user.ip" => &self.user_ip,
449 "user.username" => &self.user_username,
450 "user" => &self.user,
451 _ => return None,
452 };
453 Some(value.as_str()?.into())
454 }
455}
456
457#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
462#[metastructure(trim = false)]
463pub struct SpanData {
464 #[metastructure(field = "app_start_type")] pub app_start_type: Annotated<Value>,
469
470 #[metastructure(field = "gen_ai.request.max_tokens", pii = "maybe")]
472 pub gen_ai_request_max_tokens: Annotated<Value>,
473
474 #[metastructure(field = "gen_ai.pipeline.name", legacy_alias = "ai.pipeline.name")]
476 pub gen_ai_pipeline_name: Annotated<Value>,
477
478 #[metastructure(
480 field = "gen_ai.usage.total_tokens",
481 legacy_alias = "ai.total_tokens.used",
482 pii = "maybe"
483 )]
484 pub gen_ai_usage_total_tokens: Annotated<Value>,
485
486 #[metastructure(
488 field = "gen_ai.usage.input_tokens",
489 legacy_alias = "ai.prompt_tokens.used",
490 legacy_alias = "gen_ai.usage.prompt_tokens",
491 pii = "maybe"
492 )]
493 pub gen_ai_usage_input_tokens: Annotated<Value>,
494
495 #[metastructure(field = "gen_ai.usage.input_tokens.cached", pii = "maybe")]
498 pub gen_ai_usage_input_tokens_cached: Annotated<Value>,
499
500 #[metastructure(field = "gen_ai.usage.input_tokens.cache_write", pii = "maybe")]
502 pub gen_ai_usage_input_tokens_cache_write: Annotated<Value>,
503
504 #[metastructure(field = "gen_ai.usage.input_tokens.cache_miss", pii = "maybe")]
506 pub gen_ai_usage_input_tokens_cache_miss: Annotated<Value>,
507
508 #[metastructure(
510 field = "gen_ai.usage.output_tokens",
511 legacy_alias = "ai.completion_tokens.used",
512 legacy_alias = "gen_ai.usage.completion_tokens",
513 pii = "maybe"
514 )]
515 pub gen_ai_usage_output_tokens: Annotated<Value>,
516
517 #[metastructure(field = "gen_ai.usage.output_tokens.reasoning", pii = "maybe")]
520 pub gen_ai_usage_output_tokens_reasoning: Annotated<Value>,
521
522 #[metastructure(
524 field = "gen_ai.usage.output_tokens.prediction_accepted",
525 pii = "maybe"
526 )]
527 pub gen_ai_usage_output_tokens_prediction_accepted: Annotated<Value>,
528
529 #[metastructure(
531 field = "gen_ai.usage.output_tokens.prediction_rejected",
532 pii = "maybe"
533 )]
534 pub gen_ai_usage_output_tokens_prediction_rejected: Annotated<Value>,
535
536 #[metastructure(field = "gen_ai.response.model")]
538 pub gen_ai_response_model: Annotated<Value>,
539
540 #[metastructure(field = "gen_ai.request.model", legacy_alias = "ai.model_id")]
542 pub gen_ai_request_model: Annotated<Value>,
543
544 #[metastructure(field = "gen_ai.cost.total_tokens", pii = "maybe")]
546 pub gen_ai_cost_total_tokens: Annotated<Value>,
547
548 #[metastructure(field = "gen_ai.cost.input_tokens", pii = "maybe")]
550 pub gen_ai_cost_input_tokens: Annotated<Value>,
551
552 #[metastructure(field = "gen_ai.cost.output_tokens", pii = "maybe")]
554 pub gen_ai_cost_output_tokens: Annotated<Value>,
555
556 #[metastructure(field = "gen_ai.prompt", pii = "maybe")]
558 pub gen_ai_prompt: Annotated<Value>,
559
560 #[metastructure(
562 field = "gen_ai.request.messages",
563 pii = "maybe",
564 legacy_alias = "ai.prompt.messages"
565 )]
566 pub gen_ai_request_messages: Annotated<Value>,
567
568 #[metastructure(
570 field = "gen_ai.tool.input",
571 pii = "maybe",
572 legacy_alias = "ai.toolCall.args"
573 )]
574 pub gen_ai_tool_input: Annotated<Value>,
575
576 #[metastructure(
578 field = "gen_ai.tool.output",
579 pii = "maybe",
580 legacy_alias = "ai.toolCall.result"
581 )]
582 pub gen_ai_tool_output: Annotated<Value>,
583
584 #[metastructure(
586 field = "gen_ai.response.tool_calls",
587 legacy_alias = "ai.response.toolCalls",
588 legacy_alias = "ai.tool_calls",
589 pii = "maybe"
590 )]
591 pub gen_ai_response_tool_calls: Annotated<Value>,
592
593 #[metastructure(
595 field = "gen_ai.response.text",
596 legacy_alias = "ai.response.text",
597 legacy_alias = "ai.responses",
598 pii = "maybe"
599 )]
600 pub gen_ai_response_text: Annotated<Value>,
601
602 #[metastructure(field = "gen_ai.response.object", pii = "maybe")]
604 pub gen_ai_response_object: Annotated<Value>,
605
606 #[metastructure(field = "gen_ai.response.streaming", legacy_alias = "ai.streaming")]
608 pub gen_ai_response_streaming: Annotated<Value>,
609
610 #[metastructure(field = "gen_ai.response.tokens_per_second", pii = "maybe")]
612 pub gen_ai_response_tokens_per_second: Annotated<Value>,
613
614 #[metastructure(field = "gen_ai.response.time_to_first_token", pii = "maybe")]
616 pub gen_ai_response_time_to_first_token: Annotated<Value>,
617
618 #[metastructure(
620 field = "gen_ai.request.available_tools",
621 legacy_alias = "ai.tools",
622 pii = "maybe"
623 )]
624 pub gen_ai_request_available_tools: Annotated<Value>,
625
626 #[metastructure(
628 field = "gen_ai.request.frequency_penalty",
629 legacy_alias = "ai.frequency_penalty"
630 )]
631 pub gen_ai_request_frequency_penalty: Annotated<Value>,
632
633 #[metastructure(
635 field = "gen_ai.request.presence_penalty",
636 legacy_alias = "ai.presence_penalty"
637 )]
638 pub gen_ai_request_presence_penalty: Annotated<Value>,
639
640 #[metastructure(field = "gen_ai.request.seed", legacy_alias = "ai.seed")]
642 pub gen_ai_request_seed: Annotated<Value>,
643
644 #[metastructure(field = "gen_ai.request.temperature", legacy_alias = "ai.temperature")]
646 pub gen_ai_request_temperature: Annotated<Value>,
647
648 #[metastructure(field = "gen_ai.request.top_k", legacy_alias = "ai.top_k")]
650 pub gen_ai_request_top_k: Annotated<Value>,
651
652 #[metastructure(field = "gen_ai.request.top_p", legacy_alias = "ai.top_p")]
654 pub gen_ai_request_top_p: Annotated<Value>,
655
656 #[metastructure(
658 field = "gen_ai.response.finish_reason",
659 legacy_alias = "ai.finish_reason"
660 )]
661 pub gen_ai_response_finish_reason: Annotated<Value>,
662
663 #[metastructure(field = "gen_ai.response.id", legacy_alias = "ai.generation_id")]
665 pub gen_ai_response_id: Annotated<Value>,
666
667 #[metastructure(field = "gen_ai.system", legacy_alias = "ai.model.provider")]
669 pub gen_ai_system: Annotated<Value>,
670
671 #[metastructure(
673 field = "gen_ai.system_instructions",
674 legacy_alias = "gen_ai.system.message",
675 pii = "maybe"
676 )]
677 pub gen_ai_system_instructions: Annotated<Value>,
678
679 #[metastructure(
681 field = "gen_ai.tool.name",
682 legacy_alias = "ai.function_call",
683 pii = "maybe"
684 )]
685 pub gen_ai_tool_name: Annotated<Value>,
686
687 #[metastructure(field = "gen_ai.operation.name", pii = "maybe")]
689 pub gen_ai_operation_name: Annotated<String>,
690
691 #[metastructure(field = "gen_ai.operation.type", pii = "maybe")]
693 pub gen_ai_operation_type: Annotated<String>,
694
695 #[metastructure(field = "gen_ai.agent.name", pii = "maybe")]
697 pub gen_ai_agent_name: Annotated<String>,
698
699 #[metastructure(field = "gen_ai.function_id", pii = "maybe")]
701 pub gen_ai_function_id: Annotated<String>,
702
703 #[metastructure(field = "gen_ai.input.messages", pii = "maybe")]
705 pub gen_ai_input_messages: Annotated<Value>,
706
707 #[metastructure(field = "gen_ai.output.messages", pii = "maybe")]
709 pub gen_ai_output_messages: Annotated<Value>,
710
711 #[metastructure(field = "mcp.prompt.result", pii = "maybe")]
713 pub mcp_prompt_result: Annotated<Value>,
714
715 #[metastructure(field = "mcp.tool.result.content", pii = "maybe")]
717 pub mcp_tool_result_content: Annotated<Value>,
718
719 #[metastructure(field = "browser.name")]
721 pub browser_name: Annotated<String>,
722
723 #[metastructure(field = "code.filepath", pii = "maybe")]
725 pub code_filepath: Annotated<Value>,
726 #[metastructure(field = "code.lineno", pii = "maybe")]
728 pub code_lineno: Annotated<Value>,
729 #[metastructure(field = "code.function", pii = "maybe")]
733 pub code_function: Annotated<Value>,
734 #[metastructure(field = "code.namespace", pii = "maybe")]
740 pub code_namespace: Annotated<Value>,
741
742 #[metastructure(field = "db.operation")]
747 pub db_operation: Annotated<Value>,
748
749 #[metastructure(field = "db.system")]
753 pub db_system: Annotated<Value>,
754
755 #[metastructure(
759 field = "db.collection.name",
760 legacy_alias = "db.cassandra.table",
761 legacy_alias = "db.cosmosdb.container",
762 legacy_alias = "db.mongodb.collection",
763 legacy_alias = "db.sql.table"
764 )]
765 pub db_collection_name: Annotated<Value>,
766
767 #[metastructure(field = "sentry.environment", legacy_alias = "environment")]
769 pub environment: Annotated<String>,
770
771 #[metastructure(field = "sentry.release", legacy_alias = "release")]
773 pub release: Annotated<LenientString>,
774
775 #[metastructure(field = "http.decoded_response_content_length")]
777 pub http_decoded_response_content_length: Annotated<Value>,
778
779 #[metastructure(
781 field = "http.request_method",
782 legacy_alias = "http.method",
783 legacy_alias = "method"
784 )]
785 pub http_request_method: Annotated<Value>,
786
787 #[metastructure(field = "http.response_content_length")]
789 pub http_response_content_length: Annotated<Value>,
790
791 #[metastructure(field = "http.response_transfer_size")]
793 pub http_response_transfer_size: Annotated<Value>,
794
795 #[metastructure(field = "resource.render_blocking_status")]
797 pub resource_render_blocking_status: Annotated<Value>,
798
799 #[metastructure(field = "server.address")]
801 pub server_address: Annotated<Value>,
802
803 #[metastructure(field = "cache.hit")]
805 pub cache_hit: Annotated<Value>,
806
807 #[metastructure(field = "cache.key")]
809 pub cache_key: Annotated<Value>,
810
811 #[metastructure(field = "cache.item_size")]
813 pub cache_item_size: Annotated<Value>,
814
815 #[metastructure(field = "http.response.status_code", legacy_alias = "status_code")]
817 pub http_response_status_code: Annotated<Value>,
818
819 #[metastructure(field = "thread.name")]
821 pub thread_name: Annotated<String>,
822
823 #[metastructure(field = "thread.id")]
825 pub thread_id: Annotated<ThreadId>,
826
827 #[metastructure(field = "sentry.segment.name", legacy_alias = "transaction")]
833 pub segment_name: Annotated<String>,
834
835 #[metastructure(field = "ui.component_name")]
837 pub ui_component_name: Annotated<Value>,
838
839 #[metastructure(field = "url.scheme")]
841 pub url_scheme: Annotated<Value>,
842
843 #[metastructure(field = "user")]
845 pub user: Annotated<Value>,
846
847 #[metastructure(field = "user.email")]
851 pub user_email: Annotated<String>,
852
853 #[metastructure(field = "user.full_name")]
857 pub user_full_name: Annotated<String>,
858
859 #[metastructure(field = "user.geo.country_code")]
863 pub user_geo_country_code: Annotated<String>,
864
865 #[metastructure(field = "user.geo.city")]
869 pub user_geo_city: Annotated<String>,
870
871 #[metastructure(field = "user.geo.subdivision")]
875 pub user_geo_subdivision: Annotated<String>,
876
877 #[metastructure(field = "user.geo.region")]
881 pub user_geo_region: Annotated<String>,
882
883 #[metastructure(field = "user.hash")]
887 pub user_hash: Annotated<String>,
888
889 #[metastructure(field = "user.id")]
893 pub user_id: Annotated<String>,
894
895 #[metastructure(field = "user.name")]
899 pub user_name: Annotated<String>,
900
901 #[metastructure(field = "user.roles")]
905 pub user_roles: Annotated<Array<String>>,
906
907 #[metastructure(field = "sentry.exclusive_time")]
909 pub exclusive_time: Annotated<Value>,
910
911 #[metastructure(field = "profile_id")]
913 pub profile_id: Annotated<Value>,
914
915 #[metastructure(field = "sentry.replay_id", legacy_alias = "replay_id")]
917 pub replay_id: Annotated<Value>,
918
919 #[metastructure(field = "sentry.sdk.name")]
921 pub sdk_name: Annotated<String>,
922
923 #[metastructure(field = "sentry.sdk.version")]
925 pub sdk_version: Annotated<String>,
926
927 #[metastructure(field = "sentry.frames.slow", legacy_alias = "frames.slow")]
929 pub frames_slow: Annotated<Value>,
930
931 #[metastructure(field = "sentry.frames.frozen", legacy_alias = "frames.frozen")]
933 pub frames_frozen: Annotated<Value>,
934
935 #[metastructure(field = "sentry.frames.total", legacy_alias = "frames.total")]
937 pub frames_total: Annotated<Value>,
938
939 #[metastructure(field = "frames.delay")]
941 pub frames_delay: Annotated<Value>,
942
943 #[metastructure(field = "messaging.destination.name")]
945 pub messaging_destination_name: Annotated<String>,
946
947 #[metastructure(field = "messaging.message.retry.count")]
949 pub messaging_message_retry_count: Annotated<Value>,
950
951 #[metastructure(field = "messaging.message.receive.latency")]
953 pub messaging_message_receive_latency: Annotated<Value>,
954
955 #[metastructure(field = "messaging.message.body.size")]
957 pub messaging_message_body_size: Annotated<Value>,
958
959 #[metastructure(field = "messaging.message.id")]
961 pub messaging_message_id: Annotated<String>,
962
963 #[metastructure(field = "messaging.operation.name")]
965 pub messaging_operation_name: Annotated<String>,
966
967 #[metastructure(field = "messaging.operation.type")]
969 pub messaging_operation_type: Annotated<String>,
970
971 #[metastructure(field = "user_agent.original")]
973 pub user_agent_original: Annotated<String>,
974
975 #[metastructure(field = "url.full")]
977 pub url_full: Annotated<String>,
978
979 #[metastructure(field = "url.query")]
981 pub url_query: Annotated<String>,
982
983 #[metastructure(field = "http.query")]
985 pub http_query: Annotated<String>,
986
987 #[metastructure(field = "client.address")]
989 pub client_address: Annotated<IpAddr>,
990
991 #[metastructure(pii = "maybe", skip_serialization = "empty")]
995 pub route: Annotated<Route>,
996 #[metastructure(field = "previousRoute", pii = "maybe", skip_serialization = "empty")]
1000 pub previous_route: Annotated<Route>,
1001
1002 #[metastructure(field = "lcp.element")]
1004 pub lcp_element: Annotated<String>,
1005
1006 #[metastructure(field = "lcp.size")]
1008 pub lcp_size: Annotated<u64>,
1009
1010 #[metastructure(field = "lcp.id")]
1012 pub lcp_id: Annotated<String>,
1013
1014 #[metastructure(field = "lcp.url")]
1016 pub lcp_url: Annotated<String>,
1017
1018 #[metastructure(field = "sentry.name")]
1021 pub span_name: Annotated<String>,
1022
1023 #[metastructure(
1025 additional_properties,
1026 pii = "true",
1027 retain = true,
1028 skip_serialization = "null" )]
1030 pub other: Object<Value>,
1031}
1032
1033impl Getter for SpanData {
1034 fn get_value(&self, path: &str) -> Option<Val<'_>> {
1035 Some(match path {
1036 "app_start_type" => self.app_start_type.value()?.into(),
1037 "browser\\.name" => self.browser_name.as_str()?.into(),
1038 "code\\.filepath" => self.code_filepath.value()?.into(),
1039 "code\\.function" => self.code_function.value()?.into(),
1040 "code\\.lineno" => self.code_lineno.value()?.into(),
1041 "code\\.namespace" => self.code_namespace.value()?.into(),
1042 "db.operation" => self.db_operation.value()?.into(),
1043 "db\\.system" => self.db_system.value()?.into(),
1044 "environment" => self.environment.as_str()?.into(),
1045 "gen_ai\\.request\\.max_tokens" => self.gen_ai_request_max_tokens.value()?.into(),
1046 "gen_ai\\.usage\\.total_tokens" => self.gen_ai_usage_total_tokens.value()?.into(),
1047 "gen_ai\\.cost\\.total_tokens" => self.gen_ai_cost_total_tokens.value()?.into(),
1048 "gen_ai\\.cost\\.input_tokens" => self.gen_ai_cost_input_tokens.value()?.into(),
1049 "gen_ai\\.cost\\.output_tokens" => self.gen_ai_cost_output_tokens.value()?.into(),
1050 "gen_ai\\.input\\.messages" => self.gen_ai_input_messages.value()?.into(),
1051 "gen_ai\\.output\\.messages" => self.gen_ai_output_messages.value()?.into(),
1052 "gen_ai\\.operation\\.name" => self.gen_ai_operation_name.as_str()?.into(),
1053 "gen_ai\\.agent\\.name" => self.gen_ai_agent_name.as_str()?.into(),
1054 "gen_ai\\.request\\.model" => self.gen_ai_request_model.value()?.into(),
1055 "http\\.decoded_response_content_length" => {
1056 self.http_decoded_response_content_length.value()?.into()
1057 }
1058 "http\\.request_method" | "http\\.method" | "method" => {
1059 self.http_request_method.value()?.into()
1060 }
1061 "http\\.response_content_length" => self.http_response_content_length.value()?.into(),
1062 "http\\.response_transfer_size" => self.http_response_transfer_size.value()?.into(),
1063 "http\\.response.status_code" | "status_code" => {
1064 self.http_response_status_code.value()?.into()
1065 }
1066 "resource\\.render_blocking_status" => {
1067 self.resource_render_blocking_status.value()?.into()
1068 }
1069 "server\\.address" => self.server_address.value()?.into(),
1070 "thread\\.name" => self.thread_name.as_str()?.into(),
1071 "ui\\.component_name" => self.ui_component_name.value()?.into(),
1072 "url\\.scheme" => self.url_scheme.value()?.into(),
1073 "url\\.query" => self.url_query.as_str()?.into(),
1074 "http\\.query" => self.http_query.as_str()?.into(),
1075 "user" => self.user.value()?.into(),
1076 "user\\.email" => self.user_email.as_str()?.into(),
1077 "user\\.full_name" => self.user_full_name.as_str()?.into(),
1078 "user\\.geo\\.city" => self.user_geo_city.as_str()?.into(),
1079 "user\\.geo\\.country_code" => self.user_geo_country_code.as_str()?.into(),
1080 "user\\.geo\\.region" => self.user_geo_region.as_str()?.into(),
1081 "user\\.geo\\.subdivision" => self.user_geo_subdivision.as_str()?.into(),
1082 "user\\.hash" => self.user_hash.as_str()?.into(),
1083 "user\\.id" => self.user_id.as_str()?.into(),
1084 "user\\.name" => self.user_name.as_str()?.into(),
1085 "transaction" => self.segment_name.as_str()?.into(),
1086 "release" => self.release.as_str()?.into(),
1087 _ => {
1088 let escaped = path.replace("\\.", "\0");
1089 let mut path = escaped.split('.').map(|s| s.replace('\0', "."));
1090 let root = path.next()?;
1091
1092 let mut val = self.other.get(&root)?.value()?;
1093 for part in path {
1094 let relay_protocol::Value::Object(map) = val else {
1096 return None;
1097 };
1098 val = map.get(&part)?.value()?;
1099 }
1100 val.into()
1101 }
1102 })
1103 }
1104}
1105
1106#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
1108#[metastructure(trim = false)]
1109pub struct SpanLink {
1110 #[metastructure(required = true, trim = false)]
1112 pub trace_id: Annotated<TraceId>,
1113
1114 #[metastructure(required = true, trim = false)]
1116 pub span_id: Annotated<SpanId>,
1117
1118 #[metastructure(trim = false)]
1120 pub sampled: Annotated<bool>,
1121
1122 #[metastructure(pii = "maybe", trim = false)]
1124 pub attributes: Annotated<Object<Value>>,
1125
1126 #[metastructure(additional_properties, retain = true, pii = "maybe", trim = false)]
1128 pub other: Object<Value>,
1129}
1130
1131#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
1133pub struct Route {
1134 #[metastructure(pii = "maybe", skip_serialization = "empty")]
1136 pub name: Annotated<String>,
1137
1138 #[metastructure(
1140 pii = "true",
1141 skip_serialization = "empty",
1142 max_depth = 5,
1143 max_bytes = 2048
1144 )]
1145 pub params: Annotated<Object<Value>>,
1146
1147 #[metastructure(
1149 additional_properties,
1150 retain = true,
1151 pii = "maybe",
1152 skip_serialization = "empty"
1153 )]
1154 pub other: Object<Value>,
1155}
1156
1157impl FromValue for Route {
1158 fn from_value(value: Annotated<Value>) -> Annotated<Self>
1159 where
1160 Self: Sized,
1161 {
1162 match value {
1163 Annotated(Some(Value::String(name)), meta) => Annotated(
1164 Some(Route {
1165 name: Annotated::new(name),
1166 ..Default::default()
1167 }),
1168 meta,
1169 ),
1170 Annotated(Some(Value::Object(mut values)), meta) => {
1171 let mut route: Route = Default::default();
1172 if let Some(Annotated(Some(Value::String(name)), _)) = values.remove("name") {
1173 route.name = Annotated::new(name);
1174 }
1175 if let Some(Annotated(Some(Value::Object(params)), _)) = values.remove("params") {
1176 route.params = Annotated::new(params);
1177 }
1178
1179 if !values.is_empty() {
1180 route.other = values;
1181 }
1182
1183 Annotated(Some(route), meta)
1184 }
1185 Annotated(None, meta) => Annotated(None, meta),
1186 Annotated(Some(value), mut meta) => {
1187 meta.add_error(Error::expected("route expected to be an object"));
1188 meta.set_original_value(Some(value));
1189 Annotated(None, meta)
1190 }
1191 }
1192 }
1193}
1194
1195#[derive(Clone, Debug, PartialEq, ProcessValue, Default)]
1200pub enum SpanKind {
1201 #[default]
1203 Internal,
1204 Server,
1206 Client,
1208 Producer,
1210 Consumer,
1212 Unknown(String),
1214}
1215
1216impl SpanKind {
1217 pub fn as_str(&self) -> &str {
1218 match self {
1219 Self::Internal => "internal",
1220 Self::Server => "server",
1221 Self::Client => "client",
1222 Self::Producer => "producer",
1223 Self::Consumer => "consumer",
1224 Self::Unknown(s) => s.as_str(),
1225 }
1226 }
1227}
1228
1229impl Empty for SpanKind {
1230 fn is_empty(&self) -> bool {
1231 false
1232 }
1233}
1234
1235#[derive(Debug)]
1236pub struct ParseSpanKindError;
1237
1238impl std::str::FromStr for SpanKind {
1239 type Err = ParseSpanKindError;
1240
1241 fn from_str(s: &str) -> Result<Self, Self::Err> {
1242 Ok(match s {
1243 "internal" => SpanKind::Internal,
1244 "server" => SpanKind::Server,
1245 "client" => SpanKind::Client,
1246 "producer" => SpanKind::Producer,
1247 "consumer" => SpanKind::Consumer,
1248 other => SpanKind::Unknown(other.to_owned()),
1249 })
1250 }
1251}
1252
1253impl fmt::Display for SpanKind {
1254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1255 write!(f, "{}", self.as_str())
1256 }
1257}
1258
1259impl FromValue for SpanKind {
1260 fn from_value(value: Annotated<Value>) -> Annotated<Self>
1261 where
1262 Self: Sized,
1263 {
1264 match value {
1265 Annotated(Some(Value::String(s)), meta) => Annotated(SpanKind::from_str(&s).ok(), meta),
1266 Annotated(_, meta) => Annotated(None, meta),
1267 }
1268 }
1269}
1270
1271impl IntoValue for SpanKind {
1272 fn into_value(self) -> Value
1273 where
1274 Self: Sized,
1275 {
1276 Value::String(self.to_string())
1277 }
1278
1279 fn serialize_payload<S>(
1280 &self,
1281 s: S,
1282 _behavior: relay_protocol::SkipSerialization,
1283 ) -> Result<S::Ok, S::Error>
1284 where
1285 Self: Sized,
1286 S: serde::Serializer,
1287 {
1288 s.serialize_str(self.as_str())
1289 }
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294 use crate::protocol::Measurement;
1295 use chrono::{TimeZone, Utc};
1296 use relay_base_schema::metrics::{InformationUnit, MetricUnit};
1297 use relay_protocol::RuleCondition;
1298 use similar_asserts::assert_eq;
1299
1300 use super::*;
1301
1302 #[test]
1303 fn test_span_serialization() {
1304 let json = r#"{
1305 "timestamp": 0.0,
1306 "start_timestamp": -63158400.0,
1307 "exclusive_time": 1.23,
1308 "op": "operation",
1309 "span_id": "fa90fdead5f74052",
1310 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1311 "status": "ok",
1312 "description": "desc",
1313 "origin": "auto.http",
1314 "links": [
1315 {
1316 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1317 "span_id": "fa90fdead5f74052",
1318 "sampled": true,
1319 "attributes": {
1320 "boolAttr": true,
1321 "numAttr": 123,
1322 "stringAttr": "foo"
1323 }
1324 }
1325 ],
1326 "measurements": {
1327 "memory": {
1328 "value": 9001.0,
1329 "unit": "byte"
1330 }
1331 },
1332 "kind": "server"
1333}"#;
1334 let mut measurements = Object::new();
1335 measurements.insert(
1336 "memory".into(),
1337 Annotated::new(Measurement {
1338 value: Annotated::new(9001.0.try_into().unwrap()),
1339 unit: Annotated::new(MetricUnit::Information(InformationUnit::Byte)),
1340 }),
1341 );
1342
1343 let links = Annotated::new(vec![Annotated::new(SpanLink {
1344 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1345 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
1346 sampled: Annotated::new(true),
1347 attributes: Annotated::new({
1348 let mut map: std::collections::BTreeMap<String, Annotated<Value>> = Object::new();
1349 map.insert(
1350 "stringAttr".into(),
1351 Annotated::new(Value::String("foo".into())),
1352 );
1353 map.insert("numAttr".into(), Annotated::new(Value::I64(123)));
1354 map.insert("boolAttr".into(), Value::Bool(true).into());
1355 map
1356 }),
1357 ..Default::default()
1358 })]);
1359
1360 let span = Annotated::new(Span {
1361 timestamp: Annotated::new(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap().into()),
1362 start_timestamp: Annotated::new(
1363 Utc.with_ymd_and_hms(1968, 1, 1, 0, 0, 0).unwrap().into(),
1364 ),
1365 exclusive_time: Annotated::new(1.23),
1366 description: Annotated::new("desc".to_owned()),
1367 op: Annotated::new("operation".to_owned()),
1368 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1369 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
1370 status: Annotated::new(SpanStatus::Ok),
1371 origin: Annotated::new("auto.http".to_owned()),
1372 kind: Annotated::new(SpanKind::Server),
1373 measurements: Annotated::new(Measurements(measurements)),
1374 links,
1375 ..Default::default()
1376 });
1377 assert_eq!(json, span.to_json_pretty().unwrap());
1378
1379 let span_from_string = Annotated::from_json(json).unwrap();
1380 assert_eq!(span, span_from_string);
1381 }
1382
1383 #[test]
1384 fn test_getter_span_data() {
1385 let span = Annotated::<Span>::from_json(
1386 r#"{
1387 "data": {
1388 "foo": {"bar": 1},
1389 "foo.bar": 2
1390 },
1391 "measurements": {
1392 "some": {"value": 100.0}
1393 }
1394 }"#,
1395 )
1396 .unwrap()
1397 .into_value()
1398 .unwrap();
1399
1400 assert_eq!(span.get_value("span.data.foo.bar"), Some(Val::I64(1)));
1401 assert_eq!(span.get_value(r"span.data.foo\.bar"), Some(Val::I64(2)));
1402
1403 assert_eq!(span.get_value("span.data"), None);
1404 assert_eq!(span.get_value("span.data."), None);
1405 assert_eq!(span.get_value("span.data.x"), None);
1406
1407 assert_eq!(
1408 span.get_value("span.measurements.some.value"),
1409 Some(Val::F64(100.0))
1410 );
1411 }
1412
1413 #[test]
1414 fn test_getter_was_transaction() {
1415 let mut span = Span::default();
1416 assert_eq!(
1417 span.get_value("span.was_transaction"),
1418 Some(Val::Bool(false))
1419 );
1420 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1421 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1422
1423 span.was_transaction.set_value(Some(false));
1424 assert_eq!(
1425 span.get_value("span.was_transaction"),
1426 Some(Val::Bool(false))
1427 );
1428 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1429 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1430
1431 span.was_transaction.set_value(Some(true));
1432 assert_eq!(
1433 span.get_value("span.was_transaction"),
1434 Some(Val::Bool(true))
1435 );
1436 assert!(RuleCondition::eq("span.was_transaction", true).matches(&span));
1437 assert!(!RuleCondition::eq("span.was_transaction", false).matches(&span));
1438 }
1439
1440 #[test]
1441 fn test_span_fields_as_event() {
1442 let span = Annotated::<Span>::from_json(
1443 r#"{
1444 "data": {
1445 "release": "1.0",
1446 "environment": "prod",
1447 "sentry.segment.name": "/api/endpoint"
1448 }
1449 }"#,
1450 )
1451 .unwrap()
1452 .into_value()
1453 .unwrap();
1454
1455 assert_eq!(span.get_value("event.release"), Some(Val::String("1.0")));
1456 assert_eq!(
1457 span.get_value("event.environment"),
1458 Some(Val::String("prod"))
1459 );
1460 assert_eq!(
1461 span.get_value("event.transaction"),
1462 Some(Val::String("/api/endpoint"))
1463 );
1464 }
1465
1466 #[test]
1467 fn test_span_duration() {
1468 let span = Annotated::<Span>::from_json(
1469 r#"{
1470 "start_timestamp": 1694732407.8367,
1471 "timestamp": 1694732408.31451233
1472 }"#,
1473 )
1474 .unwrap()
1475 .into_value()
1476 .unwrap();
1477
1478 assert_eq!(span.get_value("span.duration"), Some(Val::F64(477.812)));
1479 }
1480
1481 #[test]
1482 fn test_span_data() {
1483 let data = r#"{
1484 "foo": 2,
1485 "bar": "3",
1486 "db.system": "mysql",
1487 "code.filepath": "task.py",
1488 "code.lineno": 123,
1489 "code.function": "fn()",
1490 "code.namespace": "ns",
1491 "frames.slow": 1,
1492 "frames.frozen": 2,
1493 "frames.total": 9,
1494 "frames.delay": 100,
1495 "messaging.destination.name": "default",
1496 "messaging.message.retry.count": 3,
1497 "messaging.message.receive.latency": 40,
1498 "messaging.message.body.size": 100,
1499 "messaging.message.id": "abc123",
1500 "messaging.operation.name": "publish",
1501 "messaging.operation.type": "create",
1502 "user_agent.original": "Chrome",
1503 "url.full": "my_url.com",
1504 "client.address": "192.168.0.1"
1505 }"#;
1506 let data = Annotated::<SpanData>::from_json(data)
1507 .unwrap()
1508 .into_value()
1509 .unwrap();
1510 insta::assert_debug_snapshot!(data, @r#"
1511 SpanData {
1512 app_start_type: ~,
1513 gen_ai_request_max_tokens: ~,
1514 gen_ai_pipeline_name: ~,
1515 gen_ai_usage_total_tokens: ~,
1516 gen_ai_usage_input_tokens: ~,
1517 gen_ai_usage_input_tokens_cached: ~,
1518 gen_ai_usage_input_tokens_cache_write: ~,
1519 gen_ai_usage_input_tokens_cache_miss: ~,
1520 gen_ai_usage_output_tokens: ~,
1521 gen_ai_usage_output_tokens_reasoning: ~,
1522 gen_ai_usage_output_tokens_prediction_accepted: ~,
1523 gen_ai_usage_output_tokens_prediction_rejected: ~,
1524 gen_ai_response_model: ~,
1525 gen_ai_request_model: ~,
1526 gen_ai_cost_total_tokens: ~,
1527 gen_ai_cost_input_tokens: ~,
1528 gen_ai_cost_output_tokens: ~,
1529 gen_ai_prompt: ~,
1530 gen_ai_request_messages: ~,
1531 gen_ai_tool_input: ~,
1532 gen_ai_tool_output: ~,
1533 gen_ai_response_tool_calls: ~,
1534 gen_ai_response_text: ~,
1535 gen_ai_response_object: ~,
1536 gen_ai_response_streaming: ~,
1537 gen_ai_response_tokens_per_second: ~,
1538 gen_ai_response_time_to_first_token: ~,
1539 gen_ai_request_available_tools: ~,
1540 gen_ai_request_frequency_penalty: ~,
1541 gen_ai_request_presence_penalty: ~,
1542 gen_ai_request_seed: ~,
1543 gen_ai_request_temperature: ~,
1544 gen_ai_request_top_k: ~,
1545 gen_ai_request_top_p: ~,
1546 gen_ai_response_finish_reason: ~,
1547 gen_ai_response_id: ~,
1548 gen_ai_system: ~,
1549 gen_ai_system_instructions: ~,
1550 gen_ai_tool_name: ~,
1551 gen_ai_operation_name: ~,
1552 gen_ai_operation_type: ~,
1553 gen_ai_agent_name: ~,
1554 gen_ai_function_id: ~,
1555 gen_ai_input_messages: ~,
1556 gen_ai_output_messages: ~,
1557 mcp_prompt_result: ~,
1558 mcp_tool_result_content: ~,
1559 browser_name: ~,
1560 code_filepath: String(
1561 "task.py",
1562 ),
1563 code_lineno: I64(
1564 123,
1565 ),
1566 code_function: String(
1567 "fn()",
1568 ),
1569 code_namespace: String(
1570 "ns",
1571 ),
1572 db_operation: ~,
1573 db_system: String(
1574 "mysql",
1575 ),
1576 db_collection_name: ~,
1577 environment: ~,
1578 release: ~,
1579 http_decoded_response_content_length: ~,
1580 http_request_method: ~,
1581 http_response_content_length: ~,
1582 http_response_transfer_size: ~,
1583 resource_render_blocking_status: ~,
1584 server_address: ~,
1585 cache_hit: ~,
1586 cache_key: ~,
1587 cache_item_size: ~,
1588 http_response_status_code: ~,
1589 thread_name: ~,
1590 thread_id: ~,
1591 segment_name: ~,
1592 ui_component_name: ~,
1593 url_scheme: ~,
1594 user: ~,
1595 user_email: ~,
1596 user_full_name: ~,
1597 user_geo_country_code: ~,
1598 user_geo_city: ~,
1599 user_geo_subdivision: ~,
1600 user_geo_region: ~,
1601 user_hash: ~,
1602 user_id: ~,
1603 user_name: ~,
1604 user_roles: ~,
1605 exclusive_time: ~,
1606 profile_id: ~,
1607 replay_id: ~,
1608 sdk_name: ~,
1609 sdk_version: ~,
1610 frames_slow: I64(
1611 1,
1612 ),
1613 frames_frozen: I64(
1614 2,
1615 ),
1616 frames_total: I64(
1617 9,
1618 ),
1619 frames_delay: I64(
1620 100,
1621 ),
1622 messaging_destination_name: "default",
1623 messaging_message_retry_count: I64(
1624 3,
1625 ),
1626 messaging_message_receive_latency: I64(
1627 40,
1628 ),
1629 messaging_message_body_size: I64(
1630 100,
1631 ),
1632 messaging_message_id: "abc123",
1633 messaging_operation_name: "publish",
1634 messaging_operation_type: "create",
1635 user_agent_original: "Chrome",
1636 url_full: "my_url.com",
1637 url_query: ~,
1638 http_query: ~,
1639 client_address: IpAddr(
1640 "192.168.0.1",
1641 ),
1642 route: ~,
1643 previous_route: ~,
1644 lcp_element: ~,
1645 lcp_size: ~,
1646 lcp_id: ~,
1647 lcp_url: ~,
1648 span_name: ~,
1649 other: {
1650 "bar": String(
1651 "3",
1652 ),
1653 "foo": I64(
1654 2,
1655 ),
1656 },
1657 }
1658 "#);
1659
1660 assert_eq!(data.get_value("foo"), Some(Val::U64(2)));
1661 assert_eq!(data.get_value("bar"), Some(Val::String("3")));
1662 assert_eq!(data.get_value("db\\.system"), Some(Val::String("mysql")));
1663 assert_eq!(data.get_value("code\\.lineno"), Some(Val::U64(123)));
1664 assert_eq!(data.get_value("code\\.function"), Some(Val::String("fn()")));
1665 assert_eq!(data.get_value("code\\.namespace"), Some(Val::String("ns")));
1666 assert_eq!(data.get_value("unknown"), None);
1667 }
1668
1669 #[test]
1670 fn test_span_data_empty_well_known_field() {
1671 let span = r#"{
1672 "data": {
1673 "lcp.url": ""
1674 }
1675 }"#;
1676 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1677 assert_eq!(span.to_json().unwrap(), r#"{"data":{"lcp.url":""}}"#);
1678 }
1679
1680 #[test]
1681 fn test_span_data_empty_custom_field() {
1682 let span = r#"{
1683 "data": {
1684 "custom_field_empty": ""
1685 }
1686 }"#;
1687 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1688 assert_eq!(
1689 span.to_json().unwrap(),
1690 r#"{"data":{"custom_field_empty":""}}"#
1691 );
1692 }
1693
1694 #[test]
1695 fn test_span_data_completely_empty() {
1696 let span = r#"{
1697 "data": {}
1698 }"#;
1699 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1700 assert_eq!(span.to_json().unwrap(), r#"{"data":{}}"#);
1701 }
1702
1703 #[test]
1704 fn test_span_links() {
1705 let span = r#"{
1706 "links": [
1707 {
1708 "trace_id": "5c79f60c11214eb38604f4ae0781bfb2",
1709 "span_id": "ab90fdead5f74052",
1710 "sampled": true,
1711 "attributes": {
1712 "sentry.link.type": "previous_trace"
1713 }
1714 },
1715 {
1716 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1717 "span_id": "fa90fdead5f74052",
1718 "sampled": true,
1719 "attributes": {
1720 "sentry.link.type": "next_trace"
1721 }
1722 }
1723 ]
1724 }"#;
1725
1726 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1727 assert_eq!(
1728 span.to_json().unwrap(),
1729 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"}}]}"#
1730 );
1731 }
1732
1733 #[test]
1734 fn test_span_kind() {
1735 let span = Annotated::<Span>::from_json(
1736 r#"{
1737 "kind": "???"
1738 }"#,
1739 )
1740 .unwrap()
1741 .into_value()
1742 .unwrap();
1743 assert_eq!(
1744 span.kind.value().unwrap(),
1745 &SpanKind::Unknown("???".to_owned())
1746 );
1747 }
1748}