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