1use std::borrow::Cow;
6use std::net::IpAddr;
7
8use chrono::{DateTime, Utc};
9use relay_common::time::UnixTimestamp;
10use relay_conventions::attributes::*;
11use relay_conventions::{AttributeInfo, ReplacementName, WriteBehavior};
12use relay_event_schema::protocol::{Attribute, AttributeType, Attributes, BrowserContext, Geo};
13use relay_protocol::{Annotated, Error, ErrorKind, Meta, Remark, RemarkType, Value};
14use relay_spans::derive_op_for_v2_span;
15
16use crate::span::TABLE_NAME_REGEX;
17use crate::span::description::{scrub_db_query, scrub_http};
18use crate::span::tag_extraction::{
19 domain_from_scrubbed_http, domain_from_server_address, span_op_to_category,
20 sql_action_from_query, sql_tables_from_query,
21};
22use crate::{ClientHints, EnrichedDsc, FromUserAgentInfo as _, RawUserAgentInfo};
23
24mod ai;
25mod mobile;
26mod size;
27pub mod time;
28pub mod trace_metric;
29mod trimming;
30
31pub use self::ai::normalize_ai;
32pub use self::mobile::{normalize_mobile_attributes, normalize_mobile_measurements};
33pub use self::size::*;
34pub use self::trimming::TrimmingProcessor;
35
36pub fn normalize_sentry_op(attributes: &mut Annotated<Attributes>) {
38 if attributes
39 .value()
40 .is_some_and(|attrs| attrs.contains_key(SENTRY__OP))
41 {
42 return;
43 }
44 let inferred_op = derive_op_for_v2_span(attributes);
45 let attrs = attributes.get_or_insert_with(Default::default);
46 attrs.insert_if_missing(SENTRY__OP, || inferred_op);
47}
48
49pub fn normalize_span_category(attributes: &mut Annotated<Attributes>) {
53 let Some(attributes_val) = attributes.value() else {
54 return;
55 };
56
57 if attribute_is_nonempty_string(attributes_val, SENTRY__CATEGORY) {
59 return;
60 }
61
62 if let Some(op_value) = attributes_val.get_value(SENTRY__OP)
64 && let Some(op_str) = op_value.as_str()
65 {
66 let op_lowercase = op_str.to_lowercase();
67 if let Some(category) = span_op_to_category(&op_lowercase) {
68 let attrs = attributes.get_or_insert_with(Default::default);
69 attrs.insert(SENTRY__CATEGORY, category.to_owned());
70 return;
71 }
72 }
73
74 let category = if attribute_is_nonempty_string(attributes_val, DB__SYSTEM__NAME) {
76 Some("db")
77 } else if attribute_is_nonempty_string(attributes_val, HTTP__REQUEST__METHOD) {
78 Some("http")
79 } else if attribute_is_nonempty_string(attributes_val, UI__COMPONENT_NAME) {
80 Some("ui")
81 } else if attribute_is_nonempty_string(attributes_val, RESOURCE__RENDER_BLOCKING_STATUS) {
82 Some("resource")
83 } else if attributes_val
84 .get_value(SENTRY__ORIGIN)
85 .and_then(|v| v.as_str())
86 .is_some_and(|v| v == "auto.ui.browser.metrics")
87 {
88 Some("browser")
89 } else {
90 None
91 };
92
93 if let Some(category) = category {
95 let attrs = attributes.get_or_insert_with(Default::default);
96 attrs.insert(SENTRY__CATEGORY, category.to_owned());
97 }
98}
99
100fn attribute_is_nonempty_string(attributes: &Attributes, key: &str) -> bool {
101 attributes
102 .get_value(key)
103 .and_then(|v| v.as_str())
104 .is_some_and(|s| !s.is_empty())
105}
106
107pub fn normalize_attribute_types(attributes: &mut Annotated<Attributes>) {
112 let Some(attributes) = attributes.value_mut() else {
113 return;
114 };
115
116 let attributes = attributes.0.values_mut();
117 for attribute in attributes {
118 use AttributeType::*;
119
120 let Some(inner) = attribute.value_mut() else {
121 continue;
122 };
123
124 match (&mut inner.value.ty, &mut inner.value.value) {
125 (Annotated(Some(Boolean), _), Annotated(Some(Value::Bool(_)), _)) => (),
126 (Annotated(Some(Integer), _), Annotated(Some(Value::I64(_)), _)) => (),
127 (Annotated(Some(Integer), _), Annotated(Some(Value::U64(u)), _))
128 if i64::try_from(*u).is_ok() => {}
129 (Annotated(Some(Double), _), Annotated(Some(Value::I64(_)), _)) => (),
130 (Annotated(Some(Double), _), Annotated(Some(Value::U64(_)), _)) => (),
131 (Annotated(Some(Double), _), Annotated(Some(Value::F64(_)), _)) => (),
132 (Annotated(Some(String), _), Annotated(Some(Value::String(_)), _)) => (),
133 (Annotated(Some(Array), _), Annotated(Some(Value::Array(arr)), _)) => {
134 if !is_supported_array(arr) {
135 let _ = attribute.value_mut().take();
136 attribute.meta_mut().add_error(ErrorKind::InvalidData);
137 }
138 }
139 (Annotated(Some(Unknown(_)), _), _) => {
145 let original = attribute.value_mut().take();
146 attribute.meta_mut().add_error(ErrorKind::InvalidData);
147 attribute.meta_mut().set_original_value(original);
148 }
149 (Annotated(Some(_), _), Annotated(Some(_), _)) => {
150 let original = attribute.value_mut().take();
151 attribute.meta_mut().add_error(ErrorKind::InvalidData);
152 attribute.meta_mut().set_original_value(original);
153 }
154 (Annotated(None, _), _) | (_, Annotated(None, _)) => {
155 let original = attribute.value_mut().take();
156 attribute.meta_mut().add_error(ErrorKind::MissingAttribute);
157 attribute.meta_mut().set_original_value(original);
158 }
159 }
160 }
161}
162
163fn is_supported_array(arr: &[Annotated<Value>]) -> bool {
167 let mut iter = arr.iter();
168
169 let Some(first) = iter.next() else {
170 return true;
172 };
173
174 let item = iter.try_fold(first, |prev, current| {
175 let r = match (prev.value(), current.value()) {
176 (None, None) => prev,
177 (None, Some(_)) => current,
178 (Some(_), None) => prev,
179 (Some(Value::String(_)), Some(Value::String(_))) => prev,
180 (Some(Value::Bool(_)), Some(Value::Bool(_))) => prev,
181 (
182 Some(Value::I64(_) | Value::U64(_) | Value::F64(_)),
184 Some(Value::I64(_) | Value::U64(_) | Value::F64(_)),
185 ) => prev,
186 (Some(_), Some(_)) => return None,
190 };
191
192 Some(r)
193 });
194
195 let Some(item) = item else {
196 return false;
198 };
199
200 matches!(
201 item.value(),
202 None | Some(
205 Value::String(_) | Value::Bool(_) | Value::I64(_) | Value::U64(_) | Value::F64(_)
206 )
207 )
208}
209
210pub fn normalize_received(attributes: &mut Annotated<Attributes>, received: DateTime<Utc>) {
212 attributes
213 .get_or_insert_with(Default::default)
214 .insert_if_missing(SENTRY__OBSERVED_TIMESTAMP_NANOS, || {
215 received
216 .timestamp_nanos_opt()
217 .unwrap_or_else(|| UnixTimestamp::now().as_nanos() as i64)
218 .to_string()
219 });
220}
221
222#[derive(Debug, Copy, Clone, Default)]
226pub struct ClientUserAgentInfo<'a> {
227 pub user_agent: Option<&'a str>,
229 pub hints: ClientHints<&'a str>,
231}
232
233pub fn normalize_user_agent(
241 attributes: &mut Annotated<Attributes>,
242 client_info: Option<ClientUserAgentInfo<'_>>,
243) {
244 let attributes = attributes.get_or_insert_with(Default::default);
245
246 if attributes.contains_key(BROWSER__NAME) || attributes.contains_key(BROWSER__VERSION) {
247 return;
248 }
249
250 if let Some(ua) = client_info.and_then(|ci| ci.user_agent) {
252 attributes.insert_if_missing(USER_AGENT__ORIGINAL, || ua.to_owned());
253 }
254
255 let user_agent = attributes
256 .get_value(USER_AGENT__ORIGINAL)
257 .and_then(|v| v.as_str());
258
259 let Some(context) = BrowserContext::from_hints_or_ua(&RawUserAgentInfo {
260 user_agent,
261 client_hints: client_info.map(|ci| ci.hints).unwrap_or_default(),
262 }) else {
263 return;
264 };
265
266 attributes.insert_if_missing(BROWSER__NAME, || context.name);
267 attributes.insert_if_missing(BROWSER__VERSION, || context.version);
268}
269
270pub fn normalize_client_address(attributes: &mut Annotated<Attributes>, client_ip: Option<IpAddr>) {
278 let Some(attributes) = attributes.value_mut() else {
279 return;
280 };
281
282 let client_address = attributes
283 .get_value(CLIENT__ADDRESS)
284 .and_then(|v| v.as_str());
285
286 if client_address == Some("{{auto}}") {
287 match client_ip {
288 Some(client_ip) => attributes.insert(CLIENT__ADDRESS, client_ip.to_string()),
289 None => drop(attributes.remove(CLIENT__ADDRESS)),
290 }
291 }
292}
293
294pub fn normalize_inject_client_address(
298 attributes: &mut Annotated<Attributes>,
299 client_ip: Option<IpAddr>,
300) {
301 let Some(client_ip) = client_ip else {
302 return;
303 };
304
305 let attributes = attributes.get_or_insert_with(Default::default);
306 attributes.insert_if_missing(CLIENT__ADDRESS, || client_ip.to_string());
307}
308
309pub fn normalize_user_geo(
317 attributes: &mut Annotated<Attributes>,
318 info: impl FnOnce(IpAddr) -> Option<Geo>,
319) {
320 let Some(attributes) = attributes.value_mut() else {
321 return;
322 };
323
324 if [
325 USER__GEO__COUNTRY_CODE,
326 USER__GEO__CITY,
327 USER__GEO__SUBDIVISION,
328 USER__GEO__REGION,
329 ]
330 .into_iter()
331 .any(|a| attributes.contains_key(a))
332 {
333 return;
334 }
335
336 let client_address = attributes
337 .get_value(CLIENT__ADDRESS)
338 .and_then(|v| v.as_str())
339 .and_then(|v| v.parse().ok());
340
341 let Some(geo) = client_address.and_then(info) else {
342 return;
343 };
344
345 attributes.insert_if_missing(USER__GEO__COUNTRY_CODE, || geo.country_code);
346 attributes.insert_if_missing(USER__GEO__CITY, || geo.city);
347 attributes.insert_if_missing(USER__GEO__SUBDIVISION, || geo.subdivision);
348 attributes.insert_if_missing(USER__GEO__REGION, || geo.region);
349}
350
351pub fn normalize_dsc(
357 attributes: &mut Annotated<Attributes>,
358 is_segment: &Annotated<bool>,
359 dsc: Option<EnrichedDsc>,
360) {
361 let Some(dsc) = dsc else {
362 return;
363 };
364
365 let attributes = attributes.get_or_insert_with(Default::default);
366
367 if attributes.contains_key(SENTRY__DSC__TRACE_ID) {
369 return;
370 }
371 attributes.insert(SENTRY__DSC__TRACE_ID, dsc.dsc.trace_id.to_string());
372
373 if let Some(transaction) = &dsc.dsc.transaction {
374 attributes.insert(SENTRY__DSC__TRANSACTION, transaction.clone());
375 }
376
377 attributes.insert(SENTRY__DSC__PROJECT_ID, dsc.sampling_project_id.to_string());
378
379 if is_segment.value().is_some_and(|is_segment| *is_segment) {
380 attributes.insert(SENTRY__DSC__PUBLIC_KEY, dsc.dsc.public_key.to_string());
381 if let Some(release) = &dsc.dsc.release {
382 attributes.insert(SENTRY__DSC__RELEASE, release.clone());
383 }
384 if let Some(environment) = &dsc.dsc.environment {
385 attributes.insert(SENTRY__DSC__ENVIRONMENT, environment.clone());
386 }
387 if let Some(sample_rate) = dsc.dsc.sample_rate {
388 attributes.insert(SENTRY__DSC__SAMPLE_RATE, sample_rate);
389 }
390 if let Some(sampled) = dsc.dsc.sampled {
391 attributes.insert(SENTRY__DSC__SAMPLED, sampled);
392 }
393 }
394}
395
396pub fn normalize_client_sample_rate(attributes: &mut Annotated<Attributes>) {
400 let Some(attributes) = attributes.value_mut() else {
401 return;
402 };
403
404 fn normalize_sample_rate(sr: &Annotated<Attribute>) -> Option<Annotated<Attribute>> {
408 match sr.value()?.value.value.value()?.as_f64() {
409 Some(v) if v > 0.0 && v <= 1.0 => None,
410 _ => Some(Annotated::from_error(
412 Error::expected("sample rate > 0.0, <= 1.0"),
413 None,
414 )),
415 }
416 }
417
418 if let Some(sr) = attributes.0.get_mut(SENTRY__CLIENT_SAMPLE_RATE)
419 && let Some(new_sr) = normalize_sample_rate(sr)
420 {
421 *sr = new_sr;
422 }
423}
424
425pub fn normalize_attribute_names(attributes: &mut Annotated<Attributes>) {
434 normalize_attribute_names_inner(attributes, relay_conventions::attribute_info_with_fragment)
435}
436
437type AttributeInfoFn = fn(&str) -> Option<(&'static AttributeInfo, Option<&str>)>;
438
439fn normalize_attribute_names_inner(
440 attributes: &mut Annotated<Attributes>,
441 attribute_info: AttributeInfoFn,
442) {
443 let Some(attributes) = attributes.value_mut() else {
444 return;
445 };
446
447 let attribute_names: Vec<_> = attributes.0.keys().cloned().collect();
448
449 for name in attribute_names {
450 let Some((attribute_info, fragment)) = attribute_info(&name) else {
451 continue;
452 };
453
454 match attribute_info.write_behavior {
455 WriteBehavior::CurrentName => continue,
456 WriteBehavior::NewName(new_name) => {
457 let Some(old_attribute) = attributes.0.get_mut(&name) else {
458 continue;
459 };
460
461 let Some(new_name) = resolve_attribute_name(new_name, fragment) else {
462 relay_log::error!(
463 attribute = name,
464 ?fragment,
465 "Attribute placeholder mismatch"
466 );
467 continue;
468 };
469
470 let mut meta = Meta::default();
471 meta.add_remark(Remark::new(RemarkType::Removed, "attribute.deprecated"));
473 let new_attribute = std::mem::replace(old_attribute, Annotated(None, meta));
474
475 if !attributes.contains_key(&*new_name) {
476 attributes.0.insert(new_name.into_owned(), new_attribute);
477 }
478 }
479 WriteBehavior::BothNames(new_name) => {
480 let Some(new_name) = resolve_attribute_name(new_name, fragment) else {
481 relay_log::error!(
482 attribute = name,
483 ?fragment,
484 "Attribute placeholder mismatch"
485 );
486 continue;
487 };
488
489 if !attributes.contains_key(&*new_name)
490 && let Some(current_attribute) = attributes.0.get(&name).cloned()
491 {
492 attributes
493 .0
494 .insert(new_name.into_owned(), current_attribute);
495 }
496 }
497 }
498 }
499}
500
501fn resolve_attribute_name(
509 name: ReplacementName,
510 fragment: Option<&str>,
511) -> Option<Cow<'static, str>> {
512 match (name, fragment) {
513 (ReplacementName::Static(name), None) => Some(Cow::Borrowed(name)),
516 (ReplacementName::Dynamic(name_fn), Some(fragment)) => Some(Cow::Owned(name_fn(fragment))),
520 _ => None,
524 }
525}
526
527pub fn normalize_attribute_values(
536 attributes: &mut Annotated<Attributes>,
537 http_span_allowed_hosts: &[String],
538) {
539 normalize_db_attributes(attributes);
540 normalize_http_attributes(attributes, http_span_allowed_hosts);
541 normalize_mobile_attributes(attributes);
542}
543
544fn normalize_db_attributes(annotated_attributes: &mut Annotated<Attributes>) {
553 let Some(attributes) = annotated_attributes.value() else {
554 return;
555 };
556
557 if attributes.get_value(SENTRY__NORMALIZED_DB_QUERY).is_some() {
559 return;
560 }
561
562 let (op, sub_op) = attributes
563 .get_value(SENTRY__OP)
564 .and_then(|v| v.as_str())
565 .map(|op| op.split_once('.').unwrap_or((op, "")))
566 .unwrap_or_default();
567
568 let raw_query = attributes
569 .get_value(DB__QUERY__TEXT)
570 .or_else(|| {
571 if op == "db" {
572 attributes.get_value(SENTRY__DESCRIPTION)
573 } else {
574 None
575 }
576 })
577 .and_then(|v| v.as_str());
578
579 let db_system = attributes
580 .get_value(DB__SYSTEM__NAME)
581 .and_then(|v| v.as_str());
582
583 let db_operation = attributes
584 .get_value(DB__OPERATION__NAME)
585 .and_then(|v| v.as_str());
586
587 let collection_name = attributes
588 .get_value(DB__COLLECTION__NAME)
589 .and_then(|v| v.as_str());
590
591 let span_origin = attributes
592 .get_value(SENTRY__ORIGIN)
593 .and_then(|v| v.as_str());
594
595 let (normalized_db_query, parsed_sql) = if let Some(raw_query) = raw_query {
596 scrub_db_query(
597 raw_query,
598 sub_op,
599 db_system,
600 db_operation,
601 collection_name,
602 span_origin,
603 )
604 } else {
605 (None, None)
606 };
607
608 let db_operation = if db_operation.is_none() {
609 if sub_op == "redis" || db_system == Some("redis") {
610 if let Some(query) = normalized_db_query.as_ref() {
612 let command = query.replace(" *", "");
613 if command.is_empty() {
614 None
615 } else {
616 Some(command)
617 }
618 } else {
619 None
620 }
621 } else if let Some(raw_query) = raw_query {
622 sql_action_from_query(raw_query).map(|a| a.to_uppercase())
624 } else {
625 None
626 }
627 } else {
628 db_operation.map(|db_operation| db_operation.to_uppercase())
629 };
630
631 let db_collection_name: Option<String> = if let Some(name) = collection_name {
632 if db_system == Some("mongodb") {
633 match TABLE_NAME_REGEX.replace_all(name, "{%s}") {
634 Cow::Owned(s) => Some(s),
635 Cow::Borrowed(_) => Some(name.to_owned()),
636 }
637 } else {
638 Some(name.to_owned())
639 }
640 } else if span_origin == Some("auto.db.supabase") {
641 normalized_db_query
642 .as_ref()
643 .and_then(|query| query.strip_prefix("from("))
644 .and_then(|s| s.strip_suffix(")"))
645 .map(String::from)
646 } else if let Some(raw_query) = raw_query {
647 sql_tables_from_query(raw_query, &parsed_sql)
648 } else {
649 None
650 };
651
652 if let Some(attributes) = annotated_attributes.value_mut() {
653 if let Some(normalized_db_query) = normalized_db_query {
654 let mut normalized_db_query_hash = format!("{:x}", md5::compute(&normalized_db_query));
655 normalized_db_query_hash.truncate(16);
656
657 attributes.insert(SENTRY__NORMALIZED_DB_QUERY, normalized_db_query);
658 attributes.insert(SENTRY__NORMALIZED_DB_QUERY__HASH, normalized_db_query_hash);
659 }
660 if let Some(db_operation_name) = db_operation {
661 attributes.insert(DB__OPERATION__NAME, db_operation_name)
662 }
663 if let Some(db_collection_name) = db_collection_name {
664 attributes.insert(DB__COLLECTION__NAME, db_collection_name);
665 }
666 }
667}
668
669fn normalize_http_attributes(
674 annotated_attributes: &mut Annotated<Attributes>,
675 allowed_hosts: &[String],
676) {
677 let Some(attributes) = annotated_attributes.value() else {
678 return;
679 };
680
681 if attributes
683 .get_value(SENTRY__CATEGORY)
684 .is_none_or(|category| category.as_str().unwrap_or_default() != "http")
685 {
686 return;
687 }
688
689 let op = attributes.get_value(SENTRY__OP).and_then(|v| v.as_str());
690
691 let (description_method, description_url) = match attributes
692 .get_value(SENTRY__DESCRIPTION)
693 .and_then(|v| v.as_str())
694 .and_then(|description| description.split_once(' '))
695 {
696 Some((method, url)) => (Some(method), Some(url)),
697 _ => (None, None),
698 };
699
700 let method = attributes
701 .get_value(HTTP__REQUEST__METHOD)
702 .and_then(|v| v.as_str())
703 .or(description_method);
704
705 let server_address = attributes
706 .get_value(SERVER__ADDRESS)
707 .and_then(|v| v.as_str());
708
709 let url: Option<&str> = attributes
710 .get_value(URL__FULL)
711 .and_then(|v| v.as_str())
712 .or(description_url);
713 let url_scheme = attributes.get_value(URL__SCHEME).and_then(|v| v.as_str());
714
715 let (normalized_server_address, raw_url) = if op == Some("http.client") {
718 let domain_from_scrubbed_http = method
719 .zip(url)
720 .and_then(|(method, url)| scrub_http(method, url, allowed_hosts))
721 .and_then(|scrubbed_http| domain_from_scrubbed_http(&scrubbed_http));
722
723 if let Some(domain) = domain_from_scrubbed_http {
724 (Some(domain), url.map(String::from))
725 } else {
726 domain_from_server_address(server_address, url_scheme)
727 }
728 } else {
729 (None, None)
730 };
731
732 let method = method.map(|m| m.to_uppercase());
733
734 if let Some(attributes) = annotated_attributes.value_mut() {
735 if let Some(method) = method {
736 attributes.insert(HTTP__REQUEST__METHOD, method);
737 }
738
739 if let Some(normalized_server_address) = normalized_server_address {
740 attributes.insert(SERVER__ADDRESS, normalized_server_address);
741 }
742
743 if let Some(raw_url) = raw_url {
744 attributes.insert_if_missing(URL__FULL, || raw_url);
745 }
746 }
747}
748
749pub fn write_legacy_attributes(attributes: &mut Annotated<Attributes>) {
757 let Some(attributes) = attributes.value_mut() else {
758 return;
759 };
760
761 #[allow(
763 deprecated,
764 reason = "Writing possibly deprecated legacy attributes is the point of this function."
765 )]
766 let current_to_legacy_attributes = [
767 (DB__QUERY__TEXT, SENTRY__DESCRIPTION),
769 (SENTRY__NORMALIZED_DB_QUERY, SENTRY__NORMALIZED_DESCRIPTION),
770 (DB__OPERATION__NAME, SENTRY__ACTION),
771 (DB__SYSTEM__NAME, DB__SYSTEM),
772 (SERVER__ADDRESS, SENTRY__DOMAIN),
774 (HTTP__REQUEST__METHOD, SENTRY__ACTION),
775 (HTTP__RESPONSE__STATUS_CODE, SENTRY__STATUS_CODE),
776 (SENTRY__SEGMENT__NAME, SENTRY__TRANSACTION),
778 ];
779
780 for (current_attribute, legacy_attribute) in current_to_legacy_attributes {
781 if attributes.contains_key(current_attribute) {
782 let Some(attr) = attributes.get_attribute(current_attribute) else {
783 continue;
784 };
785 attributes.insert(legacy_attribute, attr.value.clone());
786 }
787 }
788
789 if !attributes.contains_key(SENTRY__DOMAIN)
790 && let Some(db_domain) = attributes
791 .get_value(DB__COLLECTION__NAME)
792 .and_then(|value| value.as_str())
793 .map(|collection_name| collection_name.to_owned())
794 {
795 attributes.insert(
797 SENTRY__DOMAIN,
798 match (db_domain.starts_with(','), db_domain.ends_with(',')) {
799 (true, true) => db_domain,
800 (true, false) => format!("{db_domain},"),
801 (false, true) => format!(",{db_domain}"),
802 (false, false) => format!(",{db_domain},"),
803 },
804 );
805 }
806
807 if let Some(&Value::String(method)) = attributes.get_value(HTTP__REQUEST__METHOD).as_ref()
808 && let Some(&Value::String(url)) = attributes.get_value(URL__FULL).as_ref()
809 {
810 attributes.insert(SENTRY__DESCRIPTION, format!("{method} {url}"))
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use std::time::Duration;
817
818 use relay_base_schema::project::ProjectId;
819 use relay_protocol::{Empty, SerializableAnnotated, assert_annotated_snapshot};
820 use relay_sampling::DynamicSamplingContext;
821
822 use super::*;
823
824 fn mock_dsc(transaction: Option<&str>) -> DynamicSamplingContext {
825 DynamicSamplingContext {
826 trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
827 public_key: "12345678901234567890123456789012".parse().unwrap(),
828 release: None,
829 environment: None,
830 transaction: transaction.map(str::to_owned),
831 sample_rate: None,
832 user: Default::default(),
833 replay_id: None,
834 sampled: None,
835 other: Default::default(),
836 }
837 }
838
839 #[test]
840 fn test_normalize_dsc_child_span_no_dsc() {
841 let mut attributes = Annotated::empty();
842 normalize_dsc(&mut attributes, &Annotated::new(false), None);
843 assert!(attributes.value().is_none());
844 }
845
846 #[test]
847 fn test_normalize_dsc_child_span_no_transaction() {
848 let mut attributes = Annotated::empty();
849 let dsc = &mock_dsc(None);
850 let sampling_project_id = ProjectId::new(42);
851 normalize_dsc(
852 &mut attributes,
853 &Annotated::new(false),
854 Some(EnrichedDsc {
855 dsc,
856 sampling_project_id,
857 }),
858 );
859 assert_annotated_snapshot!(attributes, @r#"
860 {
861 "sentry.dsc.project_id": {
862 "type": "string",
863 "value": "42"
864 },
865 "sentry.dsc.trace_id": {
866 "type": "string",
867 "value": "67e5504410b1426f9247bb680e5fe0c8"
868 }
869 }
870 "#);
871 }
872
873 #[test]
874 fn test_normalize_dsc_child_span() {
875 let mut attributes = Annotated::empty();
876 let dsc = &mock_dsc(Some("/some/endpoint"));
877 let sampling_project_id = ProjectId::new(42);
878 normalize_dsc(
879 &mut attributes,
880 &Annotated::new(false),
881 Some(EnrichedDsc {
882 dsc,
883 sampling_project_id,
884 }),
885 );
886 assert_annotated_snapshot!(attributes, @r#"
887 {
888 "sentry.dsc.project_id": {
889 "type": "string",
890 "value": "42"
891 },
892 "sentry.dsc.trace_id": {
893 "type": "string",
894 "value": "67e5504410b1426f9247bb680e5fe0c8"
895 },
896 "sentry.dsc.transaction": {
897 "type": "string",
898 "value": "/some/endpoint"
899 }
900 }
901 "#);
902 }
903
904 #[test]
905 fn test_normalize_dsc_segment() {
906 let mut attributes = Annotated::empty();
907 let dsc = &mock_dsc(Some("/some/endpoint"));
908 let sampling_project_id = ProjectId::new(42);
909 normalize_dsc(
910 &mut attributes,
911 &Annotated::new(true),
912 Some(EnrichedDsc {
913 dsc,
914 sampling_project_id,
915 }),
916 );
917 assert_annotated_snapshot!(attributes, @r#"
918 {
919 "sentry.dsc.project_id": {
920 "type": "string",
921 "value": "42"
922 },
923 "sentry.dsc.public_key": {
924 "type": "string",
925 "value": "12345678901234567890123456789012"
926 },
927 "sentry.dsc.trace_id": {
928 "type": "string",
929 "value": "67e5504410b1426f9247bb680e5fe0c8"
930 },
931 "sentry.dsc.transaction": {
932 "type": "string",
933 "value": "/some/endpoint"
934 }
935 }
936 "#);
937 }
938
939 #[test]
940 fn test_normalize_received_none() {
941 let mut attributes = Default::default();
942
943 normalize_received(
944 &mut attributes,
945 DateTime::from_timestamp_nanos(1_234_201_337),
946 );
947
948 assert_annotated_snapshot!(attributes, @r#"
949 {
950 "sentry.observed_timestamp_nanos": {
951 "type": "string",
952 "value": "1234201337"
953 }
954 }
955 "#);
956 }
957
958 #[test]
959 fn test_normalize_received_existing() {
960 let mut attributes = Annotated::from_json(
961 r#"{
962 "sentry.observed_timestamp_nanos": {
963 "type": "string",
964 "value": "111222333"
965 }
966 }"#,
967 )
968 .unwrap();
969
970 normalize_received(
971 &mut attributes,
972 DateTime::from_timestamp_nanos(1_234_201_337),
973 );
974
975 assert_annotated_snapshot!(attributes, @r###"
976 {
977 "sentry.observed_timestamp_nanos": {
978 "type": "string",
979 "value": "111222333"
980 }
981 }
982 "###);
983 }
984
985 #[test]
986 fn test_process_attribute_types() {
987 let json = r#"{
988 "valid_bool": {
989 "type": "boolean",
990 "value": true
991 },
992 "valid_int_i64": {
993 "type": "integer",
994 "value": -42
995 },
996 "valid_int_u64": {
997 "type": "integer",
998 "value": 42
999 },
1000 "valid_int_from_string": {
1001 "type": "integer",
1002 "value": "42"
1003 },
1004 "valid_double": {
1005 "type": "double",
1006 "value": 42.5
1007 },
1008 "double_with_i64": {
1009 "type": "double",
1010 "value": -42
1011 },
1012 "valid_double_with_u64": {
1013 "type": "double",
1014 "value": 42
1015 },
1016 "valid_string": {
1017 "type": "string",
1018 "value": "test"
1019 },
1020 "valid_string_with_other": {
1021 "type": "string",
1022 "value": "test",
1023 "some_other_field": "some_other_value"
1024 },
1025 "unknown_type": {
1026 "type": "custom",
1027 "value": "test"
1028 },
1029 "invalid_int_from_invalid_string": {
1030 "type": "integer",
1031 "value": "abc"
1032 },
1033 "invalid_int": {
1034 "type": "integer",
1035 "value": 9223372036854775808
1036 },
1037 "missing_type": {
1038 "value": "value with missing type"
1039 },
1040 "missing_value": {
1041 "type": "string"
1042 },
1043 "supported_array_string": {
1044 "type": "array",
1045 "value": ["foo", "bar"]
1046 },
1047 "supported_array_double": {
1048 "type": "array",
1049 "value": [3, 3.0, 3]
1050 },
1051 "supported_array_null": {
1052 "type": "array",
1053 "value": [null, null]
1054 },
1055 "unsupported_array_mixed": {
1056 "type": "array",
1057 "value": ["foo", 1.0]
1058 },
1059 "unsupported_array_object": {
1060 "type": "array",
1061 "value": [{}]
1062 },
1063 "unsupported_array_in_array": {
1064 "type": "array",
1065 "value": [[]]
1066 }
1067 }"#;
1068
1069 let mut attributes = Annotated::<Attributes>::from_json(json).unwrap();
1070 normalize_attribute_types(&mut attributes);
1071
1072 assert_annotated_snapshot!(attributes, @r#"
1073 {
1074 "double_with_i64": {
1075 "type": "double",
1076 "value": -42
1077 },
1078 "invalid_int": null,
1079 "invalid_int_from_invalid_string": null,
1080 "missing_type": null,
1081 "missing_value": null,
1082 "supported_array_double": {
1083 "type": "array",
1084 "value": [
1085 3,
1086 3.0,
1087 3
1088 ]
1089 },
1090 "supported_array_null": {
1091 "type": "array",
1092 "value": [
1093 null,
1094 null
1095 ]
1096 },
1097 "supported_array_string": {
1098 "type": "array",
1099 "value": [
1100 "foo",
1101 "bar"
1102 ]
1103 },
1104 "unknown_type": null,
1105 "unsupported_array_in_array": null,
1106 "unsupported_array_mixed": null,
1107 "unsupported_array_object": null,
1108 "valid_bool": {
1109 "type": "boolean",
1110 "value": true
1111 },
1112 "valid_double": {
1113 "type": "double",
1114 "value": 42.5
1115 },
1116 "valid_double_with_u64": {
1117 "type": "double",
1118 "value": 42
1119 },
1120 "valid_int_from_string": null,
1121 "valid_int_i64": {
1122 "type": "integer",
1123 "value": -42
1124 },
1125 "valid_int_u64": {
1126 "type": "integer",
1127 "value": 42
1128 },
1129 "valid_string": {
1130 "type": "string",
1131 "value": "test"
1132 },
1133 "valid_string_with_other": {
1134 "type": "string",
1135 "value": "test",
1136 "some_other_field": "some_other_value"
1137 },
1138 "_meta": {
1139 "invalid_int": {
1140 "": {
1141 "err": [
1142 "invalid_data"
1143 ],
1144 "val": {
1145 "type": "integer",
1146 "value": 9223372036854775808
1147 }
1148 }
1149 },
1150 "invalid_int_from_invalid_string": {
1151 "": {
1152 "err": [
1153 "invalid_data"
1154 ],
1155 "val": {
1156 "type": "integer",
1157 "value": "abc"
1158 }
1159 }
1160 },
1161 "missing_type": {
1162 "": {
1163 "err": [
1164 "missing_attribute"
1165 ],
1166 "val": {
1167 "type": null,
1168 "value": "value with missing type"
1169 }
1170 }
1171 },
1172 "missing_value": {
1173 "": {
1174 "err": [
1175 "missing_attribute"
1176 ],
1177 "val": {
1178 "type": "string",
1179 "value": null
1180 }
1181 }
1182 },
1183 "unknown_type": {
1184 "": {
1185 "err": [
1186 "invalid_data"
1187 ],
1188 "val": {
1189 "type": "custom",
1190 "value": "test"
1191 }
1192 }
1193 },
1194 "unsupported_array_in_array": {
1195 "": {
1196 "err": [
1197 "invalid_data"
1198 ]
1199 }
1200 },
1201 "unsupported_array_mixed": {
1202 "": {
1203 "err": [
1204 "invalid_data"
1205 ]
1206 }
1207 },
1208 "unsupported_array_object": {
1209 "": {
1210 "err": [
1211 "invalid_data"
1212 ]
1213 }
1214 },
1215 "valid_int_from_string": {
1216 "": {
1217 "err": [
1218 "invalid_data"
1219 ],
1220 "val": {
1221 "type": "integer",
1222 "value": "42"
1223 }
1224 }
1225 }
1226 }
1227 }
1228 "#);
1229 }
1230
1231 #[test]
1232 fn test_normalize_user_agent_none() {
1233 let mut attributes = Default::default();
1234 normalize_user_agent(
1235 &mut attributes,
1236 Some(ClientUserAgentInfo {
1237 user_agent: Some(
1238 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
1239 ),
1240 ..Default::default()
1241 }),
1242 );
1243
1244 assert_annotated_snapshot!(attributes, @r###"
1245 {
1246 "browser.name": {
1247 "type": "string",
1248 "value": "Chrome"
1249 },
1250 "browser.version": {
1251 "type": "string",
1252 "value": "131.0.0"
1253 },
1254 "user_agent.original": {
1255 "type": "string",
1256 "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
1257 }
1258 }
1259 "###);
1260 }
1261
1262 #[test]
1263 fn test_normalize_user_agent_existing() {
1264 let mut attributes = Annotated::from_json(
1265 r#"{
1266 "browser.name": {
1267 "type": "string",
1268 "value": "Very Special"
1269 },
1270 "browser.version": {
1271 "type": "string",
1272 "value": "13.3.7"
1273 }
1274 }"#,
1275 )
1276 .unwrap();
1277
1278 normalize_user_agent(
1279 &mut attributes,
1280 Some(ClientUserAgentInfo {
1281 user_agent: Some(
1282 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
1283 ),
1284 ..Default::default()
1285 }),
1286 );
1287
1288 assert_annotated_snapshot!(attributes, @r#"
1289 {
1290 "browser.name": {
1291 "type": "string",
1292 "value": "Very Special"
1293 },
1294 "browser.version": {
1295 "type": "string",
1296 "value": "13.3.7"
1297 }
1298 }
1299 "#
1300 );
1301 }
1302
1303 #[test]
1304 fn test_normalize_user_geo_none() {
1305 let mut attributes = Annotated::from_json(
1306 r#"{
1307 "client.address": {
1308 "type": "string",
1309 "value": "192.168.2.1"
1310 }
1311 }"#,
1312 )
1313 .unwrap();
1314
1315 normalize_user_geo(&mut attributes, |addr| {
1316 Some(Geo {
1317 country_code: "XY".to_owned().into(),
1318 city: addr.to_string().into(),
1319 subdivision: Annotated::empty(),
1320 region: "Illu".to_owned().into(),
1321 other: Default::default(),
1322 })
1323 });
1324
1325 assert_annotated_snapshot!(attributes, @r#"
1326 {
1327 "client.address": {
1328 "type": "string",
1329 "value": "192.168.2.1"
1330 },
1331 "user.geo.city": {
1332 "type": "string",
1333 "value": "192.168.2.1"
1334 },
1335 "user.geo.country_code": {
1336 "type": "string",
1337 "value": "XY"
1338 },
1339 "user.geo.region": {
1340 "type": "string",
1341 "value": "Illu"
1342 }
1343 }
1344 "#);
1345 }
1346
1347 #[test]
1348 fn test_normalize_user_geo_existing() {
1349 let mut attributes = Annotated::from_json(
1350 r#"{
1351 "client.address": {
1352 "type": "string",
1353 "value": "192.168.2.1"
1354 },
1355 "user.geo.city": {
1356 "type": "string",
1357 "value": "Foo Hausen"
1358 }
1359 }"#,
1360 )
1361 .unwrap();
1362
1363 normalize_user_geo(&mut attributes, |_| unreachable!());
1364
1365 assert_annotated_snapshot!(attributes, @r#"
1366 {
1367 "client.address": {
1368 "type": "string",
1369 "value": "192.168.2.1"
1370 },
1371 "user.geo.city": {
1372 "type": "string",
1373 "value": "Foo Hausen"
1374 }
1375 }
1376 "#
1377 );
1378 }
1379
1380 #[test]
1381 fn test_normalize_attributes() {
1382 fn replace_key(fragment: &str) -> String {
1383 format!("placeholder.replaced.{fragment}")
1384 }
1385
1386 fn backfill_key(fragment: &str) -> String {
1387 format!("placeholder.backfilled.{fragment}")
1388 }
1389
1390 fn mock_attribute_info(name: &str) -> Option<(&'static AttributeInfo, Option<&str>)> {
1391 use relay_conventions::Pii;
1392
1393 match name {
1394 "replace.empty" => Some((
1395 &AttributeInfo {
1396 write_behavior: WriteBehavior::NewName(ReplacementName::Static("replaced")),
1397 pii: Pii::Maybe,
1398 aliases: &["replaced"],
1399 },
1400 None,
1401 )),
1402 "replace.existing" => Some((
1403 &AttributeInfo {
1404 write_behavior: WriteBehavior::NewName(ReplacementName::Static(
1405 "not.replaced",
1406 )),
1407 pii: Pii::Maybe,
1408 aliases: &["not.replaced"],
1409 },
1410 None,
1411 )),
1412 "backfill.empty" => Some((
1413 &AttributeInfo {
1414 write_behavior: WriteBehavior::BothNames(ReplacementName::Static(
1415 "backfilled",
1416 )),
1417 pii: Pii::Maybe,
1418 aliases: &["backfilled"],
1419 },
1420 None,
1421 )),
1422 "backfill.existing" => Some((
1423 &AttributeInfo {
1424 write_behavior: WriteBehavior::BothNames(ReplacementName::Static(
1425 "not.backfilled",
1426 )),
1427 pii: Pii::Maybe,
1428 aliases: &["not.backfilled"],
1429 },
1430 None,
1431 )),
1432 _ if let Some(fragment) = name.strip_prefix("placeholder.replace.") => Some((
1433 &AttributeInfo {
1434 write_behavior: WriteBehavior::NewName(ReplacementName::Dynamic(
1435 replace_key,
1436 )),
1437 pii: Pii::Maybe,
1438 aliases: &["placeholder.replaced.<key>"],
1439 },
1440 Some(fragment),
1441 )),
1442 _ if let Some(fragment) = name.strip_prefix("placeholder.backfill.") => Some((
1443 &AttributeInfo {
1444 write_behavior: WriteBehavior::BothNames(ReplacementName::Dynamic(
1445 backfill_key,
1446 )),
1447 pii: Pii::Maybe,
1448 aliases: &["placeholder.backfilled.<key>"],
1449 },
1450 Some(fragment),
1451 )),
1452
1453 _ => None,
1454 }
1455 }
1456
1457 let mut attributes = Annotated::new(Attributes::from([
1458 (
1459 "replace.empty".to_owned(),
1460 Annotated::new("Should be moved".to_owned().into()),
1461 ),
1462 (
1463 "replace.existing".to_owned(),
1464 Annotated::new("Should be removed".to_owned().into()),
1465 ),
1466 (
1467 "placeholder.replace.foo".to_owned(),
1468 Annotated::new("Should be moved".to_owned().into()),
1469 ),
1470 (
1471 "not.replaced".to_owned(),
1472 Annotated::new("Should be left alone".to_owned().into()),
1473 ),
1474 (
1475 "backfill.empty".to_owned(),
1476 Annotated::new("Should be copied".to_owned().into()),
1477 ),
1478 (
1479 "backfill.existing".to_owned(),
1480 Annotated::new("Should be left alone".to_owned().into()),
1481 ),
1482 (
1483 "placeholder.backfill.bar".to_owned(),
1484 Annotated::new("Should be copied".to_owned().into()),
1485 ),
1486 (
1487 "not.backfilled".to_owned(),
1488 Annotated::new("Should be left alone".to_owned().into()),
1489 ),
1490 ]));
1491
1492 normalize_attribute_names_inner(&mut attributes, mock_attribute_info);
1493
1494 assert_annotated_snapshot!(attributes, @r###"
1495 {
1496 "backfill.empty": {
1497 "type": "string",
1498 "value": "Should be copied"
1499 },
1500 "backfill.existing": {
1501 "type": "string",
1502 "value": "Should be left alone"
1503 },
1504 "backfilled": {
1505 "type": "string",
1506 "value": "Should be copied"
1507 },
1508 "not.backfilled": {
1509 "type": "string",
1510 "value": "Should be left alone"
1511 },
1512 "not.replaced": {
1513 "type": "string",
1514 "value": "Should be left alone"
1515 },
1516 "placeholder.backfill.bar": {
1517 "type": "string",
1518 "value": "Should be copied"
1519 },
1520 "placeholder.backfilled.bar": {
1521 "type": "string",
1522 "value": "Should be copied"
1523 },
1524 "placeholder.replace.foo": null,
1525 "placeholder.replaced.foo": {
1526 "type": "string",
1527 "value": "Should be moved"
1528 },
1529 "replace.empty": null,
1530 "replace.existing": null,
1531 "replaced": {
1532 "type": "string",
1533 "value": "Should be moved"
1534 },
1535 "_meta": {
1536 "placeholder.replace.foo": {
1537 "": {
1538 "rem": [
1539 [
1540 "attribute.deprecated",
1541 "x"
1542 ]
1543 ]
1544 }
1545 },
1546 "replace.empty": {
1547 "": {
1548 "rem": [
1549 [
1550 "attribute.deprecated",
1551 "x"
1552 ]
1553 ]
1554 }
1555 },
1556 "replace.existing": {
1557 "": {
1558 "rem": [
1559 [
1560 "attribute.deprecated",
1561 "x"
1562 ]
1563 ]
1564 }
1565 }
1566 }
1567 }
1568 "###);
1569 }
1570
1571 #[test]
1572 fn test_normalize_span_infers_op() {
1573 let mut attributes = Annotated::<Attributes>::from_json(
1574 r#"{
1575 "db.system.name": {
1576 "type": "string",
1577 "value": "mysql"
1578 },
1579 "db.operation.name": {
1580 "type": "string",
1581 "value": "query"
1582 }
1583 }
1584 "#,
1585 )
1586 .unwrap();
1587
1588 normalize_sentry_op(&mut attributes);
1589
1590 assert_annotated_snapshot!(attributes, @r#"
1591 {
1592 "db.operation.name": {
1593 "type": "string",
1594 "value": "query"
1595 },
1596 "db.system.name": {
1597 "type": "string",
1598 "value": "mysql"
1599 },
1600 "sentry.op": {
1601 "type": "string",
1602 "value": "db"
1603 }
1604 }
1605 "#);
1606 }
1607
1608 #[test]
1609 fn test_normalize_attribute_values_mysql_db_query_attributes() {
1610 let mut attributes = Annotated::<Attributes>::from_json(
1611 r#"
1612 {
1613 "sentry.op": {
1614 "type": "string",
1615 "value": "db.query"
1616 },
1617 "sentry.origin": {
1618 "type": "string",
1619 "value": "auto.otlp.spans"
1620 },
1621 "db.system.name": {
1622 "type": "string",
1623 "value": "mysql"
1624 },
1625 "db.query.text": {
1626 "type": "string",
1627 "value": "SELECT \"not an identifier\""
1628 }
1629 }
1630 "#,
1631 )
1632 .unwrap();
1633
1634 normalize_db_attributes(&mut attributes);
1635
1636 assert_annotated_snapshot!(attributes, @r#"
1637 {
1638 "db.operation.name": {
1639 "type": "string",
1640 "value": "SELECT"
1641 },
1642 "db.query.text": {
1643 "type": "string",
1644 "value": "SELECT \"not an identifier\""
1645 },
1646 "db.system.name": {
1647 "type": "string",
1648 "value": "mysql"
1649 },
1650 "sentry.normalized_db_query": {
1651 "type": "string",
1652 "value": "SELECT %s"
1653 },
1654 "sentry.normalized_db_query.hash": {
1655 "type": "string",
1656 "value": "3a377dcc490b1690"
1657 },
1658 "sentry.op": {
1659 "type": "string",
1660 "value": "db.query"
1661 },
1662 "sentry.origin": {
1663 "type": "string",
1664 "value": "auto.otlp.spans"
1665 }
1666 }
1667 "#);
1668 }
1669
1670 #[test]
1671 fn test_normalize_mongodb_db_query_attributes() {
1672 let mut attributes = Annotated::<Attributes>::from_json(
1673 r#"
1674 {
1675 "sentry.op": {
1676 "type": "string",
1677 "value": "db"
1678 },
1679 "db.system.name": {
1680 "type": "string",
1681 "value": "mongodb"
1682 },
1683 "db.query.text": {
1684 "type": "string",
1685 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1686 },
1687 "db.operation.name": {
1688 "type": "string",
1689 "value": "find"
1690 },
1691 "db.collection.name": {
1692 "type": "string",
1693 "value": "documents"
1694 }
1695 }
1696 "#,
1697 )
1698 .unwrap();
1699
1700 normalize_db_attributes(&mut attributes);
1701
1702 assert_annotated_snapshot!(attributes, @r#"
1703 {
1704 "db.collection.name": {
1705 "type": "string",
1706 "value": "documents"
1707 },
1708 "db.operation.name": {
1709 "type": "string",
1710 "value": "FIND"
1711 },
1712 "db.query.text": {
1713 "type": "string",
1714 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1715 },
1716 "db.system.name": {
1717 "type": "string",
1718 "value": "mongodb"
1719 },
1720 "sentry.normalized_db_query": {
1721 "type": "string",
1722 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1723 },
1724 "sentry.normalized_db_query.hash": {
1725 "type": "string",
1726 "value": "aedc5c7e8cec726b"
1727 },
1728 "sentry.op": {
1729 "type": "string",
1730 "value": "db"
1731 }
1732 }
1733 "#);
1734 }
1735
1736 #[test]
1737 fn test_normalize_db_attributes_does_not_update_attributes_if_already_normalized() {
1738 let mut attributes = Annotated::<Attributes>::from_json(
1739 r#"
1740 {
1741 "db.collection.name": {
1742 "type": "string",
1743 "value": "documents"
1744 },
1745 "db.operation.name": {
1746 "type": "string",
1747 "value": "FIND"
1748 },
1749 "db.query.text": {
1750 "type": "string",
1751 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1752 },
1753 "db.system.name": {
1754 "type": "string",
1755 "value": "mongodb"
1756 },
1757 "sentry.normalized_db_query": {
1758 "type": "string",
1759 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1760 },
1761 "sentry.op": {
1762 "type": "string",
1763 "value": "db"
1764 }
1765 }
1766 "#,
1767 )
1768 .unwrap();
1769
1770 normalize_db_attributes(&mut attributes);
1771
1772 insta::assert_json_snapshot!(
1773 SerializableAnnotated(&attributes), @r#"
1774 {
1775 "db.collection.name": {
1776 "type": "string",
1777 "value": "documents"
1778 },
1779 "db.operation.name": {
1780 "type": "string",
1781 "value": "FIND"
1782 },
1783 "db.query.text": {
1784 "type": "string",
1785 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1786 },
1787 "db.system.name": {
1788 "type": "string",
1789 "value": "mongodb"
1790 },
1791 "sentry.normalized_db_query": {
1792 "type": "string",
1793 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1794 },
1795 "sentry.op": {
1796 "type": "string",
1797 "value": "db"
1798 }
1799 }
1800 "#
1801 );
1802 }
1803
1804 #[test]
1805 fn test_normalize_db_attributes_does_not_change_non_db_spans() {
1806 let mut attributes = Annotated::<Attributes>::from_json(
1807 r#"
1808 {
1809 "sentry.op": {
1810 "type": "string",
1811 "value": "http.client"
1812 },
1813 "sentry.origin": {
1814 "type": "string",
1815 "value": "auto.otlp.spans"
1816 },
1817 "http.request.method": {
1818 "type": "string",
1819 "value": "GET"
1820 }
1821 }
1822 "#,
1823 )
1824 .unwrap();
1825
1826 normalize_db_attributes(&mut attributes);
1827
1828 assert_annotated_snapshot!(attributes, @r#"
1829 {
1830 "http.request.method": {
1831 "type": "string",
1832 "value": "GET"
1833 },
1834 "sentry.op": {
1835 "type": "string",
1836 "value": "http.client"
1837 },
1838 "sentry.origin": {
1839 "type": "string",
1840 "value": "auto.otlp.spans"
1841 }
1842 }
1843 "#);
1844 }
1845
1846 #[test]
1847 fn test_normalize_http_attributes() {
1848 let mut attributes = Annotated::<Attributes>::from_json(
1849 r#"
1850 {
1851 "sentry.op": {
1852 "type": "string",
1853 "value": "http.client"
1854 },
1855 "sentry.category": {
1856 "type": "string",
1857 "value": "http"
1858 },
1859 "http.request.method": {
1860 "type": "string",
1861 "value": "GET"
1862 },
1863 "url.full": {
1864 "type": "string",
1865 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1866 }
1867 }
1868 "#,
1869 )
1870 .unwrap();
1871
1872 normalize_http_attributes(&mut attributes, &[]);
1873
1874 assert_annotated_snapshot!(attributes, @r#"
1875 {
1876 "http.request.method": {
1877 "type": "string",
1878 "value": "GET"
1879 },
1880 "sentry.category": {
1881 "type": "string",
1882 "value": "http"
1883 },
1884 "sentry.op": {
1885 "type": "string",
1886 "value": "http.client"
1887 },
1888 "server.address": {
1889 "type": "string",
1890 "value": "*.xn--85x722f.xn--55qx5d.cn"
1891 },
1892 "url.full": {
1893 "type": "string",
1894 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1895 }
1896 }
1897 "#);
1898 }
1899
1900 #[test]
1901 fn test_normalize_http_attributes_server_address() {
1902 let mut attributes = Annotated::<Attributes>::from_json(
1903 r#"
1904 {
1905 "sentry.category": {
1906 "type": "string",
1907 "value": "http"
1908 },
1909 "sentry.op": {
1910 "type": "string",
1911 "value": "http.client"
1912 },
1913 "url.scheme": {
1914 "type": "string",
1915 "value": "https"
1916 },
1917 "server.address": {
1918 "type": "string",
1919 "value": "subdomain.example.com:5688"
1920 },
1921 "http.request.method": {
1922 "type": "string",
1923 "value": "GET"
1924 }
1925 }
1926 "#,
1927 )
1928 .unwrap();
1929
1930 normalize_http_attributes(&mut attributes, &[]);
1931
1932 assert_annotated_snapshot!(attributes, @r#"
1933 {
1934 "http.request.method": {
1935 "type": "string",
1936 "value": "GET"
1937 },
1938 "sentry.category": {
1939 "type": "string",
1940 "value": "http"
1941 },
1942 "sentry.op": {
1943 "type": "string",
1944 "value": "http.client"
1945 },
1946 "server.address": {
1947 "type": "string",
1948 "value": "*.example.com:5688"
1949 },
1950 "url.full": {
1951 "type": "string",
1952 "value": "https://subdomain.example.com:5688"
1953 },
1954 "url.scheme": {
1955 "type": "string",
1956 "value": "https"
1957 }
1958 }
1959 "#);
1960 }
1961
1962 #[test]
1963 fn test_normalize_http_attributes_allowed_hosts() {
1964 let mut attributes = Annotated::<Attributes>::from_json(
1965 r#"
1966 {
1967 "sentry.category": {
1968 "type": "string",
1969 "value": "http"
1970 },
1971 "sentry.op": {
1972 "type": "string",
1973 "value": "http.client"
1974 },
1975 "http.request.method": {
1976 "type": "string",
1977 "value": "GET"
1978 },
1979 "url.full": {
1980 "type": "string",
1981 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1982 }
1983 }
1984 "#,
1985 )
1986 .unwrap();
1987
1988 normalize_http_attributes(
1989 &mut attributes,
1990 &["application.www.xn--85x722f.xn--55qx5d.cn".to_owned()],
1991 );
1992
1993 assert_annotated_snapshot!(attributes, @r#"
1994 {
1995 "http.request.method": {
1996 "type": "string",
1997 "value": "GET"
1998 },
1999 "sentry.category": {
2000 "type": "string",
2001 "value": "http"
2002 },
2003 "sentry.op": {
2004 "type": "string",
2005 "value": "http.client"
2006 },
2007 "server.address": {
2008 "type": "string",
2009 "value": "application.www.xn--85x722f.xn--55qx5d.cn"
2010 },
2011 "url.full": {
2012 "type": "string",
2013 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
2014 }
2015 }
2016 "#);
2017 }
2018
2019 #[test]
2020 fn test_normalize_db_attributes_from_legacy_attributes() {
2021 let mut attributes = Annotated::<Attributes>::from_json(
2022 r#"
2023 {
2024 "sentry.op": {
2025 "type": "string",
2026 "value": "db"
2027 },
2028 "db.system.name": {
2029 "type": "string",
2030 "value": "mongodb"
2031 },
2032 "sentry.description": {
2033 "type": "string",
2034 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2035 },
2036 "db.operation.name": {
2037 "type": "string",
2038 "value": "find"
2039 },
2040 "db.collection.name": {
2041 "type": "string",
2042 "value": "documents"
2043 }
2044 }
2045 "#,
2046 )
2047 .unwrap();
2048
2049 normalize_db_attributes(&mut attributes);
2050
2051 assert_annotated_snapshot!(attributes, @r#"
2052 {
2053 "db.collection.name": {
2054 "type": "string",
2055 "value": "documents"
2056 },
2057 "db.operation.name": {
2058 "type": "string",
2059 "value": "FIND"
2060 },
2061 "db.system.name": {
2062 "type": "string",
2063 "value": "mongodb"
2064 },
2065 "sentry.description": {
2066 "type": "string",
2067 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2068 },
2069 "sentry.normalized_db_query": {
2070 "type": "string",
2071 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2072 },
2073 "sentry.normalized_db_query.hash": {
2074 "type": "string",
2075 "value": "aedc5c7e8cec726b"
2076 },
2077 "sentry.op": {
2078 "type": "string",
2079 "value": "db"
2080 }
2081 }
2082 "#);
2083 }
2084
2085 #[test]
2086 fn test_normalize_http_attributes_from_legacy_attributes() {
2087 let mut attributes = Annotated::<Attributes>::from_json(
2088 r#"
2089 {
2090 "sentry.category": {
2091 "type": "string",
2092 "value": "http"
2093 },
2094 "sentry.op": {
2095 "type": "string",
2096 "value": "http.client"
2097 },
2098 "http.request_method": {
2099 "type": "string",
2100 "value": "GET"
2101 }
2102 }
2103 "#,
2104 )
2105 .unwrap();
2106
2107 normalize_attribute_names(&mut attributes);
2108 normalize_http_attributes(&mut attributes, &[]);
2109
2110 assert_annotated_snapshot!(attributes, @r#"
2111 {
2112 "http.request.method": {
2113 "type": "string",
2114 "value": "GET"
2115 },
2116 "http.request_method": {
2117 "type": "string",
2118 "value": "GET"
2119 },
2120 "sentry.category": {
2121 "type": "string",
2122 "value": "http"
2123 },
2124 "sentry.op": {
2125 "type": "string",
2126 "value": "http.client"
2127 }
2128 }
2129 "#);
2130 }
2131
2132 #[test]
2133 fn test_normalize_http_attributes_from_description() {
2134 let mut attributes = Annotated::<Attributes>::from_json(
2135 r#"
2136 {
2137 "sentry.category": {
2138 "type": "string",
2139 "value": "http"
2140 },
2141 "sentry.op": {
2142 "type": "string",
2143 "value": "http.client"
2144 },
2145 "sentry.description": {
2146 "type": "string",
2147 "value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
2148 }
2149 }
2150 "#,
2151 )
2152 .unwrap();
2153
2154 normalize_http_attributes(&mut attributes, &[]);
2155
2156 assert_annotated_snapshot!(attributes, @r#"
2157 {
2158 "http.request.method": {
2159 "type": "string",
2160 "value": "GET"
2161 },
2162 "sentry.category": {
2163 "type": "string",
2164 "value": "http"
2165 },
2166 "sentry.description": {
2167 "type": "string",
2168 "value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
2169 },
2170 "sentry.op": {
2171 "type": "string",
2172 "value": "http.client"
2173 },
2174 "server.address": {
2175 "type": "string",
2176 "value": "*.xn--85x722f.xn--55qx5d.cn"
2177 },
2178 "url.full": {
2179 "type": "string",
2180 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
2181 }
2182 }
2183 "#);
2184 }
2185
2186 #[test]
2187 fn test_write_legacy_attributes() {
2188 let mut attributes = Annotated::<Attributes>::from_json(
2189 r#"
2190 {
2191 "db.collection.name": {
2192 "type": "string",
2193 "value": "documents"
2194 },
2195 "db.operation.name": {
2196 "type": "string",
2197 "value": "FIND"
2198 },
2199 "db.query.text": {
2200 "type": "string",
2201 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2202 },
2203 "db.system.name": {
2204 "type": "string",
2205 "value": "mongodb"
2206 },
2207 "sentry.normalized_db_query": {
2208 "type": "string",
2209 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2210 },
2211 "sentry.normalized_db_query.hash": {
2212 "type": "string",
2213 "value": "aedc5c7e8cec726b"
2214 },
2215 "sentry.op": {
2216 "type": "string",
2217 "value": "db"
2218 }
2219 }
2220 "#,
2221 )
2222 .unwrap();
2223
2224 write_legacy_attributes(&mut attributes);
2225
2226 assert_annotated_snapshot!(attributes, @r#"
2227 {
2228 "db.collection.name": {
2229 "type": "string",
2230 "value": "documents"
2231 },
2232 "db.operation.name": {
2233 "type": "string",
2234 "value": "FIND"
2235 },
2236 "db.query.text": {
2237 "type": "string",
2238 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2239 },
2240 "db.system": {
2241 "type": "string",
2242 "value": "mongodb"
2243 },
2244 "db.system.name": {
2245 "type": "string",
2246 "value": "mongodb"
2247 },
2248 "sentry.action": {
2249 "type": "string",
2250 "value": "FIND"
2251 },
2252 "sentry.description": {
2253 "type": "string",
2254 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2255 },
2256 "sentry.domain": {
2257 "type": "string",
2258 "value": ",documents,"
2259 },
2260 "sentry.normalized_db_query": {
2261 "type": "string",
2262 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2263 },
2264 "sentry.normalized_db_query.hash": {
2265 "type": "string",
2266 "value": "aedc5c7e8cec726b"
2267 },
2268 "sentry.normalized_description": {
2269 "type": "string",
2270 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2271 },
2272 "sentry.op": {
2273 "type": "string",
2274 "value": "db"
2275 }
2276 }
2277 "#);
2278 }
2279
2280 #[test]
2281 fn test_normalize_span_category_explicit() {
2282 let mut attributes = Annotated::<Attributes>::from_json(
2284 r#"{
2285 "sentry.category": {
2286 "type": "string",
2287 "value": "custom"
2288 },
2289 "sentry.op": {
2290 "type": "string",
2291 "value": "db.query"
2292 }
2293 }"#,
2294 )
2295 .unwrap();
2296
2297 normalize_span_category(&mut attributes);
2298
2299 assert_annotated_snapshot!(attributes, @r#"
2300 {
2301 "sentry.category": {
2302 "type": "string",
2303 "value": "custom"
2304 },
2305 "sentry.op": {
2306 "type": "string",
2307 "value": "db.query"
2308 }
2309 }
2310 "#);
2311 }
2312
2313 #[test]
2314 fn test_normalize_span_category_from_op_db() {
2315 let mut attributes = Annotated::<Attributes>::from_json(
2316 r#"{
2317 "sentry.op": {
2318 "type": "string",
2319 "value": "db.query"
2320 }
2321 }"#,
2322 )
2323 .unwrap();
2324
2325 normalize_span_category(&mut attributes);
2326
2327 assert_annotated_snapshot!(attributes, @r#"
2328 {
2329 "sentry.category": {
2330 "type": "string",
2331 "value": "db"
2332 },
2333 "sentry.op": {
2334 "type": "string",
2335 "value": "db.query"
2336 }
2337 }
2338 "#);
2339 }
2340
2341 #[test]
2342 fn test_normalize_span_category_from_op_http() {
2343 let mut attributes = Annotated::<Attributes>::from_json(
2344 r#"{
2345 "sentry.op": {
2346 "type": "string",
2347 "value": "http.client"
2348 }
2349 }"#,
2350 )
2351 .unwrap();
2352
2353 normalize_span_category(&mut attributes);
2354
2355 assert_annotated_snapshot!(attributes, @r#"
2356 {
2357 "sentry.category": {
2358 "type": "string",
2359 "value": "http"
2360 },
2361 "sentry.op": {
2362 "type": "string",
2363 "value": "http.client"
2364 }
2365 }
2366 "#);
2367 }
2368
2369 #[test]
2370 fn test_normalize_span_category_from_op_ui_framework() {
2371 let mut attributes = Annotated::<Attributes>::from_json(
2372 r#"{
2373 "sentry.op": {
2374 "type": "string",
2375 "value": "ui.react.render"
2376 }
2377 }"#,
2378 )
2379 .unwrap();
2380
2381 normalize_span_category(&mut attributes);
2382
2383 assert_annotated_snapshot!(attributes, @r#"
2384 {
2385 "sentry.category": {
2386 "type": "string",
2387 "value": "ui.react"
2388 },
2389 "sentry.op": {
2390 "type": "string",
2391 "value": "ui.react.render"
2392 }
2393 }
2394 "#);
2395 }
2396
2397 #[test]
2398 fn test_normalize_span_category_from_db_system() {
2399 let mut attributes = Annotated::<Attributes>::from_json(
2401 r#"{
2402 "db.system.name": {
2403 "type": "string",
2404 "value": "mongodb"
2405 }
2406 }"#,
2407 )
2408 .unwrap();
2409
2410 normalize_span_category(&mut attributes);
2411
2412 assert_annotated_snapshot!(attributes, @r#"
2413 {
2414 "db.system.name": {
2415 "type": "string",
2416 "value": "mongodb"
2417 },
2418 "sentry.category": {
2419 "type": "string",
2420 "value": "db"
2421 }
2422 }
2423 "#);
2424 }
2425
2426 #[test]
2427 fn test_normalize_span_category_from_http_method() {
2428 let mut attributes = Annotated::<Attributes>::from_json(
2430 r#"{
2431 "http.request.method": {
2432 "type": "string",
2433 "value": "GET"
2434 }
2435 }"#,
2436 )
2437 .unwrap();
2438
2439 normalize_span_category(&mut attributes);
2440
2441 assert_annotated_snapshot!(attributes, @r#"
2442 {
2443 "http.request.method": {
2444 "type": "string",
2445 "value": "GET"
2446 },
2447 "sentry.category": {
2448 "type": "string",
2449 "value": "http"
2450 }
2451 }
2452 "#);
2453 }
2454
2455 #[test]
2456 fn test_normalize_span_category_from_ui_component() {
2457 let mut attributes = Annotated::<Attributes>::from_json(
2459 r#"{
2460 "ui.component_name": {
2461 "type": "string",
2462 "value": "MyComponent"
2463 }
2464 }"#,
2465 )
2466 .unwrap();
2467
2468 normalize_span_category(&mut attributes);
2469
2470 assert_annotated_snapshot!(attributes, @r#"
2471 {
2472 "sentry.category": {
2473 "type": "string",
2474 "value": "ui"
2475 },
2476 "ui.component_name": {
2477 "type": "string",
2478 "value": "MyComponent"
2479 }
2480 }
2481 "#);
2482 }
2483
2484 #[test]
2485 fn test_normalize_span_category_from_resource() {
2486 let mut attributes = Annotated::<Attributes>::from_json(
2488 r#"{
2489 "resource.render_blocking_status": {
2490 "type": "string",
2491 "value": "blocking"
2492 }
2493 }"#,
2494 )
2495 .unwrap();
2496
2497 normalize_span_category(&mut attributes);
2498
2499 assert_annotated_snapshot!(attributes, @r#"
2500 {
2501 "resource.render_blocking_status": {
2502 "type": "string",
2503 "value": "blocking"
2504 },
2505 "sentry.category": {
2506 "type": "string",
2507 "value": "resource"
2508 }
2509 }
2510 "#);
2511 }
2512
2513 #[test]
2514 fn test_normalize_span_category_from_browser_origin() {
2515 let mut attributes = Annotated::from_json(
2517 r#"{
2518 "sentry.origin": {
2519 "type": "string",
2520 "value": "auto.ui.browser.metrics"
2521 }
2522 }"#,
2523 )
2524 .unwrap();
2525
2526 normalize_span_category(&mut attributes);
2527
2528 assert_annotated_snapshot!(attributes, @r#"
2529 {
2530 "sentry.category": {
2531 "type": "string",
2532 "value": "browser"
2533 },
2534 "sentry.origin": {
2535 "type": "string",
2536 "value": "auto.ui.browser.metrics"
2537 }
2538 }
2539 "#);
2540 }
2541
2542 #[test]
2543 fn test_normalize_client_address_auto_with_ip() {
2544 let mut attributes = Annotated::from_json(
2545 r#"{
2546 "client.address": {
2547 "type": "string",
2548 "value": "{{auto}}"
2549 }
2550 }"#,
2551 )
2552 .unwrap();
2553
2554 normalize_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2555
2556 assert_annotated_snapshot!(attributes, @r#"
2557 {
2558 "client.address": {
2559 "type": "string",
2560 "value": "192.168.1.1"
2561 }
2562 }
2563 "#);
2564 }
2565
2566 #[test]
2567 fn test_normalize_client_address_auto_without_ip() {
2568 let mut attributes = Annotated::from_json(
2569 r#"{
2570 "client.address": {
2571 "type": "string",
2572 "value": "{{auto}}"
2573 }
2574 }"#,
2575 )
2576 .unwrap();
2577
2578 normalize_client_address(&mut attributes, None);
2579
2580 assert_annotated_snapshot!(attributes, @r#"
2581 {}
2582 "#);
2583 }
2584
2585 #[test]
2586 fn test_normalize_client_address_explicit_not_replaced() {
2587 let mut attributes = Annotated::from_json(
2588 r#"{
2589 "client.address": {
2590 "type": "string",
2591 "value": "10.0.0.1"
2592 }
2593 }"#,
2594 )
2595 .unwrap();
2596
2597 normalize_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2598
2599 assert_annotated_snapshot!(attributes, @r#"
2600 {
2601 "client.address": {
2602 "type": "string",
2603 "value": "10.0.0.1"
2604 }
2605 }
2606 "#);
2607 }
2608
2609 #[test]
2610 fn test_normalize_client_address_missing_attribute() {
2611 let mut attributes = Annotated::empty();
2612
2613 normalize_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2614
2615 assert!(attributes.is_empty());
2616 }
2617
2618 #[test]
2619 fn test_normalize_client_address_auto_with_ipv6() {
2620 let mut attributes = Annotated::from_json(
2621 r#"{
2622 "client.address": {
2623 "type": "string",
2624 "value": "{{auto}}"
2625 }
2626 }"#,
2627 )
2628 .unwrap();
2629
2630 normalize_client_address(&mut attributes, Some("2001:db8::1".parse().unwrap()));
2631
2632 assert_annotated_snapshot!(attributes, @r#"
2633 {
2634 "client.address": {
2635 "type": "string",
2636 "value": "2001:db8::1"
2637 }
2638 }
2639 "#);
2640 }
2641
2642 #[test]
2643 fn test_normalize_inject_client_address_inserts_when_missing() {
2644 let mut attributes = Annotated::empty();
2645
2646 normalize_inject_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2647
2648 assert_annotated_snapshot!(attributes, @r#"
2649 {
2650 "client.address": {
2651 "type": "string",
2652 "value": "192.168.1.1"
2653 }
2654 }
2655 "#);
2656 }
2657
2658 #[test]
2659 fn test_normalize_inject_client_address_does_not_overwrite() {
2660 let mut attributes = Annotated::from_json(
2661 r#"{
2662 "client.address": {
2663 "type": "string",
2664 "value": "10.0.0.1"
2665 }
2666 }"#,
2667 )
2668 .unwrap();
2669
2670 normalize_inject_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2671
2672 assert_annotated_snapshot!(attributes, @r#"
2673 {
2674 "client.address": {
2675 "type": "string",
2676 "value": "10.0.0.1"
2677 }
2678 }
2679 "#);
2680 }
2681
2682 #[test]
2683 fn test_normalize_inject_client_address_none_ip() {
2684 let mut attributes = Annotated::from_json(r#"{}"#).unwrap();
2685
2686 normalize_inject_client_address(&mut attributes, None);
2687
2688 assert_annotated_snapshot!(attributes, @r#"
2689 {}
2690 "#);
2691 }
2692
2693 #[test]
2694 fn test_normalize_inject_client_address_ipv6() {
2695 let mut attributes = Annotated::empty();
2696
2697 normalize_inject_client_address(&mut attributes, Some("2001:db8::1".parse().unwrap()));
2698
2699 assert_annotated_snapshot!(attributes, @r#"
2700 {
2701 "client.address": {
2702 "type": "string",
2703 "value": "2001:db8::1"
2704 }
2705 }
2706 "#);
2707 }
2708
2709 #[test]
2710 fn test_normalize_span_category_no_match() {
2711 let mut attributes = Annotated::<Attributes>::from_json(
2713 r#"{
2714 "some.other.attribute": {
2715 "type": "string",
2716 "value": "value"
2717 }
2718 }"#,
2719 )
2720 .unwrap();
2721
2722 normalize_span_category(&mut attributes);
2723
2724 assert_annotated_snapshot!(attributes, @r#"
2725 {
2726 "some.other.attribute": {
2727 "type": "string",
2728 "value": "value"
2729 }
2730 }
2731 "#);
2732 }
2733
2734 #[test]
2735 fn test_normalize_client_sample_rate_valid() {
2736 let mut attributes = Annotated::from_json(
2737 r#"{
2738 "sentry.client_sample_rate": {
2739 "type": "double",
2740 "value": 1.0
2741 }
2742 }"#,
2743 )
2744 .unwrap();
2745
2746 normalize_client_sample_rate(&mut attributes);
2747
2748 assert_annotated_snapshot!(attributes, @r#"
2749 {
2750 "sentry.client_sample_rate": {
2751 "type": "double",
2752 "value": 1.0
2753 }
2754 }
2755 "#);
2756 }
2757
2758 #[test]
2759 fn test_normalize_client_sample_rate_invalid_too_small() {
2760 let mut attributes = {
2761 let mut attrs = Attributes::new();
2762 attrs.insert(SENTRY__CLIENT_SAMPLE_RATE, 0.0);
2763 Annotated::new(attrs)
2764 };
2765
2766 normalize_client_sample_rate(&mut attributes);
2767
2768 assert_annotated_snapshot!(attributes, @r#"
2769 {
2770 "sentry.client_sample_rate": null,
2771 "_meta": {
2772 "sentry.client_sample_rate": {
2773 "": {
2774 "err": [
2775 [
2776 "invalid_data",
2777 {
2778 "reason": "expected sample rate > 0.0, <= 1.0"
2779 }
2780 ]
2781 ]
2782 }
2783 }
2784 }
2785 }
2786 "#);
2787 }
2788
2789 #[test]
2790 fn test_normalize_client_sample_rate_invalid_too_large() {
2791 let mut attributes = {
2792 let mut attrs = Attributes::new();
2793 attrs.insert(SENTRY__CLIENT_SAMPLE_RATE, 1.1);
2794 Annotated::new(attrs)
2795 };
2796
2797 normalize_client_sample_rate(&mut attributes);
2798
2799 assert_annotated_snapshot!(attributes, @r#"
2800 {
2801 "sentry.client_sample_rate": null,
2802 "_meta": {
2803 "sentry.client_sample_rate": {
2804 "": {
2805 "err": [
2806 [
2807 "invalid_data",
2808 {
2809 "reason": "expected sample rate > 0.0, <= 1.0"
2810 }
2811 ]
2812 ]
2813 }
2814 }
2815 }
2816 }
2817 "#);
2818 }
2819
2820 #[test]
2821 fn test_normalize_client_sample_rate_invalid_type() {
2822 let mut attributes = {
2823 let mut attrs = Attributes::new();
2824 attrs.insert(SENTRY__CLIENT_SAMPLE_RATE, "foobar");
2825 Annotated::new(attrs)
2826 };
2827
2828 normalize_client_sample_rate(&mut attributes);
2829
2830 assert_annotated_snapshot!(attributes, @r#"
2831 {
2832 "sentry.client_sample_rate": null,
2833 "_meta": {
2834 "sentry.client_sample_rate": {
2835 "": {
2836 "err": [
2837 [
2838 "invalid_data",
2839 {
2840 "reason": "expected sample rate > 0.0, <= 1.0"
2841 }
2842 ]
2843 ]
2844 }
2845 }
2846 }
2847 }
2848 "#);
2849 }
2850
2851 #[test]
2852 fn test_normalize_mobile_measurements() {
2853 let json = r#"
2854 {
2855 "frames.slow": {"value": 1, "type": "integer"},
2856 "app.vitals.frames.frozen.count": {"value": 2, "type": "integer"},
2857 "frames.total": {"value": 4, "type": "integer"},
2858 "stall_total_time": {"value": 4000, "type": "integer"}
2859 }
2860 "#;
2861
2862 let mut attributes = Annotated::<Attributes>::from_json(json).unwrap();
2863
2864 normalize_attribute_names(&mut attributes);
2865 normalize_mobile_measurements(&mut attributes, Some(Duration::from_secs(5)));
2866
2867 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r###"
2868 {
2869 "app.vitals.frames.frozen.count": {
2870 "type": "integer",
2871 "value": 2
2872 },
2873 "app.vitals.frames.slow.count": {
2874 "type": "integer",
2875 "value": 1
2876 },
2877 "app.vitals.frames.total.count": {
2878 "type": "integer",
2879 "value": 4
2880 },
2881 "frames.slow": {
2882 "type": "integer",
2883 "value": 1
2884 },
2885 "frames.total": {
2886 "type": "integer",
2887 "value": 4
2888 },
2889 "frames_frozen_rate": {
2890 "type": "double",
2891 "value": 0.5
2892 },
2893 "frames_slow_rate": {
2894 "type": "double",
2895 "value": 0.25
2896 },
2897 "stall_percentage": {
2898 "type": "double",
2899 "value": 0.8
2900 },
2901 "stall_total_time": {
2902 "type": "integer",
2903 "value": 4000
2904 }
2905 }
2906 "###);
2907 }
2908}