1mod convert;
2
3use std::fmt;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use relay_conventions::attributes::{
8 BROWSER__NAME, SENTRY__ENVIRONMENT, SENTRY__RELEASE, SENTRY__SEGMENT__NAME,
9};
10use relay_protocol::{
11 Annotated, Array, Empty, Error, FromValue, Getter, IntoValue, Object, Val, Value,
12};
13
14use crate::processor::{Pii, ProcessValue, ProcessingState};
15use crate::protocol::{
16 EventId, JsonLenientString, Measurements, OperationType, OriginType, SpanId, SpanStatus,
17 Timestamp, TraceId,
18};
19
20#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
21#[metastructure(process_func = "process_span", value_type = "Span", trim = false)]
22pub struct Span {
23 #[metastructure(required = true)]
25 pub timestamp: Annotated<Timestamp>,
26
27 #[metastructure(required = true)]
29 pub start_timestamp: Annotated<Timestamp>,
30
31 pub exclusive_time: Annotated<f64>,
34
35 #[metastructure(max_chars = 128)]
37 pub op: Annotated<OperationType>,
38
39 #[metastructure(required = true)]
41 pub span_id: Annotated<SpanId>,
42
43 pub parent_span_id: Annotated<SpanId>,
45
46 #[metastructure(required = true)]
48 pub trace_id: Annotated<TraceId>,
49
50 pub segment_id: Annotated<SpanId>,
55
56 pub is_segment: Annotated<bool>,
58
59 pub is_remote: Annotated<bool>,
69
70 pub status: Annotated<SpanStatus>,
72
73 #[metastructure(pii = "maybe")]
75 pub description: Annotated<String>,
76
77 #[metastructure(pii = "maybe")]
79 pub tags: Annotated<Object<JsonLenientString>>,
80
81 #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
83 pub origin: Annotated<OriginType>,
84
85 pub profile_id: Annotated<EventId>,
87
88 #[metastructure(pii = "true")]
93 pub data: Annotated<SpanData>,
94
95 #[metastructure(pii = "maybe")]
97 pub links: Annotated<Array<SpanLink>>,
98
99 pub sentry_tags: Annotated<SentryTags>,
101
102 pub received: Annotated<Timestamp>,
104
105 #[metastructure(skip_serialization = "empty")]
107 #[metastructure(omit_from_schema)] pub measurements: Annotated<Measurements>,
109
110 #[metastructure(skip_serialization = "empty")]
114 pub platform: Annotated<String>,
115
116 #[metastructure(skip_serialization = "empty")]
118 pub was_transaction: Annotated<bool>,
119
120 #[metastructure(skip_serialization = "empty", trim = false)]
125 pub kind: Annotated<SpanKind>,
126
127 #[metastructure(additional_properties, pii = "maybe")]
129 pub other: Object<Value>,
130}
131
132impl Span {
133 fn attribute(&self, key: &str) -> Option<Val<'_>> {
138 Some(match Getter::get_value(self.data.value()?, key) {
139 Some(value) => value,
140 None => self.tags.value()?.get(key)?.as_str()?.into(),
141 })
142 }
143}
144
145impl Getter for Span {
146 fn get_value(&self, path: &str) -> Option<Val<'_>> {
147 let span_prefix = path.strip_prefix("span.");
148 if let Some(span_prefix) = span_prefix {
149 return Some(match span_prefix {
150 "exclusive_time" => self.exclusive_time.value()?.into(),
151 "description" => self.description.as_str()?.into(),
152 "op" => self.op.as_str()?.into(),
153 "span_id" => self.span_id.value()?.into(),
154 "parent_span_id" => self.parent_span_id.value()?.into(),
155 "trace_id" => self.trace_id.value()?.deref().into(),
156 "status" => self.status.as_str()?.into(),
157 "is_segment" => self.is_segment.value()?.into(),
158 "origin" => self.origin.as_str()?.into(),
159 "duration" => {
160 let start_timestamp = *self.start_timestamp.value()?;
161 let timestamp = *self.timestamp.value()?;
162 relay_common::time::chrono_to_positive_millis(timestamp - start_timestamp)
163 .into()
164 }
165 "was_transaction" => self.was_transaction.value().unwrap_or(&false).into(),
166 path => {
167 if let Some(key) = path.strip_prefix("tags.") {
168 self.tags.value()?.get(key)?.as_str()?.into()
169 } else if let Some(key) = path.strip_prefix("data.") {
170 self.attribute(key)?
171 } else if let Some(key) = path.strip_prefix("sentry_tags.") {
172 self.sentry_tags.value()?.get_value(key)?
173 } else {
174 let rest = path.strip_prefix("measurements.")?;
175 let name = rest.strip_suffix(".value")?;
176 self.measurements
177 .value()?
178 .get(name)?
179 .value()?
180 .value
181 .value()?
182 .into()
183 }
184 }
185 });
186 }
187
188 let event_prefix = path.strip_prefix("event.")?;
191 Some(match event_prefix {
192 "release" => self.data.value()?.get_str(SENTRY__RELEASE)?.into(),
193 "environment" => self.data.value()?.get_str(SENTRY__ENVIRONMENT)?.into(),
194 "transaction" => self.data.value()?.get_str(SENTRY__SEGMENT__NAME)?.into(),
195 "contexts.browser.name" => self.data.value()?.get_str(BROWSER__NAME)?.into(),
196 _ => return None,
198 })
199 }
200}
201
202#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
204#[metastructure(trim = false, pii = "maybe")]
205pub struct SentryTags {
206 pub release: Annotated<String>,
207 #[metastructure(pii = "true")]
208 pub user: Annotated<String>,
209 #[metastructure(pii = "true", field = "user.id")]
210 pub user_id: Annotated<String>,
211 #[metastructure(pii = "true", field = "user.ip")]
212 pub user_ip: Annotated<String>,
213 #[metastructure(pii = "true", field = "user.username")]
214 pub user_username: Annotated<String>,
215 #[metastructure(pii = "true", field = "user.email")]
216 pub user_email: Annotated<String>,
217 pub environment: Annotated<String>,
218 #[metastructure(field = "transaction.method")]
219 pub transaction_method: Annotated<String>,
220 #[metastructure(field = "transaction.op")]
221 pub transaction_op: Annotated<String>,
222 #[metastructure(field = "browser.name")]
223 pub browser_name: Annotated<String>,
224 #[metastructure(field = "sdk.name")]
225 pub sdk_name: Annotated<String>,
226 #[metastructure(field = "sdk.version")]
227 pub sdk_version: Annotated<String>,
228 pub platform: Annotated<String>,
229 pub mobile: Annotated<String>,
231 #[metastructure(field = "device.class")]
232 pub device_class: Annotated<String>,
233 #[metastructure(field = "device.family")]
234 pub device_family: Annotated<String>,
235 #[metastructure(field = "device.arch")]
236 pub device_arch: Annotated<String>,
237 #[metastructure(field = "device.battery_level")]
238 pub device_battery_level: Annotated<String>,
239 #[metastructure(field = "device.brand")]
240 pub device_brand: Annotated<String>,
241 #[metastructure(field = "device.charging")]
242 pub device_charging: Annotated<String>,
243 #[metastructure(field = "device.locale")]
244 pub device_locale: Annotated<String>,
245 #[metastructure(field = "device.model_id")]
246 pub device_model_id: Annotated<String>,
247 #[metastructure(field = "device.name")]
248 pub device_name: Annotated<String>,
249 #[metastructure(field = "device.online")]
250 pub device_online: Annotated<String>,
251 #[metastructure(field = "device.orientation")]
252 pub device_orientation: Annotated<String>,
253 #[metastructure(field = "device.screen_density")]
254 pub device_screen_density: Annotated<String>,
255 #[metastructure(field = "device.screen_dpi")]
256 pub device_screen_dpi: Annotated<String>,
257 #[metastructure(field = "device.screen_height_pixels")]
258 pub device_screen_height_pixels: Annotated<String>,
259 #[metastructure(field = "device.screen_width_pixels")]
260 pub device_screen_width_pixels: Annotated<String>,
261 #[metastructure(field = "device.simulator")]
262 pub device_simulator: Annotated<String>,
263 #[metastructure(field = "device.uuid")]
264 pub device_uuid: Annotated<String>,
265 #[metastructure(field = "app.device")]
266 pub app_device: Annotated<String>,
267 #[metastructure(field = "device.model")]
268 pub device_model: Annotated<String>,
269 pub runtime: Annotated<String>,
270 #[metastructure(field = "runtime.name")]
271 pub runtime_name: Annotated<String>,
272 pub browser: Annotated<String>,
273 pub os: Annotated<String>,
274 #[metastructure(field = "os.rooted")]
275 pub os_rooted: Annotated<String>,
276 #[metastructure(field = "gpu.name")]
277 pub gpu_name: Annotated<String>,
278 #[metastructure(field = "gpu.vendor")]
279 pub gpu_vendor: Annotated<String>,
280 #[metastructure(field = "monitor.id")]
281 pub monitor_id: Annotated<String>,
282 #[metastructure(field = "monitor.slug")]
283 pub monitor_slug: Annotated<String>,
284 #[metastructure(field = "request.url")]
285 pub request_url: Annotated<String>,
286 #[metastructure(field = "request.method")]
287 pub request_method: Annotated<String>,
288 #[metastructure(field = "os.name")]
290 pub os_name: Annotated<String>,
291 pub action: Annotated<String>,
292 pub category: Annotated<String>,
293 pub description: Annotated<String>,
294 pub domain: Annotated<String>,
295 pub raw_domain: Annotated<String>,
296 pub group: Annotated<String>,
297 #[metastructure(field = "http.decoded_response_content_length")]
298 pub http_decoded_response_content_length: Annotated<String>,
299 #[metastructure(field = "http.response_content_length")]
300 pub http_response_content_length: Annotated<String>,
301 #[metastructure(field = "http.response_transfer_size")]
302 pub http_response_transfer_size: Annotated<String>,
303 #[metastructure(field = "resource.render_blocking_status")]
304 pub resource_render_blocking_status: Annotated<String>,
305 pub op: Annotated<String>,
306 pub status: Annotated<String>,
307 pub status_code: Annotated<String>,
308 pub system: Annotated<String>,
309 pub ttid: Annotated<String>,
311 pub ttfd: Annotated<String>,
313 pub file_extension: Annotated<String>,
315 pub main_thread: Annotated<String>,
317 pub app_start_type: Annotated<String>,
319 pub replay_id: Annotated<String>,
320 #[metastructure(field = "cache.hit")]
321 pub cache_hit: Annotated<String>,
322 #[metastructure(field = "cache.key")]
323 pub cache_key: Annotated<String>,
324 #[metastructure(field = "trace.status")]
325 pub trace_status: Annotated<String>,
326 #[metastructure(field = "messaging.destination.name")]
327 pub messaging_destination_name: Annotated<String>,
328 #[metastructure(field = "messaging.message.id")]
329 pub messaging_message_id: Annotated<String>,
330 #[metastructure(field = "messaging.operation.name")]
331 pub messaging_operation_name: Annotated<String>,
332 #[metastructure(field = "messaging.operation.type")]
333 pub messaging_operation_type: Annotated<String>,
334 #[metastructure(field = "thread.name")]
335 pub thread_name: Annotated<String>,
336 #[metastructure(field = "thread.id")]
337 pub thread_id: Annotated<String>,
338 pub profiler_id: Annotated<String>,
339 #[metastructure(field = "user.geo.city")]
340 pub user_city: Annotated<String>,
341 #[metastructure(field = "user.geo.country_code")]
342 pub user_country_code: Annotated<String>,
343 #[metastructure(field = "user.geo.region")]
344 pub user_region: Annotated<String>,
345 #[metastructure(field = "user.geo.subdivision")]
346 pub user_subdivision: Annotated<String>,
347 #[metastructure(field = "user.geo.subregion")]
348 pub user_subregion: Annotated<String>,
349 pub name: Annotated<String>,
350 }
353
354impl Getter for SentryTags {
355 fn get_value(&self, path: &str) -> Option<Val<'_>> {
356 let value = match path {
357 "action" => &self.action,
358 "app_start_type" => &self.app_start_type,
359 "browser.name" => &self.browser_name,
360 "cache.hit" => &self.cache_hit,
361 "cache.key" => &self.cache_key,
362 "category" => &self.category,
363 "description" => &self.description,
364 "device.class" => &self.device_class,
365 "device.family" => &self.device_family,
366 "device.arch" => &self.device_arch,
367 "device.battery_level" => &self.device_battery_level,
368 "device.brand" => &self.device_brand,
369 "device.charging" => &self.device_charging,
370 "device.locale" => &self.device_locale,
371 "device.model_id" => &self.device_model_id,
372 "device.name" => &self.device_name,
373 "device.online" => &self.device_online,
374 "device.orientation" => &self.device_orientation,
375 "device.screen_density" => &self.device_screen_density,
376 "device.screen_dpi" => &self.device_screen_dpi,
377 "device.screen_height_pixels" => &self.device_screen_height_pixels,
378 "device.screen_width_pixels" => &self.device_screen_width_pixels,
379 "device.simulator" => &self.device_simulator,
380 "device.uuid" => &self.device_uuid,
381 "app.device" => &self.app_device,
382 "device.model" => &self.device_model,
383 "runtime" => &self.runtime,
384 "runtime.name" => &self.runtime_name,
385 "browser" => &self.browser,
386 "os" => &self.os,
387 "os.rooted" => &self.os_rooted,
388 "gpu.name" => &self.gpu_name,
389 "gpu.vendor" => &self.gpu_vendor,
390 "monitor.id" => &self.monitor_id,
391 "monitor.slug" => &self.monitor_slug,
392 "request.url" => &self.request_url,
393 "request.method" => &self.request_method,
394 "domain" => &self.domain,
395 "environment" => &self.environment,
396 "file_extension" => &self.file_extension,
397 "group" => &self.group,
398 "http.decoded_response_content_length" => &self.http_decoded_response_content_length,
399 "http.response_content_length" => &self.http_response_content_length,
400 "http.response_transfer_size" => &self.http_response_transfer_size,
401 "main_thread" => &self.main_thread,
402 "messaging.destination.name" => &self.messaging_destination_name,
403 "messaging.message.id" => &self.messaging_message_id,
404 "messaging.operation.name" => &self.messaging_operation_name,
405 "messaging.operation.type" => &self.messaging_operation_type,
406 "mobile" => &self.mobile,
407 "name" => &self.name,
408 "op" => &self.op,
409 "os.name" => &self.os_name,
410 "platform" => &self.platform,
411 "profiler_id" => &self.profiler_id,
412 "raw_domain" => &self.raw_domain,
413 "release" => &self.release,
414 "replay_id" => &self.replay_id,
415 "resource.render_blocking_status" => &self.resource_render_blocking_status,
416 "sdk.name" => &self.sdk_name,
417 "sdk.version" => &self.sdk_version,
418 "status_code" => &self.status_code,
419 "status" => &self.status,
420 "system" => &self.system,
421 "thread.id" => &self.thread_id,
422 "thread.name" => &self.thread_name,
423 "trace.status" => &self.trace_status,
424 "transaction.method" => &self.transaction_method,
425 "transaction.op" => &self.transaction_op,
426 "ttfd" => &self.ttfd,
427 "ttid" => &self.ttid,
428 "user.email" => &self.user_email,
429 "user.geo.city" => &self.user_city,
430 "user.geo.country_code" => &self.user_country_code,
431 "user.geo.region" => &self.user_region,
432 "user.geo.subdivision" => &self.user_subdivision,
433 "user.geo.subregion" => &self.user_subregion,
434 "user.id" => &self.user_id,
435 "user.ip" => &self.user_ip,
436 "user.username" => &self.user_username,
437 "user" => &self.user,
438 _ => return None,
439 };
440 Some(value.as_str()?.into())
441 }
442}
443
444fn span_data_pii_from_conventions(state: &ProcessingState) -> Pii {
449 fn inner(state: &ProcessingState) -> Option<Pii> {
450 let key = state.keys().next()?;
453
454 match relay_conventions::attribute_info(key)?.apply_scrubbing {
455 relay_conventions::ApplyScrubbing::Auto => Some(Pii::True),
456 relay_conventions::ApplyScrubbing::Never => Some(Pii::False),
457 relay_conventions::ApplyScrubbing::Manual => Some(Pii::Maybe),
458 }
459 }
460
461 inner(state).unwrap_or(Pii::True)
462}
463
464#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
469#[metastructure(trim = false, pii = "span_data_pii_from_conventions")]
470pub struct SpanData {
471 #[metastructure(
473 additional_properties,
474 retain = true,
475 skip_serialization = "null" )]
477 pub other: Object<Value>,
478}
479
480impl SpanData {
481 pub fn get(&self, key: &str) -> Option<&Annotated<Value>> {
483 self.other.get(key)
484 }
485
486 pub fn get_value(&self, key: &str) -> Option<&Value> {
488 self.get(key).and_then(Annotated::value)
489 }
490
491 pub fn get_str(&self, key: &str) -> Option<&str> {
493 self.get_value(key)?.as_str()
494 }
495
496 pub fn contains(&self, key: &str) -> bool {
498 self.other.contains_key(key)
499 }
500
501 pub fn insert(&mut self, key: impl Into<String>, value: Annotated<Value>) {
503 self.other.insert(key.into(), value);
504 }
505
506 pub fn insert_value<T>(&mut self, key: impl Into<String>, value: T)
508 where
509 T: IntoValue,
510 {
511 self.insert(key, Annotated::new(value.into_value()));
512 }
513
514 pub fn remove(&mut self, key: &str) -> Option<Annotated<Value>> {
516 self.other.remove(key)
517 }
518}
519
520impl Getter for SpanData {
521 fn get_value(&self, path: &str) -> Option<Val<'_>> {
522 let escaped = path.replace("\\.", "\0");
523 let mut path = escaped.split('.').map(|s| s.replace('\0', "."));
524 let root = path.next()?;
525
526 let mut val = self.get(&root)?.value()?;
527 for part in path {
528 let relay_protocol::Value::Object(map) = val else {
530 return None;
531 };
532 val = map.get(&part)?.value()?;
533 }
534 Some(val.into())
535 }
536}
537
538impl From<Object<Value>> for SpanData {
539 fn from(other: Object<Value>) -> Self {
540 Self { other }
541 }
542}
543
544impl<const N: usize> From<[(String, Annotated<Value>); N]> for SpanData {
545 fn from(value: [(String, Annotated<Value>); N]) -> Self {
546 Self::from(Object::from(value))
547 }
548}
549
550#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
552#[metastructure(trim = false)]
553pub struct SpanLink {
554 #[metastructure(required = true, trim = false)]
556 pub trace_id: Annotated<TraceId>,
557
558 #[metastructure(required = true, trim = false)]
560 pub span_id: Annotated<SpanId>,
561
562 #[metastructure(trim = false)]
564 pub sampled: Annotated<bool>,
565
566 #[metastructure(pii = "maybe", trim = false)]
568 pub attributes: Annotated<Object<Value>>,
569
570 #[metastructure(additional_properties, retain = true, pii = "maybe", trim = false)]
572 pub other: Object<Value>,
573}
574
575#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
577pub struct Route {
578 #[metastructure(pii = "maybe", skip_serialization = "empty")]
580 pub name: Annotated<String>,
581
582 #[metastructure(
584 pii = "true",
585 skip_serialization = "empty",
586 max_depth = 5,
587 max_bytes = 2048
588 )]
589 pub params: Annotated<Object<Value>>,
590
591 #[metastructure(
593 additional_properties,
594 retain = true,
595 pii = "maybe",
596 skip_serialization = "empty"
597 )]
598 pub other: Object<Value>,
599}
600
601impl FromValue for Route {
602 fn from_value(value: Annotated<Value>) -> Annotated<Self>
603 where
604 Self: Sized,
605 {
606 match value {
607 Annotated(Some(Value::String(name)), meta) => Annotated(
608 Some(Route {
609 name: Annotated::new(name),
610 ..Default::default()
611 }),
612 meta,
613 ),
614 Annotated(Some(Value::Object(mut values)), meta) => {
615 let mut route: Route = Default::default();
616 if let Some(Annotated(Some(Value::String(name)), _)) = values.remove("name") {
617 route.name = Annotated::new(name);
618 }
619 if let Some(Annotated(Some(Value::Object(params)), _)) = values.remove("params") {
620 route.params = Annotated::new(params);
621 }
622
623 if !values.is_empty() {
624 route.other = values;
625 }
626
627 Annotated(Some(route), meta)
628 }
629 Annotated(None, meta) => Annotated(None, meta),
630 Annotated(Some(value), mut meta) => {
631 meta.add_error(Error::expected("route expected to be an object"));
632 meta.set_original_value(Some(value));
633 Annotated(None, meta)
634 }
635 }
636 }
637}
638
639#[derive(Clone, Debug, PartialEq, ProcessValue, Default)]
644pub enum SpanKind {
645 #[default]
647 Internal,
648 Server,
650 Client,
652 Producer,
654 Consumer,
656 Unknown(String),
658}
659
660impl SpanKind {
661 pub fn as_str(&self) -> &str {
662 match self {
663 Self::Internal => "internal",
664 Self::Server => "server",
665 Self::Client => "client",
666 Self::Producer => "producer",
667 Self::Consumer => "consumer",
668 Self::Unknown(s) => s.as_str(),
669 }
670 }
671}
672
673impl Empty for SpanKind {
674 fn is_empty(&self) -> bool {
675 false
676 }
677}
678
679#[derive(Debug)]
680pub struct ParseSpanKindError;
681
682impl std::str::FromStr for SpanKind {
683 type Err = ParseSpanKindError;
684
685 fn from_str(s: &str) -> Result<Self, Self::Err> {
686 Ok(match s {
687 "internal" => SpanKind::Internal,
688 "server" => SpanKind::Server,
689 "client" => SpanKind::Client,
690 "producer" => SpanKind::Producer,
691 "consumer" => SpanKind::Consumer,
692 other => SpanKind::Unknown(other.to_owned()),
693 })
694 }
695}
696
697impl fmt::Display for SpanKind {
698 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699 write!(f, "{}", self.as_str())
700 }
701}
702
703impl FromValue for SpanKind {
704 fn from_value(value: Annotated<Value>) -> Annotated<Self>
705 where
706 Self: Sized,
707 {
708 match value {
709 Annotated(Some(Value::String(s)), meta) => Annotated(SpanKind::from_str(&s).ok(), meta),
710 Annotated(_, meta) => Annotated(None, meta),
711 }
712 }
713}
714
715impl IntoValue for SpanKind {
716 fn into_value(self) -> Value
717 where
718 Self: Sized,
719 {
720 Value::String(self.to_string())
721 }
722
723 fn serialize_payload<S>(
724 &self,
725 s: S,
726 _behavior: relay_protocol::SkipSerialization,
727 ) -> Result<S::Ok, S::Error>
728 where
729 Self: Sized,
730 S: serde::Serializer,
731 {
732 s.serialize_str(self.as_str())
733 }
734}
735
736#[cfg(test)]
737mod tests {
738 use crate::protocol::Measurement;
739 use chrono::{TimeZone, Utc};
740 use relay_base_schema::metrics::{InformationUnit, MetricUnit};
741 use relay_conventions::attributes::*;
742 use relay_protocol::RuleCondition;
743 use similar_asserts::assert_eq;
744
745 use super::*;
746
747 #[test]
753 fn test_span_data_attributes_follow_sentry_conventions() {
754 let my_trace = &"my_trace".to_owned();
755 let my_transaction = &"my_transaction".to_owned();
756 let my_project_id = &"my_project_id".to_owned();
757 let json = format!(
758 r#"{{
759 "{SENTRY__DSC__TRACE_ID}": "{my_trace}",
760 "{SENTRY__DSC__TRANSACTION}": "{my_transaction}",
761 "{SENTRY__DSC__PROJECT_ID}": "{my_project_id}"
762 }}"#,
763 );
764 let data = Annotated::<SpanData>::from_json(&json).unwrap();
765 let data = data.value().unwrap();
766 assert_eq!(data.get_str(SENTRY__DSC__TRACE_ID), Some(my_trace.as_str()));
767 assert_eq!(
768 data.get_str(SENTRY__DSC__TRANSACTION),
769 Some(my_transaction.as_str())
770 );
771 assert_eq!(
772 data.get_str(SENTRY__DSC__PROJECT_ID),
773 Some(my_project_id.as_str())
774 );
775 }
776
777 #[test]
778 fn test_span_serialization() {
779 let json = r#"{
780 "timestamp": 0.0,
781 "start_timestamp": -63158400.0,
782 "exclusive_time": 1.23,
783 "op": "operation",
784 "span_id": "fa90fdead5f74052",
785 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
786 "status": "ok",
787 "description": "desc",
788 "origin": "auto.http",
789 "links": [
790 {
791 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
792 "span_id": "fa90fdead5f74052",
793 "sampled": true,
794 "attributes": {
795 "boolAttr": true,
796 "numAttr": 123,
797 "stringAttr": "foo"
798 }
799 }
800 ],
801 "measurements": {
802 "memory": {
803 "value": 9001.0,
804 "unit": "byte"
805 }
806 },
807 "kind": "server"
808}"#;
809 let mut measurements = Object::new();
810 measurements.insert(
811 "memory".into(),
812 Annotated::new(Measurement {
813 value: Annotated::new(9001.0.try_into().unwrap()),
814 unit: Annotated::new(MetricUnit::Information(InformationUnit::Byte)),
815 }),
816 );
817
818 let links = Annotated::new(vec![Annotated::new(SpanLink {
819 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
820 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
821 sampled: Annotated::new(true),
822 attributes: Annotated::new({
823 let mut map: std::collections::BTreeMap<String, Annotated<Value>> = Object::new();
824 map.insert(
825 "stringAttr".into(),
826 Annotated::new(Value::String("foo".into())),
827 );
828 map.insert("numAttr".into(), Annotated::new(Value::I64(123)));
829 map.insert("boolAttr".into(), Value::Bool(true).into());
830 map
831 }),
832 ..Default::default()
833 })]);
834
835 let span = Annotated::new(Span {
836 timestamp: Annotated::new(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap().into()),
837 start_timestamp: Annotated::new(
838 Utc.with_ymd_and_hms(1968, 1, 1, 0, 0, 0).unwrap().into(),
839 ),
840 exclusive_time: Annotated::new(1.23),
841 description: Annotated::new("desc".to_owned()),
842 op: Annotated::new("operation".to_owned()),
843 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
844 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
845 status: Annotated::new(SpanStatus::Ok),
846 origin: Annotated::new("auto.http".to_owned()),
847 kind: Annotated::new(SpanKind::Server),
848 measurements: Annotated::new(Measurements(measurements)),
849 links,
850 ..Default::default()
851 });
852 assert_eq!(json, span.to_json_pretty().unwrap());
853
854 let span_from_string = Annotated::from_json(json).unwrap();
855 assert_eq!(span, span_from_string);
856 }
857
858 #[test]
859 fn test_getter_span_data() {
860 let span = Annotated::<Span>::from_json(
861 r#"{
862 "data": {
863 "foo": {"bar": 1},
864 "foo.bar": 2
865 },
866 "measurements": {
867 "some": {"value": 100.0}
868 }
869 }"#,
870 )
871 .unwrap()
872 .into_value()
873 .unwrap();
874
875 assert_eq!(span.get_value("span.data.foo.bar"), Some(Val::I64(1)));
876 assert_eq!(span.get_value(r"span.data.foo\.bar"), Some(Val::I64(2)));
877
878 assert_eq!(span.get_value("span.data"), None);
879 assert_eq!(span.get_value("span.data."), None);
880 assert_eq!(span.get_value("span.data.x"), None);
881
882 assert_eq!(
883 span.get_value("span.measurements.some.value"),
884 Some(Val::F64(100.0))
885 );
886 }
887
888 #[test]
889 fn test_getter_was_transaction() {
890 let mut span = Span::default();
891 assert_eq!(
892 span.get_value("span.was_transaction"),
893 Some(Val::Bool(false))
894 );
895 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
896 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
897
898 span.was_transaction.set_value(Some(false));
899 assert_eq!(
900 span.get_value("span.was_transaction"),
901 Some(Val::Bool(false))
902 );
903 assert!(RuleCondition::eq("span.was_transaction", false).matches(&span));
904 assert!(!RuleCondition::eq("span.was_transaction", true).matches(&span));
905
906 span.was_transaction.set_value(Some(true));
907 assert_eq!(
908 span.get_value("span.was_transaction"),
909 Some(Val::Bool(true))
910 );
911 assert!(RuleCondition::eq("span.was_transaction", true).matches(&span));
912 assert!(!RuleCondition::eq("span.was_transaction", false).matches(&span));
913 }
914
915 #[test]
916 fn test_span_fields_as_event() {
917 let span = Annotated::<Span>::from_json(
918 r#"{
919 "data": {
920 "sentry.release": "1.0",
921 "sentry.environment": "prod",
922 "sentry.segment.name": "/api/endpoint"
923 }
924 }"#,
925 )
926 .unwrap()
927 .into_value()
928 .unwrap();
929
930 assert_eq!(span.get_value("event.release"), Some(Val::String("1.0")));
931 assert_eq!(
932 span.get_value("event.environment"),
933 Some(Val::String("prod"))
934 );
935 assert_eq!(
936 span.get_value("event.transaction"),
937 Some(Val::String("/api/endpoint"))
938 );
939 }
940
941 #[test]
942 fn test_span_duration() {
943 let span = Annotated::<Span>::from_json(
944 r#"{
945 "start_timestamp": 1694732407.8367,
946 "timestamp": 1694732408.31451233
947 }"#,
948 )
949 .unwrap()
950 .into_value()
951 .unwrap();
952
953 assert_eq!(span.get_value("span.duration"), Some(Val::F64(477.812)));
954 }
955
956 #[test]
957 fn test_span_data() {
958 let data = r#"{
959 "foo": 2,
960 "bar": "3",
961 "db.system.name": "mysql",
962 "code.filepath": "task.py",
963 "code.lineno": 123,
964 "code.function": "fn()",
965 "code.namespace": "ns",
966 "frames.slow": 1,
967 "frames.frozen": 2,
968 "frames.total": 9,
969 "frames.delay": 100,
970 "messaging.destination.name": "default",
971 "messaging.message.retry.count": 3,
972 "messaging.message.receive.latency": 40,
973 "messaging.message.body.size": 100,
974 "messaging.message.id": "abc123",
975 "messaging.operation.name": "publish",
976 "messaging.operation.type": "create",
977 "user_agent.original": "Chrome",
978 "url.full": "my_url.com",
979 "client.address": "192.168.0.1"
980 }"#;
981 let mut data = Annotated::<SpanData>::from_json(data)
982 .unwrap()
983 .into_value()
984 .unwrap();
985 insta::assert_debug_snapshot!(data, @r###"
986 SpanData {
987 other: {
988 "bar": String(
989 "3",
990 ),
991 "client.address": String(
992 "192.168.0.1",
993 ),
994 "code.filepath": String(
995 "task.py",
996 ),
997 "code.function": String(
998 "fn()",
999 ),
1000 "code.lineno": I64(
1001 123,
1002 ),
1003 "code.namespace": String(
1004 "ns",
1005 ),
1006 "db.system.name": String(
1007 "mysql",
1008 ),
1009 "foo": I64(
1010 2,
1011 ),
1012 "frames.delay": I64(
1013 100,
1014 ),
1015 "frames.frozen": I64(
1016 2,
1017 ),
1018 "frames.slow": I64(
1019 1,
1020 ),
1021 "frames.total": I64(
1022 9,
1023 ),
1024 "messaging.destination.name": String(
1025 "default",
1026 ),
1027 "messaging.message.body.size": I64(
1028 100,
1029 ),
1030 "messaging.message.id": String(
1031 "abc123",
1032 ),
1033 "messaging.message.receive.latency": I64(
1034 40,
1035 ),
1036 "messaging.message.retry.count": I64(
1037 3,
1038 ),
1039 "messaging.operation.name": String(
1040 "publish",
1041 ),
1042 "messaging.operation.type": String(
1043 "create",
1044 ),
1045 "url.full": String(
1046 "my_url.com",
1047 ),
1048 "user_agent.original": String(
1049 "Chrome",
1050 ),
1051 },
1052 }
1053 "###);
1054
1055 assert_eq!(
1056 data.get("foo").and_then(Annotated::value),
1057 Some(&Value::I64(2))
1058 );
1059 assert_eq!(data.get_value("foo"), Some(&Value::I64(2)));
1060 assert_eq!(data.get_str("bar"), Some("3"));
1061 assert_eq!(data.get_str("foo"), None);
1062 data.insert("bool", Annotated::new(Value::Bool(true)));
1063 assert_eq!(data.get_value("bool"), Some(&Value::Bool(true)));
1064 data.insert_value("string", "value".to_owned());
1065 assert_eq!(data.get_str("string"), Some("value"));
1066 assert!(data.contains("string"));
1067 assert_eq!(
1068 data.remove("string").and_then(Annotated::into_value),
1069 Some(Value::String("value".to_owned()))
1070 );
1071 assert!(!data.contains("string"));
1072 assert_eq!(
1073 Getter::get_value(&data, "db\\.system\\.name"),
1074 Some(Val::String("mysql"))
1075 );
1076 assert_eq!(
1077 Getter::get_value(&data, "code\\.lineno"),
1078 Some(Val::U64(123))
1079 );
1080 assert_eq!(
1081 Getter::get_value(&data, "code\\.function"),
1082 Some(Val::String("fn()"))
1083 );
1084 assert_eq!(
1085 Getter::get_value(&data, "code\\.namespace"),
1086 Some(Val::String("ns"))
1087 );
1088 assert_eq!(data.get_value("unknown"), None);
1089 }
1090
1091 #[test]
1092 fn test_span_data_empty_well_known_field() {
1093 let span = r#"{
1094 "data": {
1095 "lcp.url": ""
1096 }
1097 }"#;
1098 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1099 assert_eq!(span.to_json().unwrap(), r#"{"data":{"lcp.url":""}}"#);
1100 }
1101
1102 #[test]
1103 fn test_span_data_empty_custom_field() {
1104 let span = r#"{
1105 "data": {
1106 "custom_field_empty": ""
1107 }
1108 }"#;
1109 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1110 assert_eq!(
1111 span.to_json().unwrap(),
1112 r#"{"data":{"custom_field_empty":""}}"#
1113 );
1114 }
1115
1116 #[test]
1117 fn test_span_data_completely_empty() {
1118 let span = r#"{
1119 "data": {}
1120 }"#;
1121 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1122 assert_eq!(span.to_json().unwrap(), r#"{"data":{}}"#);
1123 }
1124
1125 #[test]
1126 fn test_span_links() {
1127 let span = r#"{
1128 "links": [
1129 {
1130 "trace_id": "5c79f60c11214eb38604f4ae0781bfb2",
1131 "span_id": "ab90fdead5f74052",
1132 "sampled": true,
1133 "attributes": {
1134 "sentry.link.type": "previous_trace"
1135 }
1136 },
1137 {
1138 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1139 "span_id": "fa90fdead5f74052",
1140 "sampled": true,
1141 "attributes": {
1142 "sentry.link.type": "next_trace"
1143 }
1144 }
1145 ]
1146 }"#;
1147
1148 let span: Annotated<Span> = Annotated::from_json(span).unwrap();
1149 assert_eq!(
1150 span.to_json().unwrap(),
1151 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"}}]}"#
1152 );
1153 }
1154
1155 #[test]
1156 fn test_span_kind() {
1157 let span = Annotated::<Span>::from_json(
1158 r#"{
1159 "kind": "???"
1160 }"#,
1161 )
1162 .unwrap()
1163 .into_value()
1164 .unwrap();
1165 assert_eq!(
1166 span.kind.value().unwrap(),
1167 &SpanKind::Unknown("???".to_owned())
1168 );
1169 }
1170}