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