1use std::borrow::Cow;
6use std::net::IpAddr;
7
8use chrono::{DateTime, Utc};
9use relay_common::time::UnixTimestamp;
10use relay_conventions::consts::*;
11use relay_conventions::{AttributeInfo, WriteBehavior};
12use relay_event_schema::protocol::{AttributeType, Attributes, BrowserContext, Geo};
13use relay_protocol::{Annotated, ErrorKind, Meta, Remark, RemarkType, Value};
14use relay_sampling::DynamicSamplingContext;
15use relay_spans::derive_op_for_v2_span;
16
17use crate::span::TABLE_NAME_REGEX;
18use crate::span::description::{scrub_db_query, scrub_http};
19use crate::span::tag_extraction::{
20 domain_from_scrubbed_http, domain_from_server_address, span_op_to_category,
21 sql_action_from_query, sql_tables_from_query,
22};
23use crate::{ClientHints, FromUserAgentInfo as _, RawUserAgentInfo};
24
25mod ai;
26mod mobile;
27mod size;
28pub mod time;
29pub mod trace_metric;
30mod trimming;
31
32pub use self::ai::normalize_ai;
33pub use self::mobile::normalize_mobile_attributes;
34pub use self::size::*;
35pub use self::trimming::TrimmingProcessor;
36
37pub fn normalize_sentry_op(attributes: &mut Annotated<Attributes>) {
39 if attributes
40 .value()
41 .is_some_and(|attrs| attrs.contains_key(OP))
42 {
43 return;
44 }
45 let inferred_op = derive_op_for_v2_span(attributes);
46 let attrs = attributes.get_or_insert_with(Default::default);
47 attrs.insert_if_missing(OP, || inferred_op);
48}
49
50pub fn normalize_span_category(attributes: &mut Annotated<Attributes>) {
54 let Some(attributes_val) = attributes.value() else {
55 return;
56 };
57
58 if attribute_is_nonempty_string(attributes_val, SENTRY_CATEGORY) {
60 return;
61 }
62
63 if let Some(op_value) = attributes_val.get_value(OP)
65 && let Some(op_str) = op_value.as_str()
66 {
67 let op_lowercase = op_str.to_lowercase();
68 if let Some(category) = span_op_to_category(&op_lowercase) {
69 let attrs = attributes.get_or_insert_with(Default::default);
70 attrs.insert(SENTRY_CATEGORY, category.to_owned());
71 return;
72 }
73 }
74
75 let category = if attribute_is_nonempty_string(attributes_val, DB_SYSTEM_NAME) {
77 Some("db")
78 } else if attribute_is_nonempty_string(attributes_val, HTTP_REQUEST_METHOD) {
79 Some("http")
80 } else if attribute_is_nonempty_string(attributes_val, UI_COMPONENT_NAME) {
81 Some("ui")
82 } else if attribute_is_nonempty_string(attributes_val, RESOURCE_RENDER_BLOCKING_STATUS) {
83 Some("resource")
84 } else if attributes_val
85 .get_value(ORIGIN)
86 .and_then(|v| v.as_str())
87 .is_some_and(|v| v == "auto.ui.browser.metrics")
88 {
89 Some("browser")
90 } else {
91 None
92 };
93
94 if let Some(category) = category {
96 let attrs = attributes.get_or_insert_with(Default::default);
97 attrs.insert(SENTRY_CATEGORY, category.to_owned());
98 }
99}
100
101fn attribute_is_nonempty_string(attributes: &Attributes, key: &str) -> bool {
102 attributes
103 .get_value(key)
104 .and_then(|v| v.as_str())
105 .is_some_and(|s| !s.is_empty())
106}
107
108pub fn normalize_attribute_types(attributes: &mut Annotated<Attributes>) {
113 let Some(attributes) = attributes.value_mut() else {
114 return;
115 };
116
117 let attributes = attributes.0.values_mut();
118 for attribute in attributes {
119 use AttributeType::*;
120
121 let Some(inner) = attribute.value_mut() else {
122 continue;
123 };
124
125 match (&mut inner.value.ty, &mut inner.value.value) {
126 (Annotated(Some(Boolean), _), Annotated(Some(Value::Bool(_)), _)) => (),
127 (Annotated(Some(Integer), _), Annotated(Some(Value::I64(_)), _)) => (),
128 (Annotated(Some(Integer), _), Annotated(Some(Value::U64(u)), _))
129 if i64::try_from(*u).is_ok() => {}
130 (Annotated(Some(Double), _), Annotated(Some(Value::I64(_)), _)) => (),
131 (Annotated(Some(Double), _), Annotated(Some(Value::U64(_)), _)) => (),
132 (Annotated(Some(Double), _), Annotated(Some(Value::F64(_)), _)) => (),
133 (Annotated(Some(String), _), Annotated(Some(Value::String(_)), _)) => (),
134 (Annotated(Some(Array), _), Annotated(Some(Value::Array(arr)), _)) => {
135 if !is_supported_array(arr) {
136 let _ = attribute.value_mut().take();
137 attribute.meta_mut().add_error(ErrorKind::InvalidData);
138 }
139 }
140 (Annotated(Some(Unknown(_)), _), _) => {
146 let original = attribute.value_mut().take();
147 attribute.meta_mut().add_error(ErrorKind::InvalidData);
148 attribute.meta_mut().set_original_value(original);
149 }
150 (Annotated(Some(_), _), Annotated(Some(_), _)) => {
151 let original = attribute.value_mut().take();
152 attribute.meta_mut().add_error(ErrorKind::InvalidData);
153 attribute.meta_mut().set_original_value(original);
154 }
155 (Annotated(None, _), _) | (_, Annotated(None, _)) => {
156 let original = attribute.value_mut().take();
157 attribute.meta_mut().add_error(ErrorKind::MissingAttribute);
158 attribute.meta_mut().set_original_value(original);
159 }
160 }
161 }
162}
163
164fn is_supported_array(arr: &[Annotated<Value>]) -> bool {
168 let mut iter = arr.iter();
169
170 let Some(first) = iter.next() else {
171 return true;
173 };
174
175 let item = iter.try_fold(first, |prev, current| {
176 let r = match (prev.value(), current.value()) {
177 (None, None) => prev,
178 (None, Some(_)) => current,
179 (Some(_), None) => prev,
180 (Some(Value::String(_)), Some(Value::String(_))) => prev,
181 (Some(Value::Bool(_)), Some(Value::Bool(_))) => prev,
182 (
183 Some(Value::I64(_) | Value::U64(_) | Value::F64(_)),
185 Some(Value::I64(_) | Value::U64(_) | Value::F64(_)),
186 ) => prev,
187 (Some(_), Some(_)) => return None,
191 };
192
193 Some(r)
194 });
195
196 let Some(item) = item else {
197 return false;
199 };
200
201 matches!(
202 item.value(),
203 None | Some(
206 Value::String(_) | Value::Bool(_) | Value::I64(_) | Value::U64(_) | Value::F64(_)
207 )
208 )
209}
210
211pub fn normalize_received(attributes: &mut Annotated<Attributes>, received: DateTime<Utc>) {
213 attributes
214 .get_or_insert_with(Default::default)
215 .insert_if_missing(OBSERVED_TIMESTAMP_NANOS, || {
216 received
217 .timestamp_nanos_opt()
218 .unwrap_or_else(|| UnixTimestamp::now().as_nanos() as i64)
219 .to_string()
220 });
221}
222
223#[derive(Debug, Copy, Clone, Default)]
227pub struct ClientUserAgentInfo<'a> {
228 pub user_agent: Option<&'a str>,
230 pub hints: ClientHints<&'a str>,
232}
233
234pub fn normalize_user_agent(
242 attributes: &mut Annotated<Attributes>,
243 client_info: Option<ClientUserAgentInfo<'_>>,
244) {
245 let attributes = attributes.get_or_insert_with(Default::default);
246
247 if attributes.contains_key(BROWSER_NAME) || attributes.contains_key(BROWSER_VERSION) {
248 return;
249 }
250
251 let user_agent = attributes
253 .get_value(USER_AGENT_ORIGINAL)
254 .and_then(|v| v.as_str())
255 .or(client_info.and_then(|ci| ci.user_agent));
256
257 let Some(context) = BrowserContext::from_hints_or_ua(&RawUserAgentInfo {
258 user_agent,
259 client_hints: client_info.map(|ci| ci.hints).unwrap_or_default(),
260 }) else {
261 return;
262 };
263
264 attributes.insert_if_missing(BROWSER_NAME, || context.name);
265 attributes.insert_if_missing(BROWSER_VERSION, || context.version);
266}
267
268pub fn normalize_client_address(attributes: &mut Annotated<Attributes>, client_ip: Option<IpAddr>) {
276 let Some(attributes) = attributes.value_mut() else {
277 return;
278 };
279
280 let client_address = attributes
281 .get_value(CLIENT_ADDRESS)
282 .and_then(|v| v.as_str());
283
284 if client_address == Some("{{auto}}") {
285 match client_ip {
286 Some(client_ip) => attributes.insert(CLIENT_ADDRESS, client_ip.to_string()),
287 None => drop(attributes.remove(CLIENT_ADDRESS)),
288 }
289 }
290}
291
292pub fn normalize_inject_client_address(
296 attributes: &mut Annotated<Attributes>,
297 client_ip: Option<IpAddr>,
298) {
299 let Some(client_ip) = client_ip else {
300 return;
301 };
302
303 let attributes = attributes.get_or_insert_with(Default::default);
304 attributes.insert_if_missing(CLIENT_ADDRESS, || client_ip.to_string());
305}
306
307pub fn normalize_user_geo(
315 attributes: &mut Annotated<Attributes>,
316 info: impl FnOnce(IpAddr) -> Option<Geo>,
317) {
318 let Some(attributes) = attributes.value_mut() else {
319 return;
320 };
321
322 if [
323 USER_GEO_COUNTRY_CODE,
324 USER_GEO_CITY,
325 USER_GEO_SUBDIVISION,
326 USER_GEO_REGION,
327 ]
328 .into_iter()
329 .any(|a| attributes.contains_key(a))
330 {
331 return;
332 }
333
334 let client_address = attributes
335 .get_value(CLIENT_ADDRESS)
336 .and_then(|v| v.as_str())
337 .and_then(|v| v.parse().ok());
338
339 let Some(geo) = client_address.and_then(info) else {
340 return;
341 };
342
343 attributes.insert_if_missing(USER_GEO_COUNTRY_CODE, || geo.country_code);
344 attributes.insert_if_missing(USER_GEO_CITY, || geo.city);
345 attributes.insert_if_missing(USER_GEO_SUBDIVISION, || geo.subdivision);
346 attributes.insert_if_missing(USER_GEO_REGION, || geo.region);
347}
348
349pub fn normalize_dsc(attributes: &mut Annotated<Attributes>, dsc: Option<&DynamicSamplingContext>) {
351 let Some(dsc) = dsc else { return };
352
353 let attributes = attributes.get_or_insert_with(Default::default);
354
355 if attributes.contains_key(DSC_TRACE_ID) {
357 return;
358 }
359
360 attributes.insert(DSC_TRACE_ID, dsc.trace_id.to_string());
361 attributes.insert(DSC_PUBLIC_KEY, dsc.public_key.to_string());
362 if let Some(release) = &dsc.release {
363 attributes.insert(DSC_RELEASE, release.clone());
364 }
365 if let Some(environment) = &dsc.environment {
366 attributes.insert(DSC_ENVIRONMENT, environment.clone());
367 }
368 if let Some(transaction) = &dsc.transaction {
369 attributes.insert(DSC_TRANSACTION, transaction.clone());
370 }
371 if let Some(sample_rate) = dsc.sample_rate {
372 attributes.insert(DSC_SAMPLE_RATE, sample_rate);
373 }
374 if let Some(sampled) = dsc.sampled {
375 attributes.insert(DSC_SAMPLED, sampled);
376 }
377}
378
379pub fn normalize_attribute_names(attributes: &mut Annotated<Attributes>) {
388 normalize_attribute_names_inner(attributes, relay_conventions::attribute_info)
389}
390
391fn normalize_attribute_names_inner(
392 attributes: &mut Annotated<Attributes>,
393 attribute_info: fn(&str) -> Option<&'static AttributeInfo>,
394) {
395 let Some(attributes) = attributes.value_mut() else {
396 return;
397 };
398
399 let attribute_names: Vec<_> = attributes.0.keys().cloned().collect();
400
401 for name in attribute_names {
402 let Some(attribute_info) = attribute_info(&name) else {
403 continue;
404 };
405
406 match attribute_info.write_behavior {
407 WriteBehavior::CurrentName => continue,
408 WriteBehavior::NewName(new_name) => {
409 let Some(old_attribute) = attributes.0.get_mut(&name) else {
410 continue;
411 };
412
413 let mut meta = Meta::default();
414 meta.add_remark(Remark::new(RemarkType::Removed, "attribute.deprecated"));
416 let new_attribute = std::mem::replace(old_attribute, Annotated(None, meta));
417
418 if !attributes.contains_key(new_name) {
419 attributes.0.insert(new_name.to_owned(), new_attribute);
420 }
421 }
422 WriteBehavior::BothNames(new_name) => {
423 if !attributes.contains_key(new_name)
424 && let Some(current_attribute) = attributes.0.get(&name).cloned()
425 {
426 attributes.0.insert(new_name.to_owned(), current_attribute);
427 }
428 }
429 }
430 }
431}
432
433pub fn normalize_attribute_values(
442 attributes: &mut Annotated<Attributes>,
443 http_span_allowed_hosts: &[String],
444) {
445 normalize_db_attributes(attributes);
446 normalize_http_attributes(attributes, http_span_allowed_hosts);
447 normalize_mobile_attributes(attributes);
448}
449
450fn normalize_db_attributes(annotated_attributes: &mut Annotated<Attributes>) {
459 let Some(attributes) = annotated_attributes.value() else {
460 return;
461 };
462
463 if attributes.get_value(NORMALIZED_DB_QUERY).is_some() {
465 return;
466 }
467
468 let (op, sub_op) = attributes
469 .get_value(OP)
470 .and_then(|v| v.as_str())
471 .map(|op| op.split_once('.').unwrap_or((op, "")))
472 .unwrap_or_default();
473
474 let raw_query = attributes
475 .get_value(DB_QUERY_TEXT)
476 .or_else(|| {
477 if op == "db" {
478 attributes.get_value(DESCRIPTION)
479 } else {
480 None
481 }
482 })
483 .and_then(|v| v.as_str());
484
485 let db_system = attributes
486 .get_value(DB_SYSTEM_NAME)
487 .and_then(|v| v.as_str());
488
489 let db_operation = attributes
490 .get_value(DB_OPERATION_NAME)
491 .and_then(|v| v.as_str());
492
493 let collection_name = attributes
494 .get_value(DB_COLLECTION_NAME)
495 .and_then(|v| v.as_str());
496
497 let span_origin = attributes.get_value(ORIGIN).and_then(|v| v.as_str());
498
499 let (normalized_db_query, parsed_sql) = if let Some(raw_query) = raw_query {
500 scrub_db_query(
501 raw_query,
502 sub_op,
503 db_system,
504 db_operation,
505 collection_name,
506 span_origin,
507 )
508 } else {
509 (None, None)
510 };
511
512 let db_operation = if db_operation.is_none() {
513 if sub_op == "redis" || db_system == Some("redis") {
514 if let Some(query) = normalized_db_query.as_ref() {
516 let command = query.replace(" *", "");
517 if command.is_empty() {
518 None
519 } else {
520 Some(command)
521 }
522 } else {
523 None
524 }
525 } else if let Some(raw_query) = raw_query {
526 sql_action_from_query(raw_query).map(|a| a.to_uppercase())
528 } else {
529 None
530 }
531 } else {
532 db_operation.map(|db_operation| db_operation.to_uppercase())
533 };
534
535 let db_collection_name: Option<String> = if let Some(name) = collection_name {
536 if db_system == Some("mongodb") {
537 match TABLE_NAME_REGEX.replace_all(name, "{%s}") {
538 Cow::Owned(s) => Some(s),
539 Cow::Borrowed(_) => Some(name.to_owned()),
540 }
541 } else {
542 Some(name.to_owned())
543 }
544 } else if span_origin == Some("auto.db.supabase") {
545 normalized_db_query
546 .as_ref()
547 .and_then(|query| query.strip_prefix("from("))
548 .and_then(|s| s.strip_suffix(")"))
549 .map(String::from)
550 } else if let Some(raw_query) = raw_query {
551 sql_tables_from_query(raw_query, &parsed_sql)
552 } else {
553 None
554 };
555
556 if let Some(attributes) = annotated_attributes.value_mut() {
557 if let Some(normalized_db_query) = normalized_db_query {
558 let mut normalized_db_query_hash = format!("{:x}", md5::compute(&normalized_db_query));
559 normalized_db_query_hash.truncate(16);
560
561 attributes.insert(NORMALIZED_DB_QUERY, normalized_db_query);
562 attributes.insert(NORMALIZED_DB_QUERY_HASH, normalized_db_query_hash);
563 }
564 if let Some(db_operation_name) = db_operation {
565 attributes.insert(DB_OPERATION_NAME, db_operation_name)
566 }
567 if let Some(db_collection_name) = db_collection_name {
568 attributes.insert(DB_COLLECTION_NAME, db_collection_name);
569 }
570 }
571}
572
573fn normalize_http_attributes(
578 annotated_attributes: &mut Annotated<Attributes>,
579 allowed_hosts: &[String],
580) {
581 let Some(attributes) = annotated_attributes.value() else {
582 return;
583 };
584
585 if attributes
587 .get_value(SENTRY_CATEGORY)
588 .is_none_or(|category| category.as_str().unwrap_or_default() != "http")
589 {
590 return;
591 }
592
593 let op = attributes.get_value(OP).and_then(|v| v.as_str());
594
595 let (description_method, description_url) = match attributes
596 .get_value(DESCRIPTION)
597 .and_then(|v| v.as_str())
598 .and_then(|description| description.split_once(' '))
599 {
600 Some((method, url)) => (Some(method), Some(url)),
601 _ => (None, None),
602 };
603
604 let method = attributes
605 .get_value(HTTP_REQUEST_METHOD)
606 .and_then(|v| v.as_str())
607 .or(description_method);
608
609 let server_address = attributes
610 .get_value(SERVER_ADDRESS)
611 .and_then(|v| v.as_str());
612
613 let url: Option<&str> = attributes
614 .get_value(URL_FULL)
615 .and_then(|v| v.as_str())
616 .or(description_url);
617 let url_scheme = attributes.get_value(URL_SCHEME).and_then(|v| v.as_str());
618
619 let (normalized_server_address, raw_url) = if op == Some("http.client") {
622 let domain_from_scrubbed_http = method
623 .zip(url)
624 .and_then(|(method, url)| scrub_http(method, url, allowed_hosts))
625 .and_then(|scrubbed_http| domain_from_scrubbed_http(&scrubbed_http));
626
627 if let Some(domain) = domain_from_scrubbed_http {
628 (Some(domain), url.map(String::from))
629 } else {
630 domain_from_server_address(server_address, url_scheme)
631 }
632 } else {
633 (None, None)
634 };
635
636 let method = method.map(|m| m.to_uppercase());
637
638 if let Some(attributes) = annotated_attributes.value_mut() {
639 if let Some(method) = method {
640 attributes.insert(HTTP_REQUEST_METHOD, method);
641 }
642
643 if let Some(normalized_server_address) = normalized_server_address {
644 attributes.insert(SERVER_ADDRESS, normalized_server_address);
645 }
646
647 if let Some(raw_url) = raw_url {
648 attributes.insert_if_missing(URL_FULL, || raw_url);
649 }
650 }
651}
652
653pub fn write_legacy_attributes(attributes: &mut Annotated<Attributes>) {
661 let Some(attributes) = attributes.value_mut() else {
662 return;
663 };
664
665 let current_to_legacy_attributes = [
667 (DB_QUERY_TEXT, DESCRIPTION),
669 (NORMALIZED_DB_QUERY, SENTRY_NORMALIZED_DESCRIPTION),
670 (DB_OPERATION_NAME, SENTRY_ACTION),
671 (DB_SYSTEM_NAME, DB_SYSTEM),
672 (SERVER_ADDRESS, SENTRY_DOMAIN),
674 (HTTP_REQUEST_METHOD, SENTRY_ACTION),
675 (HTTP_RESPONSE_STATUS_CODE, SENTRY_STATUS_CODE),
676 ];
677
678 for (current_attribute, legacy_attribute) in current_to_legacy_attributes {
679 if attributes.contains_key(current_attribute) {
680 let Some(attr) = attributes.get_attribute(current_attribute) else {
681 continue;
682 };
683 attributes.insert(legacy_attribute, attr.value.clone());
684 }
685 }
686
687 if !attributes.contains_key(SENTRY_DOMAIN)
688 && let Some(db_domain) = attributes
689 .get_value(DB_COLLECTION_NAME)
690 .and_then(|value| value.as_str())
691 .map(|collection_name| collection_name.to_owned())
692 {
693 attributes.insert(
695 SENTRY_DOMAIN,
696 match (db_domain.starts_with(','), db_domain.ends_with(',')) {
697 (true, true) => db_domain,
698 (true, false) => format!("{db_domain},"),
699 (false, true) => format!(",{db_domain}"),
700 (false, false) => format!(",{db_domain},"),
701 },
702 );
703 }
704
705 if let Some(&Value::String(method)) = attributes.get_value(HTTP_REQUEST_METHOD).as_ref()
706 && let Some(&Value::String(url)) = attributes.get_value(URL_FULL).as_ref()
707 {
708 attributes.insert(DESCRIPTION, format!("{method} {url}"))
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use relay_protocol::{Empty, SerializableAnnotated};
715
716 use super::*;
717
718 #[test]
719 fn test_normalize_received_none() {
720 let mut attributes = Default::default();
721
722 normalize_received(
723 &mut attributes,
724 DateTime::from_timestamp_nanos(1_234_201_337),
725 );
726
727 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
728 {
729 "sentry.observed_timestamp_nanos": {
730 "type": "string",
731 "value": "1234201337"
732 }
733 }
734 "#);
735 }
736
737 #[test]
738 fn test_normalize_received_existing() {
739 let mut attributes = Annotated::from_json(
740 r#"{
741 "sentry.observed_timestamp_nanos": {
742 "type": "string",
743 "value": "111222333"
744 }
745 }"#,
746 )
747 .unwrap();
748
749 normalize_received(
750 &mut attributes,
751 DateTime::from_timestamp_nanos(1_234_201_337),
752 );
753
754 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r###"
755 {
756 "sentry.observed_timestamp_nanos": {
757 "type": "string",
758 "value": "111222333"
759 }
760 }
761 "###);
762 }
763
764 #[test]
765 fn test_process_attribute_types() {
766 let json = r#"{
767 "valid_bool": {
768 "type": "boolean",
769 "value": true
770 },
771 "valid_int_i64": {
772 "type": "integer",
773 "value": -42
774 },
775 "valid_int_u64": {
776 "type": "integer",
777 "value": 42
778 },
779 "valid_int_from_string": {
780 "type": "integer",
781 "value": "42"
782 },
783 "valid_double": {
784 "type": "double",
785 "value": 42.5
786 },
787 "double_with_i64": {
788 "type": "double",
789 "value": -42
790 },
791 "valid_double_with_u64": {
792 "type": "double",
793 "value": 42
794 },
795 "valid_string": {
796 "type": "string",
797 "value": "test"
798 },
799 "valid_string_with_other": {
800 "type": "string",
801 "value": "test",
802 "some_other_field": "some_other_value"
803 },
804 "unknown_type": {
805 "type": "custom",
806 "value": "test"
807 },
808 "invalid_int_from_invalid_string": {
809 "type": "integer",
810 "value": "abc"
811 },
812 "invalid_int": {
813 "type": "integer",
814 "value": 9223372036854775808
815 },
816 "missing_type": {
817 "value": "value with missing type"
818 },
819 "missing_value": {
820 "type": "string"
821 },
822 "supported_array_string": {
823 "type": "array",
824 "value": ["foo", "bar"]
825 },
826 "supported_array_double": {
827 "type": "array",
828 "value": [3, 3.0, 3]
829 },
830 "supported_array_null": {
831 "type": "array",
832 "value": [null, null]
833 },
834 "unsupported_array_mixed": {
835 "type": "array",
836 "value": ["foo", 1.0]
837 },
838 "unsupported_array_object": {
839 "type": "array",
840 "value": [{}]
841 },
842 "unsupported_array_in_array": {
843 "type": "array",
844 "value": [[]]
845 }
846 }"#;
847
848 let mut attributes = Annotated::<Attributes>::from_json(json).unwrap();
849 normalize_attribute_types(&mut attributes);
850
851 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
852 {
853 "double_with_i64": {
854 "type": "double",
855 "value": -42
856 },
857 "invalid_int": null,
858 "invalid_int_from_invalid_string": null,
859 "missing_type": null,
860 "missing_value": null,
861 "supported_array_double": {
862 "type": "array",
863 "value": [
864 3,
865 3.0,
866 3
867 ]
868 },
869 "supported_array_null": {
870 "type": "array",
871 "value": [
872 null,
873 null
874 ]
875 },
876 "supported_array_string": {
877 "type": "array",
878 "value": [
879 "foo",
880 "bar"
881 ]
882 },
883 "unknown_type": null,
884 "unsupported_array_in_array": null,
885 "unsupported_array_mixed": null,
886 "unsupported_array_object": null,
887 "valid_bool": {
888 "type": "boolean",
889 "value": true
890 },
891 "valid_double": {
892 "type": "double",
893 "value": 42.5
894 },
895 "valid_double_with_u64": {
896 "type": "double",
897 "value": 42
898 },
899 "valid_int_from_string": null,
900 "valid_int_i64": {
901 "type": "integer",
902 "value": -42
903 },
904 "valid_int_u64": {
905 "type": "integer",
906 "value": 42
907 },
908 "valid_string": {
909 "type": "string",
910 "value": "test"
911 },
912 "valid_string_with_other": {
913 "type": "string",
914 "value": "test",
915 "some_other_field": "some_other_value"
916 },
917 "_meta": {
918 "invalid_int": {
919 "": {
920 "err": [
921 "invalid_data"
922 ],
923 "val": {
924 "type": "integer",
925 "value": 9223372036854775808
926 }
927 }
928 },
929 "invalid_int_from_invalid_string": {
930 "": {
931 "err": [
932 "invalid_data"
933 ],
934 "val": {
935 "type": "integer",
936 "value": "abc"
937 }
938 }
939 },
940 "missing_type": {
941 "": {
942 "err": [
943 "missing_attribute"
944 ],
945 "val": {
946 "type": null,
947 "value": "value with missing type"
948 }
949 }
950 },
951 "missing_value": {
952 "": {
953 "err": [
954 "missing_attribute"
955 ],
956 "val": {
957 "type": "string",
958 "value": null
959 }
960 }
961 },
962 "unknown_type": {
963 "": {
964 "err": [
965 "invalid_data"
966 ],
967 "val": {
968 "type": "custom",
969 "value": "test"
970 }
971 }
972 },
973 "unsupported_array_in_array": {
974 "": {
975 "err": [
976 "invalid_data"
977 ]
978 }
979 },
980 "unsupported_array_mixed": {
981 "": {
982 "err": [
983 "invalid_data"
984 ]
985 }
986 },
987 "unsupported_array_object": {
988 "": {
989 "err": [
990 "invalid_data"
991 ]
992 }
993 },
994 "valid_int_from_string": {
995 "": {
996 "err": [
997 "invalid_data"
998 ],
999 "val": {
1000 "type": "integer",
1001 "value": "42"
1002 }
1003 }
1004 }
1005 }
1006 }
1007 "#);
1008 }
1009
1010 #[test]
1011 fn test_normalize_user_agent_none() {
1012 let mut attributes = Default::default();
1013 normalize_user_agent(
1014 &mut attributes,
1015 Some(ClientUserAgentInfo {
1016 user_agent: Some(
1017 "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",
1018 ),
1019 ..Default::default()
1020 }),
1021 );
1022
1023 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1024 {
1025 "sentry.browser.name": {
1026 "type": "string",
1027 "value": "Chrome"
1028 },
1029 "sentry.browser.version": {
1030 "type": "string",
1031 "value": "131.0.0"
1032 }
1033 }
1034 "#);
1035 }
1036
1037 #[test]
1038 fn test_normalize_user_agent_existing() {
1039 let mut attributes = Annotated::from_json(
1040 r#"{
1041 "sentry.browser.name": {
1042 "type": "string",
1043 "value": "Very Special"
1044 },
1045 "sentry.browser.version": {
1046 "type": "string",
1047 "value": "13.3.7"
1048 }
1049 }"#,
1050 )
1051 .unwrap();
1052
1053 normalize_user_agent(
1054 &mut attributes,
1055 Some(ClientUserAgentInfo {
1056 user_agent: Some(
1057 "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",
1058 ),
1059 ..Default::default()
1060 }),
1061 );
1062
1063 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1064 {
1065 "sentry.browser.name": {
1066 "type": "string",
1067 "value": "Very Special"
1068 },
1069 "sentry.browser.version": {
1070 "type": "string",
1071 "value": "13.3.7"
1072 }
1073 }
1074 "#,
1075 );
1076 }
1077
1078 #[test]
1079 fn test_normalize_user_geo_none() {
1080 let mut attributes = Annotated::from_json(
1081 r#"{
1082 "client.address": {
1083 "type": "string",
1084 "value": "192.168.2.1"
1085 }
1086 }"#,
1087 )
1088 .unwrap();
1089
1090 normalize_user_geo(&mut attributes, |addr| {
1091 Some(Geo {
1092 country_code: "XY".to_owned().into(),
1093 city: addr.to_string().into(),
1094 subdivision: Annotated::empty(),
1095 region: "Illu".to_owned().into(),
1096 other: Default::default(),
1097 })
1098 });
1099
1100 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1101 {
1102 "client.address": {
1103 "type": "string",
1104 "value": "192.168.2.1"
1105 },
1106 "user.geo.city": {
1107 "type": "string",
1108 "value": "192.168.2.1"
1109 },
1110 "user.geo.country_code": {
1111 "type": "string",
1112 "value": "XY"
1113 },
1114 "user.geo.region": {
1115 "type": "string",
1116 "value": "Illu"
1117 }
1118 }
1119 "#);
1120 }
1121
1122 #[test]
1123 fn test_normalize_user_geo_existing() {
1124 let mut attributes = Annotated::from_json(
1125 r#"{
1126 "client.address": {
1127 "type": "string",
1128 "value": "192.168.2.1"
1129 },
1130 "user.geo.city": {
1131 "type": "string",
1132 "value": "Foo Hausen"
1133 }
1134 }"#,
1135 )
1136 .unwrap();
1137
1138 normalize_user_geo(&mut attributes, |_| unreachable!());
1139
1140 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1141 {
1142 "client.address": {
1143 "type": "string",
1144 "value": "192.168.2.1"
1145 },
1146 "user.geo.city": {
1147 "type": "string",
1148 "value": "Foo Hausen"
1149 }
1150 }
1151 "#,
1152 );
1153 }
1154
1155 #[test]
1156 fn test_normalize_attributes() {
1157 fn mock_attribute_info(name: &str) -> Option<&'static AttributeInfo> {
1158 use relay_conventions::Pii;
1159
1160 match name {
1161 "replace.empty" => Some(&AttributeInfo {
1162 write_behavior: WriteBehavior::NewName("replaced"),
1163 pii: Pii::Maybe,
1164 aliases: &["replaced"],
1165 }),
1166 "replace.existing" => Some(&AttributeInfo {
1167 write_behavior: WriteBehavior::NewName("not.replaced"),
1168 pii: Pii::Maybe,
1169 aliases: &["not.replaced"],
1170 }),
1171 "backfill.empty" => Some(&AttributeInfo {
1172 write_behavior: WriteBehavior::BothNames("backfilled"),
1173 pii: Pii::Maybe,
1174 aliases: &["backfilled"],
1175 }),
1176 "backfill.existing" => Some(&AttributeInfo {
1177 write_behavior: WriteBehavior::BothNames("not.backfilled"),
1178 pii: Pii::Maybe,
1179 aliases: &["not.backfilled"],
1180 }),
1181 _ => None,
1182 }
1183 }
1184
1185 let mut attributes = Annotated::new(Attributes::from([
1186 (
1187 "replace.empty".to_owned(),
1188 Annotated::new("Should be moved".to_owned().into()),
1189 ),
1190 (
1191 "replace.existing".to_owned(),
1192 Annotated::new("Should be removed".to_owned().into()),
1193 ),
1194 (
1195 "not.replaced".to_owned(),
1196 Annotated::new("Should be left alone".to_owned().into()),
1197 ),
1198 (
1199 "backfill.empty".to_owned(),
1200 Annotated::new("Should be copied".to_owned().into()),
1201 ),
1202 (
1203 "backfill.existing".to_owned(),
1204 Annotated::new("Should be left alone".to_owned().into()),
1205 ),
1206 (
1207 "not.backfilled".to_owned(),
1208 Annotated::new("Should be left alone".to_owned().into()),
1209 ),
1210 ]));
1211
1212 normalize_attribute_names_inner(&mut attributes, mock_attribute_info);
1213
1214 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r###"
1215 {
1216 "backfill.empty": {
1217 "type": "string",
1218 "value": "Should be copied"
1219 },
1220 "backfill.existing": {
1221 "type": "string",
1222 "value": "Should be left alone"
1223 },
1224 "backfilled": {
1225 "type": "string",
1226 "value": "Should be copied"
1227 },
1228 "not.backfilled": {
1229 "type": "string",
1230 "value": "Should be left alone"
1231 },
1232 "not.replaced": {
1233 "type": "string",
1234 "value": "Should be left alone"
1235 },
1236 "replace.empty": null,
1237 "replace.existing": null,
1238 "replaced": {
1239 "type": "string",
1240 "value": "Should be moved"
1241 },
1242 "_meta": {
1243 "replace.empty": {
1244 "": {
1245 "rem": [
1246 [
1247 "attribute.deprecated",
1248 "x"
1249 ]
1250 ]
1251 }
1252 },
1253 "replace.existing": {
1254 "": {
1255 "rem": [
1256 [
1257 "attribute.deprecated",
1258 "x"
1259 ]
1260 ]
1261 }
1262 }
1263 }
1264 }
1265 "###);
1266 }
1267
1268 #[test]
1269 fn test_normalize_span_infers_op() {
1270 let mut attributes = Annotated::<Attributes>::from_json(
1271 r#"{
1272 "db.system.name": {
1273 "type": "string",
1274 "value": "mysql"
1275 },
1276 "db.operation.name": {
1277 "type": "string",
1278 "value": "query"
1279 }
1280 }
1281 "#,
1282 )
1283 .unwrap();
1284
1285 normalize_sentry_op(&mut attributes);
1286
1287 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1288 {
1289 "db.operation.name": {
1290 "type": "string",
1291 "value": "query"
1292 },
1293 "db.system.name": {
1294 "type": "string",
1295 "value": "mysql"
1296 },
1297 "sentry.op": {
1298 "type": "string",
1299 "value": "db"
1300 }
1301 }
1302 "#);
1303 }
1304
1305 #[test]
1306 fn test_normalize_attribute_values_mysql_db_query_attributes() {
1307 let mut attributes = Annotated::<Attributes>::from_json(
1308 r#"
1309 {
1310 "sentry.op": {
1311 "type": "string",
1312 "value": "db.query"
1313 },
1314 "sentry.origin": {
1315 "type": "string",
1316 "value": "auto.otlp.spans"
1317 },
1318 "db.system.name": {
1319 "type": "string",
1320 "value": "mysql"
1321 },
1322 "db.query.text": {
1323 "type": "string",
1324 "value": "SELECT \"not an identifier\""
1325 }
1326 }
1327 "#,
1328 )
1329 .unwrap();
1330
1331 normalize_db_attributes(&mut attributes);
1332
1333 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1334 {
1335 "db.operation.name": {
1336 "type": "string",
1337 "value": "SELECT"
1338 },
1339 "db.query.text": {
1340 "type": "string",
1341 "value": "SELECT \"not an identifier\""
1342 },
1343 "db.system.name": {
1344 "type": "string",
1345 "value": "mysql"
1346 },
1347 "sentry.normalized_db_query": {
1348 "type": "string",
1349 "value": "SELECT %s"
1350 },
1351 "sentry.normalized_db_query.hash": {
1352 "type": "string",
1353 "value": "3a377dcc490b1690"
1354 },
1355 "sentry.op": {
1356 "type": "string",
1357 "value": "db.query"
1358 },
1359 "sentry.origin": {
1360 "type": "string",
1361 "value": "auto.otlp.spans"
1362 }
1363 }
1364 "#);
1365 }
1366
1367 #[test]
1368 fn test_normalize_mongodb_db_query_attributes() {
1369 let mut attributes = Annotated::<Attributes>::from_json(
1370 r#"
1371 {
1372 "sentry.op": {
1373 "type": "string",
1374 "value": "db"
1375 },
1376 "db.system.name": {
1377 "type": "string",
1378 "value": "mongodb"
1379 },
1380 "db.query.text": {
1381 "type": "string",
1382 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1383 },
1384 "db.operation.name": {
1385 "type": "string",
1386 "value": "find"
1387 },
1388 "db.collection.name": {
1389 "type": "string",
1390 "value": "documents"
1391 }
1392 }
1393 "#,
1394 )
1395 .unwrap();
1396
1397 normalize_db_attributes(&mut attributes);
1398
1399 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1400 {
1401 "db.collection.name": {
1402 "type": "string",
1403 "value": "documents"
1404 },
1405 "db.operation.name": {
1406 "type": "string",
1407 "value": "FIND"
1408 },
1409 "db.query.text": {
1410 "type": "string",
1411 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1412 },
1413 "db.system.name": {
1414 "type": "string",
1415 "value": "mongodb"
1416 },
1417 "sentry.normalized_db_query": {
1418 "type": "string",
1419 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1420 },
1421 "sentry.normalized_db_query.hash": {
1422 "type": "string",
1423 "value": "aedc5c7e8cec726b"
1424 },
1425 "sentry.op": {
1426 "type": "string",
1427 "value": "db"
1428 }
1429 }
1430 "#);
1431 }
1432
1433 #[test]
1434 fn test_normalize_db_attributes_does_not_update_attributes_if_already_normalized() {
1435 let mut attributes = Annotated::<Attributes>::from_json(
1436 r#"
1437 {
1438 "db.collection.name": {
1439 "type": "string",
1440 "value": "documents"
1441 },
1442 "db.operation.name": {
1443 "type": "string",
1444 "value": "FIND"
1445 },
1446 "db.query.text": {
1447 "type": "string",
1448 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1449 },
1450 "db.system.name": {
1451 "type": "string",
1452 "value": "mongodb"
1453 },
1454 "sentry.normalized_db_query": {
1455 "type": "string",
1456 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1457 },
1458 "sentry.op": {
1459 "type": "string",
1460 "value": "db"
1461 }
1462 }
1463 "#,
1464 )
1465 .unwrap();
1466
1467 normalize_db_attributes(&mut attributes);
1468
1469 insta::assert_json_snapshot!(
1470 SerializableAnnotated(&attributes), @r#"
1471 {
1472 "db.collection.name": {
1473 "type": "string",
1474 "value": "documents"
1475 },
1476 "db.operation.name": {
1477 "type": "string",
1478 "value": "FIND"
1479 },
1480 "db.query.text": {
1481 "type": "string",
1482 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1483 },
1484 "db.system.name": {
1485 "type": "string",
1486 "value": "mongodb"
1487 },
1488 "sentry.normalized_db_query": {
1489 "type": "string",
1490 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1491 },
1492 "sentry.op": {
1493 "type": "string",
1494 "value": "db"
1495 }
1496 }
1497 "#
1498 );
1499 }
1500
1501 #[test]
1502 fn test_normalize_db_attributes_does_not_change_non_db_spans() {
1503 let mut attributes = Annotated::<Attributes>::from_json(
1504 r#"
1505 {
1506 "sentry.op": {
1507 "type": "string",
1508 "value": "http.client"
1509 },
1510 "sentry.origin": {
1511 "type": "string",
1512 "value": "auto.otlp.spans"
1513 },
1514 "http.request.method": {
1515 "type": "string",
1516 "value": "GET"
1517 }
1518 }
1519 "#,
1520 )
1521 .unwrap();
1522
1523 normalize_db_attributes(&mut attributes);
1524
1525 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1526 {
1527 "http.request.method": {
1528 "type": "string",
1529 "value": "GET"
1530 },
1531 "sentry.op": {
1532 "type": "string",
1533 "value": "http.client"
1534 },
1535 "sentry.origin": {
1536 "type": "string",
1537 "value": "auto.otlp.spans"
1538 }
1539 }
1540 "#);
1541 }
1542
1543 #[test]
1544 fn test_normalize_http_attributes() {
1545 let mut attributes = Annotated::<Attributes>::from_json(
1546 r#"
1547 {
1548 "sentry.op": {
1549 "type": "string",
1550 "value": "http.client"
1551 },
1552 "sentry.category": {
1553 "type": "string",
1554 "value": "http"
1555 },
1556 "http.request.method": {
1557 "type": "string",
1558 "value": "GET"
1559 },
1560 "url.full": {
1561 "type": "string",
1562 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1563 }
1564 }
1565 "#,
1566 )
1567 .unwrap();
1568
1569 normalize_http_attributes(&mut attributes, &[]);
1570
1571 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1572 {
1573 "http.request.method": {
1574 "type": "string",
1575 "value": "GET"
1576 },
1577 "sentry.category": {
1578 "type": "string",
1579 "value": "http"
1580 },
1581 "sentry.op": {
1582 "type": "string",
1583 "value": "http.client"
1584 },
1585 "server.address": {
1586 "type": "string",
1587 "value": "*.xn--85x722f.xn--55qx5d.cn"
1588 },
1589 "url.full": {
1590 "type": "string",
1591 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1592 }
1593 }
1594 "#);
1595 }
1596
1597 #[test]
1598 fn test_normalize_http_attributes_server_address() {
1599 let mut attributes = Annotated::<Attributes>::from_json(
1600 r#"
1601 {
1602 "sentry.category": {
1603 "type": "string",
1604 "value": "http"
1605 },
1606 "sentry.op": {
1607 "type": "string",
1608 "value": "http.client"
1609 },
1610 "url.scheme": {
1611 "type": "string",
1612 "value": "https"
1613 },
1614 "server.address": {
1615 "type": "string",
1616 "value": "subdomain.example.com:5688"
1617 },
1618 "http.request.method": {
1619 "type": "string",
1620 "value": "GET"
1621 }
1622 }
1623 "#,
1624 )
1625 .unwrap();
1626
1627 normalize_http_attributes(&mut attributes, &[]);
1628
1629 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1630 {
1631 "http.request.method": {
1632 "type": "string",
1633 "value": "GET"
1634 },
1635 "sentry.category": {
1636 "type": "string",
1637 "value": "http"
1638 },
1639 "sentry.op": {
1640 "type": "string",
1641 "value": "http.client"
1642 },
1643 "server.address": {
1644 "type": "string",
1645 "value": "*.example.com:5688"
1646 },
1647 "url.full": {
1648 "type": "string",
1649 "value": "https://subdomain.example.com:5688"
1650 },
1651 "url.scheme": {
1652 "type": "string",
1653 "value": "https"
1654 }
1655 }
1656 "#);
1657 }
1658
1659 #[test]
1660 fn test_normalize_http_attributes_allowed_hosts() {
1661 let mut attributes = Annotated::<Attributes>::from_json(
1662 r#"
1663 {
1664 "sentry.category": {
1665 "type": "string",
1666 "value": "http"
1667 },
1668 "sentry.op": {
1669 "type": "string",
1670 "value": "http.client"
1671 },
1672 "http.request.method": {
1673 "type": "string",
1674 "value": "GET"
1675 },
1676 "url.full": {
1677 "type": "string",
1678 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1679 }
1680 }
1681 "#,
1682 )
1683 .unwrap();
1684
1685 normalize_http_attributes(
1686 &mut attributes,
1687 &["application.www.xn--85x722f.xn--55qx5d.cn".to_owned()],
1688 );
1689
1690 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1691 {
1692 "http.request.method": {
1693 "type": "string",
1694 "value": "GET"
1695 },
1696 "sentry.category": {
1697 "type": "string",
1698 "value": "http"
1699 },
1700 "sentry.op": {
1701 "type": "string",
1702 "value": "http.client"
1703 },
1704 "server.address": {
1705 "type": "string",
1706 "value": "application.www.xn--85x722f.xn--55qx5d.cn"
1707 },
1708 "url.full": {
1709 "type": "string",
1710 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1711 }
1712 }
1713 "#);
1714 }
1715
1716 #[test]
1717 fn test_normalize_db_attributes_from_legacy_attributes() {
1718 let mut attributes = Annotated::<Attributes>::from_json(
1719 r#"
1720 {
1721 "sentry.op": {
1722 "type": "string",
1723 "value": "db"
1724 },
1725 "db.system.name": {
1726 "type": "string",
1727 "value": "mongodb"
1728 },
1729 "sentry.description": {
1730 "type": "string",
1731 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1732 },
1733 "db.operation.name": {
1734 "type": "string",
1735 "value": "find"
1736 },
1737 "db.collection.name": {
1738 "type": "string",
1739 "value": "documents"
1740 }
1741 }
1742 "#,
1743 )
1744 .unwrap();
1745
1746 normalize_db_attributes(&mut attributes);
1747
1748 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1749 {
1750 "db.collection.name": {
1751 "type": "string",
1752 "value": "documents"
1753 },
1754 "db.operation.name": {
1755 "type": "string",
1756 "value": "FIND"
1757 },
1758 "db.system.name": {
1759 "type": "string",
1760 "value": "mongodb"
1761 },
1762 "sentry.description": {
1763 "type": "string",
1764 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1765 },
1766 "sentry.normalized_db_query": {
1767 "type": "string",
1768 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1769 },
1770 "sentry.normalized_db_query.hash": {
1771 "type": "string",
1772 "value": "aedc5c7e8cec726b"
1773 },
1774 "sentry.op": {
1775 "type": "string",
1776 "value": "db"
1777 }
1778 }
1779 "#);
1780 }
1781
1782 #[test]
1783 fn test_normalize_http_attributes_from_legacy_attributes() {
1784 let mut attributes = Annotated::<Attributes>::from_json(
1785 r#"
1786 {
1787 "sentry.category": {
1788 "type": "string",
1789 "value": "http"
1790 },
1791 "sentry.op": {
1792 "type": "string",
1793 "value": "http.client"
1794 },
1795 "http.request_method": {
1796 "type": "string",
1797 "value": "GET"
1798 }
1799 }
1800 "#,
1801 )
1802 .unwrap();
1803
1804 normalize_attribute_names(&mut attributes);
1805 normalize_http_attributes(&mut attributes, &[]);
1806
1807 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1808 {
1809 "http.request.method": {
1810 "type": "string",
1811 "value": "GET"
1812 },
1813 "http.request_method": {
1814 "type": "string",
1815 "value": "GET"
1816 },
1817 "sentry.category": {
1818 "type": "string",
1819 "value": "http"
1820 },
1821 "sentry.op": {
1822 "type": "string",
1823 "value": "http.client"
1824 }
1825 }
1826 "#);
1827 }
1828
1829 #[test]
1830 fn test_normalize_http_attributes_from_description() {
1831 let mut attributes = Annotated::<Attributes>::from_json(
1832 r#"
1833 {
1834 "sentry.category": {
1835 "type": "string",
1836 "value": "http"
1837 },
1838 "sentry.op": {
1839 "type": "string",
1840 "value": "http.client"
1841 },
1842 "sentry.description": {
1843 "type": "string",
1844 "value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
1845 }
1846 }
1847 "#,
1848 )
1849 .unwrap();
1850
1851 normalize_http_attributes(&mut attributes, &[]);
1852
1853 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1854 {
1855 "http.request.method": {
1856 "type": "string",
1857 "value": "GET"
1858 },
1859 "sentry.category": {
1860 "type": "string",
1861 "value": "http"
1862 },
1863 "sentry.description": {
1864 "type": "string",
1865 "value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
1866 },
1867 "sentry.op": {
1868 "type": "string",
1869 "value": "http.client"
1870 },
1871 "server.address": {
1872 "type": "string",
1873 "value": "*.xn--85x722f.xn--55qx5d.cn"
1874 },
1875 "url.full": {
1876 "type": "string",
1877 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1878 }
1879 }
1880 "#);
1881 }
1882
1883 #[test]
1884 fn test_write_legacy_attributes() {
1885 let mut attributes = Annotated::<Attributes>::from_json(
1886 r#"
1887 {
1888 "db.collection.name": {
1889 "type": "string",
1890 "value": "documents"
1891 },
1892 "db.operation.name": {
1893 "type": "string",
1894 "value": "FIND"
1895 },
1896 "db.query.text": {
1897 "type": "string",
1898 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1899 },
1900 "db.system.name": {
1901 "type": "string",
1902 "value": "mongodb"
1903 },
1904 "sentry.normalized_db_query": {
1905 "type": "string",
1906 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1907 },
1908 "sentry.normalized_db_query.hash": {
1909 "type": "string",
1910 "value": "aedc5c7e8cec726b"
1911 },
1912 "sentry.op": {
1913 "type": "string",
1914 "value": "db"
1915 }
1916 }
1917 "#,
1918 )
1919 .unwrap();
1920
1921 write_legacy_attributes(&mut attributes);
1922
1923 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1924 {
1925 "db.collection.name": {
1926 "type": "string",
1927 "value": "documents"
1928 },
1929 "db.operation.name": {
1930 "type": "string",
1931 "value": "FIND"
1932 },
1933 "db.query.text": {
1934 "type": "string",
1935 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1936 },
1937 "db.system": {
1938 "type": "string",
1939 "value": "mongodb"
1940 },
1941 "db.system.name": {
1942 "type": "string",
1943 "value": "mongodb"
1944 },
1945 "sentry.action": {
1946 "type": "string",
1947 "value": "FIND"
1948 },
1949 "sentry.description": {
1950 "type": "string",
1951 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1952 },
1953 "sentry.domain": {
1954 "type": "string",
1955 "value": ",documents,"
1956 },
1957 "sentry.normalized_db_query": {
1958 "type": "string",
1959 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1960 },
1961 "sentry.normalized_db_query.hash": {
1962 "type": "string",
1963 "value": "aedc5c7e8cec726b"
1964 },
1965 "sentry.normalized_description": {
1966 "type": "string",
1967 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1968 },
1969 "sentry.op": {
1970 "type": "string",
1971 "value": "db"
1972 }
1973 }
1974 "#);
1975 }
1976
1977 #[test]
1978 fn test_normalize_span_category_explicit() {
1979 let mut attributes = Annotated::<Attributes>::from_json(
1981 r#"{
1982 "sentry.category": {
1983 "type": "string",
1984 "value": "custom"
1985 },
1986 "sentry.op": {
1987 "type": "string",
1988 "value": "db.query"
1989 }
1990 }"#,
1991 )
1992 .unwrap();
1993
1994 normalize_span_category(&mut attributes);
1995
1996 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
1997 {
1998 "sentry.category": {
1999 "type": "string",
2000 "value": "custom"
2001 },
2002 "sentry.op": {
2003 "type": "string",
2004 "value": "db.query"
2005 }
2006 }
2007 "#);
2008 }
2009
2010 #[test]
2011 fn test_normalize_span_category_from_op_db() {
2012 let mut attributes = Annotated::<Attributes>::from_json(
2013 r#"{
2014 "sentry.op": {
2015 "type": "string",
2016 "value": "db.query"
2017 }
2018 }"#,
2019 )
2020 .unwrap();
2021
2022 normalize_span_category(&mut attributes);
2023
2024 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2025 {
2026 "sentry.category": {
2027 "type": "string",
2028 "value": "db"
2029 },
2030 "sentry.op": {
2031 "type": "string",
2032 "value": "db.query"
2033 }
2034 }
2035 "#);
2036 }
2037
2038 #[test]
2039 fn test_normalize_span_category_from_op_http() {
2040 let mut attributes = Annotated::<Attributes>::from_json(
2041 r#"{
2042 "sentry.op": {
2043 "type": "string",
2044 "value": "http.client"
2045 }
2046 }"#,
2047 )
2048 .unwrap();
2049
2050 normalize_span_category(&mut attributes);
2051
2052 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2053 {
2054 "sentry.category": {
2055 "type": "string",
2056 "value": "http"
2057 },
2058 "sentry.op": {
2059 "type": "string",
2060 "value": "http.client"
2061 }
2062 }
2063 "#);
2064 }
2065
2066 #[test]
2067 fn test_normalize_span_category_from_op_ui_framework() {
2068 let mut attributes = Annotated::<Attributes>::from_json(
2069 r#"{
2070 "sentry.op": {
2071 "type": "string",
2072 "value": "ui.react.render"
2073 }
2074 }"#,
2075 )
2076 .unwrap();
2077
2078 normalize_span_category(&mut attributes);
2079
2080 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2081 {
2082 "sentry.category": {
2083 "type": "string",
2084 "value": "ui.react"
2085 },
2086 "sentry.op": {
2087 "type": "string",
2088 "value": "ui.react.render"
2089 }
2090 }
2091 "#);
2092 }
2093
2094 #[test]
2095 fn test_normalize_span_category_from_db_system() {
2096 let mut attributes = Annotated::<Attributes>::from_json(
2098 r#"{
2099 "db.system.name": {
2100 "type": "string",
2101 "value": "mongodb"
2102 }
2103 }"#,
2104 )
2105 .unwrap();
2106
2107 normalize_span_category(&mut attributes);
2108
2109 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2110 {
2111 "db.system.name": {
2112 "type": "string",
2113 "value": "mongodb"
2114 },
2115 "sentry.category": {
2116 "type": "string",
2117 "value": "db"
2118 }
2119 }
2120 "#);
2121 }
2122
2123 #[test]
2124 fn test_normalize_span_category_from_http_method() {
2125 let mut attributes = Annotated::<Attributes>::from_json(
2127 r#"{
2128 "http.request.method": {
2129 "type": "string",
2130 "value": "GET"
2131 }
2132 }"#,
2133 )
2134 .unwrap();
2135
2136 normalize_span_category(&mut attributes);
2137
2138 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2139 {
2140 "http.request.method": {
2141 "type": "string",
2142 "value": "GET"
2143 },
2144 "sentry.category": {
2145 "type": "string",
2146 "value": "http"
2147 }
2148 }
2149 "#);
2150 }
2151
2152 #[test]
2153 fn test_normalize_span_category_from_ui_component() {
2154 let mut attributes = Annotated::<Attributes>::from_json(
2156 r#"{
2157 "ui.component_name": {
2158 "type": "string",
2159 "value": "MyComponent"
2160 }
2161 }"#,
2162 )
2163 .unwrap();
2164
2165 normalize_span_category(&mut attributes);
2166
2167 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2168 {
2169 "sentry.category": {
2170 "type": "string",
2171 "value": "ui"
2172 },
2173 "ui.component_name": {
2174 "type": "string",
2175 "value": "MyComponent"
2176 }
2177 }
2178 "#);
2179 }
2180
2181 #[test]
2182 fn test_normalize_span_category_from_resource() {
2183 let mut attributes = Annotated::<Attributes>::from_json(
2185 r#"{
2186 "resource.render_blocking_status": {
2187 "type": "string",
2188 "value": "blocking"
2189 }
2190 }"#,
2191 )
2192 .unwrap();
2193
2194 normalize_span_category(&mut attributes);
2195
2196 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2197 {
2198 "resource.render_blocking_status": {
2199 "type": "string",
2200 "value": "blocking"
2201 },
2202 "sentry.category": {
2203 "type": "string",
2204 "value": "resource"
2205 }
2206 }
2207 "#);
2208 }
2209
2210 #[test]
2211 fn test_normalize_span_category_from_browser_origin() {
2212 let mut attributes = Annotated::from_json(
2214 r#"{
2215 "sentry.origin": {
2216 "type": "string",
2217 "value": "auto.ui.browser.metrics"
2218 }
2219 }"#,
2220 )
2221 .unwrap();
2222
2223 normalize_span_category(&mut attributes);
2224
2225 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2226 {
2227 "sentry.category": {
2228 "type": "string",
2229 "value": "browser"
2230 },
2231 "sentry.origin": {
2232 "type": "string",
2233 "value": "auto.ui.browser.metrics"
2234 }
2235 }
2236 "#);
2237 }
2238
2239 #[test]
2240 fn test_normalize_client_address_auto_with_ip() {
2241 let mut attributes = Annotated::from_json(
2242 r#"{
2243 "client.address": {
2244 "type": "string",
2245 "value": "{{auto}}"
2246 }
2247 }"#,
2248 )
2249 .unwrap();
2250
2251 normalize_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2252
2253 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2254 {
2255 "client.address": {
2256 "type": "string",
2257 "value": "192.168.1.1"
2258 }
2259 }
2260 "#);
2261 }
2262
2263 #[test]
2264 fn test_normalize_client_address_auto_without_ip() {
2265 let mut attributes = Annotated::from_json(
2266 r#"{
2267 "client.address": {
2268 "type": "string",
2269 "value": "{{auto}}"
2270 }
2271 }"#,
2272 )
2273 .unwrap();
2274
2275 normalize_client_address(&mut attributes, None);
2276
2277 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2278 {}
2279 "#);
2280 }
2281
2282 #[test]
2283 fn test_normalize_client_address_explicit_not_replaced() {
2284 let mut attributes = Annotated::from_json(
2285 r#"{
2286 "client.address": {
2287 "type": "string",
2288 "value": "10.0.0.1"
2289 }
2290 }"#,
2291 )
2292 .unwrap();
2293
2294 normalize_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2295
2296 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2297 {
2298 "client.address": {
2299 "type": "string",
2300 "value": "10.0.0.1"
2301 }
2302 }
2303 "#);
2304 }
2305
2306 #[test]
2307 fn test_normalize_client_address_missing_attribute() {
2308 let mut attributes = Annotated::empty();
2309
2310 normalize_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2311
2312 assert!(attributes.is_empty());
2313 }
2314
2315 #[test]
2316 fn test_normalize_client_address_auto_with_ipv6() {
2317 let mut attributes = Annotated::from_json(
2318 r#"{
2319 "client.address": {
2320 "type": "string",
2321 "value": "{{auto}}"
2322 }
2323 }"#,
2324 )
2325 .unwrap();
2326
2327 normalize_client_address(&mut attributes, Some("2001:db8::1".parse().unwrap()));
2328
2329 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2330 {
2331 "client.address": {
2332 "type": "string",
2333 "value": "2001:db8::1"
2334 }
2335 }
2336 "#);
2337 }
2338
2339 #[test]
2340 fn test_normalize_inject_client_address_inserts_when_missing() {
2341 let mut attributes = Annotated::empty();
2342
2343 normalize_inject_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2344
2345 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2346 {
2347 "client.address": {
2348 "type": "string",
2349 "value": "192.168.1.1"
2350 }
2351 }
2352 "#);
2353 }
2354
2355 #[test]
2356 fn test_normalize_inject_client_address_does_not_overwrite() {
2357 let mut attributes = Annotated::from_json(
2358 r#"{
2359 "client.address": {
2360 "type": "string",
2361 "value": "10.0.0.1"
2362 }
2363 }"#,
2364 )
2365 .unwrap();
2366
2367 normalize_inject_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2368
2369 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2370 {
2371 "client.address": {
2372 "type": "string",
2373 "value": "10.0.0.1"
2374 }
2375 }
2376 "#);
2377 }
2378
2379 #[test]
2380 fn test_normalize_inject_client_address_none_ip() {
2381 let mut attributes = Annotated::from_json(r#"{}"#).unwrap();
2382
2383 normalize_inject_client_address(&mut attributes, None);
2384
2385 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2386 {}
2387 "#);
2388 }
2389
2390 #[test]
2391 fn test_normalize_inject_client_address_ipv6() {
2392 let mut attributes = Annotated::empty();
2393
2394 normalize_inject_client_address(&mut attributes, Some("2001:db8::1".parse().unwrap()));
2395
2396 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2397 {
2398 "client.address": {
2399 "type": "string",
2400 "value": "2001:db8::1"
2401 }
2402 }
2403 "#);
2404 }
2405
2406 #[test]
2407 fn test_normalize_span_category_no_match() {
2408 let mut attributes = Annotated::<Attributes>::from_json(
2410 r#"{
2411 "some.other.attribute": {
2412 "type": "string",
2413 "value": "value"
2414 }
2415 }"#,
2416 )
2417 .unwrap();
2418
2419 normalize_span_category(&mut attributes);
2420
2421 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
2422 {
2423 "some.other.attribute": {
2424 "type": "string",
2425 "value": "value"
2426 }
2427 }
2428 "#);
2429 }
2430}