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