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(attributes: &mut Annotated<Attributes>) {
546 let Some(attributes) = attributes.value_mut() else {
547 return;
548 };
549
550 fn normalize_sample_rate(sr: &Annotated<Attribute>) -> Option<Annotated<Attribute>> {
554 match sr.value()?.value.value.value()?.as_f64() {
555 Some(v) if v > 0.0 && v <= 1.0 => None,
556 _ => Some(Annotated::from_error(
558 Error::expected("sample rate > 0.0, <= 1.0"),
559 None,
560 )),
561 }
562 }
563
564 if let Some(sr) = attributes.0.get_mut(SENTRY__CLIENT_SAMPLE_RATE)
565 && let Some(new_sr) = normalize_sample_rate(sr)
566 {
567 *sr = new_sr;
568 }
569}
570
571pub fn normalize_attribute_names(attributes: &mut Annotated<impl AttributesLike>) {
580 let Some(attributes) = attributes.value_mut() else {
581 return;
582 };
583
584 normalize_attribute_names_inner(
585 attributes.as_object_mut(),
586 relay_conventions::attribute_info_with_fragment,
587 )
588}
589
590type AttributeInfoFn = fn(&str) -> Option<(&'static AttributeInfo, Option<&str>)>;
591
592fn normalize_attribute_names_inner<T>(attributes: &mut Object<T>, attribute_info: AttributeInfoFn)
593where
594 T: Clone,
595{
596 let attribute_names: Vec<_> = attributes.keys().cloned().collect();
597
598 for name in attribute_names {
599 let Some((attribute_info, fragment)) = attribute_info(&name) else {
600 continue;
601 };
602
603 match attribute_info.write_behavior {
604 WriteBehavior::CurrentName => continue,
605 WriteBehavior::NewName(new_name) => {
606 let Some(old_attribute) = attributes.get_mut(&name) else {
607 continue;
608 };
609
610 let Some(new_name) = resolve_attribute_name(new_name, fragment) else {
611 relay_log::error!(
612 attribute = name,
613 ?fragment,
614 "Attribute placeholder mismatch"
615 );
616 continue;
617 };
618
619 let mut meta = Meta::default();
620 meta.add_remark(Remark::new(RemarkType::Removed, "attribute.deprecated"));
622 let new_attribute = std::mem::replace(old_attribute, Annotated(None, meta));
623
624 if !attributes.contains_key(&*new_name) {
625 attributes.insert(new_name.into_owned(), new_attribute);
626 }
627 }
628 WriteBehavior::BothNames(new_name) => {
629 let Some(new_name) = resolve_attribute_name(new_name, fragment) else {
630 relay_log::error!(
631 attribute = name,
632 ?fragment,
633 "Attribute placeholder mismatch"
634 );
635 continue;
636 };
637
638 if !attributes.contains_key(&*new_name)
639 && let Some(current_attribute) = attributes.get(&name).cloned()
640 {
641 attributes.insert(new_name.into_owned(), current_attribute);
642 }
643 }
644 }
645 }
646}
647
648fn resolve_attribute_name(
656 name: ReplacementName,
657 fragment: Option<&str>,
658) -> Option<Cow<'static, str>> {
659 match (name, fragment) {
660 (ReplacementName::Static(name), None) => Some(Cow::Borrowed(name)),
663 (ReplacementName::Dynamic(name_fn), Some(fragment)) => Some(Cow::Owned(name_fn(fragment))),
667 _ => None,
671 }
672}
673
674pub fn normalize_attribute_values(
683 attributes: &mut Annotated<Attributes>,
684 http_span_allowed_hosts: &[String],
685) {
686 normalize_db_attributes(attributes);
687 normalize_http_attributes(attributes, http_span_allowed_hosts);
688 normalize_mobile_attributes(attributes);
689}
690
691fn normalize_db_attributes(annotated_attributes: &mut Annotated<Attributes>) {
700 let Some(attributes) = annotated_attributes.value() else {
701 return;
702 };
703
704 if attributes.get_value(SENTRY__NORMALIZED_DB_QUERY).is_some() {
706 return;
707 }
708
709 let (op, sub_op) = attributes
710 .get_value(SENTRY__OP)
711 .and_then(|v| v.as_str())
712 .map(|op| op.split_once('.').unwrap_or((op, "")))
713 .unwrap_or_default();
714
715 let raw_query = attributes
716 .get_value(DB__QUERY__TEXT)
717 .or_else(|| {
718 if op == "db" {
719 attributes.get_value(SENTRY__DESCRIPTION)
720 } else {
721 None
722 }
723 })
724 .and_then(|v| v.as_str());
725
726 let db_system = attributes
727 .get_value(DB__SYSTEM__NAME)
728 .and_then(|v| v.as_str());
729
730 let db_operation = attributes
731 .get_value(DB__OPERATION__NAME)
732 .and_then(|v| v.as_str());
733
734 let collection_name = attributes
735 .get_value(DB__COLLECTION__NAME)
736 .and_then(|v| v.as_str());
737
738 let span_origin = attributes
739 .get_value(SENTRY__ORIGIN)
740 .and_then(|v| v.as_str());
741
742 let (normalized_db_query, parsed_sql) = if let Some(raw_query) = raw_query {
743 scrub_db_query(
744 raw_query,
745 sub_op,
746 db_system,
747 db_operation,
748 collection_name,
749 span_origin,
750 )
751 } else {
752 (None, None)
753 };
754
755 let db_operation = if db_operation.is_none() {
756 if sub_op == "redis" || db_system == Some("redis") {
757 if let Some(query) = normalized_db_query.as_ref() {
759 let command = query.replace(" *", "");
760 if command.is_empty() {
761 None
762 } else {
763 Some(command)
764 }
765 } else {
766 None
767 }
768 } else if let Some(raw_query) = raw_query {
769 sql_action_from_query(raw_query).map(|a| a.to_uppercase())
771 } else {
772 None
773 }
774 } else {
775 db_operation.map(|db_operation| db_operation.to_uppercase())
776 };
777
778 let db_collection_name: Option<String> = if let Some(name) = collection_name {
779 if db_system == Some("mongodb") {
780 match TABLE_NAME_REGEX.replace_all(name, "{%s}") {
781 Cow::Owned(s) => Some(s),
782 Cow::Borrowed(_) => Some(name.to_owned()),
783 }
784 } else {
785 Some(name.to_owned())
786 }
787 } else if span_origin == Some("auto.db.supabase") {
788 normalized_db_query
789 .as_ref()
790 .and_then(|query| query.strip_prefix("from("))
791 .and_then(|s| s.strip_suffix(")"))
792 .map(String::from)
793 } else if let Some(raw_query) = raw_query {
794 sql_tables_from_query(raw_query, &parsed_sql)
795 } else {
796 None
797 };
798
799 if let Some(attributes) = annotated_attributes.value_mut() {
800 if let Some(normalized_db_query) = normalized_db_query {
801 let mut normalized_db_query_hash = format!("{:x}", md5::compute(&normalized_db_query));
802 normalized_db_query_hash.truncate(16);
803
804 attributes.insert(SENTRY__NORMALIZED_DB_QUERY, normalized_db_query);
805 attributes.insert(SENTRY__NORMALIZED_DB_QUERY__HASH, normalized_db_query_hash);
806 }
807 if let Some(db_operation_name) = db_operation {
808 attributes.insert(DB__OPERATION__NAME, db_operation_name)
809 }
810 if let Some(db_collection_name) = db_collection_name {
811 attributes.insert(DB__COLLECTION__NAME, db_collection_name);
812 }
813 }
814}
815
816fn normalize_http_attributes(
821 annotated_attributes: &mut Annotated<Attributes>,
822 allowed_hosts: &[String],
823) {
824 let Some(attributes) = annotated_attributes.value() else {
825 return;
826 };
827
828 if attributes
830 .get_value(SENTRY__CATEGORY)
831 .is_none_or(|category| category.as_str().unwrap_or_default() != "http")
832 {
833 return;
834 }
835
836 let op = attributes.get_value(SENTRY__OP).and_then(|v| v.as_str());
837
838 let (description_method, description_url) = match attributes
839 .get_value(SENTRY__DESCRIPTION)
840 .and_then(|v| v.as_str())
841 .and_then(|description| description.split_once(' '))
842 {
843 Some((method, url)) => (Some(method), Some(url)),
844 _ => (None, None),
845 };
846
847 let method = attributes
848 .get_value(HTTP__REQUEST__METHOD)
849 .and_then(|v| v.as_str())
850 .or(description_method);
851
852 let server_address = attributes
853 .get_value(SERVER__ADDRESS)
854 .and_then(|v| v.as_str());
855
856 let url: Option<&str> = attributes
857 .get_value(URL__FULL)
858 .and_then(|v| v.as_str())
859 .or(description_url);
860 let url_scheme = attributes.get_value(URL__SCHEME).and_then(|v| v.as_str());
861
862 let (normalized_server_address, raw_url) = if op == Some("http.client") {
865 let domain_from_scrubbed_http = method
866 .zip(url)
867 .and_then(|(method, url)| scrub_http(method, url, allowed_hosts))
868 .and_then(|scrubbed_http| domain_from_scrubbed_http(&scrubbed_http));
869
870 if let Some(domain) = domain_from_scrubbed_http {
871 (Some(domain), url.map(String::from))
872 } else {
873 domain_from_server_address(server_address, url_scheme)
874 }
875 } else {
876 (None, None)
877 };
878
879 let method = method.map(|m| m.to_uppercase());
880
881 if let Some(attributes) = annotated_attributes.value_mut() {
882 if let Some(method) = method {
883 attributes.insert(HTTP__REQUEST__METHOD, method);
884 }
885
886 if let Some(normalized_server_address) = normalized_server_address {
887 attributes.insert(SERVER__ADDRESS, normalized_server_address);
888 }
889
890 if let Some(raw_url) = raw_url {
891 attributes.insert_if_missing(URL__FULL, || raw_url);
892 }
893 }
894}
895
896pub fn normalize_web_vital_span_segment(span: &mut SpanV2) {
902 let Some(attributes) = span.attributes.value_mut() else {
903 return;
904 };
905
906 if let Some(op) = attributes.get_value(SENTRY__OP)
907 && let Some(op_name) = op.as_str()
908 && (op_name.starts_with("ui.interaction.") || op_name.starts_with("ui.webvital."))
909 {
910 span.is_segment = None.into();
911 span.parent_span_id = None.into();
912 attributes.remove(SENTRY__SEGMENT__ID);
913 }
914}
915
916pub fn normalize_segment_name(
921 attributes: &mut Annotated<Attributes>,
922 tx_name_rules: &[TransactionNameRule],
923) {
924 let Some(attributes) = attributes.value_mut() else {
925 return;
926 };
927
928 let Some(attr_value) = attributes.get_annotated_value_mut(SENTRY__SEGMENT__NAME) else {
929 return;
930 };
931
932 let mut segment_name = match &attr_value.0 {
933 Some(Value::String(s)) => Annotated(Some(s.to_owned()), attr_value.1.clone()),
934 _ => return,
935 };
936
937 normalize_transaction_name(&mut segment_name, tx_name_rules);
938
939 *attr_value = segment_name.map_value(Value::String);
940}
941
942pub fn write_legacy_attributes(attributes: &mut Annotated<Attributes>) {
950 let Some(attributes) = attributes.value_mut() else {
951 return;
952 };
953
954 let current_to_legacy_attributes = [
956 (SENTRY__NORMALIZED_DB_QUERY, SENTRY__NORMALIZED_DESCRIPTION),
958 (DB__OPERATION__NAME, SENTRY__ACTION),
959 (SERVER__ADDRESS, SENTRY__DOMAIN),
961 (HTTP__REQUEST__METHOD, SENTRY__ACTION),
962 (HTTP__RESPONSE__STATUS_CODE, SENTRY__STATUS_CODE),
963 ];
964
965 for (current_attribute, legacy_attribute) in current_to_legacy_attributes {
966 if attributes.contains_key(legacy_attribute) {
967 continue;
968 }
969
970 let Some(attr) = attributes.get_attribute(current_attribute) else {
971 continue;
972 };
973
974 attributes.insert(legacy_attribute, attr.value.clone());
975 }
976
977 if !attributes.contains_key(SENTRY__DOMAIN)
978 && let Some(db_domain) = attributes
979 .get_value(DB__COLLECTION__NAME)
980 .and_then(|value| value.as_str())
981 .map(|collection_name| collection_name.to_owned())
982 {
983 attributes.insert(
985 SENTRY__DOMAIN,
986 match (db_domain.starts_with(','), db_domain.ends_with(',')) {
987 (true, true) => db_domain,
988 (true, false) => format!("{db_domain},"),
989 (false, true) => format!(",{db_domain}"),
990 (false, false) => format!(",{db_domain},"),
991 },
992 );
993 }
994}
995
996#[cfg(test)]
997mod tests {
998 use std::time::Duration;
999
1000 use relay_base_schema::project::ProjectId;
1001 use relay_protocol::{Empty, SerializableAnnotated, assert_annotated_snapshot};
1002 use relay_sampling::DynamicSamplingContext;
1003
1004 use super::*;
1005
1006 fn mock_dsc(transaction: Option<&str>) -> DynamicSamplingContext {
1007 DynamicSamplingContext {
1008 trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
1009 public_key: "12345678901234567890123456789012".parse().unwrap(),
1010 project_id: Some(ProjectId::new(42)),
1011 release: None,
1012 environment: None,
1013 transaction: transaction.map(str::to_owned),
1014 sample_rate: None,
1015 user: Default::default(),
1016 replay_id: None,
1017 sampled: None,
1018 other: Default::default(),
1019 }
1020 }
1021
1022 #[test]
1023 fn test_normalize_dsc_child_span_no_dsc() {
1024 let mut attributes = Annotated::empty();
1025 normalize_dsc(&mut attributes, &Annotated::new(false), None);
1026 assert!(attributes.value().is_none());
1027 }
1028
1029 #[test]
1030 fn test_normalize_dsc_child_span_no_transaction() {
1031 let mut attributes = Annotated::empty();
1032 let dsc = &mock_dsc(None);
1033 normalize_dsc(&mut attributes, &Annotated::new(false), Some(dsc));
1034 assert_annotated_snapshot!(attributes, @r#"
1035 {
1036 "sentry.dsc.project_id": {
1037 "type": "string",
1038 "value": "42"
1039 },
1040 "sentry.dsc.trace_id": {
1041 "type": "string",
1042 "value": "67e5504410b1426f9247bb680e5fe0c8"
1043 }
1044 }
1045 "#);
1046 }
1047
1048 #[test]
1049 fn test_normalize_dsc_child_span() {
1050 let mut attributes = Annotated::empty();
1051 let dsc = &mock_dsc(Some("/some/endpoint"));
1052 normalize_dsc(&mut attributes, &Annotated::new(false), Some(dsc));
1053 assert_annotated_snapshot!(attributes, @r#"
1054 {
1055 "sentry.dsc.project_id": {
1056 "type": "string",
1057 "value": "42"
1058 },
1059 "sentry.dsc.trace_id": {
1060 "type": "string",
1061 "value": "67e5504410b1426f9247bb680e5fe0c8"
1062 },
1063 "sentry.dsc.transaction": {
1064 "type": "string",
1065 "value": "/some/endpoint"
1066 }
1067 }
1068 "#);
1069 }
1070
1071 #[test]
1072 fn test_normalize_dsc_segment() {
1073 let mut attributes = Annotated::empty();
1074 let dsc = &mock_dsc(Some("/some/endpoint"));
1075 normalize_dsc(&mut attributes, &Annotated::new(true), Some(dsc));
1076 assert_annotated_snapshot!(attributes, @r#"
1077 {
1078 "sentry.dsc.project_id": {
1079 "type": "string",
1080 "value": "42"
1081 },
1082 "sentry.dsc.public_key": {
1083 "type": "string",
1084 "value": "12345678901234567890123456789012"
1085 },
1086 "sentry.dsc.trace_id": {
1087 "type": "string",
1088 "value": "67e5504410b1426f9247bb680e5fe0c8"
1089 },
1090 "sentry.dsc.transaction": {
1091 "type": "string",
1092 "value": "/some/endpoint"
1093 }
1094 }
1095 "#);
1096 }
1097
1098 #[test]
1099 fn test_normalize_trace_status_not_segment() {
1100 let mut attributes = Annotated::empty();
1101 normalize_trace_status(
1102 &mut attributes,
1103 &Annotated::new(false),
1104 &Annotated::new(SpanV2Status::Ok),
1105 );
1106 assert!(attributes.value().is_none());
1107 }
1108
1109 #[test]
1110 fn test_normalize_trace_status_already_set() {
1111 let mut attributes = Annotated::from_json(
1112 r#"{"sentry.trace.status": {"type": "string", "value": "internal_error"}}"#,
1113 )
1114 .unwrap();
1115 normalize_trace_status(
1116 &mut attributes,
1117 &Annotated::new(true),
1118 &Annotated::new(SpanV2Status::Error),
1119 );
1120 assert_eq!(
1121 attributes
1122 .value()
1123 .unwrap()
1124 .get_value("sentry.trace.status")
1125 .and_then(|v| v.as_str()),
1126 Some("internal_error"),
1127 );
1128 }
1129
1130 #[test]
1131 fn test_normalize_trace_status_from_sentry_status_attribute() {
1132 let mut attributes = Annotated::from_json(
1133 r#"{"sentry.status": {"type": "string", "value": "internal_error"}}"#,
1134 )
1135 .unwrap();
1136 normalize_trace_status(
1137 &mut attributes,
1138 &Annotated::new(true),
1139 &Annotated::new(SpanV2Status::Error),
1140 );
1141 assert_eq!(
1142 attributes
1143 .value()
1144 .unwrap()
1145 .get_value("sentry.trace.status")
1146 .and_then(|v| v.as_str()),
1147 Some("internal_error"),
1148 );
1149 }
1150
1151 #[test]
1152 fn test_normalize_trace_status_from_span_status() {
1153 let mut attributes = Annotated::empty();
1154 normalize_trace_status(
1155 &mut attributes,
1156 &Annotated::new(true),
1157 &Annotated::new(SpanV2Status::Error),
1158 );
1159 assert_eq!(
1160 attributes
1161 .value()
1162 .unwrap()
1163 .get_value("sentry.trace.status")
1164 .and_then(|v| v.as_str()),
1165 Some("error"),
1166 );
1167 }
1168
1169 #[test]
1170 fn test_normalize_trace_status_no_status() {
1171 let mut attributes = Annotated::empty();
1172 normalize_trace_status(&mut attributes, &Annotated::new(true), &Annotated::empty());
1173 assert!(
1174 attributes
1175 .value()
1176 .unwrap()
1177 .get_value("sentry.trace.status")
1178 .is_none(),
1179 );
1180 }
1181
1182 #[test]
1183 fn test_normalize_received_none() {
1184 let mut attributes = Default::default();
1185
1186 normalize_received(
1187 &mut attributes,
1188 DateTime::from_timestamp_nanos(1_234_201_337),
1189 );
1190
1191 assert_annotated_snapshot!(attributes, @r#"
1192 {
1193 "sentry.observed_timestamp_nanos": {
1194 "type": "string",
1195 "value": "1234201337"
1196 }
1197 }
1198 "#);
1199 }
1200
1201 #[test]
1202 fn test_normalize_received_existing() {
1203 let mut attributes = Annotated::from_json(
1204 r#"{
1205 "sentry.observed_timestamp_nanos": {
1206 "type": "string",
1207 "value": "111222333"
1208 }
1209 }"#,
1210 )
1211 .unwrap();
1212
1213 normalize_received(
1214 &mut attributes,
1215 DateTime::from_timestamp_nanos(1_234_201_337),
1216 );
1217
1218 assert_annotated_snapshot!(attributes, @r###"
1219 {
1220 "sentry.observed_timestamp_nanos": {
1221 "type": "string",
1222 "value": "111222333"
1223 }
1224 }
1225 "###);
1226 }
1227
1228 #[test]
1229 fn test_process_attribute_types() {
1230 let json = r#"{
1231 "valid_bool": {
1232 "type": "boolean",
1233 "value": true
1234 },
1235 "valid_int_i64": {
1236 "type": "integer",
1237 "value": -42
1238 },
1239 "valid_int_u64": {
1240 "type": "integer",
1241 "value": 42
1242 },
1243 "valid_int_from_string": {
1244 "type": "integer",
1245 "value": "42"
1246 },
1247 "valid_double": {
1248 "type": "double",
1249 "value": 42.5
1250 },
1251 "double_with_i64": {
1252 "type": "double",
1253 "value": -42
1254 },
1255 "valid_double_with_u64": {
1256 "type": "double",
1257 "value": 42
1258 },
1259 "valid_string": {
1260 "type": "string",
1261 "value": "test"
1262 },
1263 "valid_string_with_other": {
1264 "type": "string",
1265 "value": "test",
1266 "some_other_field": "some_other_value"
1267 },
1268 "unknown_type": {
1269 "type": "custom",
1270 "value": "test"
1271 },
1272 "invalid_int_from_invalid_string": {
1273 "type": "integer",
1274 "value": "abc"
1275 },
1276 "invalid_int": {
1277 "type": "integer",
1278 "value": 9223372036854775808
1279 },
1280 "missing_type": {
1281 "value": "value with missing type"
1282 },
1283 "missing_value": {
1284 "type": "string"
1285 },
1286 "supported_array_string": {
1287 "type": "array",
1288 "value": ["foo", "bar"]
1289 },
1290 "supported_array_double": {
1291 "type": "array",
1292 "value": [3, 3.0, 3]
1293 },
1294 "supported_array_null": {
1295 "type": "array",
1296 "value": [null, null]
1297 },
1298 "unsupported_array_mixed": {
1299 "type": "array",
1300 "value": ["foo", 1.0]
1301 },
1302 "unsupported_array_object": {
1303 "type": "array",
1304 "value": [{}]
1305 },
1306 "unsupported_array_in_array": {
1307 "type": "array",
1308 "value": [[]]
1309 }
1310 }"#;
1311
1312 let mut attributes = Annotated::<Attributes>::from_json(json).unwrap();
1313 normalize_attribute_types(&mut attributes);
1314
1315 assert_annotated_snapshot!(attributes, @r#"
1316 {
1317 "double_with_i64": {
1318 "type": "double",
1319 "value": -42
1320 },
1321 "invalid_int": null,
1322 "invalid_int_from_invalid_string": null,
1323 "missing_type": null,
1324 "missing_value": null,
1325 "supported_array_double": {
1326 "type": "array",
1327 "value": [
1328 3,
1329 3.0,
1330 3
1331 ]
1332 },
1333 "supported_array_null": {
1334 "type": "array",
1335 "value": [
1336 null,
1337 null
1338 ]
1339 },
1340 "supported_array_string": {
1341 "type": "array",
1342 "value": [
1343 "foo",
1344 "bar"
1345 ]
1346 },
1347 "unknown_type": null,
1348 "unsupported_array_in_array": null,
1349 "unsupported_array_mixed": null,
1350 "unsupported_array_object": null,
1351 "valid_bool": {
1352 "type": "boolean",
1353 "value": true
1354 },
1355 "valid_double": {
1356 "type": "double",
1357 "value": 42.5
1358 },
1359 "valid_double_with_u64": {
1360 "type": "double",
1361 "value": 42
1362 },
1363 "valid_int_from_string": null,
1364 "valid_int_i64": {
1365 "type": "integer",
1366 "value": -42
1367 },
1368 "valid_int_u64": {
1369 "type": "integer",
1370 "value": 42
1371 },
1372 "valid_string": {
1373 "type": "string",
1374 "value": "test"
1375 },
1376 "valid_string_with_other": {
1377 "type": "string",
1378 "value": "test",
1379 "some_other_field": "some_other_value"
1380 },
1381 "_meta": {
1382 "invalid_int": {
1383 "": {
1384 "err": [
1385 "invalid_data"
1386 ],
1387 "val": {
1388 "type": "integer",
1389 "value": 9223372036854775808
1390 }
1391 }
1392 },
1393 "invalid_int_from_invalid_string": {
1394 "": {
1395 "err": [
1396 "invalid_data"
1397 ],
1398 "val": {
1399 "type": "integer",
1400 "value": "abc"
1401 }
1402 }
1403 },
1404 "missing_type": {
1405 "": {
1406 "err": [
1407 "missing_attribute"
1408 ],
1409 "val": {
1410 "type": null,
1411 "value": "value with missing type"
1412 }
1413 }
1414 },
1415 "missing_value": {
1416 "": {
1417 "err": [
1418 "missing_attribute"
1419 ],
1420 "val": {
1421 "type": "string",
1422 "value": null
1423 }
1424 }
1425 },
1426 "unknown_type": {
1427 "": {
1428 "err": [
1429 "invalid_data"
1430 ],
1431 "val": {
1432 "type": "custom",
1433 "value": "test"
1434 }
1435 }
1436 },
1437 "unsupported_array_in_array": {
1438 "": {
1439 "err": [
1440 "invalid_data"
1441 ]
1442 }
1443 },
1444 "unsupported_array_mixed": {
1445 "": {
1446 "err": [
1447 "invalid_data"
1448 ]
1449 }
1450 },
1451 "unsupported_array_object": {
1452 "": {
1453 "err": [
1454 "invalid_data"
1455 ]
1456 }
1457 },
1458 "valid_int_from_string": {
1459 "": {
1460 "err": [
1461 "invalid_data"
1462 ],
1463 "val": {
1464 "type": "integer",
1465 "value": "42"
1466 }
1467 }
1468 }
1469 }
1470 }
1471 "#);
1472 }
1473
1474 #[test]
1475 fn test_normalize_user_agent_none() {
1476 let mut attributes = Default::default();
1477 normalize_user_agent(
1478 &mut attributes,
1479 Some(ClientUserAgentInfo {
1480 user_agent: Some(
1481 "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",
1482 ),
1483 ..Default::default()
1484 }),
1485 );
1486
1487 assert_annotated_snapshot!(attributes, @r###"
1488 {
1489 "browser.name": {
1490 "type": "string",
1491 "value": "Chrome"
1492 },
1493 "browser.version": {
1494 "type": "string",
1495 "value": "131.0.0"
1496 },
1497 "user_agent.original": {
1498 "type": "string",
1499 "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"
1500 }
1501 }
1502 "###);
1503 }
1504
1505 #[test]
1506 fn test_normalize_user_agent_existing() {
1507 let mut attributes = Annotated::from_json(
1508 r#"{
1509 "browser.name": {
1510 "type": "string",
1511 "value": "Very Special"
1512 },
1513 "browser.version": {
1514 "type": "string",
1515 "value": "13.3.7"
1516 }
1517 }"#,
1518 )
1519 .unwrap();
1520
1521 normalize_user_agent(
1522 &mut attributes,
1523 Some(ClientUserAgentInfo {
1524 user_agent: Some(
1525 "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",
1526 ),
1527 ..Default::default()
1528 }),
1529 );
1530
1531 assert_annotated_snapshot!(attributes, @r#"
1532 {
1533 "browser.name": {
1534 "type": "string",
1535 "value": "Very Special"
1536 },
1537 "browser.version": {
1538 "type": "string",
1539 "value": "13.3.7"
1540 }
1541 }
1542 "#
1543 );
1544 }
1545
1546 #[test]
1547 fn test_normalize_user_geo_none() {
1548 let mut attributes = Annotated::from_json(
1549 r#"{
1550 "client.address": {
1551 "type": "string",
1552 "value": "192.168.2.1"
1553 }
1554 }"#,
1555 )
1556 .unwrap();
1557
1558 normalize_user_geo(&mut attributes, |addr| {
1559 Some(Geo {
1560 country_code: "XY".to_owned().into(),
1561 city: addr.to_string().into(),
1562 subdivision: Annotated::empty(),
1563 region: "Illu".to_owned().into(),
1564 other: Default::default(),
1565 })
1566 });
1567
1568 assert_annotated_snapshot!(attributes, @r#"
1569 {
1570 "client.address": {
1571 "type": "string",
1572 "value": "192.168.2.1"
1573 },
1574 "user.geo.city": {
1575 "type": "string",
1576 "value": "192.168.2.1"
1577 },
1578 "user.geo.country_code": {
1579 "type": "string",
1580 "value": "XY"
1581 },
1582 "user.geo.region": {
1583 "type": "string",
1584 "value": "Illu"
1585 }
1586 }
1587 "#);
1588 }
1589
1590 #[test]
1591 fn test_normalize_user_geo_existing() {
1592 let mut attributes = Annotated::from_json(
1593 r#"{
1594 "client.address": {
1595 "type": "string",
1596 "value": "192.168.2.1"
1597 },
1598 "user.geo.city": {
1599 "type": "string",
1600 "value": "Foo Hausen"
1601 }
1602 }"#,
1603 )
1604 .unwrap();
1605
1606 normalize_user_geo(&mut attributes, |_| unreachable!());
1607
1608 assert_annotated_snapshot!(attributes, @r#"
1609 {
1610 "client.address": {
1611 "type": "string",
1612 "value": "192.168.2.1"
1613 },
1614 "user.geo.city": {
1615 "type": "string",
1616 "value": "Foo Hausen"
1617 }
1618 }
1619 "#
1620 );
1621 }
1622
1623 #[test]
1624 fn test_normalize_attributes() {
1625 fn replace_key(fragment: &str) -> String {
1626 format!("placeholder.replaced.{fragment}")
1627 }
1628
1629 fn backfill_key(fragment: &str) -> String {
1630 format!("placeholder.backfilled.{fragment}")
1631 }
1632
1633 fn mock_attribute_info(name: &str) -> Option<(&'static AttributeInfo, Option<&str>)> {
1634 use relay_conventions::ApplyScrubbing;
1635
1636 match name {
1637 "replace.empty" => Some((
1638 &AttributeInfo {
1639 write_behavior: WriteBehavior::NewName(ReplacementName::Static("replaced")),
1640 apply_scrubbing: ApplyScrubbing::Manual,
1641 aliases: &["replaced"],
1642 },
1643 None,
1644 )),
1645 "replace.existing" => Some((
1646 &AttributeInfo {
1647 write_behavior: WriteBehavior::NewName(ReplacementName::Static(
1648 "not.replaced",
1649 )),
1650 apply_scrubbing: ApplyScrubbing::Manual,
1651 aliases: &["not.replaced"],
1652 },
1653 None,
1654 )),
1655 "backfill.empty" => Some((
1656 &AttributeInfo {
1657 write_behavior: WriteBehavior::BothNames(ReplacementName::Static(
1658 "backfilled",
1659 )),
1660 apply_scrubbing: ApplyScrubbing::Manual,
1661 aliases: &["backfilled"],
1662 },
1663 None,
1664 )),
1665 "backfill.existing" => Some((
1666 &AttributeInfo {
1667 write_behavior: WriteBehavior::BothNames(ReplacementName::Static(
1668 "not.backfilled",
1669 )),
1670 apply_scrubbing: ApplyScrubbing::Manual,
1671 aliases: &["not.backfilled"],
1672 },
1673 None,
1674 )),
1675 _ if let Some(fragment) = name.strip_prefix("placeholder.replace.") => Some((
1676 &AttributeInfo {
1677 write_behavior: WriteBehavior::NewName(ReplacementName::Dynamic(
1678 replace_key,
1679 )),
1680 apply_scrubbing: ApplyScrubbing::Manual,
1681 aliases: &["placeholder.replaced.<key>"],
1682 },
1683 Some(fragment),
1684 )),
1685 _ if let Some(fragment) = name.strip_prefix("placeholder.backfill.") => Some((
1686 &AttributeInfo {
1687 write_behavior: WriteBehavior::BothNames(ReplacementName::Dynamic(
1688 backfill_key,
1689 )),
1690 apply_scrubbing: ApplyScrubbing::Manual,
1691 aliases: &["placeholder.backfilled.<key>"],
1692 },
1693 Some(fragment),
1694 )),
1695
1696 _ => None,
1697 }
1698 }
1699
1700 let mut attributes = Attributes::from([
1701 (
1702 "replace.empty".to_owned(),
1703 Annotated::new("Should be moved".to_owned().into()),
1704 ),
1705 (
1706 "replace.existing".to_owned(),
1707 Annotated::new("Should be removed".to_owned().into()),
1708 ),
1709 (
1710 "placeholder.replace.foo".to_owned(),
1711 Annotated::new("Should be moved".to_owned().into()),
1712 ),
1713 (
1714 "not.replaced".to_owned(),
1715 Annotated::new("Should be left alone".to_owned().into()),
1716 ),
1717 (
1718 "backfill.empty".to_owned(),
1719 Annotated::new("Should be copied".to_owned().into()),
1720 ),
1721 (
1722 "backfill.existing".to_owned(),
1723 Annotated::new("Should be left alone".to_owned().into()),
1724 ),
1725 (
1726 "placeholder.backfill.bar".to_owned(),
1727 Annotated::new("Should be copied".to_owned().into()),
1728 ),
1729 (
1730 "not.backfilled".to_owned(),
1731 Annotated::new("Should be left alone".to_owned().into()),
1732 ),
1733 ]);
1734
1735 normalize_attribute_names_inner(&mut attributes.0, mock_attribute_info);
1736
1737 assert_annotated_snapshot!(Annotated::new(attributes), @r###"
1738 {
1739 "backfill.empty": {
1740 "type": "string",
1741 "value": "Should be copied"
1742 },
1743 "backfill.existing": {
1744 "type": "string",
1745 "value": "Should be left alone"
1746 },
1747 "backfilled": {
1748 "type": "string",
1749 "value": "Should be copied"
1750 },
1751 "not.backfilled": {
1752 "type": "string",
1753 "value": "Should be left alone"
1754 },
1755 "not.replaced": {
1756 "type": "string",
1757 "value": "Should be left alone"
1758 },
1759 "placeholder.backfill.bar": {
1760 "type": "string",
1761 "value": "Should be copied"
1762 },
1763 "placeholder.backfilled.bar": {
1764 "type": "string",
1765 "value": "Should be copied"
1766 },
1767 "placeholder.replace.foo": null,
1768 "placeholder.replaced.foo": {
1769 "type": "string",
1770 "value": "Should be moved"
1771 },
1772 "replace.empty": null,
1773 "replace.existing": null,
1774 "replaced": {
1775 "type": "string",
1776 "value": "Should be moved"
1777 },
1778 "_meta": {
1779 "placeholder.replace.foo": {
1780 "": {
1781 "rem": [
1782 [
1783 "attribute.deprecated",
1784 "x"
1785 ]
1786 ]
1787 }
1788 },
1789 "replace.empty": {
1790 "": {
1791 "rem": [
1792 [
1793 "attribute.deprecated",
1794 "x"
1795 ]
1796 ]
1797 }
1798 },
1799 "replace.existing": {
1800 "": {
1801 "rem": [
1802 [
1803 "attribute.deprecated",
1804 "x"
1805 ]
1806 ]
1807 }
1808 }
1809 }
1810 }
1811 "###);
1812 }
1813
1814 #[test]
1815 fn test_normalize_span_infers_op() {
1816 let mut attributes = Annotated::<Attributes>::from_json(
1817 r#"{
1818 "db.system.name": {
1819 "type": "string",
1820 "value": "mysql"
1821 },
1822 "db.operation.name": {
1823 "type": "string",
1824 "value": "query"
1825 }
1826 }
1827 "#,
1828 )
1829 .unwrap();
1830
1831 normalize_sentry_op(&mut attributes);
1832
1833 assert_annotated_snapshot!(attributes, @r#"
1834 {
1835 "db.operation.name": {
1836 "type": "string",
1837 "value": "query"
1838 },
1839 "db.system.name": {
1840 "type": "string",
1841 "value": "mysql"
1842 },
1843 "sentry.op": {
1844 "type": "string",
1845 "value": "db"
1846 }
1847 }
1848 "#);
1849 }
1850
1851 #[test]
1852 fn test_normalize_attribute_values_mysql_db_query_attributes() {
1853 let mut attributes = Annotated::<Attributes>::from_json(
1854 r#"
1855 {
1856 "sentry.op": {
1857 "type": "string",
1858 "value": "db.query"
1859 },
1860 "sentry.origin": {
1861 "type": "string",
1862 "value": "auto.otlp.spans"
1863 },
1864 "db.system.name": {
1865 "type": "string",
1866 "value": "mysql"
1867 },
1868 "db.query.text": {
1869 "type": "string",
1870 "value": "SELECT \"not an identifier\""
1871 }
1872 }
1873 "#,
1874 )
1875 .unwrap();
1876
1877 normalize_db_attributes(&mut attributes);
1878
1879 assert_annotated_snapshot!(attributes, @r#"
1880 {
1881 "db.operation.name": {
1882 "type": "string",
1883 "value": "SELECT"
1884 },
1885 "db.query.text": {
1886 "type": "string",
1887 "value": "SELECT \"not an identifier\""
1888 },
1889 "db.system.name": {
1890 "type": "string",
1891 "value": "mysql"
1892 },
1893 "sentry.normalized_db_query": {
1894 "type": "string",
1895 "value": "SELECT %s"
1896 },
1897 "sentry.normalized_db_query.hash": {
1898 "type": "string",
1899 "value": "3a377dcc490b1690"
1900 },
1901 "sentry.op": {
1902 "type": "string",
1903 "value": "db.query"
1904 },
1905 "sentry.origin": {
1906 "type": "string",
1907 "value": "auto.otlp.spans"
1908 }
1909 }
1910 "#);
1911 }
1912
1913 #[test]
1914 fn test_normalize_mongodb_db_query_attributes() {
1915 let mut attributes = Annotated::<Attributes>::from_json(
1916 r#"
1917 {
1918 "sentry.op": {
1919 "type": "string",
1920 "value": "db"
1921 },
1922 "db.system.name": {
1923 "type": "string",
1924 "value": "mongodb"
1925 },
1926 "db.query.text": {
1927 "type": "string",
1928 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1929 },
1930 "db.operation.name": {
1931 "type": "string",
1932 "value": "find"
1933 },
1934 "db.collection.name": {
1935 "type": "string",
1936 "value": "documents"
1937 }
1938 }
1939 "#,
1940 )
1941 .unwrap();
1942
1943 normalize_db_attributes(&mut attributes);
1944
1945 assert_annotated_snapshot!(attributes, @r#"
1946 {
1947 "db.collection.name": {
1948 "type": "string",
1949 "value": "documents"
1950 },
1951 "db.operation.name": {
1952 "type": "string",
1953 "value": "FIND"
1954 },
1955 "db.query.text": {
1956 "type": "string",
1957 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1958 },
1959 "db.system.name": {
1960 "type": "string",
1961 "value": "mongodb"
1962 },
1963 "sentry.normalized_db_query": {
1964 "type": "string",
1965 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1966 },
1967 "sentry.normalized_db_query.hash": {
1968 "type": "string",
1969 "value": "aedc5c7e8cec726b"
1970 },
1971 "sentry.op": {
1972 "type": "string",
1973 "value": "db"
1974 }
1975 }
1976 "#);
1977 }
1978
1979 #[test]
1980 fn test_normalize_db_attributes_does_not_update_attributes_if_already_normalized() {
1981 let mut attributes = Annotated::<Attributes>::from_json(
1982 r#"
1983 {
1984 "db.collection.name": {
1985 "type": "string",
1986 "value": "documents"
1987 },
1988 "db.operation.name": {
1989 "type": "string",
1990 "value": "FIND"
1991 },
1992 "db.query.text": {
1993 "type": "string",
1994 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1995 },
1996 "db.system.name": {
1997 "type": "string",
1998 "value": "mongodb"
1999 },
2000 "sentry.normalized_db_query": {
2001 "type": "string",
2002 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2003 },
2004 "sentry.op": {
2005 "type": "string",
2006 "value": "db"
2007 }
2008 }
2009 "#,
2010 )
2011 .unwrap();
2012
2013 normalize_db_attributes(&mut attributes);
2014
2015 insta::assert_json_snapshot!(
2016 SerializableAnnotated(&attributes), @r#"
2017 {
2018 "db.collection.name": {
2019 "type": "string",
2020 "value": "documents"
2021 },
2022 "db.operation.name": {
2023 "type": "string",
2024 "value": "FIND"
2025 },
2026 "db.query.text": {
2027 "type": "string",
2028 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2029 },
2030 "db.system.name": {
2031 "type": "string",
2032 "value": "mongodb"
2033 },
2034 "sentry.normalized_db_query": {
2035 "type": "string",
2036 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2037 },
2038 "sentry.op": {
2039 "type": "string",
2040 "value": "db"
2041 }
2042 }
2043 "#
2044 );
2045 }
2046
2047 #[test]
2048 fn test_normalize_db_attributes_does_not_change_non_db_spans() {
2049 let mut attributes = Annotated::<Attributes>::from_json(
2050 r#"
2051 {
2052 "sentry.op": {
2053 "type": "string",
2054 "value": "http.client"
2055 },
2056 "sentry.origin": {
2057 "type": "string",
2058 "value": "auto.otlp.spans"
2059 },
2060 "http.request.method": {
2061 "type": "string",
2062 "value": "GET"
2063 }
2064 }
2065 "#,
2066 )
2067 .unwrap();
2068
2069 normalize_db_attributes(&mut attributes);
2070
2071 assert_annotated_snapshot!(attributes, @r#"
2072 {
2073 "http.request.method": {
2074 "type": "string",
2075 "value": "GET"
2076 },
2077 "sentry.op": {
2078 "type": "string",
2079 "value": "http.client"
2080 },
2081 "sentry.origin": {
2082 "type": "string",
2083 "value": "auto.otlp.spans"
2084 }
2085 }
2086 "#);
2087 }
2088
2089 #[test]
2090 fn test_normalize_http_attributes() {
2091 let mut attributes = Annotated::<Attributes>::from_json(
2092 r#"
2093 {
2094 "sentry.op": {
2095 "type": "string",
2096 "value": "http.client"
2097 },
2098 "sentry.category": {
2099 "type": "string",
2100 "value": "http"
2101 },
2102 "http.request.method": {
2103 "type": "string",
2104 "value": "GET"
2105 },
2106 "url.full": {
2107 "type": "string",
2108 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
2109 }
2110 }
2111 "#,
2112 )
2113 .unwrap();
2114
2115 normalize_http_attributes(&mut attributes, &[]);
2116
2117 assert_annotated_snapshot!(attributes, @r#"
2118 {
2119 "http.request.method": {
2120 "type": "string",
2121 "value": "GET"
2122 },
2123 "sentry.category": {
2124 "type": "string",
2125 "value": "http"
2126 },
2127 "sentry.op": {
2128 "type": "string",
2129 "value": "http.client"
2130 },
2131 "server.address": {
2132 "type": "string",
2133 "value": "*.xn--85x722f.xn--55qx5d.cn"
2134 },
2135 "url.full": {
2136 "type": "string",
2137 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
2138 }
2139 }
2140 "#);
2141 }
2142
2143 #[test]
2144 fn test_normalize_http_attributes_server_address() {
2145 let mut attributes = Annotated::<Attributes>::from_json(
2146 r#"
2147 {
2148 "sentry.category": {
2149 "type": "string",
2150 "value": "http"
2151 },
2152 "sentry.op": {
2153 "type": "string",
2154 "value": "http.client"
2155 },
2156 "url.scheme": {
2157 "type": "string",
2158 "value": "https"
2159 },
2160 "server.address": {
2161 "type": "string",
2162 "value": "subdomain.example.com:5688"
2163 },
2164 "http.request.method": {
2165 "type": "string",
2166 "value": "GET"
2167 }
2168 }
2169 "#,
2170 )
2171 .unwrap();
2172
2173 normalize_http_attributes(&mut attributes, &[]);
2174
2175 assert_annotated_snapshot!(attributes, @r#"
2176 {
2177 "http.request.method": {
2178 "type": "string",
2179 "value": "GET"
2180 },
2181 "sentry.category": {
2182 "type": "string",
2183 "value": "http"
2184 },
2185 "sentry.op": {
2186 "type": "string",
2187 "value": "http.client"
2188 },
2189 "server.address": {
2190 "type": "string",
2191 "value": "*.example.com:5688"
2192 },
2193 "url.full": {
2194 "type": "string",
2195 "value": "https://subdomain.example.com:5688"
2196 },
2197 "url.scheme": {
2198 "type": "string",
2199 "value": "https"
2200 }
2201 }
2202 "#);
2203 }
2204
2205 #[test]
2206 fn test_normalize_http_attributes_allowed_hosts() {
2207 let mut attributes = Annotated::<Attributes>::from_json(
2208 r#"
2209 {
2210 "sentry.category": {
2211 "type": "string",
2212 "value": "http"
2213 },
2214 "sentry.op": {
2215 "type": "string",
2216 "value": "http.client"
2217 },
2218 "http.request.method": {
2219 "type": "string",
2220 "value": "GET"
2221 },
2222 "url.full": {
2223 "type": "string",
2224 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
2225 }
2226 }
2227 "#,
2228 )
2229 .unwrap();
2230
2231 normalize_http_attributes(
2232 &mut attributes,
2233 &["application.www.xn--85x722f.xn--55qx5d.cn".to_owned()],
2234 );
2235
2236 assert_annotated_snapshot!(attributes, @r#"
2237 {
2238 "http.request.method": {
2239 "type": "string",
2240 "value": "GET"
2241 },
2242 "sentry.category": {
2243 "type": "string",
2244 "value": "http"
2245 },
2246 "sentry.op": {
2247 "type": "string",
2248 "value": "http.client"
2249 },
2250 "server.address": {
2251 "type": "string",
2252 "value": "application.www.xn--85x722f.xn--55qx5d.cn"
2253 },
2254 "url.full": {
2255 "type": "string",
2256 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
2257 }
2258 }
2259 "#);
2260 }
2261
2262 #[test]
2263 fn test_normalize_db_attributes_from_legacy_attributes() {
2264 let mut attributes = Annotated::<Attributes>::from_json(
2265 r#"
2266 {
2267 "sentry.op": {
2268 "type": "string",
2269 "value": "db"
2270 },
2271 "db.system.name": {
2272 "type": "string",
2273 "value": "mongodb"
2274 },
2275 "sentry.description": {
2276 "type": "string",
2277 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2278 },
2279 "db.operation.name": {
2280 "type": "string",
2281 "value": "find"
2282 },
2283 "db.collection.name": {
2284 "type": "string",
2285 "value": "documents"
2286 }
2287 }
2288 "#,
2289 )
2290 .unwrap();
2291
2292 normalize_db_attributes(&mut attributes);
2293
2294 assert_annotated_snapshot!(attributes, @r#"
2295 {
2296 "db.collection.name": {
2297 "type": "string",
2298 "value": "documents"
2299 },
2300 "db.operation.name": {
2301 "type": "string",
2302 "value": "FIND"
2303 },
2304 "db.system.name": {
2305 "type": "string",
2306 "value": "mongodb"
2307 },
2308 "sentry.description": {
2309 "type": "string",
2310 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2311 },
2312 "sentry.normalized_db_query": {
2313 "type": "string",
2314 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2315 },
2316 "sentry.normalized_db_query.hash": {
2317 "type": "string",
2318 "value": "aedc5c7e8cec726b"
2319 },
2320 "sentry.op": {
2321 "type": "string",
2322 "value": "db"
2323 }
2324 }
2325 "#);
2326 }
2327
2328 #[test]
2329 fn test_normalize_http_attributes_from_legacy_attributes() {
2330 let mut attributes = Annotated::<Attributes>::from_json(
2331 r#"
2332 {
2333 "sentry.category": {
2334 "type": "string",
2335 "value": "http"
2336 },
2337 "sentry.op": {
2338 "type": "string",
2339 "value": "http.client"
2340 },
2341 "http.request_method": {
2342 "type": "string",
2343 "value": "GET"
2344 }
2345 }
2346 "#,
2347 )
2348 .unwrap();
2349
2350 normalize_attribute_names(&mut attributes);
2351 normalize_http_attributes(&mut attributes, &[]);
2352
2353 assert_annotated_snapshot!(attributes, @r#"
2354 {
2355 "http.request.method": {
2356 "type": "string",
2357 "value": "GET"
2358 },
2359 "http.request_method": {
2360 "type": "string",
2361 "value": "GET"
2362 },
2363 "sentry.category": {
2364 "type": "string",
2365 "value": "http"
2366 },
2367 "sentry.op": {
2368 "type": "string",
2369 "value": "http.client"
2370 }
2371 }
2372 "#);
2373 }
2374
2375 #[test]
2376 fn test_normalize_http_attributes_from_description() {
2377 let mut attributes = Annotated::<Attributes>::from_json(
2378 r#"
2379 {
2380 "sentry.category": {
2381 "type": "string",
2382 "value": "http"
2383 },
2384 "sentry.op": {
2385 "type": "string",
2386 "value": "http.client"
2387 },
2388 "sentry.description": {
2389 "type": "string",
2390 "value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
2391 }
2392 }
2393 "#,
2394 )
2395 .unwrap();
2396
2397 normalize_http_attributes(&mut attributes, &[]);
2398
2399 assert_annotated_snapshot!(attributes, @r#"
2400 {
2401 "http.request.method": {
2402 "type": "string",
2403 "value": "GET"
2404 },
2405 "sentry.category": {
2406 "type": "string",
2407 "value": "http"
2408 },
2409 "sentry.description": {
2410 "type": "string",
2411 "value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
2412 },
2413 "sentry.op": {
2414 "type": "string",
2415 "value": "http.client"
2416 },
2417 "server.address": {
2418 "type": "string",
2419 "value": "*.xn--85x722f.xn--55qx5d.cn"
2420 },
2421 "url.full": {
2422 "type": "string",
2423 "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
2424 }
2425 }
2426 "#);
2427 }
2428
2429 #[test]
2430 fn test_write_legacy_attributes() {
2431 let mut attributes = Annotated::<Attributes>::from_json(
2432 r#"
2433 {
2434 "db.collection.name": {
2435 "type": "string",
2436 "value": "documents"
2437 },
2438 "db.operation.name": {
2439 "type": "string",
2440 "value": "FIND"
2441 },
2442 "db.query.text": {
2443 "type": "string",
2444 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2445 },
2446 "db.system.name": {
2447 "type": "string",
2448 "value": "mongodb"
2449 },
2450 "sentry.normalized_db_query": {
2451 "type": "string",
2452 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2453 },
2454 "sentry.normalized_db_query.hash": {
2455 "type": "string",
2456 "value": "aedc5c7e8cec726b"
2457 },
2458 "sentry.op": {
2459 "type": "string",
2460 "value": "db"
2461 }
2462 }
2463 "#,
2464 )
2465 .unwrap();
2466
2467 write_legacy_attributes(&mut attributes);
2468
2469 assert_annotated_snapshot!(attributes, @r#"
2470 {
2471 "db.collection.name": {
2472 "type": "string",
2473 "value": "documents"
2474 },
2475 "db.operation.name": {
2476 "type": "string",
2477 "value": "FIND"
2478 },
2479 "db.query.text": {
2480 "type": "string",
2481 "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
2482 },
2483 "db.system.name": {
2484 "type": "string",
2485 "value": "mongodb"
2486 },
2487 "sentry.action": {
2488 "type": "string",
2489 "value": "FIND"
2490 },
2491 "sentry.domain": {
2492 "type": "string",
2493 "value": ",documents,"
2494 },
2495 "sentry.normalized_db_query": {
2496 "type": "string",
2497 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2498 },
2499 "sentry.normalized_db_query.hash": {
2500 "type": "string",
2501 "value": "aedc5c7e8cec726b"
2502 },
2503 "sentry.normalized_description": {
2504 "type": "string",
2505 "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
2506 },
2507 "sentry.op": {
2508 "type": "string",
2509 "value": "db"
2510 }
2511 }
2512 "#);
2513 }
2514
2515 #[test]
2516 fn test_normalize_span_category_explicit() {
2517 let mut attributes = Annotated::<Attributes>::from_json(
2519 r#"{
2520 "sentry.category": {
2521 "type": "string",
2522 "value": "custom"
2523 },
2524 "sentry.op": {
2525 "type": "string",
2526 "value": "db.query"
2527 }
2528 }"#,
2529 )
2530 .unwrap();
2531
2532 normalize_span_category(&mut attributes);
2533
2534 assert_annotated_snapshot!(attributes, @r#"
2535 {
2536 "sentry.category": {
2537 "type": "string",
2538 "value": "custom"
2539 },
2540 "sentry.op": {
2541 "type": "string",
2542 "value": "db.query"
2543 }
2544 }
2545 "#);
2546 }
2547
2548 #[test]
2549 fn test_normalize_span_category_from_op_db() {
2550 let mut attributes = Annotated::<Attributes>::from_json(
2551 r#"{
2552 "sentry.op": {
2553 "type": "string",
2554 "value": "db.query"
2555 }
2556 }"#,
2557 )
2558 .unwrap();
2559
2560 normalize_span_category(&mut attributes);
2561
2562 assert_annotated_snapshot!(attributes, @r#"
2563 {
2564 "sentry.category": {
2565 "type": "string",
2566 "value": "db"
2567 },
2568 "sentry.op": {
2569 "type": "string",
2570 "value": "db.query"
2571 }
2572 }
2573 "#);
2574 }
2575
2576 #[test]
2577 fn test_normalize_span_category_from_op_http() {
2578 let mut attributes = Annotated::<Attributes>::from_json(
2579 r#"{
2580 "sentry.op": {
2581 "type": "string",
2582 "value": "http.client"
2583 }
2584 }"#,
2585 )
2586 .unwrap();
2587
2588 normalize_span_category(&mut attributes);
2589
2590 assert_annotated_snapshot!(attributes, @r#"
2591 {
2592 "sentry.category": {
2593 "type": "string",
2594 "value": "http"
2595 },
2596 "sentry.op": {
2597 "type": "string",
2598 "value": "http.client"
2599 }
2600 }
2601 "#);
2602 }
2603
2604 #[test]
2605 fn test_normalize_span_category_from_op_ui_framework() {
2606 let mut attributes = Annotated::<Attributes>::from_json(
2607 r#"{
2608 "sentry.op": {
2609 "type": "string",
2610 "value": "ui.react.render"
2611 }
2612 }"#,
2613 )
2614 .unwrap();
2615
2616 normalize_span_category(&mut attributes);
2617
2618 assert_annotated_snapshot!(attributes, @r#"
2619 {
2620 "sentry.category": {
2621 "type": "string",
2622 "value": "ui.react"
2623 },
2624 "sentry.op": {
2625 "type": "string",
2626 "value": "ui.react.render"
2627 }
2628 }
2629 "#);
2630 }
2631
2632 #[test]
2633 fn test_normalize_span_category_from_db_system() {
2634 let mut attributes = Annotated::<Attributes>::from_json(
2636 r#"{
2637 "db.system.name": {
2638 "type": "string",
2639 "value": "mongodb"
2640 }
2641 }"#,
2642 )
2643 .unwrap();
2644
2645 normalize_span_category(&mut attributes);
2646
2647 assert_annotated_snapshot!(attributes, @r#"
2648 {
2649 "db.system.name": {
2650 "type": "string",
2651 "value": "mongodb"
2652 },
2653 "sentry.category": {
2654 "type": "string",
2655 "value": "db"
2656 }
2657 }
2658 "#);
2659 }
2660
2661 #[test]
2662 fn test_normalize_span_category_from_http_method() {
2663 let mut attributes = Annotated::<Attributes>::from_json(
2665 r#"{
2666 "http.request.method": {
2667 "type": "string",
2668 "value": "GET"
2669 }
2670 }"#,
2671 )
2672 .unwrap();
2673
2674 normalize_span_category(&mut attributes);
2675
2676 assert_annotated_snapshot!(attributes, @r#"
2677 {
2678 "http.request.method": {
2679 "type": "string",
2680 "value": "GET"
2681 },
2682 "sentry.category": {
2683 "type": "string",
2684 "value": "http"
2685 }
2686 }
2687 "#);
2688 }
2689
2690 #[test]
2691 fn test_normalize_span_category_from_ui_component() {
2692 let mut attributes = Annotated::<Attributes>::from_json(
2694 r#"{
2695 "ui.component_name": {
2696 "type": "string",
2697 "value": "MyComponent"
2698 }
2699 }"#,
2700 )
2701 .unwrap();
2702
2703 normalize_span_category(&mut attributes);
2704
2705 assert_annotated_snapshot!(attributes, @r#"
2706 {
2707 "sentry.category": {
2708 "type": "string",
2709 "value": "ui"
2710 },
2711 "ui.component_name": {
2712 "type": "string",
2713 "value": "MyComponent"
2714 }
2715 }
2716 "#);
2717 }
2718
2719 #[test]
2720 fn test_normalize_span_category_from_resource() {
2721 let mut attributes = Annotated::<Attributes>::from_json(
2723 r#"{
2724 "resource.render_blocking_status": {
2725 "type": "string",
2726 "value": "blocking"
2727 }
2728 }"#,
2729 )
2730 .unwrap();
2731
2732 normalize_span_category(&mut attributes);
2733
2734 assert_annotated_snapshot!(attributes, @r#"
2735 {
2736 "resource.render_blocking_status": {
2737 "type": "string",
2738 "value": "blocking"
2739 },
2740 "sentry.category": {
2741 "type": "string",
2742 "value": "resource"
2743 }
2744 }
2745 "#);
2746 }
2747
2748 #[test]
2749 fn test_normalize_span_category_from_browser_origin() {
2750 let mut attributes = Annotated::from_json(
2752 r#"{
2753 "sentry.origin": {
2754 "type": "string",
2755 "value": "auto.ui.browser.metrics"
2756 }
2757 }"#,
2758 )
2759 .unwrap();
2760
2761 normalize_span_category(&mut attributes);
2762
2763 assert_annotated_snapshot!(attributes, @r#"
2764 {
2765 "sentry.category": {
2766 "type": "string",
2767 "value": "browser"
2768 },
2769 "sentry.origin": {
2770 "type": "string",
2771 "value": "auto.ui.browser.metrics"
2772 }
2773 }
2774 "#);
2775 }
2776
2777 #[test]
2778 fn test_normalize_client_address_auto_with_ip() {
2779 let mut attributes = Annotated::from_json(
2780 r#"{
2781 "client.address": {
2782 "type": "string",
2783 "value": "{{auto}}"
2784 }
2785 }"#,
2786 )
2787 .unwrap();
2788
2789 normalize_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2790
2791 assert_annotated_snapshot!(attributes, @r#"
2792 {
2793 "client.address": {
2794 "type": "string",
2795 "value": "192.168.1.1"
2796 }
2797 }
2798 "#);
2799 }
2800
2801 #[test]
2802 fn test_normalize_client_address_auto_without_ip() {
2803 let mut attributes = Annotated::from_json(
2804 r#"{
2805 "client.address": {
2806 "type": "string",
2807 "value": "{{auto}}"
2808 }
2809 }"#,
2810 )
2811 .unwrap();
2812
2813 normalize_client_address(&mut attributes, None);
2814
2815 assert_annotated_snapshot!(attributes, @r#"
2816 {}
2817 "#);
2818 }
2819
2820 #[test]
2821 fn test_normalize_client_address_explicit_not_replaced() {
2822 let mut attributes = Annotated::from_json(
2823 r#"{
2824 "client.address": {
2825 "type": "string",
2826 "value": "10.0.0.1"
2827 }
2828 }"#,
2829 )
2830 .unwrap();
2831
2832 normalize_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2833
2834 assert_annotated_snapshot!(attributes, @r#"
2835 {
2836 "client.address": {
2837 "type": "string",
2838 "value": "10.0.0.1"
2839 }
2840 }
2841 "#);
2842 }
2843
2844 #[test]
2845 fn test_normalize_client_address_missing_attribute() {
2846 let mut attributes = Annotated::empty();
2847
2848 normalize_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2849
2850 assert!(attributes.is_empty());
2851 }
2852
2853 #[test]
2854 fn test_normalize_client_address_auto_with_ipv6() {
2855 let mut attributes = Annotated::from_json(
2856 r#"{
2857 "client.address": {
2858 "type": "string",
2859 "value": "{{auto}}"
2860 }
2861 }"#,
2862 )
2863 .unwrap();
2864
2865 normalize_client_address(&mut attributes, Some("2001:db8::1".parse().unwrap()));
2866
2867 assert_annotated_snapshot!(attributes, @r#"
2868 {
2869 "client.address": {
2870 "type": "string",
2871 "value": "2001:db8::1"
2872 }
2873 }
2874 "#);
2875 }
2876
2877 #[test]
2878 fn test_normalize_inject_client_address_inserts_when_missing() {
2879 let mut attributes = Annotated::empty();
2880
2881 normalize_inject_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2882
2883 assert_annotated_snapshot!(attributes, @r#"
2884 {
2885 "client.address": {
2886 "type": "string",
2887 "value": "192.168.1.1"
2888 }
2889 }
2890 "#);
2891 }
2892
2893 #[test]
2894 fn test_normalize_inject_client_address_does_not_overwrite() {
2895 let mut attributes = Annotated::from_json(
2896 r#"{
2897 "client.address": {
2898 "type": "string",
2899 "value": "10.0.0.1"
2900 }
2901 }"#,
2902 )
2903 .unwrap();
2904
2905 normalize_inject_client_address(&mut attributes, Some("192.168.1.1".parse().unwrap()));
2906
2907 assert_annotated_snapshot!(attributes, @r#"
2908 {
2909 "client.address": {
2910 "type": "string",
2911 "value": "10.0.0.1"
2912 }
2913 }
2914 "#);
2915 }
2916
2917 #[test]
2918 fn test_normalize_inject_client_address_none_ip() {
2919 let mut attributes = Annotated::from_json(r#"{}"#).unwrap();
2920
2921 normalize_inject_client_address(&mut attributes, None);
2922
2923 assert_annotated_snapshot!(attributes, @r#"
2924 {}
2925 "#);
2926 }
2927
2928 #[test]
2929 fn test_normalize_inject_client_address_ipv6() {
2930 let mut attributes = Annotated::empty();
2931
2932 normalize_inject_client_address(&mut attributes, Some("2001:db8::1".parse().unwrap()));
2933
2934 assert_annotated_snapshot!(attributes, @r#"
2935 {
2936 "client.address": {
2937 "type": "string",
2938 "value": "2001:db8::1"
2939 }
2940 }
2941 "#);
2942 }
2943
2944 #[test]
2945 fn test_normalize_span_category_no_match() {
2946 let mut attributes = Annotated::<Attributes>::from_json(
2948 r#"{
2949 "some.other.attribute": {
2950 "type": "string",
2951 "value": "value"
2952 }
2953 }"#,
2954 )
2955 .unwrap();
2956
2957 normalize_span_category(&mut attributes);
2958
2959 assert_annotated_snapshot!(attributes, @r#"
2960 {
2961 "some.other.attribute": {
2962 "type": "string",
2963 "value": "value"
2964 }
2965 }
2966 "#);
2967 }
2968
2969 #[test]
2970 fn test_normalize_client_sample_rate_valid() {
2971 let mut attributes = Annotated::from_json(
2972 r#"{
2973 "sentry.client_sample_rate": {
2974 "type": "double",
2975 "value": 1.0
2976 }
2977 }"#,
2978 )
2979 .unwrap();
2980
2981 normalize_client_sample_rate(&mut attributes);
2982
2983 assert_annotated_snapshot!(attributes, @r#"
2984 {
2985 "sentry.client_sample_rate": {
2986 "type": "double",
2987 "value": 1.0
2988 }
2989 }
2990 "#);
2991 }
2992
2993 #[test]
2994 fn test_normalize_client_sample_rate_invalid_too_small() {
2995 let mut attributes = {
2996 let mut attrs = Attributes::new();
2997 attrs.insert(SENTRY__CLIENT_SAMPLE_RATE, 0.0);
2998 Annotated::new(attrs)
2999 };
3000
3001 normalize_client_sample_rate(&mut attributes);
3002
3003 assert_annotated_snapshot!(attributes, @r#"
3004 {
3005 "sentry.client_sample_rate": null,
3006 "_meta": {
3007 "sentry.client_sample_rate": {
3008 "": {
3009 "err": [
3010 [
3011 "invalid_data",
3012 {
3013 "reason": "expected sample rate > 0.0, <= 1.0"
3014 }
3015 ]
3016 ]
3017 }
3018 }
3019 }
3020 }
3021 "#);
3022 }
3023
3024 #[test]
3025 fn test_normalize_client_sample_rate_invalid_too_large() {
3026 let mut attributes = {
3027 let mut attrs = Attributes::new();
3028 attrs.insert(SENTRY__CLIENT_SAMPLE_RATE, 1.1);
3029 Annotated::new(attrs)
3030 };
3031
3032 normalize_client_sample_rate(&mut attributes);
3033
3034 assert_annotated_snapshot!(attributes, @r#"
3035 {
3036 "sentry.client_sample_rate": null,
3037 "_meta": {
3038 "sentry.client_sample_rate": {
3039 "": {
3040 "err": [
3041 [
3042 "invalid_data",
3043 {
3044 "reason": "expected sample rate > 0.0, <= 1.0"
3045 }
3046 ]
3047 ]
3048 }
3049 }
3050 }
3051 }
3052 "#);
3053 }
3054
3055 #[test]
3056 fn test_normalize_client_sample_rate_invalid_type() {
3057 let mut attributes = {
3058 let mut attrs = Attributes::new();
3059 attrs.insert(SENTRY__CLIENT_SAMPLE_RATE, "foobar");
3060 Annotated::new(attrs)
3061 };
3062
3063 normalize_client_sample_rate(&mut attributes);
3064
3065 assert_annotated_snapshot!(attributes, @r#"
3066 {
3067 "sentry.client_sample_rate": null,
3068 "_meta": {
3069 "sentry.client_sample_rate": {
3070 "": {
3071 "err": [
3072 [
3073 "invalid_data",
3074 {
3075 "reason": "expected sample rate > 0.0, <= 1.0"
3076 }
3077 ]
3078 ]
3079 }
3080 }
3081 }
3082 }
3083 "#);
3084 }
3085
3086 #[test]
3087 fn test_normalize_mobile_measurements() {
3088 let json = r#"
3089 {
3090 "frames.slow": {"value": 1, "type": "integer"},
3091 "app.vitals.frames.frozen.count": {"value": 2, "type": "integer"},
3092 "frames.total": {"value": 4, "type": "integer"},
3093 "stall_total_time": {"value": 4000, "type": "integer"}
3094 }
3095 "#;
3096
3097 let mut attributes = Annotated::<Attributes>::from_json(json).unwrap();
3098
3099 normalize_attribute_names(&mut attributes);
3100 normalize_mobile_measurements(&mut attributes, Some(Duration::from_secs(5)));
3101
3102 insta::assert_json_snapshot!(SerializableAnnotated(&attributes), @r#"
3103 {
3104 "app.vitals.frames.frozen.count": {
3105 "type": "integer",
3106 "value": 2
3107 },
3108 "app.vitals.frames.frozen.rate": {
3109 "type": "double",
3110 "value": 0.5
3111 },
3112 "app.vitals.frames.slow.count": {
3113 "type": "integer",
3114 "value": 1
3115 },
3116 "app.vitals.frames.slow.rate": {
3117 "type": "double",
3118 "value": 0.25
3119 },
3120 "app.vitals.frames.total.count": {
3121 "type": "integer",
3122 "value": 4
3123 },
3124 "app.vitals.stall.duration": {
3125 "type": "integer",
3126 "value": 4000
3127 },
3128 "app.vitals.stall.percentage": {
3129 "type": "double",
3130 "value": 0.8
3131 },
3132 "frames.slow": {
3133 "type": "integer",
3134 "value": 1
3135 },
3136 "frames.total": {
3137 "type": "integer",
3138 "value": 4
3139 },
3140 "stall_total_time": {
3141 "type": "integer",
3142 "value": 4000
3143 }
3144 }
3145 "#);
3146 }
3147}