1mod convert;
2
3use std::fmt;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use relay_protocol::{
8 Annotated, Array, Empty, Error, FromValue, Getter, IntoValue, Object, Val, Value,
9};
10
11use crate::processor::{Pii, ProcessValue, ProcessingState};
12use crate::protocol::{
13 EventId, IpAddr, JsonLenientString, LenientString, Measurements, OperationType, OriginType,
14 SpanId, SpanStatus, ThreadId, Timestamp, TraceId,
15};
16
17#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
18#[metastructure(process_func = "process_span", value_type = "Span", trim = false)]
19pub struct Span {
20 #[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(additional_properties, pii = "maybe")]
126 pub other: Object<Value>,
127}
128
129impl Span {
130 fn attribute(&self, key: &str) -> Option<Val<'_>> {
135 Some(match self.data.value()?.get_value(key) {
136 Some(value) => value,
137 None => self.tags.value()?.get(key)?.as_str()?.into(),
138 })
139 }
140}
141
142impl Getter for Span {
143 fn get_value(&self, path: &str) -> Option<Val<'_>> {
144 let span_prefix = path.strip_prefix("span.");
145 if let Some(span_prefix) = span_prefix {
146 return Some(match span_prefix {
147 "exclusive_time" => self.exclusive_time.value()?.into(),
148 "description" => self.description.as_str()?.into(),
149 "op" => self.op.as_str()?.into(),
150 "span_id" => self.span_id.value()?.into(),
151 "parent_span_id" => self.parent_span_id.value()?.into(),
152 "trace_id" => self.trace_id.value()?.deref().into(),
153 "status" => self.status.as_str()?.into(),
154 "is_segment" => self.is_segment.value()?.into(),
155 "origin" => self.origin.as_str()?.into(),
156 "duration" => {
157 let start_timestamp = *self.start_timestamp.value()?;
158 let timestamp = *self.timestamp.value()?;
159 relay_common::time::chrono_to_positive_millis(timestamp - start_timestamp)
160 .into()
161 }
162 "was_transaction" => self.was_transaction.value().unwrap_or(&false).into(),
163 path => {
164 if let Some(key) = path.strip_prefix("tags.") {
165 self.tags.value()?.get(key)?.as_str()?.into()
166 } else if let Some(key) = path.strip_prefix("data.") {
167 self.attribute(key)?
168 } else if let Some(key) = path.strip_prefix("sentry_tags.") {
169 self.sentry_tags.value()?.get_value(key)?
170 } else if let Some(rest) = path.strip_prefix("measurements.") {
171 let name = rest.strip_suffix(".value")?;
172 self.measurements
173 .value()?
174 .get(name)?
175 .value()?
176 .value
177 .value()?
178 .into()
179 } else {
180 return None;
181 }
182 }
183 });
184 }
185
186 let event_prefix = path.strip_prefix("event.")?;
189 Some(match event_prefix {
190 "release" => self.data.value()?.release.as_str()?.into(),
191 "environment" => self.data.value()?.environment.as_str()?.into(),
192 "transaction" => self.data.value()?.segment_name.as_str()?.into(),
193 "contexts.browser.name" => self.data.value()?.browser_name.as_str()?.into(),
194 _ => return None,
196 })
197 }
198}
199
200#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
202#[metastructure(trim = false, pii = "maybe")]
203pub struct SentryTags {
204 pub release: Annotated<String>,
205 #[metastructure(pii = "true")]
206 pub user: Annotated<String>,
207 #[metastructure(pii = "true", field = "user.id")]
208 pub user_id: Annotated<String>,
209 #[metastructure(pii = "true", field = "user.ip")]
210 pub user_ip: Annotated<String>,
211 #[metastructure(pii = "true", field = "user.username")]
212 pub user_username: Annotated<String>,
213 #[metastructure(pii = "true", field = "user.email")]
214 pub user_email: Annotated<String>,
215 pub environment: Annotated<String>,
216 pub transaction: Annotated<String>,
217 #[metastructure(field = "transaction.method")]
218 pub transaction_method: Annotated<String>,
219 #[metastructure(field = "transaction.op")]
220 pub transaction_op: Annotated<String>,
221 #[metastructure(field = "browser.name")]
222 pub browser_name: Annotated<String>,
223 #[metastructure(field = "sdk.name")]
224 pub sdk_name: Annotated<String>,
225 #[metastructure(field = "sdk.version")]
226 pub sdk_version: Annotated<String>,
227 pub platform: Annotated<String>,
228 pub mobile: Annotated<String>,
230 #[metastructure(field = "device.class")]
231 pub device_class: Annotated<String>,
232 #[metastructure(field = "device.family")]
233 pub device_family: Annotated<String>,
234 #[metastructure(field = "device.arch")]
235 pub device_arch: Annotated<String>,
236 #[metastructure(field = "device.battery_level")]
237 pub device_battery_level: Annotated<String>,
238 #[metastructure(field = "device.brand")]
239 pub device_brand: Annotated<String>,
240 #[metastructure(field = "device.charging")]
241 pub device_charging: Annotated<String>,
242 #[metastructure(field = "device.locale")]
243 pub device_locale: Annotated<String>,
244 #[metastructure(field = "device.model_id")]
245 pub device_model_id: Annotated<String>,
246 #[metastructure(field = "device.name")]
247 pub device_name: Annotated<String>,
248 #[metastructure(field = "device.online")]
249 pub device_online: Annotated<String>,
250 #[metastructure(field = "device.orientation")]
251 pub device_orientation: Annotated<String>,
252 #[metastructure(field = "device.screen_density")]
253 pub device_screen_density: Annotated<String>,
254 #[metastructure(field = "device.screen_dpi")]
255 pub device_screen_dpi: Annotated<String>,
256 #[metastructure(field = "device.screen_height_pixels")]
257 pub device_screen_height_pixels: Annotated<String>,
258 #[metastructure(field = "device.screen_width_pixels")]
259 pub device_screen_width_pixels: Annotated<String>,
260 #[metastructure(field = "device.simulator")]
261 pub device_simulator: Annotated<String>,
262 #[metastructure(field = "device.uuid")]
263 pub device_uuid: Annotated<String>,
264 #[metastructure(field = "app.device")]
265 pub app_device: Annotated<String>,
266 #[metastructure(field = "device.model")]
267 pub device_model: Annotated<String>,
268 pub runtime: Annotated<String>,
269 #[metastructure(field = "runtime.name")]
270 pub runtime_name: Annotated<String>,
271 pub browser: Annotated<String>,
272 pub os: Annotated<String>,
273 #[metastructure(field = "os.rooted")]
274 pub os_rooted: Annotated<String>,
275 #[metastructure(field = "gpu.name")]
276 pub gpu_name: Annotated<String>,
277 #[metastructure(field = "gpu.vendor")]
278 pub gpu_vendor: Annotated<String>,
279 #[metastructure(field = "monitor.id")]
280 pub monitor_id: Annotated<String>,
281 #[metastructure(field = "monitor.slug")]
282 pub monitor_slug: Annotated<String>,
283 #[metastructure(field = "request.url")]
284 pub request_url: Annotated<String>,
285 #[metastructure(field = "request.method")]
286 pub request_method: Annotated<String>,
287 #[metastructure(field = "os.name")]
289 pub os_name: Annotated<String>,
290 pub action: Annotated<String>,
291 pub category: Annotated<String>,
292 pub description: Annotated<String>,
293 pub domain: Annotated<String>,
294 pub raw_domain: Annotated<String>,
295 pub group: Annotated<String>,
296 #[metastructure(field = "http.decoded_response_content_length")]
297 pub http_decoded_response_content_length: Annotated<String>,
298 #[metastructure(field = "http.response_content_length")]
299 pub http_response_content_length: Annotated<String>,
300 #[metastructure(field = "http.response_transfer_size")]
301 pub http_response_transfer_size: Annotated<String>,
302 #[metastructure(field = "resource.render_blocking_status")]
303 pub resource_render_blocking_status: Annotated<String>,
304 pub op: Annotated<String>,
305 pub status: Annotated<String>,
306 pub status_code: Annotated<String>,
307 pub system: Annotated<String>,
308 pub ttid: Annotated<String>,
310 pub ttfd: Annotated<String>,
312 pub file_extension: Annotated<String>,
314 pub main_thread: Annotated<String>,
316 pub app_start_type: Annotated<String>,
318 pub replay_id: Annotated<String>,
319 #[metastructure(field = "cache.hit")]
320 pub cache_hit: Annotated<String>,
321 #[metastructure(field = "cache.key")]
322 pub cache_key: Annotated<String>,
323 #[metastructure(field = "trace.status")]
324 pub trace_status: Annotated<String>,
325 #[metastructure(field = "messaging.destination.name")]
326 pub messaging_destination_name: Annotated<String>,
327 #[metastructure(field = "messaging.message.id")]
328 pub messaging_message_id: Annotated<String>,
329 #[metastructure(field = "messaging.operation.name")]
330 pub messaging_operation_name: Annotated<String>,
331 #[metastructure(field = "messaging.operation.type")]
332 pub messaging_operation_type: Annotated<String>,
333 #[metastructure(field = "thread.name")]
334 pub thread_name: Annotated<String>,
335 #[metastructure(field = "thread.id")]
336 pub thread_id: Annotated<String>,
337 pub profiler_id: Annotated<String>,
338 #[metastructure(field = "user.geo.city")]
339 pub user_city: Annotated<String>,
340 #[metastructure(field = "user.geo.country_code")]
341 pub user_country_code: Annotated<String>,
342 #[metastructure(field = "user.geo.region")]
343 pub user_region: Annotated<String>,
344 #[metastructure(field = "user.geo.subdivision")]
345 pub user_subdivision: Annotated<String>,
346 #[metastructure(field = "user.geo.subregion")]
347 pub user_subregion: Annotated<String>,
348 pub name: Annotated<String>,
349 }
352
353impl Getter for SentryTags {
354 fn get_value(&self, path: &str) -> Option<Val<'_>> {
355 let value = match path {
356 "action" => &self.action,
357 "app_start_type" => &self.app_start_type,
358 "browser.name" => &self.browser_name,
359 "cache.hit" => &self.cache_hit,
360 "cache.key" => &self.cache_key,
361 "category" => &self.category,
362 "description" => &self.description,
363 "device.class" => &self.device_class,
364 "device.family" => &self.device_family,
365 "device.arch" => &self.device_arch,
366 "device.battery_level" => &self.device_battery_level,
367 "device.brand" => &self.device_brand,
368 "device.charging" => &self.device_charging,
369 "device.locale" => &self.device_locale,
370 "device.model_id" => &self.device_model_id,
371 "device.name" => &self.device_name,
372 "device.online" => &self.device_online,
373 "device.orientation" => &self.device_orientation,
374 "device.screen_density" => &self.device_screen_density,
375 "device.screen_dpi" => &self.device_screen_dpi,
376 "device.screen_height_pixels" => &self.device_screen_height_pixels,
377 "device.screen_width_pixels" => &self.device_screen_width_pixels,
378 "device.simulator" => &self.device_simulator,
379 "device.uuid" => &self.device_uuid,
380 "app.device" => &self.app_device,
381 "device.model" => &self.device_model,
382 "runtime" => &self.runtime,
383 "runtime.name" => &self.runtime_name,
384 "browser" => &self.browser,
385 "os" => &self.os,
386 "os.rooted" => &self.os_rooted,
387 "gpu.name" => &self.gpu_name,
388 "gpu.vendor" => &self.gpu_vendor,
389 "monitor.id" => &self.monitor_id,
390 "monitor.slug" => &self.monitor_slug,
391 "request.url" => &self.request_url,
392 "request.method" => &self.request_method,
393 "domain" => &self.domain,
394 "environment" => &self.environment,
395 "file_extension" => &self.file_extension,
396 "group" => &self.group,
397 "http.decoded_response_content_length" => &self.http_decoded_response_content_length,
398 "http.response_content_length" => &self.http_response_content_length,
399 "http.response_transfer_size" => &self.http_response_transfer_size,
400 "main_thread" => &self.main_thread,
401 "messaging.destination.name" => &self.messaging_destination_name,
402 "messaging.message.id" => &self.messaging_message_id,
403 "messaging.operation.name" => &self.messaging_operation_name,
404 "messaging.operation.type" => &self.messaging_operation_type,
405 "mobile" => &self.mobile,
406 "name" => &self.name,
407 "op" => &self.op,
408 "os.name" => &self.os_name,
409 "platform" => &self.platform,
410 "profiler_id" => &self.profiler_id,
411 "raw_domain" => &self.raw_domain,
412 "release" => &self.release,
413 "replay_id" => &self.replay_id,
414 "resource.render_blocking_status" => &self.resource_render_blocking_status,
415 "sdk.name" => &self.sdk_name,
416 "sdk.version" => &self.sdk_version,
417 "status_code" => &self.status_code,
418 "status" => &self.status,
419 "system" => &self.system,
420 "thread.id" => &self.thread_id,
421 "thread.name" => &self.thread_name,
422 "trace.status" => &self.trace_status,
423 "transaction.method" => &self.transaction_method,
424 "transaction.op" => &self.transaction_op,
425 "transaction" => &self.transaction,
426 "ttfd" => &self.ttfd,
427 "ttid" => &self.ttid,
428 "user.email" => &self.user_email,
429 "user.geo.city" => &self.user_city,
430 "user.geo.country_code" => &self.user_country_code,
431 "user.geo.region" => &self.user_region,
432 "user.geo.subdivision" => &self.user_subdivision,
433 "user.geo.subregion" => &self.user_subregion,
434 "user.id" => &self.user_id,
435 "user.ip" => &self.user_ip,
436 "user.username" => &self.user_username,
437 "user" => &self.user,
438 _ => return None,
439 };
440 Some(value.as_str()?.into())
441 }
442}
443
444fn span_data_pii_from_conventions(state: &ProcessingState) -> Pii {
449 fn inner(state: &ProcessingState) -> Option<Pii> {
450 let key = state.keys().next()?;
453
454 match relay_conventions::attribute_info(key)?.pii {
455 relay_conventions::Pii::True => Some(Pii::True),
456 relay_conventions::Pii::False => Some(Pii::False),
457 relay_conventions::Pii::Maybe => Some(Pii::Maybe),
458 }
459 }
460
461 inner(state).unwrap_or(Pii::True)
462}
463
464#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
469#[metastructure(trim = false, pii = "span_data_pii_from_conventions")]
470pub struct SpanData {
471 #[metastructure(field = "app_start_type")] pub app_start_type: Annotated<Value>,
476
477 #[metastructure(field = "gen_ai.pipeline.name", legacy_alias = "ai.pipeline.name")]
479 pub gen_ai_pipeline_name: Annotated<Value>,
480
481 #[metastructure(
483 field = "gen_ai.usage.total_tokens",
484 legacy_alias = "ai.total_tokens.used"
485 )]
486 pub gen_ai_usage_total_tokens: Annotated<Value>,
487
488 #[metastructure(
490 field = "gen_ai.usage.input_tokens",
491 legacy_alias = "ai.prompt_tokens.used",
492 legacy_alias = "gen_ai.usage.prompt_tokens"
493 )]
494 pub gen_ai_usage_input_tokens: Annotated<Value>,
495
496 #[metastructure(field = "gen_ai.usage.input_tokens.cached")]
499 pub gen_ai_usage_input_tokens_cached: Annotated<Value>,
500
501 #[metastructure(field = "gen_ai.usage.input_tokens.cache_write")]
503 pub gen_ai_usage_input_tokens_cache_write: Annotated<Value>,
504
505 #[metastructure(
507 field = "gen_ai.usage.output_tokens",
508 legacy_alias = "ai.completion_tokens.used",
509 legacy_alias = "gen_ai.usage.completion_tokens"
510 )]
511 pub gen_ai_usage_output_tokens: Annotated<Value>,
512
513 #[metastructure(field = "gen_ai.usage.output_tokens.reasoning")]
516 pub gen_ai_usage_output_tokens_reasoning: Annotated<Value>,
517
518 #[metastructure(field = "gen_ai.response.model")]
520 pub gen_ai_response_model: Annotated<Value>,
521
522 #[metastructure(field = "gen_ai.request.model", legacy_alias = "ai.model_id")]
524 pub gen_ai_request_model: Annotated<Value>,
525
526 #[metastructure(field = "gen_ai.context.window_size")]
528 pub gen_ai_context_window_size: Annotated<Value>,
529
530 #[metastructure(field = "gen_ai.context.utilization")]
532 pub gen_ai_context_utilization: Annotated<Value>,
533
534 #[metastructure(field = "gen_ai.cost.total_tokens")]
536 pub gen_ai_cost_total_tokens: Annotated<Value>,
537
538 #[metastructure(field = "gen_ai.cost.input_tokens")]
540 pub gen_ai_cost_input_tokens: Annotated<Value>,
541
542 #[metastructure(field = "gen_ai.cost.output_tokens")]
544 pub gen_ai_cost_output_tokens: Annotated<Value>,
545
546 #[metastructure(
548 field = "gen_ai.input.messages",
549 legacy_alias = "gen_ai.prompt",
550 legacy_alias = "gen_ai.request.messages",
551 legacy_alias = "ai.prompt.messages"
552 )]
553 pub gen_ai_input_messages: Annotated<Value>,
554
555 #[metastructure(
557 field = "gen_ai.tool.call.arguments",
558 legacy_alias = "gen_ai.tool.input",
559 legacy_alias = "ai.toolCall.args"
560 )]
561 pub gen_ai_tool_call_arguments: Annotated<Value>,
562
563 #[metastructure(
565 field = "gen_ai.tool.call.result",
566 legacy_alias = "gen_ai.tool.output",
567 legacy_alias = "ai.toolCall.result"
568 )]
569 pub gen_ai_tool_call_result: Annotated<Value>,
570
571 #[metastructure(
573 field = "gen_ai.output.messages",
574 legacy_alias = "gen_ai.response.tool_calls",
575 legacy_alias = "ai.response.toolCalls",
576 legacy_alias = "ai.tool_calls",
577 legacy_alias = "gen_ai.response.text",
578 legacy_alias = "ai.response.text",
579 legacy_alias = "ai.responses"
580 )]
581 pub gen_ai_output_messages: Annotated<Value>,
582
583 #[metastructure(field = "gen_ai.response.streaming", legacy_alias = "ai.streaming")]
585 pub gen_ai_response_streaming: Annotated<Value>,
586
587 #[metastructure(field = "gen_ai.response.tokens_per_second")]
589 pub gen_ai_response_tokens_per_second: Annotated<Value>,
590
591 #[metastructure(
593 field = "gen_ai.tool.definitions",
594 legacy_alias = "gen_ai.request.available_tools",
595 legacy_alias = "ai.tools"
596 )]
597 pub gen_ai_tool_definitions: Annotated<Value>,
598
599 #[metastructure(
601 field = "gen_ai.request.frequency_penalty",
602 legacy_alias = "ai.frequency_penalty"
603 )]
604 pub gen_ai_request_frequency_penalty: Annotated<Value>,
605
606 #[metastructure(
608 field = "gen_ai.request.presence_penalty",
609 legacy_alias = "ai.presence_penalty"
610 )]
611 pub gen_ai_request_presence_penalty: Annotated<Value>,
612
613 #[metastructure(field = "gen_ai.request.seed", legacy_alias = "ai.seed")]
615 pub gen_ai_request_seed: Annotated<Value>,
616
617 #[metastructure(field = "gen_ai.request.temperature", legacy_alias = "ai.temperature")]
619 pub gen_ai_request_temperature: Annotated<Value>,
620
621 #[metastructure(field = "gen_ai.request.top_k", legacy_alias = "ai.top_k")]
623 pub gen_ai_request_top_k: Annotated<Value>,
624
625 #[metastructure(field = "gen_ai.request.top_p", legacy_alias = "ai.top_p")]
627 pub gen_ai_request_top_p: Annotated<Value>,
628
629 #[metastructure(
631 field = "gen_ai.response.finish_reasons",
632 legacy_alias = "gen_ai.response.finish_reason",
633 legacy_alias = "ai.finish_reason"
634 )]
635 pub gen_ai_response_finish_reasons: Annotated<Value>,
636
637 #[metastructure(field = "gen_ai.response.id", legacy_alias = "ai.generation_id")]
639 pub gen_ai_response_id: Annotated<Value>,
640
641 #[metastructure(
643 field = "gen_ai.provider.name",
644 legacy_alias = "gen_ai.system",
645 legacy_alias = "ai.model.provider"
646 )]
647 pub gen_ai_provider_name: Annotated<Value>,
648
649 #[metastructure(
651 field = "gen_ai.system_instructions",
652 legacy_alias = "gen_ai.system.message"
653 )]
654 pub gen_ai_system_instructions: Annotated<Value>,
655
656 #[metastructure(field = "gen_ai.tool.name", legacy_alias = "ai.function_call")]
658 pub gen_ai_tool_name: Annotated<Value>,
659
660 #[metastructure(field = "gen_ai.operation.name")]
662 pub gen_ai_operation_name: Annotated<String>,
663
664 #[metastructure(field = "gen_ai.operation.type")]
666 pub gen_ai_operation_type: Annotated<String>,
667
668 #[metastructure(field = "gen_ai.agent.name")]
670 pub gen_ai_agent_name: Annotated<String>,
671
672 #[metastructure(field = "gen_ai.function_id")]
674 pub gen_ai_function_id: Annotated<String>,
675
676 #[metastructure(field = "browser.name")]
678 pub browser_name: Annotated<String>,
679
680 #[metastructure(field = "db.operation")]
685 pub db_operation: Annotated<Value>,
686
687 #[metastructure(field = "db.system")]
691 pub db_system: Annotated<Value>,
692
693 #[metastructure(
697 field = "db.collection.name",
698 legacy_alias = "db.cassandra.table",
699 legacy_alias = "db.cosmosdb.container",
700 legacy_alias = "db.mongodb.collection",
701 legacy_alias = "db.sql.table"
702 )]
703 pub db_collection_name: Annotated<Value>,
704
705 #[metastructure(field = "sentry.environment", legacy_alias = "environment")]
707 pub environment: Annotated<String>,
708
709 #[metastructure(field = "sentry.release", legacy_alias = "release")]
711 pub release: Annotated<LenientString>,
712
713 #[metastructure(field = "http.decoded_response_content_length")]
715 pub http_decoded_response_content_length: Annotated<Value>,
716
717 #[metastructure(
719 field = "http.request_method",
720 legacy_alias = "http.method",
721 legacy_alias = "method"
722 )]
723 pub http_request_method: Annotated<Value>,
724
725 #[metastructure(field = "http.response_content_length")]
727 pub http_response_content_length: Annotated<Value>,
728
729 #[metastructure(field = "http.response_transfer_size")]
731 pub http_response_transfer_size: Annotated<Value>,
732
733 #[metastructure(field = "resource.render_blocking_status")]
735 pub resource_render_blocking_status: Annotated<Value>,
736
737 #[metastructure(field = "server.address")]
739 pub server_address: Annotated<Value>,
740
741 #[metastructure(field = "cache.hit")]
743 pub cache_hit: Annotated<Value>,
744
745 #[metastructure(field = "cache.key")]
747 pub cache_key: Annotated<Value>,
748
749 #[metastructure(field = "cache.item_size")]
751 pub cache_item_size: Annotated<Value>,
752
753 #[metastructure(field = "http.response.status_code", legacy_alias = "status_code")]
755 pub http_response_status_code: Annotated<Value>,
756
757 #[metastructure(field = "thread.name")]
759 pub thread_name: Annotated<String>,
760
761 #[metastructure(field = "thread.id")]
763 pub thread_id: Annotated<ThreadId>,
764
765 #[metastructure(field = "sentry.segment.name", legacy_alias = "transaction")]
771 pub segment_name: Annotated<String>,
772
773 #[metastructure(field = "ui.component_name")]
775 pub ui_component_name: Annotated<Value>,
776
777 #[metastructure(field = "url.scheme")]
779 pub url_scheme: Annotated<Value>,
780
781 #[metastructure(field = "user")]
783 pub user: Annotated<Value>,
784
785 #[metastructure(field = "user.geo.country_code")]
789 pub user_geo_country_code: Annotated<String>,
790
791 #[metastructure(field = "user.geo.city")]
795 pub user_geo_city: Annotated<String>,
796
797 #[metastructure(field = "user.geo.subdivision")]
801 pub user_geo_subdivision: Annotated<String>,
802
803 #[metastructure(field = "user.geo.region")]
807 pub user_geo_region: Annotated<String>,
808
809 #[metastructure(field = "sentry.exclusive_time")]
811 pub exclusive_time: Annotated<Value>,
812
813 #[metastructure(
815 field = "profile_id",
816 pii = "false"
819 )]
820 pub profile_id: Annotated<Value>,
821
822 #[metastructure(field = "sentry.replay_id", legacy_alias = "replay_id")]
824 pub replay_id: Annotated<Value>,
825
826 #[metastructure(field = "sentry.sdk.name")]
828 pub sdk_name: Annotated<String>,
829
830 #[metastructure(field = "sentry.sdk.version")]
832 pub sdk_version: Annotated<String>,
833
834 #[metastructure(field = "sentry.frames.slow", legacy_alias = "frames.slow")]
836 pub frames_slow: Annotated<Value>,
837
838 #[metastructure(field = "sentry.frames.frozen", legacy_alias = "frames.frozen")]
840 pub frames_frozen: Annotated<Value>,
841
842 #[metastructure(field = "sentry.frames.total", legacy_alias = "frames.total")]
844 pub frames_total: Annotated<Value>,
845
846 #[metastructure(field = "frames.delay")]
848 pub frames_delay: Annotated<Value>,
849
850 #[metastructure(field = "messaging.destination.name")]
852 pub messaging_destination_name: Annotated<String>,
853
854 #[metastructure(field = "messaging.message.retry.count")]
856 pub messaging_message_retry_count: Annotated<Value>,
857
858 #[metastructure(field = "messaging.message.receive.latency")]
860 pub messaging_message_receive_latency: Annotated<Value>,
861
862 #[metastructure(field = "messaging.message.body.size")]
864 pub messaging_message_body_size: Annotated<Value>,
865
866 #[metastructure(field = "messaging.message.id")]
868 pub messaging_message_id: Annotated<String>,
869
870 #[metastructure(field = "messaging.operation.name")]
872 pub messaging_operation_name: Annotated<String>,
873
874 #[metastructure(field = "messaging.operation.type")]
876 pub messaging_operation_type: Annotated<String>,
877
878 #[metastructure(field = "user_agent.original")]
880 pub user_agent_original: Annotated<String>,
881
882 #[metastructure(field = "url.full")]
884 pub url_full: Annotated<String>,
885
886 #[metastructure(field = "url.query")]
888 pub url_query: Annotated<String>,
889
890 #[metastructure(field = "http.query")]
892 pub http_query: Annotated<String>,
893
894 #[metastructure(field = "client.address")]
896 pub client_address: Annotated<IpAddr>,
897
898 #[metastructure(skip_serialization = "empty")]
902 pub route: Annotated<Route>,
903
904 #[metastructure(field = "previousRoute", skip_serialization = "empty")]
908 pub previous_route: Annotated<Route>,
909
910 #[metastructure(field = "lcp.element")]
912 pub lcp_element: Annotated<String>,
913
914 #[metastructure(field = "lcp.size")]
916 pub lcp_size: Annotated<u64>,
917
918 #[metastructure(field = "lcp.id")]
920 pub lcp_id: Annotated<String>,
921
922 #[metastructure(field = "lcp.url")]
924 pub lcp_url: Annotated<String>,
925
926 #[metastructure(field = "sentry.dsc.trace_id")]
928 pub sentry_dsc_trace_id: Annotated<String>,
929
930 #[metastructure(field = "sentry.dsc.transaction")]
932 pub sentry_dsc_transaction: Annotated<String>,
933
934 #[metastructure(field = "sentry.dsc.project_id")]
936 pub sentry_dsc_project_id: Annotated<String>,
937
938 #[metastructure(field = "sentry.name")]
941 pub span_name: Annotated<String>,
942
943 #[metastructure(
945 additional_properties,
946 retain = true,
947 skip_serialization = "null" )]
949 pub other: Object<Value>,
950}
951
952impl Getter for SpanData {
953 fn get_value(&self, path: &str) -> Option<Val<'_>> {
954 Some(match path {
955 "app_start_type" => self.app_start_type.value()?.into(),
956 "browser\\.name" => self.browser_name.as_str()?.into(),
957 "db.operation" => self.db_operation.value()?.into(),
958 "db\\.system" => self.db_system.value()?.into(),
959 "environment" => self.environment.as_str()?.into(),
960 "gen_ai\\.usage\\.total_tokens" => self.gen_ai_usage_total_tokens.value()?.into(),
961 "gen_ai\\.cost\\.total_tokens" => self.gen_ai_cost_total_tokens.value()?.into(),
962 "gen_ai\\.cost\\.input_tokens" => self.gen_ai_cost_input_tokens.value()?.into(),
963 "gen_ai\\.cost\\.output_tokens" => self.gen_ai_cost_output_tokens.value()?.into(),
964 "gen_ai\\.input\\.messages" => self.gen_ai_input_messages.value()?.into(),
965 "gen_ai\\.output\\.messages" => self.gen_ai_output_messages.value()?.into(),
966 "gen_ai\\.operation\\.name" => self.gen_ai_operation_name.as_str()?.into(),
967 "gen_ai\\.agent\\.name" => self.gen_ai_agent_name.as_str()?.into(),
968 "gen_ai\\.request\\.model" => self.gen_ai_request_model.value()?.into(),
969 "http\\.decoded_response_content_length" => {
970 self.http_decoded_response_content_length.value()?.into()
971 }
972 "http\\.request_method" | "http\\.method" | "method" => {
973 self.http_request_method.value()?.into()
974 }
975 "http\\.response_content_length" => self.http_response_content_length.value()?.into(),
976 "http\\.response_transfer_size" => self.http_response_transfer_size.value()?.into(),
977 "http\\.response.status_code" | "status_code" => {
978 self.http_response_status_code.value()?.into()
979 }
980 "resource\\.render_blocking_status" => {
981 self.resource_render_blocking_status.value()?.into()
982 }
983 "server\\.address" => self.server_address.value()?.into(),
984 "thread\\.name" => self.thread_name.as_str()?.into(),
985 "ui\\.component_name" => self.ui_component_name.value()?.into(),
986 "url\\.scheme" => self.url_scheme.value()?.into(),
987 "url\\.query" => self.url_query.as_str()?.into(),
988 "http\\.query" => self.http_query.as_str()?.into(),
989 "user" => self.user.value()?.into(),
990 "user\\.geo\\.city" => self.user_geo_city.as_str()?.into(),
991 "user\\.geo\\.country_code" => self.user_geo_country_code.as_str()?.into(),
992 "user\\.geo\\.region" => self.user_geo_region.as_str()?.into(),
993 "user\\.geo\\.subdivision" => self.user_geo_subdivision.as_str()?.into(),
994 "transaction" => self.segment_name.as_str()?.into(),
995 "release" => self.release.as_str()?.into(),
996 _ => {
997 let escaped = path.replace("\\.", "\0");
998 let mut path = escaped.split('.').map(|s| s.replace('\0', "."));
999 let root = path.next()?;
1000
1001 let mut val = self.other.get(&root)?.value()?;
1002 for part in path {
1003 let relay_protocol::Value::Object(map) = val else {
1005 return None;
1006 };
1007 val = map.get(&part)?.value()?;
1008 }
1009 val.into()
1010 }
1011 })
1012 }
1013}
1014
1015#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
1017#[metastructure(trim = false)]
1018pub struct SpanLink {
1019 #[metastructure(required = true, trim = false)]
1021 pub trace_id: Annotated<TraceId>,
1022
1023 #[metastructure(required = true, trim = false)]
1025 pub span_id: Annotated<SpanId>,
1026
1027 #[metastructure(trim = false)]
1029 pub sampled: Annotated<bool>,
1030
1031 #[metastructure(pii = "maybe", trim = false)]
1033 pub attributes: Annotated<Object<Value>>,
1034
1035 #[metastructure(additional_properties, retain = true, pii = "maybe", trim = false)]
1037 pub other: Object<Value>,
1038}
1039
1040#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
1042pub struct Route {
1043 #[metastructure(pii = "maybe", skip_serialization = "empty")]
1045 pub name: Annotated<String>,
1046
1047 #[metastructure(
1049 pii = "true",
1050 skip_serialization = "empty",
1051 max_depth = 5,
1052 max_bytes = 2048
1053 )]
1054 pub params: Annotated<Object<Value>>,
1055
1056 #[metastructure(
1058 additional_properties,
1059 retain = true,
1060 pii = "maybe",
1061 skip_serialization = "empty"
1062 )]
1063 pub other: Object<Value>,
1064}
1065
1066impl FromValue for Route {
1067 fn from_value(value: Annotated<Value>) -> Annotated<Self>
1068 where
1069 Self: Sized,
1070 {
1071 match value {
1072 Annotated(Some(Value::String(name)), meta) => Annotated(
1073 Some(Route {
1074 name: Annotated::new(name),
1075 ..Default::default()
1076 }),
1077 meta,
1078 ),
1079 Annotated(Some(Value::Object(mut values)), meta) => {
1080 let mut route: Route = Default::default();
1081 if let Some(Annotated(Some(Value::String(name)), _)) = values.remove("name") {
1082 route.name = Annotated::new(name);
1083 }
1084 if let Some(Annotated(Some(Value::Object(params)), _)) = values.remove("params") {
1085 route.params = Annotated::new(params);
1086 }
1087
1088 if !values.is_empty() {
1089 route.other = values;
1090 }
1091
1092 Annotated(Some(route), meta)
1093 }
1094 Annotated(None, meta) => Annotated(None, meta),
1095 Annotated(Some(value), mut meta) => {
1096 meta.add_error(Error::expected("route expected to be an object"));
1097 meta.set_original_value(Some(value));
1098 Annotated(None, meta)
1099 }
1100 }
1101 }
1102}
1103
1104#[derive(Clone, Debug, PartialEq, ProcessValue, Default)]
1109pub enum SpanKind {
1110 #[default]
1112 Internal,
1113 Server,
1115 Client,
1117 Producer,
1119 Consumer,
1121 Unknown(String),
1123}
1124
1125impl SpanKind {
1126 pub fn as_str(&self) -> &str {
1127 match self {
1128 Self::Internal => "internal",
1129 Self::Server => "server",
1130 Self::Client => "client",
1131 Self::Producer => "producer",
1132 Self::Consumer => "consumer",
1133 Self::Unknown(s) => s.as_str(),
1134 }
1135 }
1136}
1137
1138impl Empty for SpanKind {
1139 fn is_empty(&self) -> bool {
1140 false
1141 }
1142}
1143
1144#[derive(Debug)]
1145pub struct ParseSpanKindError;
1146
1147impl std::str::FromStr for SpanKind {
1148 type Err = ParseSpanKindError;
1149
1150 fn from_str(s: &str) -> Result<Self, Self::Err> {
1151 Ok(match s {
1152 "internal" => SpanKind::Internal,
1153 "server" => SpanKind::Server,
1154 "client" => SpanKind::Client,
1155 "producer" => SpanKind::Producer,
1156 "consumer" => SpanKind::Consumer,
1157 other => SpanKind::Unknown(other.to_owned()),
1158 })
1159 }
1160}
1161
1162impl fmt::Display for SpanKind {
1163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1164 write!(f, "{}", self.as_str())
1165 }
1166}
1167
1168impl FromValue for SpanKind {
1169 fn from_value(value: Annotated<Value>) -> Annotated<Self>
1170 where
1171 Self: Sized,
1172 {
1173 match value {
1174 Annotated(Some(Value::String(s)), meta) => Annotated(SpanKind::from_str(&s).ok(), meta),
1175 Annotated(_, meta) => Annotated(None, meta),
1176 }
1177 }
1178}
1179
1180impl IntoValue for SpanKind {
1181 fn into_value(self) -> Value
1182 where
1183 Self: Sized,
1184 {
1185 Value::String(self.to_string())
1186 }
1187
1188 fn serialize_payload<S>(
1189 &self,
1190 s: S,
1191 _behavior: relay_protocol::SkipSerialization,
1192 ) -> Result<S::Ok, S::Error>
1193 where
1194 Self: Sized,
1195 S: serde::Serializer,
1196 {
1197 s.serialize_str(self.as_str())
1198 }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use crate::protocol::Measurement;
1204 use chrono::{TimeZone, Utc};
1205 use relay_base_schema::metrics::{InformationUnit, MetricUnit};
1206 use relay_conventions::attributes::*;
1207 use relay_protocol::RuleCondition;
1208 use similar_asserts::assert_eq;
1209
1210 use super::*;
1211
1212 #[test]
1218 fn test_span_data_attributes_follow_sentry_conventions() {
1219 let my_trace = &"my_trace".to_owned();
1220 let my_transaction = &"my_transaction".to_owned();
1221 let my_project_id = &"my_project_id".to_owned();
1222 let json = format!(
1223 r#"{{
1224 "{SENTRY__DSC__TRACE_ID}": "{my_trace}",
1225 "{SENTRY__DSC__TRANSACTION}": "{my_transaction}",
1226 "{SENTRY__DSC__PROJECT_ID}": "{my_project_id}"
1227 }}"#,
1228 );
1229 let data = Annotated::<SpanData>::from_json(&json).unwrap();
1230 let data = data.value().unwrap();
1231 assert_eq!(data.sentry_dsc_trace_id.value(), Some(my_trace));
1232 assert_eq!(data.sentry_dsc_transaction.value(), Some(my_transaction));
1233 assert_eq!(data.sentry_dsc_project_id.value(), Some(my_project_id));
1234 assert!(data.other.is_empty());
1235 }
1236
1237 #[test]
1238 fn test_span_serialization() {
1239 let json = r#"{
1240 "timestamp": 0.0,
1241 "start_timestamp": -63158400.0,
1242 "exclusive_time": 1.23,
1243 "op": "operation",
1244 "span_id": "fa90fdead5f74052",
1245 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1246 "status": "ok",
1247 "description": "desc",
1248 "origin": "auto.http",
1249 "links": [
1250 {
1251 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1252 "span_id": "fa90fdead5f74052",
1253 "sampled": true,
1254 "attributes": {
1255 "boolAttr": true,
1256 "numAttr": 123,
1257 "stringAttr": "foo"
1258 }
1259 }
1260 ],
1261 "measurements": {
1262 "memory": {
1263 "value": 9001.0,
1264 "unit": "byte"
1265 }
1266 },
1267 "kind": "server"
1268}"#;
1269 let mut measurements = Object::new();
1270 measurements.insert(
1271 "memory".into(),
1272 Annotated::new(Measurement {
1273 value: Annotated::new(9001.0.try_into().unwrap()),
1274 unit: Annotated::new(MetricUnit::Information(InformationUnit::Byte)),
1275 }),
1276 );
1277
1278 let links = Annotated::new(vec![Annotated::new(SpanLink {
1279 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1280 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
1281 sampled: Annotated::new(true),
1282 attributes: Annotated::new({
1283 let mut map: std::collections::BTreeMap<String, Annotated<Value>> = Object::new();
1284 map.insert(
1285 "stringAttr".into(),
1286 Annotated::new(Value::String("foo".into())),
1287 );
1288 map.insert("numAttr".into(), Annotated::new(Value::I64(123)));
1289 map.insert("boolAttr".into(), Value::Bool(true).into());
1290 map
1291 }),
1292 ..Default::default()
1293 })]);
1294
1295 let span = Annotated::new(Span {
1296 timestamp: Annotated::new(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap().into()),
1297 start_timestamp: Annotated::new(
1298 Utc.with_ymd_and_hms(1968, 1, 1, 0, 0, 0).unwrap().into(),
1299 ),
1300 exclusive_time: Annotated::new(1.23),
1301 description: Annotated::new("desc".to_owned()),
1302 op: Annotated::new("operation".to_owned()),
1303 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1304 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
1305 status: Annotated::new(SpanStatus::Ok),
1306 origin: Annotated::new("auto.http".to_owned()),
1307 kind: Annotated::new(SpanKind::Server),
1308 measurements: Annotated::new(Measurements(measurements)),
1309 links,
1310 ..Default::default()
1311 });
1312 assert_eq!(json, span.to_json_pretty().unwrap());
1313
1314 let span_from_string = Annotated::from_json(json).unwrap();
1315 assert_eq!(span, span_from_string);
1316 }
1317
1318 #[test]
1319 fn test_getter_span_data() {
1320 let span = Annotated::<Span>::from_json(
1321 r#"{
1322 "data": {
1323 "foo": {"bar": 1},
1324 "foo.bar": 2
1325 },
1326 "measurements": {
1327 "some": {"value": 100.0}
1328 }
1329 }"#,
1330 )
1331 .unwrap()
1332 .into_value()
1333 .unwrap();
1334
1335 assert_eq!(span.get_value("span.data.foo.bar"), Some(Val::I64(1)));
1336 assert_eq!(span.get_value(r"span.data.foo\.bar"), Some(Val::I64(2)));
1337
1338 assert_eq!(span.get_value("span.data"), None);
1339 assert_eq!(span.get_value("span.data."), None);
1340 assert_eq!(span.get_value("span.data.x"), None);
1341
1342 assert_eq!(
1343 span.get_value("span.measurements.some.value"),
1344 Some(Val::F64(100.0))
1345 );
1346 }
1347
1348 #[test]
1349 fn test_getter_was_transaction() {
1350 let mut span = Span::default();
1351 assert_eq!(
1352 span.get_value("span.was_transaction"),
1353 Some(Val::Bool(false))
1354 );
1355 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1356 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1357
1358 span.was_transaction.set_value(Some(false));
1359 assert_eq!(
1360 span.get_value("span.was_transaction"),
1361 Some(Val::Bool(false))
1362 );
1363 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1364 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1365
1366 span.was_transaction.set_value(Some(true));
1367 assert_eq!(
1368 span.get_value("span.was_transaction"),
1369 Some(Val::Bool(true))
1370 );
1371 assert!(RuleCondition::eq("span.was_transaction", true).matches(&span));
1372 assert!(!RuleCondition::eq("span.was_transaction", false).matches(&span));
1373 }
1374
1375 #[test]
1376 fn test_span_fields_as_event() {
1377 let span = Annotated::<Span>::from_json(
1378 r#"{
1379 "data": {
1380 "release": "1.0",
1381 "environment": "prod",
1382 "sentry.segment.name": "/api/endpoint"
1383 }
1384 }"#,
1385 )
1386 .unwrap()
1387 .into_value()
1388 .unwrap();
1389
1390 assert_eq!(span.get_value("event.release"), Some(Val::String("1.0")));
1391 assert_eq!(
1392 span.get_value("event.environment"),
1393 Some(Val::String("prod"))
1394 );
1395 assert_eq!(
1396 span.get_value("event.transaction"),
1397 Some(Val::String("/api/endpoint"))
1398 );
1399 }
1400
1401 #[test]
1402 fn test_span_duration() {
1403 let span = Annotated::<Span>::from_json(
1404 r#"{
1405 "start_timestamp": 1694732407.8367,
1406 "timestamp": 1694732408.31451233
1407 }"#,
1408 )
1409 .unwrap()
1410 .into_value()
1411 .unwrap();
1412
1413 assert_eq!(span.get_value("span.duration"), Some(Val::F64(477.812)));
1414 }
1415
1416 #[test]
1417 fn test_span_data() {
1418 let data = r#"{
1419 "foo": 2,
1420 "bar": "3",
1421 "db.system": "mysql",
1422 "code.filepath": "task.py",
1423 "code.lineno": 123,
1424 "code.function": "fn()",
1425 "code.namespace": "ns",
1426 "frames.slow": 1,
1427 "frames.frozen": 2,
1428 "frames.total": 9,
1429 "frames.delay": 100,
1430 "messaging.destination.name": "default",
1431 "messaging.message.retry.count": 3,
1432 "messaging.message.receive.latency": 40,
1433 "messaging.message.body.size": 100,
1434 "messaging.message.id": "abc123",
1435 "messaging.operation.name": "publish",
1436 "messaging.operation.type": "create",
1437 "user_agent.original": "Chrome",
1438 "url.full": "my_url.com",
1439 "client.address": "192.168.0.1"
1440 }"#;
1441 let data = Annotated::<SpanData>::from_json(data)
1442 .unwrap()
1443 .into_value()
1444 .unwrap();
1445 insta::assert_debug_snapshot!(data, @r###"
1446 SpanData {
1447 app_start_type: ~,
1448 gen_ai_pipeline_name: ~,
1449 gen_ai_usage_total_tokens: ~,
1450 gen_ai_usage_input_tokens: ~,
1451 gen_ai_usage_input_tokens_cached: ~,
1452 gen_ai_usage_input_tokens_cache_write: ~,
1453 gen_ai_usage_output_tokens: ~,
1454 gen_ai_usage_output_tokens_reasoning: ~,
1455 gen_ai_response_model: ~,
1456 gen_ai_request_model: ~,
1457 gen_ai_context_window_size: ~,
1458 gen_ai_context_utilization: ~,
1459 gen_ai_cost_total_tokens: ~,
1460 gen_ai_cost_input_tokens: ~,
1461 gen_ai_cost_output_tokens: ~,
1462 gen_ai_input_messages: ~,
1463 gen_ai_tool_call_arguments: ~,
1464 gen_ai_tool_call_result: ~,
1465 gen_ai_output_messages: ~,
1466 gen_ai_response_streaming: ~,
1467 gen_ai_response_tokens_per_second: ~,
1468 gen_ai_tool_definitions: ~,
1469 gen_ai_request_frequency_penalty: ~,
1470 gen_ai_request_presence_penalty: ~,
1471 gen_ai_request_seed: ~,
1472 gen_ai_request_temperature: ~,
1473 gen_ai_request_top_k: ~,
1474 gen_ai_request_top_p: ~,
1475 gen_ai_response_finish_reasons: ~,
1476 gen_ai_response_id: ~,
1477 gen_ai_provider_name: ~,
1478 gen_ai_system_instructions: ~,
1479 gen_ai_tool_name: ~,
1480 gen_ai_operation_name: ~,
1481 gen_ai_operation_type: ~,
1482 gen_ai_agent_name: ~,
1483 gen_ai_function_id: ~,
1484 browser_name: ~,
1485 db_operation: ~,
1486 db_system: String(
1487 "mysql",
1488 ),
1489 db_collection_name: ~,
1490 environment: ~,
1491 release: ~,
1492 http_decoded_response_content_length: ~,
1493 http_request_method: ~,
1494 http_response_content_length: ~,
1495 http_response_transfer_size: ~,
1496 resource_render_blocking_status: ~,
1497 server_address: ~,
1498 cache_hit: ~,
1499 cache_key: ~,
1500 cache_item_size: ~,
1501 http_response_status_code: ~,
1502 thread_name: ~,
1503 thread_id: ~,
1504 segment_name: ~,
1505 ui_component_name: ~,
1506 url_scheme: ~,
1507 user: ~,
1508 user_geo_country_code: ~,
1509 user_geo_city: ~,
1510 user_geo_subdivision: ~,
1511 user_geo_region: ~,
1512 exclusive_time: ~,
1513 profile_id: ~,
1514 replay_id: ~,
1515 sdk_name: ~,
1516 sdk_version: ~,
1517 frames_slow: I64(
1518 1,
1519 ),
1520 frames_frozen: I64(
1521 2,
1522 ),
1523 frames_total: I64(
1524 9,
1525 ),
1526 frames_delay: I64(
1527 100,
1528 ),
1529 messaging_destination_name: "default",
1530 messaging_message_retry_count: I64(
1531 3,
1532 ),
1533 messaging_message_receive_latency: I64(
1534 40,
1535 ),
1536 messaging_message_body_size: I64(
1537 100,
1538 ),
1539 messaging_message_id: "abc123",
1540 messaging_operation_name: "publish",
1541 messaging_operation_type: "create",
1542 user_agent_original: "Chrome",
1543 url_full: "my_url.com",
1544 url_query: ~,
1545 http_query: ~,
1546 client_address: IpAddr(
1547 "192.168.0.1",
1548 ),
1549 route: ~,
1550 previous_route: ~,
1551 lcp_element: ~,
1552 lcp_size: ~,
1553 lcp_id: ~,
1554 lcp_url: ~,
1555 sentry_dsc_trace_id: ~,
1556 sentry_dsc_transaction: ~,
1557 sentry_dsc_project_id: ~,
1558 span_name: ~,
1559 other: {
1560 "bar": String(
1561 "3",
1562 ),
1563 "code.filepath": String(
1564 "task.py",
1565 ),
1566 "code.function": String(
1567 "fn()",
1568 ),
1569 "code.lineno": I64(
1570 123,
1571 ),
1572 "code.namespace": String(
1573 "ns",
1574 ),
1575 "foo": I64(
1576 2,
1577 ),
1578 },
1579 }
1580 "###);
1581
1582 assert_eq!(data.get_value("foo"), Some(Val::U64(2)));
1583 assert_eq!(data.get_value("bar"), Some(Val::String("3")));
1584 assert_eq!(data.get_value("db\\.system"), Some(Val::String("mysql")));
1585 assert_eq!(data.get_value("code\\.lineno"), Some(Val::U64(123)));
1586 assert_eq!(data.get_value("code\\.function"), Some(Val::String("fn()")));
1587 assert_eq!(data.get_value("code\\.namespace"), Some(Val::String("ns")));
1588 assert_eq!(data.get_value("unknown"), None);
1589 }
1590
1591 #[test]
1592 fn test_span_data_empty_well_known_field() {
1593 let span = r#"{
1594 "data": {
1595 "lcp.url": ""
1596 }
1597 }"#;
1598 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1599 assert_eq!(span.to_json().unwrap(), r#"{"data":{"lcp.url":""}}"#);
1600 }
1601
1602 #[test]
1603 fn test_span_data_empty_custom_field() {
1604 let span = r#"{
1605 "data": {
1606 "custom_field_empty": ""
1607 }
1608 }"#;
1609 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1610 assert_eq!(
1611 span.to_json().unwrap(),
1612 r#"{"data":{"custom_field_empty":""}}"#
1613 );
1614 }
1615
1616 #[test]
1617 fn test_span_data_completely_empty() {
1618 let span = r#"{
1619 "data": {}
1620 }"#;
1621 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1622 assert_eq!(span.to_json().unwrap(), r#"{"data":{}}"#);
1623 }
1624
1625 #[test]
1626 fn test_span_links() {
1627 let span = r#"{
1628 "links": [
1629 {
1630 "trace_id": "5c79f60c11214eb38604f4ae0781bfb2",
1631 "span_id": "ab90fdead5f74052",
1632 "sampled": true,
1633 "attributes": {
1634 "sentry.link.type": "previous_trace"
1635 }
1636 },
1637 {
1638 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1639 "span_id": "fa90fdead5f74052",
1640 "sampled": true,
1641 "attributes": {
1642 "sentry.link.type": "next_trace"
1643 }
1644 }
1645 ]
1646 }"#;
1647
1648 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1649 assert_eq!(
1650 span.to_json().unwrap(),
1651 r#"{"links":[{"trace_id":"5c79f60c11214eb38604f4ae0781bfb2","span_id":"ab90fdead5f74052","sampled":true,"attributes":{"sentry.link.type":"previous_trace"}},{"trace_id":"4c79f60c11214eb38604f4ae0781bfb2","span_id":"fa90fdead5f74052","sampled":true,"attributes":{"sentry.link.type":"next_trace"}}]}"#
1652 );
1653 }
1654
1655 #[test]
1656 fn test_span_kind() {
1657 let span = Annotated::<Span>::from_json(
1658 r#"{
1659 "kind": "???"
1660 }"#,
1661 )
1662 .unwrap()
1663 .into_value()
1664 .unwrap();
1665 assert_eq!(
1666 span.kind.value().unwrap(),
1667 &SpanKind::Unknown("???".to_owned())
1668 );
1669 }
1670}