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