1mod convert;
2
3use std::fmt;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use relay_protocol::{
8 Annotated, Array, Empty, Error, FromValue, Getter, IntoValue, Object, Val, Value,
9};
10
11use crate::processor::ProcessValue;
12use crate::protocol::{
13 EventId, IpAddr, JsonLenientString, LenientString, Measurements, OperationType, OriginType,
14 SpanId, SpanStatus, ThreadId, Timestamp, TraceId,
15};
16
17#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
18#[metastructure(process_func = "process_span", value_type = "Span", trim = false)]
19pub struct Span {
20 #[metastructure(required = true)]
22 pub timestamp: Annotated<Timestamp>,
23
24 #[metastructure(required = true)]
26 pub start_timestamp: Annotated<Timestamp>,
27
28 pub exclusive_time: Annotated<f64>,
31
32 #[metastructure(max_chars = 128)]
34 pub op: Annotated<OperationType>,
35
36 #[metastructure(required = true)]
38 pub span_id: Annotated<SpanId>,
39
40 pub parent_span_id: Annotated<SpanId>,
42
43 #[metastructure(required = true)]
45 pub trace_id: Annotated<TraceId>,
46
47 pub segment_id: Annotated<SpanId>,
52
53 pub is_segment: Annotated<bool>,
55
56 pub is_remote: Annotated<bool>,
66
67 pub status: Annotated<SpanStatus>,
69
70 #[metastructure(pii = "maybe")]
72 pub description: Annotated<String>,
73
74 #[metastructure(pii = "maybe")]
76 pub tags: Annotated<Object<JsonLenientString>>,
77
78 #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
80 pub origin: Annotated<OriginType>,
81
82 pub profile_id: Annotated<EventId>,
84
85 #[metastructure(pii = "true")]
90 pub data: Annotated<SpanData>,
91
92 #[metastructure(pii = "maybe")]
94 pub links: Annotated<Array<SpanLink>>,
95
96 pub sentry_tags: Annotated<SentryTags>,
98
99 pub received: Annotated<Timestamp>,
101
102 #[metastructure(skip_serialization = "empty")]
104 #[metastructure(omit_from_schema)] pub measurements: Annotated<Measurements>,
106
107 #[metastructure(skip_serialization = "empty")]
111 pub platform: Annotated<String>,
112
113 #[metastructure(skip_serialization = "empty")]
115 pub was_transaction: Annotated<bool>,
116
117 #[metastructure(skip_serialization = "empty", trim = false)]
122 pub kind: Annotated<SpanKind>,
123
124 #[metastructure(skip_serialization = "empty", trim = false)]
131 pub _performance_issues_spans: Annotated<bool>,
132
133 #[metastructure(additional_properties, retain = true, pii = "maybe")]
136 pub other: Object<Value>,
137}
138
139impl Span {
140 fn attribute(&self, key: &str) -> Option<Val<'_>> {
145 Some(match self.data.value()?.get_value(key) {
146 Some(value) => value,
147 None => self.tags.value()?.get(key)?.as_str()?.into(),
148 })
149 }
150}
151
152impl Getter for Span {
153 fn get_value(&self, path: &str) -> Option<Val<'_>> {
154 let span_prefix = path.strip_prefix("span.");
155 if let Some(span_prefix) = span_prefix {
156 return Some(match span_prefix {
157 "exclusive_time" => self.exclusive_time.value()?.into(),
158 "description" => self.description.as_str()?.into(),
159 "op" => self.op.as_str()?.into(),
160 "span_id" => self.span_id.value()?.into(),
161 "parent_span_id" => self.parent_span_id.value()?.into(),
162 "trace_id" => self.trace_id.value()?.deref().into(),
163 "status" => self.status.as_str()?.into(),
164 "origin" => self.origin.as_str()?.into(),
165 "duration" => {
166 let start_timestamp = *self.start_timestamp.value()?;
167 let timestamp = *self.timestamp.value()?;
168 relay_common::time::chrono_to_positive_millis(timestamp - start_timestamp)
169 .into()
170 }
171 "was_transaction" => self.was_transaction.value().unwrap_or(&false).into(),
172 path => {
173 if let Some(key) = path.strip_prefix("tags.") {
174 self.tags.value()?.get(key)?.as_str()?.into()
175 } else if let Some(key) = path.strip_prefix("data.") {
176 self.attribute(key)?
177 } else if let Some(key) = path.strip_prefix("sentry_tags.") {
178 self.sentry_tags.value()?.get_value(key)?
179 } else if let Some(rest) = path.strip_prefix("measurements.") {
180 let name = rest.strip_suffix(".value")?;
181 self.measurements
182 .value()?
183 .get(name)?
184 .value()?
185 .value
186 .value()?
187 .into()
188 } else {
189 return None;
190 }
191 }
192 });
193 }
194
195 let event_prefix = path.strip_prefix("event.")?;
198 Some(match event_prefix {
199 "release" => self.data.value()?.release.as_str()?.into(),
200 "environment" => self.data.value()?.environment.as_str()?.into(),
201 "transaction" => self.data.value()?.segment_name.as_str()?.into(),
202 "contexts.browser.name" => self.data.value()?.browser_name.as_str()?.into(),
203 _ => return None,
205 })
206 }
207}
208
209#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
211#[metastructure(trim = false, pii = "maybe")]
212pub struct SentryTags {
213 pub release: Annotated<String>,
214 #[metastructure(pii = "true")]
215 pub user: Annotated<String>,
216 #[metastructure(pii = "true", field = "user.id")]
217 pub user_id: Annotated<String>,
218 #[metastructure(pii = "true", field = "user.ip")]
219 pub user_ip: Annotated<String>,
220 #[metastructure(pii = "true", field = "user.username")]
221 pub user_username: Annotated<String>,
222 #[metastructure(pii = "true", field = "user.email")]
223 pub user_email: Annotated<String>,
224 pub environment: Annotated<String>,
225 pub transaction: Annotated<String>,
226 #[metastructure(field = "transaction.method")]
227 pub transaction_method: Annotated<String>,
228 #[metastructure(field = "transaction.op")]
229 pub transaction_op: Annotated<String>,
230 #[metastructure(field = "browser.name")]
231 pub browser_name: Annotated<String>,
232 #[metastructure(field = "sdk.name")]
233 pub sdk_name: Annotated<String>,
234 #[metastructure(field = "sdk.version")]
235 pub sdk_version: Annotated<String>,
236 pub platform: Annotated<String>,
237 pub mobile: Annotated<String>,
239 #[metastructure(field = "device.class")]
240 pub device_class: Annotated<String>,
241 #[metastructure(field = "device.family")]
242 pub device_family: Annotated<String>,
243 #[metastructure(field = "device.arch")]
244 pub device_arch: Annotated<String>,
245 #[metastructure(field = "device.battery_level")]
246 pub device_battery_level: Annotated<String>,
247 #[metastructure(field = "device.brand")]
248 pub device_brand: Annotated<String>,
249 #[metastructure(field = "device.charging")]
250 pub device_charging: Annotated<String>,
251 #[metastructure(field = "device.locale")]
252 pub device_locale: Annotated<String>,
253 #[metastructure(field = "device.model_id")]
254 pub device_model_id: Annotated<String>,
255 #[metastructure(field = "device.name")]
256 pub device_name: Annotated<String>,
257 #[metastructure(field = "device.online")]
258 pub device_online: Annotated<String>,
259 #[metastructure(field = "device.orientation")]
260 pub device_orientation: Annotated<String>,
261 #[metastructure(field = "device.screen_density")]
262 pub device_screen_density: Annotated<String>,
263 #[metastructure(field = "device.screen_dpi")]
264 pub device_screen_dpi: Annotated<String>,
265 #[metastructure(field = "device.screen_height_pixels")]
266 pub device_screen_height_pixels: Annotated<String>,
267 #[metastructure(field = "device.screen_width_pixels")]
268 pub device_screen_width_pixels: Annotated<String>,
269 #[metastructure(field = "device.simulator")]
270 pub device_simulator: Annotated<String>,
271 #[metastructure(field = "device.uuid")]
272 pub device_uuid: Annotated<String>,
273 #[metastructure(field = "app.device")]
274 pub app_device: Annotated<String>,
275 #[metastructure(field = "device.model")]
276 pub device_model: Annotated<String>,
277 pub runtime: Annotated<String>,
278 #[metastructure(field = "runtime.name")]
279 pub runtime_name: Annotated<String>,
280 pub browser: Annotated<String>,
281 pub os: Annotated<String>,
282 #[metastructure(field = "os.rooted")]
283 pub os_rooted: Annotated<String>,
284 #[metastructure(field = "gpu.name")]
285 pub gpu_name: Annotated<String>,
286 #[metastructure(field = "gpu.vendor")]
287 pub gpu_vendor: Annotated<String>,
288 #[metastructure(field = "monitor.id")]
289 pub monitor_id: Annotated<String>,
290 #[metastructure(field = "monitor.slug")]
291 pub monitor_slug: Annotated<String>,
292 #[metastructure(field = "request.url")]
293 pub request_url: Annotated<String>,
294 #[metastructure(field = "request.method")]
295 pub request_method: Annotated<String>,
296 #[metastructure(field = "os.name")]
298 pub os_name: Annotated<String>,
299 pub action: Annotated<String>,
300 pub ai_pipeline_group: Annotated<String>,
302 pub category: Annotated<String>,
303 pub description: Annotated<String>,
304 pub domain: Annotated<String>,
305 pub raw_domain: Annotated<String>,
306 pub group: Annotated<String>,
307 #[metastructure(field = "http.decoded_response_content_length")]
308 pub http_decoded_response_content_length: Annotated<String>,
309 #[metastructure(field = "http.response_content_length")]
310 pub http_response_content_length: Annotated<String>,
311 #[metastructure(field = "http.response_transfer_size")]
312 pub http_response_transfer_size: Annotated<String>,
313 #[metastructure(field = "resource.render_blocking_status")]
314 pub resource_render_blocking_status: Annotated<String>,
315 pub op: Annotated<String>,
316 pub status: Annotated<String>,
317 pub status_code: Annotated<String>,
318 pub system: Annotated<String>,
319 pub ttid: Annotated<String>,
321 pub ttfd: Annotated<String>,
323 pub file_extension: Annotated<String>,
325 pub main_thread: Annotated<String>,
327 pub app_start_type: Annotated<String>,
329 pub replay_id: Annotated<String>,
330 #[metastructure(field = "cache.hit")]
331 pub cache_hit: Annotated<String>,
332 #[metastructure(field = "cache.key")]
333 pub cache_key: Annotated<String>,
334 #[metastructure(field = "trace.status")]
335 pub trace_status: Annotated<String>,
336 #[metastructure(field = "messaging.destination.name")]
337 pub messaging_destination_name: Annotated<String>,
338 #[metastructure(field = "messaging.message.id")]
339 pub messaging_message_id: Annotated<String>,
340 #[metastructure(field = "messaging.operation.name")]
341 pub messaging_operation_name: Annotated<String>,
342 #[metastructure(field = "messaging.operation.type")]
343 pub messaging_operation_type: Annotated<String>,
344 #[metastructure(field = "thread.name")]
345 pub thread_name: Annotated<String>,
346 #[metastructure(field = "thread.id")]
347 pub thread_id: Annotated<String>,
348 pub profiler_id: Annotated<String>,
349 #[metastructure(field = "user.geo.country_code")]
350 pub user_country_code: Annotated<String>,
351 #[metastructure(field = "user.geo.subregion")]
352 pub user_subregion: Annotated<String>,
353 }
356
357impl Getter for SentryTags {
358 fn get_value(&self, path: &str) -> Option<Val<'_>> {
359 let value = match path {
360 "action" => &self.action,
361 "ai_pipeline_group" => &self.ai_pipeline_group,
362 "app_start_type" => &self.app_start_type,
363 "browser.name" => &self.browser_name,
364 "cache.hit" => &self.cache_hit,
365 "cache.key" => &self.cache_key,
366 "category" => &self.category,
367 "description" => &self.description,
368 "device.class" => &self.device_class,
369 "device.family" => &self.device_family,
370 "device.arch" => &self.device_arch,
371 "device.battery_level" => &self.device_battery_level,
372 "device.brand" => &self.device_brand,
373 "device.charging" => &self.device_charging,
374 "device.locale" => &self.device_locale,
375 "device.model_id" => &self.device_model_id,
376 "device.name" => &self.device_name,
377 "device.online" => &self.device_online,
378 "device.orientation" => &self.device_orientation,
379 "device.screen_density" => &self.device_screen_density,
380 "device.screen_dpi" => &self.device_screen_dpi,
381 "device.screen_height_pixels" => &self.device_screen_height_pixels,
382 "device.screen_width_pixels" => &self.device_screen_width_pixels,
383 "device.simulator" => &self.device_simulator,
384 "device.uuid" => &self.device_uuid,
385 "app.device" => &self.app_device,
386 "device.model" => &self.device_model,
387 "runtime" => &self.runtime,
388 "runtime.name" => &self.runtime_name,
389 "browser" => &self.browser,
390 "os" => &self.os,
391 "os.rooted" => &self.os_rooted,
392 "gpu.name" => &self.gpu_name,
393 "gpu.vendor" => &self.gpu_vendor,
394 "monitor.id" => &self.monitor_id,
395 "monitor.slug" => &self.monitor_slug,
396 "request.url" => &self.request_url,
397 "request.method" => &self.request_method,
398 "domain" => &self.domain,
399 "environment" => &self.environment,
400 "file_extension" => &self.file_extension,
401 "group" => &self.group,
402 "http.decoded_response_content_length" => &self.http_decoded_response_content_length,
403 "http.response_content_length" => &self.http_response_content_length,
404 "http.response_transfer_size" => &self.http_response_transfer_size,
405 "main_thread" => &self.main_thread,
406 "messaging.destination.name" => &self.messaging_destination_name,
407 "messaging.message.id" => &self.messaging_message_id,
408 "messaging.operation.name" => &self.messaging_operation_name,
409 "messaging.operation.type" => &self.messaging_operation_type,
410 "mobile" => &self.mobile,
411 "op" => &self.op,
412 "os.name" => &self.os_name,
413 "platform" => &self.platform,
414 "profiler_id" => &self.profiler_id,
415 "raw_domain" => &self.raw_domain,
416 "release" => &self.release,
417 "replay_id" => &self.replay_id,
418 "resource.render_blocking_status" => &self.resource_render_blocking_status,
419 "sdk.name" => &self.sdk_name,
420 "sdk.version" => &self.sdk_version,
421 "status_code" => &self.status_code,
422 "status" => &self.status,
423 "system" => &self.system,
424 "thread.id" => &self.thread_id,
425 "thread.name" => &self.thread_name,
426 "trace.status" => &self.trace_status,
427 "transaction.method" => &self.transaction_method,
428 "transaction.op" => &self.transaction_op,
429 "transaction" => &self.transaction,
430 "ttfd" => &self.ttfd,
431 "ttid" => &self.ttid,
432 "user.email" => &self.user_email,
433 "user.geo.country_code" => &self.user_country_code,
434 "user.geo.subregion" => &self.user_subregion,
435 "user.id" => &self.user_id,
436 "user.ip" => &self.user_ip,
437 "user.username" => &self.user_username,
438 "user" => &self.user,
439 _ => return None,
440 };
441 Some(value.as_str()?.into())
442 }
443}
444
445#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
450#[metastructure(trim = false)]
451pub struct SpanData {
452 #[metastructure(field = "app_start_type")] pub app_start_type: Annotated<Value>,
457
458 #[metastructure(
460 field = "gen_ai.usage.total_tokens",
461 legacy_alias = "ai.total_tokens.used"
462 )]
463 pub gen_ai_usage_total_tokens: Annotated<Value>,
464
465 #[metastructure(
467 field = "gen_ai.usage.input_tokens",
468 legacy_alias = "ai.prompt_tokens.used"
469 )]
470 pub gen_ai_usage_input_tokens: Annotated<Value>,
471
472 #[metastructure(
474 field = "gen_ai.usage.output_tokens",
475 legacy_alias = "ai.completion_tokens.used"
476 )]
477 pub gen_ai_usage_output_tokens: Annotated<Value>,
478
479 #[metastructure(field = "gen_ai.usage.total_cost", legacy_alias = "ai.total_cost")]
481 pub gen_ai_usage_total_cost: Annotated<Value>,
482
483 #[metastructure(field = "browser.name")]
485 pub browser_name: Annotated<String>,
486
487 #[metastructure(field = "code.filepath", pii = "maybe")]
489 pub code_filepath: Annotated<Value>,
490 #[metastructure(field = "code.lineno", pii = "maybe")]
492 pub code_lineno: Annotated<Value>,
493 #[metastructure(field = "code.function", pii = "maybe")]
497 pub code_function: Annotated<Value>,
498 #[metastructure(field = "code.namespace", pii = "maybe")]
504 pub code_namespace: Annotated<Value>,
505
506 #[metastructure(field = "db.operation")]
511 pub db_operation: Annotated<Value>,
512
513 #[metastructure(field = "db.system")]
517 pub db_system: Annotated<Value>,
518
519 #[metastructure(
523 field = "db.collection.name",
524 legacy_alias = "db.cassandra.table",
525 legacy_alias = "db.cosmosdb.container",
526 legacy_alias = "db.mongodb.collection",
527 legacy_alias = "db.sql.table"
528 )]
529 pub db_collection_name: Annotated<Value>,
530
531 #[metastructure(field = "sentry.environment", legacy_alias = "environment")]
533 pub environment: Annotated<String>,
534
535 #[metastructure(field = "sentry.release", legacy_alias = "release")]
537 pub release: Annotated<LenientString>,
538
539 #[metastructure(field = "http.decoded_response_content_length")]
541 pub http_decoded_response_content_length: Annotated<Value>,
542
543 #[metastructure(
545 field = "http.request_method",
546 legacy_alias = "http.method",
547 legacy_alias = "method"
548 )]
549 pub http_request_method: Annotated<Value>,
550
551 #[metastructure(field = "http.response_content_length")]
553 pub http_response_content_length: Annotated<Value>,
554
555 #[metastructure(field = "http.response_transfer_size")]
557 pub http_response_transfer_size: Annotated<Value>,
558
559 #[metastructure(field = "resource.render_blocking_status")]
561 pub resource_render_blocking_status: Annotated<Value>,
562
563 #[metastructure(field = "server.address")]
565 pub server_address: Annotated<Value>,
566
567 #[metastructure(field = "cache.hit")]
569 pub cache_hit: Annotated<Value>,
570
571 #[metastructure(field = "cache.key")]
573 pub cache_key: Annotated<Value>,
574
575 #[metastructure(field = "cache.item_size")]
577 pub cache_item_size: Annotated<Value>,
578
579 #[metastructure(field = "http.response.status_code", legacy_alias = "status_code")]
581 pub http_response_status_code: Annotated<Value>,
582
583 #[metastructure(field = "ai.pipeline.name")]
585 pub ai_pipeline_name: Annotated<Value>,
586
587 #[metastructure(field = "ai.model_id")]
589 pub ai_model_id: Annotated<Value>,
590
591 #[metastructure(field = "ai.input_messages")]
593 pub ai_input_messages: Annotated<Value>,
594
595 #[metastructure(field = "ai.responses")]
597 pub ai_responses: Annotated<Value>,
598
599 #[metastructure(field = "thread.name")]
601 pub thread_name: Annotated<String>,
602
603 #[metastructure(field = "thread.id")]
605 pub thread_id: Annotated<ThreadId>,
606
607 #[metastructure(field = "sentry.segment.name", legacy_alias = "transaction")]
613 pub segment_name: Annotated<String>,
614
615 #[metastructure(field = "ui.component_name")]
617 pub ui_component_name: Annotated<Value>,
618
619 #[metastructure(field = "url.scheme")]
621 pub url_scheme: Annotated<Value>,
622
623 #[metastructure(field = "user")]
625 pub user: Annotated<Value>,
626
627 #[metastructure(field = "user.email")]
631 pub user_email: Annotated<String>,
632
633 #[metastructure(field = "user.full_name")]
637 pub user_full_name: Annotated<String>,
638
639 #[metastructure(field = "user.geo.country_code")]
643 pub user_geo_country_code: Annotated<String>,
644
645 #[metastructure(field = "user.geo.city")]
649 pub user_geo_city: Annotated<String>,
650
651 #[metastructure(field = "user.geo.subdivision")]
655 pub user_geo_subdivision: Annotated<String>,
656
657 #[metastructure(field = "user.geo.region")]
661 pub user_geo_region: Annotated<String>,
662
663 #[metastructure(field = "user.hash")]
667 pub user_hash: Annotated<String>,
668
669 #[metastructure(field = "user.id")]
673 pub user_id: Annotated<String>,
674
675 #[metastructure(field = "user.name")]
679 pub user_name: Annotated<String>,
680
681 #[metastructure(field = "user.roles")]
685 pub user_roles: Annotated<Array<String>>,
686
687 #[metastructure(field = "sentry.exclusive_time")]
689 pub exclusive_time: Annotated<Value>,
690
691 #[metastructure(field = "profile_id")]
693 pub profile_id: Annotated<Value>,
694
695 #[metastructure(field = "sentry.replay.id", legacy_alias = "replay_id")]
697 pub replay_id: Annotated<Value>,
698
699 #[metastructure(field = "sentry.sdk.name")]
701 pub sdk_name: Annotated<String>,
702
703 #[metastructure(field = "sentry.sdk.version")]
705 pub sdk_version: Annotated<String>,
706
707 #[metastructure(field = "sentry.frames.slow", legacy_alias = "frames.slow")]
709 pub frames_slow: Annotated<Value>,
710
711 #[metastructure(field = "sentry.frames.frozen", legacy_alias = "frames.frozen")]
713 pub frames_frozen: Annotated<Value>,
714
715 #[metastructure(field = "sentry.frames.total", legacy_alias = "frames.total")]
717 pub frames_total: Annotated<Value>,
718
719 #[metastructure(field = "frames.delay")]
721 pub frames_delay: Annotated<Value>,
722
723 #[metastructure(field = "messaging.destination.name")]
725 pub messaging_destination_name: Annotated<String>,
726
727 #[metastructure(field = "messaging.message.retry.count")]
729 pub messaging_message_retry_count: Annotated<Value>,
730
731 #[metastructure(field = "messaging.message.receive.latency")]
733 pub messaging_message_receive_latency: Annotated<Value>,
734
735 #[metastructure(field = "messaging.message.body.size")]
737 pub messaging_message_body_size: Annotated<Value>,
738
739 #[metastructure(field = "messaging.message.id")]
741 pub messaging_message_id: Annotated<String>,
742
743 #[metastructure(field = "messaging.operation.name")]
745 pub messaging_operation_name: Annotated<String>,
746
747 #[metastructure(field = "messaging.operation.type")]
749 pub messaging_operation_type: Annotated<String>,
750
751 #[metastructure(field = "user_agent.original")]
753 pub user_agent_original: Annotated<String>,
754
755 #[metastructure(field = "url.full")]
757 pub url_full: Annotated<String>,
758
759 #[metastructure(field = "client.address")]
761 pub client_address: Annotated<IpAddr>,
762
763 #[metastructure(pii = "maybe", skip_serialization = "empty")]
767 pub route: Annotated<Route>,
768 #[metastructure(field = "previousRoute", pii = "maybe", skip_serialization = "empty")]
772 pub previous_route: Annotated<Route>,
773
774 #[metastructure(field = "lcp.element")]
776 pub lcp_element: Annotated<String>,
777
778 #[metastructure(field = "lcp.size")]
780 pub lcp_size: Annotated<u64>,
781
782 #[metastructure(field = "lcp.id")]
784 pub lcp_id: Annotated<String>,
785
786 #[metastructure(field = "lcp.url")]
788 pub lcp_url: Annotated<String>,
789
790 #[metastructure(
792 additional_properties,
793 pii = "true",
794 retain = true,
795 skip_serialization = "null" )]
797 pub other: Object<Value>,
798}
799
800impl Getter for SpanData {
801 fn get_value(&self, path: &str) -> Option<Val<'_>> {
802 Some(match path {
803 "app_start_type" => self.app_start_type.value()?.into(),
804 "browser\\.name" => self.browser_name.as_str()?.into(),
805 "code\\.filepath" => self.code_filepath.value()?.into(),
806 "code\\.function" => self.code_function.value()?.into(),
807 "code\\.lineno" => self.code_lineno.value()?.into(),
808 "code\\.namespace" => self.code_namespace.value()?.into(),
809 "db.operation" => self.db_operation.value()?.into(),
810 "db\\.system" => self.db_system.value()?.into(),
811 "environment" => self.environment.as_str()?.into(),
812 "gen_ai\\.usage\\.total_tokens" => self.gen_ai_usage_total_tokens.value()?.into(),
813 "gen_ai\\.usage\\.total_cost" => self.gen_ai_usage_total_cost.value()?.into(),
814 "http\\.decoded_response_content_length" => {
815 self.http_decoded_response_content_length.value()?.into()
816 }
817 "http\\.request_method" | "http\\.method" | "method" => {
818 self.http_request_method.value()?.into()
819 }
820 "http\\.response_content_length" => self.http_response_content_length.value()?.into(),
821 "http\\.response_transfer_size" => self.http_response_transfer_size.value()?.into(),
822 "http\\.response.status_code" | "status_code" => {
823 self.http_response_status_code.value()?.into()
824 }
825 "resource\\.render_blocking_status" => {
826 self.resource_render_blocking_status.value()?.into()
827 }
828 "server\\.address" => self.server_address.value()?.into(),
829 "thread\\.name" => self.thread_name.as_str()?.into(),
830 "ui\\.component_name" => self.ui_component_name.value()?.into(),
831 "url\\.scheme" => self.url_scheme.value()?.into(),
832 "user" => self.user.value()?.into(),
833 "user\\.email" => self.user_email.as_str()?.into(),
834 "user\\.full_name" => self.user_full_name.as_str()?.into(),
835 "user\\.geo\\.city" => self.user_geo_city.as_str()?.into(),
836 "user\\.geo\\.country_code" => self.user_geo_country_code.as_str()?.into(),
837 "user\\.geo\\.region" => self.user_geo_region.as_str()?.into(),
838 "user\\.geo\\.subdivision" => self.user_geo_subdivision.as_str()?.into(),
839 "user\\.hash" => self.user_hash.as_str()?.into(),
840 "user\\.id" => self.user_id.as_str()?.into(),
841 "user\\.name" => self.user_name.as_str()?.into(),
842 "transaction" => self.segment_name.as_str()?.into(),
843 "release" => self.release.as_str()?.into(),
844 _ => {
845 let escaped = path.replace("\\.", "\0");
846 let mut path = escaped.split('.').map(|s| s.replace('\0', "."));
847 let root = path.next()?;
848
849 let mut val = self.other.get(&root)?.value()?;
850 for part in path {
851 let relay_protocol::Value::Object(map) = val else {
853 return None;
854 };
855 val = map.get(&part)?.value()?;
856 }
857 val.into()
858 }
859 })
860 }
861}
862
863#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
865#[metastructure(trim = false)]
866pub struct SpanLink {
867 #[metastructure(required = true, trim = false)]
869 pub trace_id: Annotated<TraceId>,
870
871 #[metastructure(required = true, trim = false)]
873 pub span_id: Annotated<SpanId>,
874
875 #[metastructure(trim = false)]
877 pub sampled: Annotated<bool>,
878
879 #[metastructure(pii = "maybe", trim = false)]
881 pub attributes: Annotated<Object<Value>>,
882
883 #[metastructure(additional_properties, retain = true, pii = "maybe", trim = false)]
885 pub other: Object<Value>,
886}
887
888#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
890pub struct Route {
891 #[metastructure(pii = "maybe", skip_serialization = "empty")]
893 pub name: Annotated<String>,
894
895 #[metastructure(
897 pii = "true",
898 skip_serialization = "empty",
899 max_depth = 5,
900 max_bytes = 2048
901 )]
902 pub params: Annotated<Object<Value>>,
903
904 #[metastructure(
906 additional_properties,
907 retain = true,
908 pii = "maybe",
909 skip_serialization = "empty"
910 )]
911 pub other: Object<Value>,
912}
913
914impl FromValue for Route {
915 fn from_value(value: Annotated<Value>) -> Annotated<Self>
916 where
917 Self: Sized,
918 {
919 match value {
920 Annotated(Some(Value::String(name)), meta) => Annotated(
921 Some(Route {
922 name: Annotated::new(name),
923 ..Default::default()
924 }),
925 meta,
926 ),
927 Annotated(Some(Value::Object(mut values)), meta) => {
928 let mut route: Route = Default::default();
929 if let Some(Annotated(Some(Value::String(name)), _)) = values.remove("name") {
930 route.name = Annotated::new(name);
931 }
932 if let Some(Annotated(Some(Value::Object(params)), _)) = values.remove("params") {
933 route.params = Annotated::new(params);
934 }
935
936 if !values.is_empty() {
937 route.other = values;
938 }
939
940 Annotated(Some(route), meta)
941 }
942 Annotated(None, meta) => Annotated(None, meta),
943 Annotated(Some(value), mut meta) => {
944 meta.add_error(Error::expected("route expected to be an object"));
945 meta.set_original_value(Some(value));
946 Annotated(None, meta)
947 }
948 }
949 }
950}
951
952#[derive(Clone, Debug, PartialEq, ProcessValue)]
953pub enum SpanKind {
954 Internal,
955 Server,
956 Client,
957 Producer,
958 Consumer,
959}
960
961impl SpanKind {
962 pub fn as_str(&self) -> &'static str {
963 match self {
964 Self::Internal => "internal",
965 Self::Server => "server",
966 Self::Client => "client",
967 Self::Producer => "producer",
968 Self::Consumer => "consumer",
969 }
970 }
971}
972
973impl Empty for SpanKind {
974 fn is_empty(&self) -> bool {
975 false
976 }
977}
978
979#[derive(Debug)]
980pub struct ParseSpanKindError;
981
982impl std::str::FromStr for SpanKind {
983 type Err = ParseSpanKindError;
984
985 fn from_str(s: &str) -> Result<Self, Self::Err> {
986 Ok(match s {
987 "internal" => SpanKind::Internal,
988 "server" => SpanKind::Server,
989 "client" => SpanKind::Client,
990 "producer" => SpanKind::Producer,
991 "consumer" => SpanKind::Consumer,
992 _ => return Err(ParseSpanKindError),
993 })
994 }
995}
996
997impl Default for SpanKind {
998 fn default() -> Self {
999 Self::Internal
1000 }
1001}
1002
1003impl fmt::Display for SpanKind {
1004 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1005 write!(f, "{}", self.as_str())
1006 }
1007}
1008
1009impl FromValue for SpanKind {
1010 fn from_value(value: Annotated<Value>) -> Annotated<Self>
1011 where
1012 Self: Sized,
1013 {
1014 match value {
1015 Annotated(Some(Value::String(s)), meta) => Annotated(SpanKind::from_str(&s).ok(), meta),
1016 Annotated(_, meta) => Annotated(None, meta),
1017 }
1018 }
1019}
1020
1021impl IntoValue for SpanKind {
1022 fn into_value(self) -> Value
1023 where
1024 Self: Sized,
1025 {
1026 Value::String(self.to_string())
1027 }
1028
1029 fn serialize_payload<S>(
1030 &self,
1031 s: S,
1032 _behavior: relay_protocol::SkipSerialization,
1033 ) -> Result<S::Ok, S::Error>
1034 where
1035 Self: Sized,
1036 S: serde::Serializer,
1037 {
1038 s.serialize_str(self.as_str())
1039 }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use crate::protocol::Measurement;
1045 use chrono::{TimeZone, Utc};
1046 use relay_base_schema::metrics::{InformationUnit, MetricUnit};
1047 use relay_protocol::RuleCondition;
1048 use similar_asserts::assert_eq;
1049
1050 use super::*;
1051
1052 #[test]
1053 fn test_span_serialization() {
1054 let json = r#"{
1055 "timestamp": 0.0,
1056 "start_timestamp": -63158400.0,
1057 "exclusive_time": 1.23,
1058 "op": "operation",
1059 "span_id": "fa90fdead5f74052",
1060 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1061 "status": "ok",
1062 "description": "desc",
1063 "origin": "auto.http",
1064 "links": [
1065 {
1066 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1067 "span_id": "fa90fdead5f74052",
1068 "sampled": true,
1069 "attributes": {
1070 "boolAttr": true,
1071 "numAttr": 123,
1072 "stringAttr": "foo"
1073 }
1074 }
1075 ],
1076 "measurements": {
1077 "memory": {
1078 "value": 9001.0,
1079 "unit": "byte"
1080 }
1081 },
1082 "kind": "server"
1083}"#;
1084 let mut measurements = Object::new();
1085 measurements.insert(
1086 "memory".into(),
1087 Annotated::new(Measurement {
1088 value: Annotated::new(9001.0),
1089 unit: Annotated::new(MetricUnit::Information(InformationUnit::Byte)),
1090 }),
1091 );
1092
1093 let links = Annotated::new(vec![Annotated::new(SpanLink {
1094 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1095 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
1096 sampled: Annotated::new(true),
1097 attributes: Annotated::new({
1098 let mut map: std::collections::BTreeMap<String, Annotated<Value>> = Object::new();
1099 map.insert(
1100 "stringAttr".into(),
1101 Annotated::new(Value::String("foo".into())),
1102 );
1103 map.insert("numAttr".into(), Annotated::new(Value::I64(123)));
1104 map.insert("boolAttr".into(), Value::Bool(true).into());
1105 map
1106 }),
1107 ..Default::default()
1108 })]);
1109
1110 let span = Annotated::new(Span {
1111 timestamp: Annotated::new(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap().into()),
1112 start_timestamp: Annotated::new(
1113 Utc.with_ymd_and_hms(1968, 1, 1, 0, 0, 0).unwrap().into(),
1114 ),
1115 exclusive_time: Annotated::new(1.23),
1116 description: Annotated::new("desc".to_owned()),
1117 op: Annotated::new("operation".to_owned()),
1118 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1119 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
1120 status: Annotated::new(SpanStatus::Ok),
1121 origin: Annotated::new("auto.http".to_owned()),
1122 kind: Annotated::new(SpanKind::Server),
1123 measurements: Annotated::new(Measurements(measurements)),
1124 links,
1125 ..Default::default()
1126 });
1127 assert_eq!(json, span.to_json_pretty().unwrap());
1128
1129 let span_from_string = Annotated::from_json(json).unwrap();
1130 assert_eq!(span, span_from_string);
1131 }
1132
1133 #[test]
1134 fn test_getter_span_data() {
1135 let span = Annotated::<Span>::from_json(
1136 r#"{
1137 "data": {
1138 "foo": {"bar": 1},
1139 "foo.bar": 2
1140 },
1141 "measurements": {
1142 "some": {"value": 100.0}
1143 }
1144 }"#,
1145 )
1146 .unwrap()
1147 .into_value()
1148 .unwrap();
1149
1150 assert_eq!(span.get_value("span.data.foo.bar"), Some(Val::I64(1)));
1151 assert_eq!(span.get_value(r"span.data.foo\.bar"), Some(Val::I64(2)));
1152
1153 assert_eq!(span.get_value("span.data"), None);
1154 assert_eq!(span.get_value("span.data."), None);
1155 assert_eq!(span.get_value("span.data.x"), None);
1156
1157 assert_eq!(
1158 span.get_value("span.measurements.some.value"),
1159 Some(Val::F64(100.0))
1160 );
1161 }
1162
1163 #[test]
1164 fn test_getter_was_transaction() {
1165 let mut span = Span::default();
1166 assert_eq!(
1167 span.get_value("span.was_transaction"),
1168 Some(Val::Bool(false))
1169 );
1170 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1171 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1172
1173 span.was_transaction.set_value(Some(false));
1174 assert_eq!(
1175 span.get_value("span.was_transaction"),
1176 Some(Val::Bool(false))
1177 );
1178 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
1179 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
1180
1181 span.was_transaction.set_value(Some(true));
1182 assert_eq!(
1183 span.get_value("span.was_transaction"),
1184 Some(Val::Bool(true))
1185 );
1186 assert!(RuleCondition::eq("span.was_transaction", true).matches(&span));
1187 assert!(!RuleCondition::eq("span.was_transaction", false).matches(&span));
1188 }
1189
1190 #[test]
1191 fn test_span_fields_as_event() {
1192 let span = Annotated::<Span>::from_json(
1193 r#"{
1194 "data": {
1195 "release": "1.0",
1196 "environment": "prod",
1197 "sentry.segment.name": "/api/endpoint"
1198 }
1199 }"#,
1200 )
1201 .unwrap()
1202 .into_value()
1203 .unwrap();
1204
1205 assert_eq!(span.get_value("event.release"), Some(Val::String("1.0")));
1206 assert_eq!(
1207 span.get_value("event.environment"),
1208 Some(Val::String("prod"))
1209 );
1210 assert_eq!(
1211 span.get_value("event.transaction"),
1212 Some(Val::String("/api/endpoint"))
1213 );
1214 }
1215
1216 #[test]
1217 fn test_span_duration() {
1218 let span = Annotated::<Span>::from_json(
1219 r#"{
1220 "start_timestamp": 1694732407.8367,
1221 "timestamp": 1694732408.3145
1222 }"#,
1223 )
1224 .unwrap()
1225 .into_value()
1226 .unwrap();
1227
1228 assert_eq!(span.get_value("span.duration"), Some(Val::F64(477.800131)));
1229 }
1230
1231 #[test]
1232 fn test_span_data() {
1233 let data = r#"{
1234 "foo": 2,
1235 "bar": "3",
1236 "db.system": "mysql",
1237 "code.filepath": "task.py",
1238 "code.lineno": 123,
1239 "code.function": "fn()",
1240 "code.namespace": "ns",
1241 "frames.slow": 1,
1242 "frames.frozen": 2,
1243 "frames.total": 9,
1244 "frames.delay": 100,
1245 "messaging.destination.name": "default",
1246 "messaging.message.retry.count": 3,
1247 "messaging.message.receive.latency": 40,
1248 "messaging.message.body.size": 100,
1249 "messaging.message.id": "abc123",
1250 "messaging.operation.name": "publish",
1251 "messaging.operation.type": "create",
1252 "user_agent.original": "Chrome",
1253 "url.full": "my_url.com",
1254 "client.address": "192.168.0.1"
1255 }"#;
1256 let data = Annotated::<SpanData>::from_json(data)
1257 .unwrap()
1258 .into_value()
1259 .unwrap();
1260 insta::assert_debug_snapshot!(data, @r#"
1261 SpanData {
1262 app_start_type: ~,
1263 gen_ai_usage_total_tokens: ~,
1264 gen_ai_usage_input_tokens: ~,
1265 gen_ai_usage_output_tokens: ~,
1266 gen_ai_usage_total_cost: ~,
1267 browser_name: ~,
1268 code_filepath: String(
1269 "task.py",
1270 ),
1271 code_lineno: I64(
1272 123,
1273 ),
1274 code_function: String(
1275 "fn()",
1276 ),
1277 code_namespace: String(
1278 "ns",
1279 ),
1280 db_operation: ~,
1281 db_system: String(
1282 "mysql",
1283 ),
1284 db_collection_name: ~,
1285 environment: ~,
1286 release: ~,
1287 http_decoded_response_content_length: ~,
1288 http_request_method: ~,
1289 http_response_content_length: ~,
1290 http_response_transfer_size: ~,
1291 resource_render_blocking_status: ~,
1292 server_address: ~,
1293 cache_hit: ~,
1294 cache_key: ~,
1295 cache_item_size: ~,
1296 http_response_status_code: ~,
1297 ai_pipeline_name: ~,
1298 ai_model_id: ~,
1299 ai_input_messages: ~,
1300 ai_responses: ~,
1301 thread_name: ~,
1302 thread_id: ~,
1303 segment_name: ~,
1304 ui_component_name: ~,
1305 url_scheme: ~,
1306 user: ~,
1307 user_email: ~,
1308 user_full_name: ~,
1309 user_geo_country_code: ~,
1310 user_geo_city: ~,
1311 user_geo_subdivision: ~,
1312 user_geo_region: ~,
1313 user_hash: ~,
1314 user_id: ~,
1315 user_name: ~,
1316 user_roles: ~,
1317 exclusive_time: ~,
1318 profile_id: ~,
1319 replay_id: ~,
1320 sdk_name: ~,
1321 sdk_version: ~,
1322 frames_slow: I64(
1323 1,
1324 ),
1325 frames_frozen: I64(
1326 2,
1327 ),
1328 frames_total: I64(
1329 9,
1330 ),
1331 frames_delay: I64(
1332 100,
1333 ),
1334 messaging_destination_name: "default",
1335 messaging_message_retry_count: I64(
1336 3,
1337 ),
1338 messaging_message_receive_latency: I64(
1339 40,
1340 ),
1341 messaging_message_body_size: I64(
1342 100,
1343 ),
1344 messaging_message_id: "abc123",
1345 messaging_operation_name: "publish",
1346 messaging_operation_type: "create",
1347 user_agent_original: "Chrome",
1348 url_full: "my_url.com",
1349 client_address: IpAddr(
1350 "192.168.0.1",
1351 ),
1352 route: ~,
1353 previous_route: ~,
1354 lcp_element: ~,
1355 lcp_size: ~,
1356 lcp_id: ~,
1357 lcp_url: ~,
1358 other: {
1359 "bar": String(
1360 "3",
1361 ),
1362 "foo": I64(
1363 2,
1364 ),
1365 },
1366 }
1367 "#);
1368
1369 assert_eq!(data.get_value("foo"), Some(Val::U64(2)));
1370 assert_eq!(data.get_value("bar"), Some(Val::String("3")));
1371 assert_eq!(data.get_value("db\\.system"), Some(Val::String("mysql")));
1372 assert_eq!(data.get_value("code\\.lineno"), Some(Val::U64(123)));
1373 assert_eq!(data.get_value("code\\.function"), Some(Val::String("fn()")));
1374 assert_eq!(data.get_value("code\\.namespace"), Some(Val::String("ns")));
1375 assert_eq!(data.get_value("unknown"), None);
1376 }
1377
1378 #[test]
1379 fn test_span_data_empty_well_known_field() {
1380 let span = r#"{
1381 "data": {
1382 "lcp.url": ""
1383 }
1384 }"#;
1385 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1386 assert_eq!(span.to_json().unwrap(), r#"{"data":{"lcp.url":""}}"#);
1387 }
1388
1389 #[test]
1390 fn test_span_data_empty_custom_field() {
1391 let span = r#"{
1392 "data": {
1393 "custom_field_empty": ""
1394 }
1395 }"#;
1396 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1397 assert_eq!(
1398 span.to_json().unwrap(),
1399 r#"{"data":{"custom_field_empty":""}}"#
1400 );
1401 }
1402
1403 #[test]
1404 fn test_span_data_completely_empty() {
1405 let span = r#"{
1406 "data": {}
1407 }"#;
1408 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1409 assert_eq!(span.to_json().unwrap(), r#"{"data":{}}"#);
1410 }
1411
1412 #[test]
1413 fn test_span_links() {
1414 let span = r#"{
1415 "links": [
1416 {
1417 "trace_id": "5c79f60c11214eb38604f4ae0781bfb2",
1418 "span_id": "ab90fdead5f74052",
1419 "sampled": true,
1420 "attributes": {
1421 "sentry.link.type": "previous_trace"
1422 }
1423 },
1424 {
1425 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1426 "span_id": "fa90fdead5f74052",
1427 "sampled": true,
1428 "attributes": {
1429 "sentry.link.type": "next_trace"
1430 }
1431 }
1432 ]
1433 }"#;
1434
1435 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1436 assert_eq!(
1437 span.to_json().unwrap(),
1438 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"}}]}"#
1439 );
1440 }
1441}