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