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(skip_serialization = "empty", trim = false)]
131 pub _performance_issues_spans: Annotated<bool>,
132
133 #[metastructure(additional_properties, retain = true, pii = "maybe")]
136 pub other: Object<Value>,
137}
138
139impl Span {
140 fn attribute(&self, key: &str) -> Option<Val<'_>> {
145 Some(match self.data.value()?.get_value(key) {
146 Some(value) => value,
147 None => self.tags.value()?.get(key)?.as_str()?.into(),
148 })
149 }
150}
151
152impl Getter for Span {
153 fn get_value(&self, path: &str) -> Option<Val<'_>> {
154 let span_prefix = path.strip_prefix("span.");
155 if let Some(span_prefix) = span_prefix {
156 return Some(match span_prefix {
157 "exclusive_time" => self.exclusive_time.value()?.into(),
158 "description" => self.description.as_str()?.into(),
159 "op" => self.op.as_str()?.into(),
160 "span_id" => self.span_id.value()?.into(),
161 "parent_span_id" => self.parent_span_id.value()?.into(),
162 "trace_id" => self.trace_id.value()?.deref().into(),
163 "status" => self.status.as_str()?.into(),
164 "origin" => self.origin.as_str()?.into(),
165 "duration" => {
166 let start_timestamp = *self.start_timestamp.value()?;
167 let timestamp = *self.timestamp.value()?;
168 relay_common::time::chrono_to_positive_millis(timestamp - start_timestamp)
169 .into()
170 }
171 "was_transaction" => self.was_transaction.value().unwrap_or(&false).into(),
172 path => {
173 if let Some(key) = path.strip_prefix("tags.") {
174 self.tags.value()?.get(key)?.as_str()?.into()
175 } else if let Some(key) = path.strip_prefix("data.") {
176 self.attribute(key)?
177 } else if let Some(key) = path.strip_prefix("sentry_tags.") {
178 self.sentry_tags.value()?.get_value(key)?
179 } else if let Some(rest) = path.strip_prefix("measurements.") {
180 let name = rest.strip_suffix(".value")?;
181 self.measurements
182 .value()?
183 .get(name)?
184 .value()?
185 .value
186 .value()?
187 .into()
188 } else {
189 return None;
190 }
191 }
192 });
193 }
194
195 let event_prefix = path.strip_prefix("event.")?;
198 Some(match event_prefix {
199 "release" => self.data.value()?.release.as_str()?.into(),
200 "environment" => self.data.value()?.environment.as_str()?.into(),
201 "transaction" => self.data.value()?.segment_name.as_str()?.into(),
202 "contexts.browser.name" => self.data.value()?.browser_name.as_str()?.into(),
203 _ => return None,
205 })
206 }
207}
208
209#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
211#[metastructure(trim = false, pii = "maybe")]
212pub struct SentryTags {
213 pub release: Annotated<String>,
214 #[metastructure(pii = "true")]
215 pub user: Annotated<String>,
216 #[metastructure(pii = "true", field = "user.id")]
217 pub user_id: Annotated<String>,
218 #[metastructure(pii = "true", field = "user.ip")]
219 pub user_ip: Annotated<String>,
220 #[metastructure(pii = "true", field = "user.username")]
221 pub user_username: Annotated<String>,
222 #[metastructure(pii = "true", field = "user.email")]
223 pub user_email: Annotated<String>,
224 pub environment: Annotated<String>,
225 pub transaction: Annotated<String>,
226 #[metastructure(field = "transaction.method")]
227 pub transaction_method: Annotated<String>,
228 #[metastructure(field = "transaction.op")]
229 pub transaction_op: Annotated<String>,
230 #[metastructure(field = "browser.name")]
231 pub browser_name: Annotated<String>,
232 #[metastructure(field = "sdk.name")]
233 pub sdk_name: Annotated<String>,
234 #[metastructure(field = "sdk.version")]
235 pub sdk_version: Annotated<String>,
236 pub platform: Annotated<String>,
237 pub mobile: Annotated<String>,
239 #[metastructure(field = "device.class")]
240 pub device_class: Annotated<String>,
241 #[metastructure(field = "device.family")]
242 pub device_family: Annotated<String>,
243 #[metastructure(field = "device.arch")]
244 pub device_arch: Annotated<String>,
245 #[metastructure(field = "device.battery_level")]
246 pub device_battery_level: Annotated<String>,
247 #[metastructure(field = "device.brand")]
248 pub device_brand: Annotated<String>,
249 #[metastructure(field = "device.charging")]
250 pub device_charging: Annotated<String>,
251 #[metastructure(field = "device.locale")]
252 pub device_locale: Annotated<String>,
253 #[metastructure(field = "device.model_id")]
254 pub device_model_id: Annotated<String>,
255 #[metastructure(field = "device.name")]
256 pub device_name: Annotated<String>,
257 #[metastructure(field = "device.online")]
258 pub device_online: Annotated<String>,
259 #[metastructure(field = "device.orientation")]
260 pub device_orientation: Annotated<String>,
261 #[metastructure(field = "device.screen_density")]
262 pub device_screen_density: Annotated<String>,
263 #[metastructure(field = "device.screen_dpi")]
264 pub device_screen_dpi: Annotated<String>,
265 #[metastructure(field = "device.screen_height_pixels")]
266 pub device_screen_height_pixels: Annotated<String>,
267 #[metastructure(field = "device.screen_width_pixels")]
268 pub device_screen_width_pixels: Annotated<String>,
269 #[metastructure(field = "device.simulator")]
270 pub device_simulator: Annotated<String>,
271 #[metastructure(field = "device.uuid")]
272 pub device_uuid: Annotated<String>,
273 #[metastructure(field = "app.device")]
274 pub app_device: Annotated<String>,
275 #[metastructure(field = "device.model")]
276 pub device_model: Annotated<String>,
277 pub runtime: Annotated<String>,
278 #[metastructure(field = "runtime.name")]
279 pub runtime_name: Annotated<String>,
280 pub browser: Annotated<String>,
281 pub os: Annotated<String>,
282 #[metastructure(field = "os.rooted")]
283 pub os_rooted: Annotated<String>,
284 #[metastructure(field = "gpu.name")]
285 pub gpu_name: Annotated<String>,
286 #[metastructure(field = "gpu.vendor")]
287 pub gpu_vendor: Annotated<String>,
288 #[metastructure(field = "monitor.id")]
289 pub monitor_id: Annotated<String>,
290 #[metastructure(field = "monitor.slug")]
291 pub monitor_slug: Annotated<String>,
292 #[metastructure(field = "request.url")]
293 pub request_url: Annotated<String>,
294 #[metastructure(field = "request.method")]
295 pub request_method: Annotated<String>,
296 #[metastructure(field = "os.name")]
298 pub os_name: Annotated<String>,
299 pub action: Annotated<String>,
300 pub category: Annotated<String>,
301 pub description: Annotated<String>,
302 pub domain: Annotated<String>,
303 pub raw_domain: Annotated<String>,
304 pub group: Annotated<String>,
305 #[metastructure(field = "http.decoded_response_content_length")]
306 pub http_decoded_response_content_length: Annotated<String>,
307 #[metastructure(field = "http.response_content_length")]
308 pub http_response_content_length: Annotated<String>,
309 #[metastructure(field = "http.response_transfer_size")]
310 pub http_response_transfer_size: Annotated<String>,
311 #[metastructure(field = "resource.render_blocking_status")]
312 pub resource_render_blocking_status: Annotated<String>,
313 pub op: Annotated<String>,
314 pub status: Annotated<String>,
315 pub status_code: Annotated<String>,
316 pub system: Annotated<String>,
317 pub ttid: Annotated<String>,
319 pub ttfd: Annotated<String>,
321 pub file_extension: Annotated<String>,
323 pub main_thread: Annotated<String>,
325 pub app_start_type: Annotated<String>,
327 pub replay_id: Annotated<String>,
328 #[metastructure(field = "cache.hit")]
329 pub cache_hit: Annotated<String>,
330 #[metastructure(field = "cache.key")]
331 pub cache_key: Annotated<String>,
332 #[metastructure(field = "trace.status")]
333 pub trace_status: Annotated<String>,
334 #[metastructure(field = "messaging.destination.name")]
335 pub messaging_destination_name: Annotated<String>,
336 #[metastructure(field = "messaging.message.id")]
337 pub messaging_message_id: Annotated<String>,
338 #[metastructure(field = "messaging.operation.name")]
339 pub messaging_operation_name: Annotated<String>,
340 #[metastructure(field = "messaging.operation.type")]
341 pub messaging_operation_type: Annotated<String>,
342 #[metastructure(field = "thread.name")]
343 pub thread_name: Annotated<String>,
344 #[metastructure(field = "thread.id")]
345 pub thread_id: Annotated<String>,
346 pub profiler_id: Annotated<String>,
347 #[metastructure(field = "user.geo.country_code")]
348 pub user_country_code: Annotated<String>,
349 #[metastructure(field = "user.geo.subregion")]
350 pub user_subregion: Annotated<String>,
351 }
354
355impl Getter for SentryTags {
356 fn get_value(&self, path: &str) -> Option<Val<'_>> {
357 let value = match path {
358 "action" => &self.action,
359 "app_start_type" => &self.app_start_type,
360 "browser.name" => &self.browser_name,
361 "cache.hit" => &self.cache_hit,
362 "cache.key" => &self.cache_key,
363 "category" => &self.category,
364 "description" => &self.description,
365 "device.class" => &self.device_class,
366 "device.family" => &self.device_family,
367 "device.arch" => &self.device_arch,
368 "device.battery_level" => &self.device_battery_level,
369 "device.brand" => &self.device_brand,
370 "device.charging" => &self.device_charging,
371 "device.locale" => &self.device_locale,
372 "device.model_id" => &self.device_model_id,
373 "device.name" => &self.device_name,
374 "device.online" => &self.device_online,
375 "device.orientation" => &self.device_orientation,
376 "device.screen_density" => &self.device_screen_density,
377 "device.screen_dpi" => &self.device_screen_dpi,
378 "device.screen_height_pixels" => &self.device_screen_height_pixels,
379 "device.screen_width_pixels" => &self.device_screen_width_pixels,
380 "device.simulator" => &self.device_simulator,
381 "device.uuid" => &self.device_uuid,
382 "app.device" => &self.app_device,
383 "device.model" => &self.device_model,
384 "runtime" => &self.runtime,
385 "runtime.name" => &self.runtime_name,
386 "browser" => &self.browser,
387 "os" => &self.os,
388 "os.rooted" => &self.os_rooted,
389 "gpu.name" => &self.gpu_name,
390 "gpu.vendor" => &self.gpu_vendor,
391 "monitor.id" => &self.monitor_id,
392 "monitor.slug" => &self.monitor_slug,
393 "request.url" => &self.request_url,
394 "request.method" => &self.request_method,
395 "domain" => &self.domain,
396 "environment" => &self.environment,
397 "file_extension" => &self.file_extension,
398 "group" => &self.group,
399 "http.decoded_response_content_length" => &self.http_decoded_response_content_length,
400 "http.response_content_length" => &self.http_response_content_length,
401 "http.response_transfer_size" => &self.http_response_transfer_size,
402 "main_thread" => &self.main_thread,
403 "messaging.destination.name" => &self.messaging_destination_name,
404 "messaging.message.id" => &self.messaging_message_id,
405 "messaging.operation.name" => &self.messaging_operation_name,
406 "messaging.operation.type" => &self.messaging_operation_type,
407 "mobile" => &self.mobile,
408 "op" => &self.op,
409 "os.name" => &self.os_name,
410 "platform" => &self.platform,
411 "profiler_id" => &self.profiler_id,
412 "raw_domain" => &self.raw_domain,
413 "release" => &self.release,
414 "replay_id" => &self.replay_id,
415 "resource.render_blocking_status" => &self.resource_render_blocking_status,
416 "sdk.name" => &self.sdk_name,
417 "sdk.version" => &self.sdk_version,
418 "status_code" => &self.status_code,
419 "status" => &self.status,
420 "system" => &self.system,
421 "thread.id" => &self.thread_id,
422 "thread.name" => &self.thread_name,
423 "trace.status" => &self.trace_status,
424 "transaction.method" => &self.transaction_method,
425 "transaction.op" => &self.transaction_op,
426 "transaction" => &self.transaction,
427 "ttfd" => &self.ttfd,
428 "ttid" => &self.ttid,
429 "user.email" => &self.user_email,
430 "user.geo.country_code" => &self.user_country_code,
431 "user.geo.subregion" => &self.user_subregion,
432 "user.id" => &self.user_id,
433 "user.ip" => &self.user_ip,
434 "user.username" => &self.user_username,
435 "user" => &self.user,
436 _ => return None,
437 };
438 Some(value.as_str()?.into())
439 }
440}
441
442#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
447#[metastructure(trim = false)]
448pub struct SpanData {
449 #[metastructure(field = "app_start_type")] pub app_start_type: Annotated<Value>,
454
455 #[metastructure(field = "gen_ai.request.max_tokens")]
457 pub gen_ai_request_max_tokens: Annotated<Value>,
458
459 #[metastructure(field = "gen_ai.pipeline.name", legacy_alias = "ai.pipeline.name")]
461 pub gen_ai_pipeline_name: Annotated<Value>,
462
463 #[metastructure(
465 field = "gen_ai.usage.total_tokens",
466 legacy_alias = "ai.total_tokens.used"
467 )]
468 pub gen_ai_usage_total_tokens: Annotated<Value>,
469
470 #[metastructure(
472 field = "gen_ai.usage.input_tokens",
473 legacy_alias = "ai.prompt_tokens.used",
474 legacy_alias = "gen_ai.usage.prompt_tokens"
475 )]
476 pub gen_ai_usage_input_tokens: Annotated<Value>,
477
478 #[metastructure(field = "gen_ai.usage.input_tokens.cached")]
481 pub gen_ai_usage_input_tokens_cached: Annotated<Value>,
482
483 #[metastructure(
485 field = "gen_ai.usage.output_tokens",
486 legacy_alias = "ai.completion_tokens.used",
487 legacy_alias = "gen_ai.usage.completion_tokens"
488 )]
489 pub gen_ai_usage_output_tokens: Annotated<Value>,
490
491 #[metastructure(field = "gen_ai.usage.output_tokens.reasoning")]
494 pub gen_ai_usage_output_tokens_reasoning: Annotated<Value>,
495
496 #[metastructure(field = "gen_ai.response.model")]
498 pub gen_ai_response_model: Annotated<Value>,
499
500 #[metastructure(field = "gen_ai.request.model", legacy_alias = "ai.model_id")]
502 pub gen_ai_request_model: Annotated<Value>,
503
504 #[metastructure(field = "gen_ai.usage.total_cost", legacy_alias = "ai.total_cost")]
506 pub gen_ai_usage_total_cost: Annotated<Value>,
507
508 #[metastructure(field = "gen_ai.cost.input_tokens")]
510 pub gen_ai_cost_input_tokens: Annotated<Value>,
511
512 #[metastructure(field = "gen_ai.cost.output_tokens")]
514 pub gen_ai_cost_output_tokens: Annotated<Value>,
515
516 #[metastructure(field = "gen_ai.prompt", pii = "maybe")]
518 pub gen_ai_prompt: Annotated<Value>,
519
520 #[metastructure(
522 field = "gen_ai.request.messages",
523 pii = "maybe",
524 legacy_alias = "ai.prompt.messages"
525 )]
526 pub gen_ai_request_messages: Annotated<Value>,
527
528 #[metastructure(
530 field = "gen_ai.tool.input",
531 pii = "maybe",
532 legacy_alias = "ai.toolCall.args"
533 )]
534 pub gen_ai_tool_input: Annotated<Value>,
535
536 #[metastructure(
538 field = "gen_ai.tool.output",
539 pii = "maybe",
540 legacy_alias = "ai.toolCall.result"
541 )]
542 pub gen_ai_tool_output: Annotated<Value>,
543
544 #[metastructure(
546 field = "gen_ai.response.tool_calls",
547 legacy_alias = "ai.response.toolCalls",
548 legacy_alias = "ai.tool_calls",
549 pii = "maybe"
550 )]
551 pub gen_ai_response_tool_calls: Annotated<Value>,
552
553 #[metastructure(
555 field = "gen_ai.response.text",
556 legacy_alias = "ai.response.text",
557 legacy_alias = "ai.responses",
558 pii = "maybe"
559 )]
560 pub gen_ai_response_text: Annotated<Value>,
561
562 #[metastructure(field = "gen_ai.response.object", pii = "maybe")]
564 pub gen_ai_response_object: Annotated<Value>,
565
566 #[metastructure(field = "gen_ai.response.streaming", legacy_alias = "ai.streaming")]
568 pub gen_ai_response_streaming: Annotated<Value>,
569
570 #[metastructure(field = "gen_ai.response.tokens_per_second")]
572 pub gen_ai_response_tokens_per_second: Annotated<Value>,
573
574 #[metastructure(
576 field = "gen_ai.request.available_tools",
577 legacy_alias = "ai.tools",
578 pii = "maybe"
579 )]
580 pub gen_ai_request_available_tools: Annotated<Value>,
581
582 #[metastructure(
584 field = "gen_ai.request.frequency_penalty",
585 legacy_alias = "ai.frequency_penalty"
586 )]
587 pub gen_ai_request_frequency_penalty: Annotated<Value>,
588
589 #[metastructure(
591 field = "gen_ai.request.presence_penalty",
592 legacy_alias = "ai.presence_penalty"
593 )]
594 pub gen_ai_request_presence_penalty: Annotated<Value>,
595
596 #[metastructure(field = "gen_ai.request.seed", legacy_alias = "ai.seed")]
598 pub gen_ai_request_seed: Annotated<Value>,
599
600 #[metastructure(field = "gen_ai.request.temperature", legacy_alias = "ai.temperature")]
602 pub gen_ai_request_temperature: Annotated<Value>,
603
604 #[metastructure(field = "gen_ai.request.top_k", legacy_alias = "ai.top_k")]
606 pub gen_ai_request_top_k: Annotated<Value>,
607
608 #[metastructure(field = "gen_ai.request.top_p", legacy_alias = "ai.top_p")]
610 pub gen_ai_request_top_p: Annotated<Value>,
611
612 #[metastructure(
614 field = "gen_ai.response.finish_reason",
615 legacy_alias = "ai.finish_reason"
616 )]
617 pub gen_ai_response_finish_reason: Annotated<Value>,
618
619 #[metastructure(field = "gen_ai.response.id", legacy_alias = "ai.generation_id")]
621 pub gen_ai_response_id: Annotated<Value>,
622
623 #[metastructure(field = "gen_ai.system", legacy_alias = "ai.model.provider")]
625 pub gen_ai_system: Annotated<Value>,
626
627 #[metastructure(
629 field = "gen_ai.tool.name",
630 legacy_alias = "ai.function_call",
631 pii = "maybe"
632 )]
633 pub gen_ai_tool_name: Annotated<Value>,
634
635 #[metastructure(field = "gen_ai.operation.name", pii = "maybe")]
637 pub gen_ai_operation_name: Annotated<String>,
638
639 #[metastructure(field = "browser.name")]
641 pub browser_name: Annotated<String>,
642
643 #[metastructure(field = "code.filepath", pii = "maybe")]
645 pub code_filepath: Annotated<Value>,
646 #[metastructure(field = "code.lineno", pii = "maybe")]
648 pub code_lineno: Annotated<Value>,
649 #[metastructure(field = "code.function", pii = "maybe")]
653 pub code_function: Annotated<Value>,
654 #[metastructure(field = "code.namespace", pii = "maybe")]
660 pub code_namespace: Annotated<Value>,
661
662 #[metastructure(field = "db.operation")]
667 pub db_operation: Annotated<Value>,
668
669 #[metastructure(field = "db.system")]
673 pub db_system: Annotated<Value>,
674
675 #[metastructure(
679 field = "db.collection.name",
680 legacy_alias = "db.cassandra.table",
681 legacy_alias = "db.cosmosdb.container",
682 legacy_alias = "db.mongodb.collection",
683 legacy_alias = "db.sql.table"
684 )]
685 pub db_collection_name: Annotated<Value>,
686
687 #[metastructure(field = "sentry.environment", legacy_alias = "environment")]
689 pub environment: Annotated<String>,
690
691 #[metastructure(field = "sentry.release", legacy_alias = "release")]
693 pub release: Annotated<LenientString>,
694
695 #[metastructure(field = "http.decoded_response_content_length")]
697 pub http_decoded_response_content_length: Annotated<Value>,
698
699 #[metastructure(
701 field = "http.request_method",
702 legacy_alias = "http.method",
703 legacy_alias = "method"
704 )]
705 pub http_request_method: Annotated<Value>,
706
707 #[metastructure(field = "http.response_content_length")]
709 pub http_response_content_length: Annotated<Value>,
710
711 #[metastructure(field = "http.response_transfer_size")]
713 pub http_response_transfer_size: Annotated<Value>,
714
715 #[metastructure(field = "resource.render_blocking_status")]
717 pub resource_render_blocking_status: Annotated<Value>,
718
719 #[metastructure(field = "server.address")]
721 pub server_address: Annotated<Value>,
722
723 #[metastructure(field = "cache.hit")]
725 pub cache_hit: Annotated<Value>,
726
727 #[metastructure(field = "cache.key")]
729 pub cache_key: Annotated<Value>,
730
731 #[metastructure(field = "cache.item_size")]
733 pub cache_item_size: Annotated<Value>,
734
735 #[metastructure(field = "http.response.status_code", legacy_alias = "status_code")]
737 pub http_response_status_code: Annotated<Value>,
738
739 #[metastructure(field = "thread.name")]
741 pub thread_name: Annotated<String>,
742
743 #[metastructure(field = "thread.id")]
745 pub thread_id: Annotated<ThreadId>,
746
747 #[metastructure(field = "sentry.segment.name", legacy_alias = "transaction")]
753 pub segment_name: Annotated<String>,
754
755 #[metastructure(field = "ui.component_name")]
757 pub ui_component_name: Annotated<Value>,
758
759 #[metastructure(field = "url.scheme")]
761 pub url_scheme: Annotated<Value>,
762
763 #[metastructure(field = "user")]
765 pub user: Annotated<Value>,
766
767 #[metastructure(field = "user.email")]
771 pub user_email: Annotated<String>,
772
773 #[metastructure(field = "user.full_name")]
777 pub user_full_name: Annotated<String>,
778
779 #[metastructure(field = "user.geo.country_code")]
783 pub user_geo_country_code: Annotated<String>,
784
785 #[metastructure(field = "user.geo.city")]
789 pub user_geo_city: Annotated<String>,
790
791 #[metastructure(field = "user.geo.subdivision")]
795 pub user_geo_subdivision: Annotated<String>,
796
797 #[metastructure(field = "user.geo.region")]
801 pub user_geo_region: Annotated<String>,
802
803 #[metastructure(field = "user.hash")]
807 pub user_hash: Annotated<String>,
808
809 #[metastructure(field = "user.id")]
813 pub user_id: Annotated<String>,
814
815 #[metastructure(field = "user.name")]
819 pub user_name: Annotated<String>,
820
821 #[metastructure(field = "user.roles")]
825 pub user_roles: Annotated<Array<String>>,
826
827 #[metastructure(field = "sentry.exclusive_time")]
829 pub exclusive_time: Annotated<Value>,
830
831 #[metastructure(field = "profile_id")]
833 pub profile_id: Annotated<Value>,
834
835 #[metastructure(field = "sentry.replay.id", legacy_alias = "replay_id")]
837 pub replay_id: Annotated<Value>,
838
839 #[metastructure(field = "sentry.sdk.name")]
841 pub sdk_name: Annotated<String>,
842
843 #[metastructure(field = "sentry.sdk.version")]
845 pub sdk_version: Annotated<String>,
846
847 #[metastructure(field = "sentry.frames.slow", legacy_alias = "frames.slow")]
849 pub frames_slow: Annotated<Value>,
850
851 #[metastructure(field = "sentry.frames.frozen", legacy_alias = "frames.frozen")]
853 pub frames_frozen: Annotated<Value>,
854
855 #[metastructure(field = "sentry.frames.total", legacy_alias = "frames.total")]
857 pub frames_total: Annotated<Value>,
858
859 #[metastructure(field = "frames.delay")]
861 pub frames_delay: Annotated<Value>,
862
863 #[metastructure(field = "messaging.destination.name")]
865 pub messaging_destination_name: Annotated<String>,
866
867 #[metastructure(field = "messaging.message.retry.count")]
869 pub messaging_message_retry_count: Annotated<Value>,
870
871 #[metastructure(field = "messaging.message.receive.latency")]
873 pub messaging_message_receive_latency: Annotated<Value>,
874
875 #[metastructure(field = "messaging.message.body.size")]
877 pub messaging_message_body_size: Annotated<Value>,
878
879 #[metastructure(field = "messaging.message.id")]
881 pub messaging_message_id: Annotated<String>,
882
883 #[metastructure(field = "messaging.operation.name")]
885 pub messaging_operation_name: Annotated<String>,
886
887 #[metastructure(field = "messaging.operation.type")]
889 pub messaging_operation_type: Annotated<String>,
890
891 #[metastructure(field = "user_agent.original")]
893 pub user_agent_original: Annotated<String>,
894
895 #[metastructure(field = "url.full")]
897 pub url_full: Annotated<String>,
898
899 #[metastructure(field = "client.address")]
901 pub client_address: Annotated<IpAddr>,
902
903 #[metastructure(pii = "maybe", skip_serialization = "empty")]
907 pub route: Annotated<Route>,
908 #[metastructure(field = "previousRoute", pii = "maybe", skip_serialization = "empty")]
912 pub previous_route: Annotated<Route>,
913
914 #[metastructure(field = "lcp.element")]
916 pub lcp_element: Annotated<String>,
917
918 #[metastructure(field = "lcp.size")]
920 pub lcp_size: Annotated<u64>,
921
922 #[metastructure(field = "lcp.id")]
924 pub lcp_id: Annotated<String>,
925
926 #[metastructure(field = "lcp.url")]
928 pub lcp_url: Annotated<String>,
929
930 #[metastructure(
932 additional_properties,
933 pii = "true",
934 retain = true,
935 skip_serialization = "null" )]
937 pub other: Object<Value>,
938}
939
940impl Getter for SpanData {
941 fn get_value(&self, path: &str) -> Option<Val<'_>> {
942 Some(match path {
943 "app_start_type" => self.app_start_type.value()?.into(),
944 "browser\\.name" => self.browser_name.as_str()?.into(),
945 "code\\.filepath" => self.code_filepath.value()?.into(),
946 "code\\.function" => self.code_function.value()?.into(),
947 "code\\.lineno" => self.code_lineno.value()?.into(),
948 "code\\.namespace" => self.code_namespace.value()?.into(),
949 "db.operation" => self.db_operation.value()?.into(),
950 "db\\.system" => self.db_system.value()?.into(),
951 "environment" => self.environment.as_str()?.into(),
952 "gen_ai\\.request\\.max_tokens" => self.gen_ai_request_max_tokens.value()?.into(),
953 "gen_ai\\.usage\\.total_tokens" => self.gen_ai_usage_total_tokens.value()?.into(),
954 "gen_ai\\.usage\\.total_cost" => self.gen_ai_usage_total_cost.value()?.into(),
955 "gen_ai\\.cost\\.input_tokens" => self.gen_ai_cost_input_tokens.value()?.into(),
956 "gen_ai\\.cost\\.output_tokens" => self.gen_ai_cost_output_tokens.value()?.into(),
957 "http\\.decoded_response_content_length" => {
958 self.http_decoded_response_content_length.value()?.into()
959 }
960 "http\\.request_method" | "http\\.method" | "method" => {
961 self.http_request_method.value()?.into()
962 }
963 "http\\.response_content_length" => self.http_response_content_length.value()?.into(),
964 "http\\.response_transfer_size" => self.http_response_transfer_size.value()?.into(),
965 "http\\.response.status_code" | "status_code" => {
966 self.http_response_status_code.value()?.into()
967 }
968 "resource\\.render_blocking_status" => {
969 self.resource_render_blocking_status.value()?.into()
970 }
971 "server\\.address" => self.server_address.value()?.into(),
972 "thread\\.name" => self.thread_name.as_str()?.into(),
973 "ui\\.component_name" => self.ui_component_name.value()?.into(),
974 "url\\.scheme" => self.url_scheme.value()?.into(),
975 "user" => self.user.value()?.into(),
976 "user\\.email" => self.user_email.as_str()?.into(),
977 "user\\.full_name" => self.user_full_name.as_str()?.into(),
978 "user\\.geo\\.city" => self.user_geo_city.as_str()?.into(),
979 "user\\.geo\\.country_code" => self.user_geo_country_code.as_str()?.into(),
980 "user\\.geo\\.region" => self.user_geo_region.as_str()?.into(),
981 "user\\.geo\\.subdivision" => self.user_geo_subdivision.as_str()?.into(),
982 "user\\.hash" => self.user_hash.as_str()?.into(),
983 "user\\.id" => self.user_id.as_str()?.into(),
984 "user\\.name" => self.user_name.as_str()?.into(),
985 "transaction" => self.segment_name.as_str()?.into(),
986 "release" => self.release.as_str()?.into(),
987 _ => {
988 let escaped = path.replace("\\.", "\0");
989 let mut path = escaped.split('.').map(|s| s.replace('\0', "."));
990 let root = path.next()?;
991
992 let mut val = self.other.get(&root)?.value()?;
993 for part in path {
994 let relay_protocol::Value::Object(map) = val else {
996 return None;
997 };
998 val = map.get(&part)?.value()?;
999 }
1000 val.into()
1001 }
1002 })
1003 }
1004}
1005
1006#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
1008#[metastructure(trim = false)]
1009pub struct SpanLink {
1010 #[metastructure(required = true, trim = false)]
1012 pub trace_id: Annotated<TraceId>,
1013
1014 #[metastructure(required = true, trim = false)]
1016 pub span_id: Annotated<SpanId>,
1017
1018 #[metastructure(trim = false)]
1020 pub sampled: Annotated<bool>,
1021
1022 #[metastructure(pii = "maybe", trim = false)]
1024 pub attributes: Annotated<Object<Value>>,
1025
1026 #[metastructure(additional_properties, retain = true, pii = "maybe", trim = false)]
1028 pub other: Object<Value>,
1029}
1030
1031#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
1033pub struct Route {
1034 #[metastructure(pii = "maybe", skip_serialization = "empty")]
1036 pub name: Annotated<String>,
1037
1038 #[metastructure(
1040 pii = "true",
1041 skip_serialization = "empty",
1042 max_depth = 5,
1043 max_bytes = 2048
1044 )]
1045 pub params: Annotated<Object<Value>>,
1046
1047 #[metastructure(
1049 additional_properties,
1050 retain = true,
1051 pii = "maybe",
1052 skip_serialization = "empty"
1053 )]
1054 pub other: Object<Value>,
1055}
1056
1057impl FromValue for Route {
1058 fn from_value(value: Annotated<Value>) -> Annotated<Self>
1059 where
1060 Self: Sized,
1061 {
1062 match value {
1063 Annotated(Some(Value::String(name)), meta) => Annotated(
1064 Some(Route {
1065 name: Annotated::new(name),
1066 ..Default::default()
1067 }),
1068 meta,
1069 ),
1070 Annotated(Some(Value::Object(mut values)), meta) => {
1071 let mut route: Route = Default::default();
1072 if let Some(Annotated(Some(Value::String(name)), _)) = values.remove("name") {
1073 route.name = Annotated::new(name);
1074 }
1075 if let Some(Annotated(Some(Value::Object(params)), _)) = values.remove("params") {
1076 route.params = Annotated::new(params);
1077 }
1078
1079 if !values.is_empty() {
1080 route.other = values;
1081 }
1082
1083 Annotated(Some(route), meta)
1084 }
1085 Annotated(None, meta) => Annotated(None, meta),
1086 Annotated(Some(value), mut meta) => {
1087 meta.add_error(Error::expected("route expected to be an object"));
1088 meta.set_original_value(Some(value));
1089 Annotated(None, meta)
1090 }
1091 }
1092 }
1093}
1094
1095#[derive(Clone, Debug, PartialEq, ProcessValue)]
1096pub enum SpanKind {
1097 Internal,
1098 Server,
1099 Client,
1100 Producer,
1101 Consumer,
1102}
1103
1104impl SpanKind {
1105 pub fn as_str(&self) -> &'static str {
1106 match self {
1107 Self::Internal => "internal",
1108 Self::Server => "server",
1109 Self::Client => "client",
1110 Self::Producer => "producer",
1111 Self::Consumer => "consumer",
1112 }
1113 }
1114}
1115
1116impl Empty for SpanKind {
1117 fn is_empty(&self) -> bool {
1118 false
1119 }
1120}
1121
1122#[derive(Debug)]
1123pub struct ParseSpanKindError;
1124
1125impl std::str::FromStr for SpanKind {
1126 type Err = ParseSpanKindError;
1127
1128 fn from_str(s: &str) -> Result<Self, Self::Err> {
1129 Ok(match s {
1130 "internal" => SpanKind::Internal,
1131 "server" => SpanKind::Server,
1132 "client" => SpanKind::Client,
1133 "producer" => SpanKind::Producer,
1134 "consumer" => SpanKind::Consumer,
1135 _ => return Err(ParseSpanKindError),
1136 })
1137 }
1138}
1139
1140impl Default for SpanKind {
1141 fn default() -> Self {
1142 Self::Internal
1143 }
1144}
1145
1146impl fmt::Display for SpanKind {
1147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1148 write!(f, "{}", self.as_str())
1149 }
1150}
1151
1152impl FromValue for SpanKind {
1153 fn from_value(value: Annotated<Value>) -> Annotated<Self>
1154 where
1155 Self: Sized,
1156 {
1157 match value {
1158 Annotated(Some(Value::String(s)), meta) => Annotated(SpanKind::from_str(&s).ok(), meta),
1159 Annotated(_, meta) => Annotated(None, meta),
1160 }
1161 }
1162}
1163
1164impl IntoValue for SpanKind {
1165 fn into_value(self) -> Value
1166 where
1167 Self: Sized,
1168 {
1169 Value::String(self.to_string())
1170 }
1171
1172 fn serialize_payload<S>(
1173 &self,
1174 s: S,
1175 _behavior: relay_protocol::SkipSerialization,
1176 ) -> Result<S::Ok, S::Error>
1177 where
1178 Self: Sized,
1179 S: serde::Serializer,
1180 {
1181 s.serialize_str(self.as_str())
1182 }
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187 use crate::protocol::Measurement;
1188 use chrono::{TimeZone, Utc};
1189 use relay_base_schema::metrics::{InformationUnit, MetricUnit};
1190 use relay_protocol::RuleCondition;
1191 use similar_asserts::assert_eq;
1192
1193 use super::*;
1194
1195 #[test]
1196 fn test_span_serialization() {
1197 let json = r#"{
1198 "timestamp": 0.0,
1199 "start_timestamp": -63158400.0,
1200 "exclusive_time": 1.23,
1201 "op": "operation",
1202 "span_id": "fa90fdead5f74052",
1203 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1204 "status": "ok",
1205 "description": "desc",
1206 "origin": "auto.http",
1207 "links": [
1208 {
1209 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1210 "span_id": "fa90fdead5f74052",
1211 "sampled": true,
1212 "attributes": {
1213 "boolAttr": true,
1214 "numAttr": 123,
1215 "stringAttr": "foo"
1216 }
1217 }
1218 ],
1219 "measurements": {
1220 "memory": {
1221 "value": 9001.0,
1222 "unit": "byte"
1223 }
1224 },
1225 "kind": "server"
1226}"#;
1227 let mut measurements = Object::new();
1228 measurements.insert(
1229 "memory".into(),
1230 Annotated::new(Measurement {
1231 value: Annotated::new(9001.0.try_into().unwrap()),
1232 unit: Annotated::new(MetricUnit::Information(InformationUnit::Byte)),
1233 }),
1234 );
1235
1236 let links = Annotated::new(vec![Annotated::new(SpanLink {
1237 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1238 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
1239 sampled: Annotated::new(true),
1240 attributes: Annotated::new({
1241 let mut map: std::collections::BTreeMap<String, Annotated<Value>> = Object::new();
1242 map.insert(
1243 "stringAttr".into(),
1244 Annotated::new(Value::String("foo".into())),
1245 );
1246 map.insert("numAttr".into(), Annotated::new(Value::I64(123)));
1247 map.insert("boolAttr".into(), Value::Bool(true).into());
1248 map
1249 }),
1250 ..Default::default()
1251 })]);
1252
1253 let span = Annotated::new(Span {
1254 timestamp: Annotated::new(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap().into()),
1255 start_timestamp: Annotated::new(
1256 Utc.with_ymd_and_hms(1968, 1, 1, 0, 0, 0).unwrap().into(),
1257 ),
1258 exclusive_time: Annotated::new(1.23),
1259 description: Annotated::new("desc".to_owned()),
1260 op: Annotated::new("operation".to_owned()),
1261 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1262 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
1263 status: Annotated::new(SpanStatus::Ok),
1264 origin: Annotated::new("auto.http".to_owned()),
1265 kind: Annotated::new(SpanKind::Server),
1266 measurements: Annotated::new(Measurements(measurements)),
1267 links,
1268 ..Default::default()
1269 });
1270 assert_eq!(json, span.to_json_pretty().unwrap());
1271
1272 let span_from_string = Annotated::from_json(json).unwrap();
1273 assert_eq!(span, span_from_string);
1274 }
1275
1276 #[test]
1277 fn test_getter_span_data() {
1278 let span = Annotated::<Span>::from_json(
1279 r#"{
1280 "data": {
1281 "foo": {"bar": 1},
1282 "foo.bar": 2
1283 },
1284 "measurements": {
1285 "some": {"value": 100.0}
1286 }
1287 }"#,
1288 )
1289 .unwrap()
1290 .into_value()
1291 .unwrap();
1292
1293 assert_eq!(span.get_value("span.data.foo.bar"), Some(Val::I64(1)));
1294 assert_eq!(span.get_value(r"span.data.foo\.bar"), Some(Val::I64(2)));
1295
1296 assert_eq!(span.get_value("span.data"), None);
1297 assert_eq!(span.get_value("span.data."), None);
1298 assert_eq!(span.get_value("span.data.x"), None);
1299
1300 assert_eq!(
1301 span.get_value("span.measurements.some.value"),
1302 Some(Val::F64(100.0))
1303 );
1304 }
1305
1306 #[test]
1307 fn test_getter_was_transaction() {
1308 let mut span = Span::default();
1309 assert_eq!(
1310 span.get_value("span.was_transaction"),
1311 Some(Val::Bool(false))
1312 );
1313 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1314 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1315
1316 span.was_transaction.set_value(Some(false));
1317 assert_eq!(
1318 span.get_value("span.was_transaction"),
1319 Some(Val::Bool(false))
1320 );
1321 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1322 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1323
1324 span.was_transaction.set_value(Some(true));
1325 assert_eq!(
1326 span.get_value("span.was_transaction"),
1327 Some(Val::Bool(true))
1328 );
1329 assert!(RuleCondition::eq("span.was_transaction", true).matches(&span));
1330 assert!(!RuleCondition::eq("span.was_transaction", false).matches(&span));
1331 }
1332
1333 #[test]
1334 fn test_span_fields_as_event() {
1335 let span = Annotated::<Span>::from_json(
1336 r#"{
1337 "data": {
1338 "release": "1.0",
1339 "environment": "prod",
1340 "sentry.segment.name": "/api/endpoint"
1341 }
1342 }"#,
1343 )
1344 .unwrap()
1345 .into_value()
1346 .unwrap();
1347
1348 assert_eq!(span.get_value("event.release"), Some(Val::String("1.0")));
1349 assert_eq!(
1350 span.get_value("event.environment"),
1351 Some(Val::String("prod"))
1352 );
1353 assert_eq!(
1354 span.get_value("event.transaction"),
1355 Some(Val::String("/api/endpoint"))
1356 );
1357 }
1358
1359 #[test]
1360 fn test_span_duration() {
1361 let span = Annotated::<Span>::from_json(
1362 r#"{
1363 "start_timestamp": 1694732407.8367,
1364 "timestamp": 1694732408.31451233
1365 }"#,
1366 )
1367 .unwrap()
1368 .into_value()
1369 .unwrap();
1370
1371 assert_eq!(span.get_value("span.duration"), Some(Val::F64(477.812)));
1372 }
1373
1374 #[test]
1375 fn test_span_data() {
1376 let data = r#"{
1377 "foo": 2,
1378 "bar": "3",
1379 "db.system": "mysql",
1380 "code.filepath": "task.py",
1381 "code.lineno": 123,
1382 "code.function": "fn()",
1383 "code.namespace": "ns",
1384 "frames.slow": 1,
1385 "frames.frozen": 2,
1386 "frames.total": 9,
1387 "frames.delay": 100,
1388 "messaging.destination.name": "default",
1389 "messaging.message.retry.count": 3,
1390 "messaging.message.receive.latency": 40,
1391 "messaging.message.body.size": 100,
1392 "messaging.message.id": "abc123",
1393 "messaging.operation.name": "publish",
1394 "messaging.operation.type": "create",
1395 "user_agent.original": "Chrome",
1396 "url.full": "my_url.com",
1397 "client.address": "192.168.0.1"
1398 }"#;
1399 let data = Annotated::<SpanData>::from_json(data)
1400 .unwrap()
1401 .into_value()
1402 .unwrap();
1403 insta::assert_debug_snapshot!(data, @r#"
1404 SpanData {
1405 app_start_type: ~,
1406 gen_ai_request_max_tokens: ~,
1407 gen_ai_pipeline_name: ~,
1408 gen_ai_usage_total_tokens: ~,
1409 gen_ai_usage_input_tokens: ~,
1410 gen_ai_usage_input_tokens_cached: ~,
1411 gen_ai_usage_output_tokens: ~,
1412 gen_ai_usage_output_tokens_reasoning: ~,
1413 gen_ai_response_model: ~,
1414 gen_ai_request_model: ~,
1415 gen_ai_usage_total_cost: ~,
1416 gen_ai_cost_input_tokens: ~,
1417 gen_ai_cost_output_tokens: ~,
1418 gen_ai_prompt: ~,
1419 gen_ai_request_messages: ~,
1420 gen_ai_tool_input: ~,
1421 gen_ai_tool_output: ~,
1422 gen_ai_response_tool_calls: ~,
1423 gen_ai_response_text: ~,
1424 gen_ai_response_object: ~,
1425 gen_ai_response_streaming: ~,
1426 gen_ai_response_tokens_per_second: ~,
1427 gen_ai_request_available_tools: ~,
1428 gen_ai_request_frequency_penalty: ~,
1429 gen_ai_request_presence_penalty: ~,
1430 gen_ai_request_seed: ~,
1431 gen_ai_request_temperature: ~,
1432 gen_ai_request_top_k: ~,
1433 gen_ai_request_top_p: ~,
1434 gen_ai_response_finish_reason: ~,
1435 gen_ai_response_id: ~,
1436 gen_ai_system: ~,
1437 gen_ai_tool_name: ~,
1438 gen_ai_operation_name: ~,
1439 browser_name: ~,
1440 code_filepath: String(
1441 "task.py",
1442 ),
1443 code_lineno: I64(
1444 123,
1445 ),
1446 code_function: String(
1447 "fn()",
1448 ),
1449 code_namespace: String(
1450 "ns",
1451 ),
1452 db_operation: ~,
1453 db_system: String(
1454 "mysql",
1455 ),
1456 db_collection_name: ~,
1457 environment: ~,
1458 release: ~,
1459 http_decoded_response_content_length: ~,
1460 http_request_method: ~,
1461 http_response_content_length: ~,
1462 http_response_transfer_size: ~,
1463 resource_render_blocking_status: ~,
1464 server_address: ~,
1465 cache_hit: ~,
1466 cache_key: ~,
1467 cache_item_size: ~,
1468 http_response_status_code: ~,
1469 thread_name: ~,
1470 thread_id: ~,
1471 segment_name: ~,
1472 ui_component_name: ~,
1473 url_scheme: ~,
1474 user: ~,
1475 user_email: ~,
1476 user_full_name: ~,
1477 user_geo_country_code: ~,
1478 user_geo_city: ~,
1479 user_geo_subdivision: ~,
1480 user_geo_region: ~,
1481 user_hash: ~,
1482 user_id: ~,
1483 user_name: ~,
1484 user_roles: ~,
1485 exclusive_time: ~,
1486 profile_id: ~,
1487 replay_id: ~,
1488 sdk_name: ~,
1489 sdk_version: ~,
1490 frames_slow: I64(
1491 1,
1492 ),
1493 frames_frozen: I64(
1494 2,
1495 ),
1496 frames_total: I64(
1497 9,
1498 ),
1499 frames_delay: I64(
1500 100,
1501 ),
1502 messaging_destination_name: "default",
1503 messaging_message_retry_count: I64(
1504 3,
1505 ),
1506 messaging_message_receive_latency: I64(
1507 40,
1508 ),
1509 messaging_message_body_size: I64(
1510 100,
1511 ),
1512 messaging_message_id: "abc123",
1513 messaging_operation_name: "publish",
1514 messaging_operation_type: "create",
1515 user_agent_original: "Chrome",
1516 url_full: "my_url.com",
1517 client_address: IpAddr(
1518 "192.168.0.1",
1519 ),
1520 route: ~,
1521 previous_route: ~,
1522 lcp_element: ~,
1523 lcp_size: ~,
1524 lcp_id: ~,
1525 lcp_url: ~,
1526 other: {
1527 "bar": String(
1528 "3",
1529 ),
1530 "foo": I64(
1531 2,
1532 ),
1533 },
1534 }
1535 "#);
1536
1537 assert_eq!(data.get_value("foo"), Some(Val::U64(2)));
1538 assert_eq!(data.get_value("bar"), Some(Val::String("3")));
1539 assert_eq!(data.get_value("db\\.system"), Some(Val::String("mysql")));
1540 assert_eq!(data.get_value("code\\.lineno"), Some(Val::U64(123)));
1541 assert_eq!(data.get_value("code\\.function"), Some(Val::String("fn()")));
1542 assert_eq!(data.get_value("code\\.namespace"), Some(Val::String("ns")));
1543 assert_eq!(data.get_value("unknown"), None);
1544 }
1545
1546 #[test]
1547 fn test_span_data_empty_well_known_field() {
1548 let span = r#"{
1549 "data": {
1550 "lcp.url": ""
1551 }
1552 }"#;
1553 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1554 assert_eq!(span.to_json().unwrap(), r#"{"data":{"lcp.url":""}}"#);
1555 }
1556
1557 #[test]
1558 fn test_span_data_empty_custom_field() {
1559 let span = r#"{
1560 "data": {
1561 "custom_field_empty": ""
1562 }
1563 }"#;
1564 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1565 assert_eq!(
1566 span.to_json().unwrap(),
1567 r#"{"data":{"custom_field_empty":""}}"#
1568 );
1569 }
1570
1571 #[test]
1572 fn test_span_data_completely_empty() {
1573 let span = r#"{
1574 "data": {}
1575 }"#;
1576 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1577 assert_eq!(span.to_json().unwrap(), r#"{"data":{}}"#);
1578 }
1579
1580 #[test]
1581 fn test_span_links() {
1582 let span = r#"{
1583 "links": [
1584 {
1585 "trace_id": "5c79f60c11214eb38604f4ae0781bfb2",
1586 "span_id": "ab90fdead5f74052",
1587 "sampled": true,
1588 "attributes": {
1589 "sentry.link.type": "previous_trace"
1590 }
1591 },
1592 {
1593 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1594 "span_id": "fa90fdead5f74052",
1595 "sampled": true,
1596 "attributes": {
1597 "sentry.link.type": "next_trace"
1598 }
1599 }
1600 ]
1601 }"#;
1602
1603 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1604 assert_eq!(
1605 span.to_json().unwrap(),
1606 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"}}]}"#
1607 );
1608 }
1609}