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