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