1use std::borrow::Cow;
2
3use regex::Regex;
4use relay_base_schema::events::EventType;
5use relay_event_schema::processor::{
6 self, ProcessValue, ProcessingResult, ProcessingState, Processor,
7};
8use relay_event_schema::protocol::{Event, Span, SpanStatus, TraceContext, TransactionSource};
9use relay_protocol::{Annotated, Meta, Remark, RemarkType, RuleCondition};
10use relay_sampling::DynamicSamplingContext;
11use serde::{Deserialize, Serialize};
12
13use crate::TransactionNameRule;
14use crate::regexes::TRANSACTION_NAME_NORMALIZER_REGEX;
15
16#[derive(Clone, Copy, Debug, Default)]
18pub struct TransactionNameConfig<'r> {
19 pub rules: &'r [TransactionNameRule],
21}
22
23pub fn normalize_transaction_name(
25 transaction: &mut Annotated<String>,
26 rules: &[TransactionNameRule],
27) {
28 scrub_identifiers(transaction);
31
32 if !rules.is_empty() {
34 apply_transaction_rename_rules(transaction, rules);
35 }
36}
37
38pub fn parameterize_dsc_transaction(
40 dsc: &mut DynamicSamplingContext,
41 rules: &[TransactionNameRule],
42) {
43 let Some(transaction) = dsc.transaction.as_mut() else {
44 return;
45 };
46 if !transaction.contains('/') {
50 return;
51 }
52
53 dsc.transaction = {
54 let mut transaction = Annotated::new(transaction.clone());
55 normalize_transaction_name(&mut transaction, rules);
56 transaction.into_value()
57 };
58}
59
60pub fn apply_transaction_rename_rules(
70 transaction: &mut Annotated<String>,
71 rules: &[TransactionNameRule],
72) {
73 let _ = processor::apply(transaction, |transaction, meta| {
74 let result = rules.iter().find_map(|rule| {
75 rule.match_and_apply(Cow::Borrowed(transaction))
76 .map(|applied_result| (rule.pattern.compiled().pattern(), applied_result))
77 });
78
79 if let Some((rule, result)) = result
80 && *transaction != result
81 {
82 if meta.original_value().is_none() {
87 meta.set_original_value(Some(transaction.clone()));
88 }
89 meta.add_remark(Remark::new(RemarkType::Substituted, rule));
91 *transaction = result;
92 }
93
94 Ok(())
95 });
96}
97
98#[derive(Debug, Default)]
100pub struct TransactionsProcessor<'r> {
101 name_config: TransactionNameConfig<'r>,
102 span_op_defaults: BorrowedSpanOpDefaults<'r>,
103}
104
105impl<'r> TransactionsProcessor<'r> {
106 pub fn new(
108 name_config: TransactionNameConfig<'r>,
109 span_op_defaults: BorrowedSpanOpDefaults<'r>,
110 ) -> Self {
111 Self {
112 name_config,
113 span_op_defaults,
114 }
115 }
116
117 #[cfg(test)]
118 fn new_name_config(name_config: TransactionNameConfig<'r>) -> Self {
119 Self {
120 name_config,
121 ..Default::default()
122 }
123 }
124
125 fn treat_transaction_as_url(&self, event: &Event) -> bool {
135 let source = event
136 .transaction_info
137 .value()
138 .and_then(|i| i.source.value());
139
140 matches!(
141 source,
142 Some(&TransactionSource::Url | &TransactionSource::Sanitized)
143 ) || (source.is_none() && event.transaction.value().is_some_and(|t| t.contains('/')))
144 }
145
146 fn normalize_transaction_name(&self, event: &mut Event) {
147 if self.treat_transaction_as_url(event) {
148 normalize_transaction_name(&mut event.transaction, self.name_config.rules);
149
150 event
158 .transaction_info
159 .get_or_insert_with(Default::default)
160 .source
161 .set_value(Some(TransactionSource::Sanitized));
162 }
163 }
164}
165
166impl Processor for TransactionsProcessor<'_> {
167 fn process_event(
168 &mut self,
169 event: &mut Event,
170 _meta: &mut Meta,
171 state: &ProcessingState<'_>,
172 ) -> ProcessingResult {
173 if event.ty.value() != Some(&EventType::Transaction) {
174 return Ok(());
175 }
176
177 if event.transaction.value().is_none_or(|s| s.is_empty()) {
183 event
184 .transaction
185 .set_value(Some("<unlabeled transaction>".to_owned()))
186 }
187
188 set_default_transaction_source(event);
189 self.normalize_transaction_name(event);
190 if let Some(trace_context) = event.context_mut::<TraceContext>() {
191 trace_context.op.get_or_insert_with(|| "default".to_owned());
192 }
193
194 event.process_child_values(self, state)?;
195 Ok(())
196 }
197
198 fn process_span(
199 &mut self,
200 span: &mut Span,
201 _meta: &mut Meta,
202 state: &ProcessingState<'_>,
203 ) -> ProcessingResult {
204 if span.op.value().is_none() {
205 *span.op.value_mut() = Some(self.span_op_defaults.infer(span));
206 }
207 span.process_child_values(self, state)?;
208
209 Ok(())
210 }
211}
212
213#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
215pub struct SpanOpDefaults {
216 pub rules: Vec<SpanOpDefaultRule>,
218}
219
220impl SpanOpDefaults {
221 pub fn borrow(&self) -> BorrowedSpanOpDefaults<'_> {
223 BorrowedSpanOpDefaults {
224 rules: self.rules.as_slice(),
225 }
226 }
227}
228
229#[derive(Clone, Copy, Debug, Default)]
231pub struct BorrowedSpanOpDefaults<'a> {
232 rules: &'a [SpanOpDefaultRule],
233}
234
235impl BorrowedSpanOpDefaults<'_> {
236 fn infer(&self, span: &Span) -> String {
241 for rule in self.rules {
242 if rule.condition.matches(span) {
243 return rule.value.clone();
244 }
245 }
246 "default".to_owned()
247 }
248}
249
250#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
252pub struct SpanOpDefaultRule {
253 pub condition: RuleCondition,
255 pub value: String,
257}
258
259const RUBY_URL_STATUSES: &[SpanStatus] = &[
264 SpanStatus::InvalidArgument,
265 SpanStatus::Unauthenticated,
266 SpanStatus::PermissionDenied,
267 SpanStatus::NotFound,
268 SpanStatus::AlreadyExists,
269 SpanStatus::ResourceExhausted,
270 SpanStatus::Cancelled,
271 SpanStatus::InternalError,
272 SpanStatus::Unimplemented,
273 SpanStatus::Unavailable,
274 SpanStatus::DeadlineExceeded,
275];
276
277const RAW_URL_SDKS: &[&str] = &[
280 "sentry.javascript.angular",
281 "sentry.javascript.browser",
282 "sentry.javascript.ember",
283 "sentry.javascript.gatsby",
284 "sentry.javascript.react",
285 "sentry.javascript.remix",
286 "sentry.javascript.vue",
287 "sentry.javascript.nextjs",
288 "sentry.php.laravel",
289 "sentry.php.symfony",
290];
291
292pub fn is_high_cardinality_sdk(event: &Event) -> bool {
298 let Some(client_sdk) = event.client_sdk.value() else {
299 return false;
300 };
301
302 let sdk_name = event.sdk_name();
303 if RAW_URL_SDKS.contains(&sdk_name) {
304 return true;
305 }
306
307 let is_http_status_404 = event.tag_value("http.status_code") == Some("404");
308 if sdk_name == "sentry.python" && is_http_status_404 && client_sdk.has_integration("django") {
309 return true;
310 }
311
312 let http_method = event
313 .request
314 .value()
315 .and_then(|r| r.method.as_str())
316 .unwrap_or_default();
317
318 if sdk_name == "sentry.javascript.node"
319 && http_method.eq_ignore_ascii_case("options")
320 && client_sdk.has_integration("Express")
321 {
322 return true;
323 }
324
325 if sdk_name == "sentry.ruby"
326 && event.has_module("rack")
327 && let Some(trace) = event.context::<TraceContext>()
328 && RUBY_URL_STATUSES.contains(trace.status.value().unwrap_or(&SpanStatus::Unknown))
329 {
330 return true;
331 }
332
333 false
334}
335
336pub fn set_default_transaction_source(event: &mut Event) {
343 let source = event
344 .transaction_info
345 .value()
346 .and_then(|info| info.source.value());
347
348 if source.is_none() && !is_high_cardinality_transaction(event) {
349 let transaction_info = event.transaction_info.get_or_insert_with(Default::default);
352 transaction_info
353 .source
354 .set_value(Some(TransactionSource::Unknown));
355 }
356}
357
358fn is_high_cardinality_transaction(event: &Event) -> bool {
359 let transaction = event.transaction.as_str().unwrap_or_default();
360 transaction.contains('/') && is_high_cardinality_sdk(event)
363}
364
365pub(crate) fn scrub_identifiers(string: &mut Annotated<String>) {
370 scrub_identifiers_with_regex(string, &TRANSACTION_NAME_NORMALIZER_REGEX, "*");
371}
372
373fn scrub_identifiers_with_regex(string: &mut Annotated<String>, pattern: &Regex, replacer: &str) {
374 let capture_names = pattern.capture_names().flatten().collect::<Vec<_>>();
375
376 let _ = processor::apply(string, |trans, meta| {
377 let mut caps = Vec::new();
378 for captures in pattern.captures_iter(trans) {
380 for name in &capture_names {
381 if let Some(capture) = captures.name(name) {
382 let remark = Remark::with_range(
383 RemarkType::Substituted,
384 *name,
385 (capture.start(), capture.end()),
386 );
387 caps.push((capture, remark));
388 break;
389 }
390 }
391 }
392
393 if caps.is_empty() {
394 return Ok(());
396 }
397
398 caps.sort_by_key(|(capture, _)| capture.end());
400 let mut changed = String::with_capacity(trans.len() + caps.len() * replacer.len());
401 let mut last_end = 0usize;
402 for (capture, remark) in caps {
403 changed.push_str(&trans[last_end..capture.start()]);
404 changed.push_str(replacer);
405 last_end = capture.end();
406 meta.add_remark(remark);
407 }
408 changed.push_str(&trans[last_end..]);
409
410 if !changed.is_empty() && changed != "*" {
411 meta.set_original_value(Some(trans.to_string()));
412 *trans = changed;
413 }
414 Ok(())
415 });
416}
417
418#[cfg(test)]
419mod tests {
420 use chrono::{Duration, TimeZone, Utc};
421 use insta::assert_debug_snapshot;
422 use itertools::Itertools;
423 use relay_common::glob2::LazyGlob;
424 use relay_event_schema::processor::process_value;
425 use relay_event_schema::protocol::{ClientSdkInfo, Contexts};
426 use relay_protocol::{assert_annotated_snapshot, get_value};
427 use serde_json::json;
428
429 use crate::validation::validate_event;
430 use crate::{EventValidationConfig, RedactionRule};
431
432 use super::*;
433
434 #[test]
435 fn test_is_high_cardinality_sdk_ruby_ok() {
436 let json = r#"
437 {
438 "type": "transaction",
439 "transaction": "foo",
440 "timestamp": "2021-04-26T08:00:00+0100",
441 "start_timestamp": "2021-04-26T07:59:01+0100",
442 "contexts": {
443 "trace": {
444 "op": "rails.request",
445 "status": "ok"
446 }
447 },
448 "sdk": {"name": "sentry.ruby"},
449 "modules": {"rack": "1.2.3"}
450 }
451 "#;
452 let event = Annotated::<Event>::from_json(json).unwrap();
453
454 assert!(!is_high_cardinality_sdk(&event.0.unwrap()));
455 }
456
457 #[test]
458 fn test_is_high_cardinality_sdk_ruby_error() {
459 let json = r#"
460 {
461 "type": "transaction",
462 "transaction": "foo",
463 "timestamp": "2021-04-26T08:00:00+0100",
464 "start_timestamp": "2021-04-26T07:59:01+0100",
465 "contexts": {
466 "trace": {
467 "op": "rails.request",
468 "status": "internal_error"
469 }
470 },
471 "sdk": {"name": "sentry.ruby"},
472 "modules": {"rack": "1.2.3"}
473 }
474 "#;
475 let event = Annotated::<Event>::from_json(json).unwrap();
476 assert!(!event.meta().has_errors());
477
478 assert!(is_high_cardinality_sdk(&event.0.unwrap()));
479 }
480
481 #[test]
482 fn test_skips_non_transaction_events() {
483 let mut event = Annotated::new(Event::default());
484 process_value(
485 &mut event,
486 &mut TransactionsProcessor::default(),
487 ProcessingState::root(),
488 )
489 .unwrap();
490 assert!(event.value().is_some());
491 }
492
493 fn new_test_event() -> Annotated<Event> {
494 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
495 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
496 Annotated::new(Event {
497 ty: Annotated::new(EventType::Transaction),
498 transaction: Annotated::new("/".to_owned()),
499 start_timestamp: Annotated::new(start.into()),
500 timestamp: Annotated::new(end.into()),
501 contexts: {
502 let mut contexts = Contexts::new();
503 contexts.add(TraceContext {
504 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
505 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
506 op: Annotated::new("http.server".to_owned()),
507 ..Default::default()
508 });
509 Annotated::new(contexts)
510 },
511 spans: Annotated::new(vec![Annotated::new(Span {
512 start_timestamp: Annotated::new(start.into()),
513 timestamp: Annotated::new(end.into()),
514 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
515 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
516 op: Annotated::new("db.statement".to_owned()),
517 ..Default::default()
518 })]),
519 ..Default::default()
520 })
521 }
522
523 #[test]
524 fn test_defaults_missing_op_in_context() {
525 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
526 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
527
528 let mut event = Annotated::new(Event {
529 ty: Annotated::new(EventType::Transaction),
530 transaction: Annotated::new("/".to_owned()),
531 timestamp: Annotated::new(end.into()),
532 start_timestamp: Annotated::new(start.into()),
533 contexts: {
534 let mut contexts = Contexts::new();
535 contexts.add(TraceContext {
536 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
537 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
538 ..Default::default()
539 });
540 Annotated::new(contexts)
541 },
542 ..Default::default()
543 });
544
545 process_value(
546 &mut event,
547 &mut TransactionsProcessor::default(),
548 ProcessingState::root(),
549 )
550 .unwrap();
551
552 let trace_context = get_value!(event.contexts)
553 .unwrap()
554 .get::<TraceContext>()
555 .unwrap();
556 let trace_op = trace_context.op.value().unwrap();
557 assert_eq!(trace_op, "default");
558 }
559
560 #[test]
561 fn test_allows_transaction_event_without_span_list() {
562 let mut event = Annotated::new(Event {
563 ty: Annotated::new(EventType::Transaction),
564 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
565 start_timestamp: Annotated::new(
566 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
567 ),
568 contexts: {
569 let mut contexts = Contexts::new();
570 contexts.add(TraceContext {
571 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
572 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
573 op: Annotated::new("http.server".to_owned()),
574 ..Default::default()
575 });
576 Annotated::new(contexts)
577 },
578 ..Default::default()
579 });
580
581 process_value(
582 &mut event,
583 &mut TransactionsProcessor::default(),
584 ProcessingState::root(),
585 )
586 .unwrap();
587 assert!(event.value().is_some());
588 }
589
590 #[test]
591 fn test_allows_transaction_event_with_empty_span_list() {
592 let mut event = Annotated::new(Event {
593 ty: Annotated::new(EventType::Transaction),
594 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
595 start_timestamp: Annotated::new(
596 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
597 ),
598 contexts: {
599 let mut contexts = Contexts::new();
600 contexts.add(TraceContext {
601 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
602 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
603 op: Annotated::new("http.server".to_owned()),
604 ..Default::default()
605 });
606 Annotated::new(contexts)
607 },
608 spans: Annotated::new(vec![]),
609 ..Default::default()
610 });
611
612 process_value(
613 &mut event,
614 &mut TransactionsProcessor::default(),
615 ProcessingState::root(),
616 )
617 .unwrap();
618 assert!(event.value().is_some());
619 }
620
621 #[test]
622 fn test_allows_transaction_event_with_null_span_list() {
623 let mut event = new_test_event();
624
625 processor::apply(&mut event, |event, _| {
626 event.spans.set_value(None);
627 Ok(())
628 })
629 .unwrap();
630
631 validate_event(&mut event, &EventValidationConfig::default()).unwrap();
632 process_value(
633 &mut event,
634 &mut TransactionsProcessor::default(),
635 ProcessingState::root(),
636 )
637 .unwrap();
638 assert!(get_value!(event.spans).unwrap().is_empty());
639 }
640
641 #[test]
642 fn test_defaults_transaction_event_with_span_with_missing_op() {
643 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
644 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
645
646 let mut event = Annotated::new(Event {
647 ty: Annotated::new(EventType::Transaction),
648 transaction: Annotated::new("/".to_owned()),
649 timestamp: Annotated::new(end.into()),
650 start_timestamp: Annotated::new(start.into()),
651 contexts: {
652 let mut contexts = Contexts::new();
653 contexts.add(TraceContext {
654 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
655 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
656 op: Annotated::new("http.server".to_owned()),
657 ..Default::default()
658 });
659 Annotated::new(contexts)
660 },
661 spans: Annotated::new(vec![Annotated::new(Span {
662 timestamp: Annotated::new(
663 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
664 ),
665 start_timestamp: Annotated::new(
666 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
667 ),
668 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
669 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
670
671 ..Default::default()
672 })]),
673 ..Default::default()
674 });
675
676 process_value(
677 &mut event,
678 &mut TransactionsProcessor::default(),
679 ProcessingState::root(),
680 )
681 .unwrap();
682
683 assert_annotated_snapshot!(event, @r###"
684 {
685 "type": "transaction",
686 "transaction": "/",
687 "transaction_info": {
688 "source": "unknown"
689 },
690 "timestamp": 946684810.0,
691 "start_timestamp": 946684800.0,
692 "contexts": {
693 "trace": {
694 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
695 "span_id": "fa90fdead5f74053",
696 "op": "http.server",
697 "type": "trace"
698 }
699 },
700 "spans": [
701 {
702 "timestamp": 946684810.0,
703 "start_timestamp": 946684800.0,
704 "op": "default",
705 "span_id": "fa90fdead5f74053",
706 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
707 }
708 ]
709 }
710 "###);
711 }
712
713 #[test]
714 fn test_default_transaction_source_unknown() {
715 let mut event = Annotated::<Event>::from_json(
716 r#"
717 {
718 "type": "transaction",
719 "transaction": "/",
720 "timestamp": 946684810.0,
721 "start_timestamp": 946684800.0,
722 "contexts": {
723 "trace": {
724 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
725 "span_id": "fa90fdead5f74053",
726 "op": "http.server",
727 "type": "trace"
728 }
729 },
730 "sdk": {
731 "name": "sentry.dart.flutter"
732 },
733 "spans": []
734 }
735 "#,
736 )
737 .unwrap();
738
739 process_value(
740 &mut event,
741 &mut TransactionsProcessor::default(),
742 ProcessingState::root(),
743 )
744 .unwrap();
745
746 let source = event
747 .value()
748 .unwrap()
749 .transaction_info
750 .value()
751 .and_then(|info| info.source.value())
752 .unwrap();
753
754 assert_eq!(source, &TransactionSource::Unknown);
755 }
756
757 #[test]
758 fn test_allows_valid_transaction_event_with_spans() {
759 let mut event = new_test_event();
760
761 assert!(
762 process_value(
763 &mut event,
764 &mut TransactionsProcessor::default(),
765 ProcessingState::root(),
766 )
767 .is_ok()
768 );
769 }
770
771 #[test]
772 fn test_defaults_transaction_name_when_missing() {
773 let mut event = new_test_event();
774
775 processor::apply(&mut event, |event, _| {
776 event.transaction.set_value(None);
777 Ok(())
778 })
779 .unwrap();
780
781 process_value(
782 &mut event,
783 &mut TransactionsProcessor::default(),
784 ProcessingState::root(),
785 )
786 .unwrap();
787
788 assert_eq!(get_value!(event.transaction!), "<unlabeled transaction>");
789 }
790
791 #[test]
792 fn test_defaults_transaction_name_when_empty() {
793 let mut event = new_test_event();
794
795 processor::apply(&mut event, |event, _| {
796 event.transaction.set_value(Some("".to_owned()));
797 Ok(())
798 })
799 .unwrap();
800
801 process_value(
802 &mut event,
803 &mut TransactionsProcessor::default(),
804 ProcessingState::root(),
805 )
806 .unwrap();
807
808 assert_eq!(get_value!(event.transaction!), "<unlabeled transaction>");
809 }
810
811 #[test]
812 fn test_transaction_name_normalize() {
813 let json = r#"
814 {
815 "type": "transaction",
816 "transaction": "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0",
817 "transaction_info": {
818 "source": "url"
819 },
820 "timestamp": "2021-04-26T08:00:00+0100",
821 "start_timestamp": "2021-04-26T07:59:01+0100",
822 "contexts": {
823 "trace": {
824 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
825 "span_id": "fa90fdead5f74053",
826 "op": "rails.request",
827 "status": "ok"
828 }
829 },
830 "sdk": {"name": "sentry.ruby"},
831 "modules": {"rack": "1.2.3"}
832 }
833 "#;
834 let mut event = Annotated::<Event>::from_json(json).unwrap();
835
836 process_value(
837 &mut event,
838 &mut TransactionsProcessor::default(),
839 ProcessingState::root(),
840 )
841 .unwrap();
842
843 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0");
844 assert_eq!(
845 get_value!(event.transaction_info.source!).as_str(),
846 "sanitized"
847 );
848
849 let remarks = get_value!(event!)
850 .transaction
851 .meta()
852 .iter_remarks()
853 .collect_vec();
854 assert_debug_snapshot!(remarks, @r###"
855 [
856 Remark {
857 ty: Substituted,
858 rule_id: "int",
859 range: Some(
860 (
861 5,
862 45,
863 ),
864 ),
865 },
866 Remark {
867 ty: Substituted,
868 rule_id: "int",
869 range: Some(
870 (
871 51,
872 54,
873 ),
874 ),
875 },
876 ]
877 "###);
878 }
879
880 #[test]
882 fn test_transaction_name_skip_original_value() {
883 let json = r#"
884 {
885 "type": "transaction",
886 "transaction": "/foo/static/page",
887 "transaction_info": {
888 "source": "url"
889 },
890 "timestamp": "2021-04-26T08:00:00+0100",
891 "start_timestamp": "2021-04-26T07:59:01+0100",
892 "contexts": {
893 "trace": {
894 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
895 "span_id": "fa90fdead5f74053",
896 "op": "rails.request",
897 "status": "ok"
898 }
899 },
900 "sdk": {"name": "sentry.ruby"},
901 "modules": {"rack": "1.2.3"}
902 }
903 "#;
904 let mut event = Annotated::<Event>::from_json(json).unwrap();
905
906 process_value(
907 &mut event,
908 &mut TransactionsProcessor::default(),
909 ProcessingState::root(),
910 )
911 .unwrap();
912
913 assert!(event.meta().is_empty());
914 }
915
916 #[test]
917 fn test_transaction_name_normalize_mark_as_sanitized() {
918 let json = r#"
919 {
920 "type": "transaction",
921 "transaction": "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0",
922 "transaction_info": {
923 "source": "url"
924 },
925 "timestamp": "2021-04-26T08:00:00+0100",
926 "start_timestamp": "2021-04-26T07:59:01+0100",
927 "contexts": {
928 "trace": {
929 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
930 "span_id": "fa90fdead5f74053",
931 "op": "rails.request",
932 "status": "ok"
933 }
934 }
935
936 }
937 "#;
938 let mut event = Annotated::<Event>::from_json(json).unwrap();
939
940 process_value(
941 &mut event,
942 &mut TransactionsProcessor::default(),
943 ProcessingState::root(),
944 )
945 .unwrap();
946
947 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0");
948 assert_eq!(
949 get_value!(event.transaction_info.source!).as_str(),
950 "sanitized"
951 );
952 }
953
954 #[test]
955 fn test_transaction_name_rename_with_rules() {
956 let json = r#"
957 {
958 "type": "transaction",
959 "transaction": "/foo/rule-target/user/123/0/",
960 "transaction_info": {
961 "source": "url"
962 },
963 "timestamp": "2021-04-26T08:00:00+0100",
964 "start_timestamp": "2021-04-26T07:59:01+0100",
965 "contexts": {
966 "trace": {
967 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
968 "span_id": "fa90fdead5f74053",
969 "op": "rails.request",
970 "status": "ok"
971 }
972 },
973 "sdk": {"name": "sentry.ruby"},
974 "modules": {"rack": "1.2.3"}
975 }
976 "#;
977
978 let rule1 = TransactionNameRule {
979 pattern: LazyGlob::new("/foo/*/user/*/**".to_owned()),
980 expiry: Utc::now() + Duration::hours(1),
981 redaction: Default::default(),
982 };
983 let rule2 = TransactionNameRule {
984 pattern: LazyGlob::new("/foo/*/**".to_owned()),
985 expiry: Utc::now() + Duration::hours(1),
986 redaction: Default::default(),
987 };
988 let rule3 = TransactionNameRule {
990 pattern: LazyGlob::new("/*/**".to_owned()),
991 expiry: Utc::now() + Duration::hours(1),
992 redaction: Default::default(),
993 };
994
995 let mut event = Annotated::<Event>::from_json(json).unwrap();
996
997 process_value(
998 &mut event,
999 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1000 rules: &[rule1, rule2, rule3],
1001 }),
1002 ProcessingState::root(),
1003 )
1004 .unwrap();
1005
1006 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1007 assert_eq!(
1008 get_value!(event.transaction_info.source!).as_str(),
1009 "sanitized"
1010 );
1011
1012 let remarks = get_value!(event!)
1013 .transaction
1014 .meta()
1015 .iter_remarks()
1016 .collect_vec();
1017 assert_debug_snapshot!(remarks, @r###"
1018 [
1019 Remark {
1020 ty: Substituted,
1021 rule_id: "int",
1022 range: Some(
1023 (
1024 22,
1025 25,
1026 ),
1027 ),
1028 },
1029 Remark {
1030 ty: Substituted,
1031 rule_id: "/foo/*/user/*/**",
1032 range: None,
1033 },
1034 ]
1035 "###);
1036 }
1037
1038 #[test]
1039 fn test_transaction_name_rules_skip_expired() {
1040 let json = r#"
1041 {
1042 "type": "transaction",
1043 "transaction": "/foo/rule-target/user/123/0/",
1044 "transaction_info": {
1045 "source": "url"
1046 },
1047 "timestamp": "2021-04-26T08:00:00+0100",
1048 "start_timestamp": "2021-04-26T07:59:01+0100",
1049 "contexts": {
1050 "trace": {
1051 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1052 "span_id": "fa90fdead5f74053",
1053 "op": "rails.request",
1054 "status": "ok"
1055 }
1056 },
1057 "sdk": {"name": "sentry.ruby"},
1058 "modules": {"rack": "1.2.3"}
1059 }
1060 "#;
1061 let mut event = Annotated::<Event>::from_json(json).unwrap();
1062
1063 let rule1 = TransactionNameRule {
1064 pattern: LazyGlob::new("/foo/*/user/*/**".to_owned()),
1065 expiry: Utc::now() - Duration::hours(1), redaction: Default::default(),
1067 };
1068 let rule2 = TransactionNameRule {
1069 pattern: LazyGlob::new("/foo/*/**".to_owned()),
1070 expiry: Utc::now() + Duration::hours(1),
1071 redaction: Default::default(),
1072 };
1073 let rule3 = TransactionNameRule {
1075 pattern: LazyGlob::new("/*/**".to_owned()),
1076 expiry: Utc::now() + Duration::hours(1),
1077 redaction: Default::default(),
1078 };
1079
1080 process_value(
1081 &mut event,
1082 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1083 rules: &[rule1, rule2, rule3],
1084 }),
1085 ProcessingState::root(),
1086 )
1087 .unwrap();
1088
1089 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1090 assert_eq!(
1091 get_value!(event.transaction_info.source!).as_str(),
1092 "sanitized"
1093 );
1094
1095 let remarks = get_value!(event!)
1096 .transaction
1097 .meta()
1098 .iter_remarks()
1099 .collect_vec();
1100 assert_debug_snapshot!(remarks, @r###"
1101 [
1102 Remark {
1103 ty: Substituted,
1104 rule_id: "int",
1105 range: Some(
1106 (
1107 22,
1108 25,
1109 ),
1110 ),
1111 },
1112 Remark {
1113 ty: Substituted,
1114 rule_id: "/foo/*/**",
1115 range: None,
1116 },
1117 ]
1118 "###);
1119 }
1120
1121 #[test]
1122 fn test_normalize_twice() {
1123 let json = r#"
1125 {
1126 "type": "transaction",
1127 "transaction": "/foo/rule-target/user/123/0/",
1128 "transaction_info": {
1129 "source": "url"
1130 },
1131 "timestamp": "2021-04-26T08:00:00+0100",
1132 "start_timestamp": "2021-04-26T07:59:01+0100",
1133 "contexts": {
1134 "trace": {
1135 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1136 "span_id": "fa90fdead5f74053",
1137 "op": "rails.request"
1138 }
1139 }
1140 }
1141 "#;
1142
1143 let rules = vec![TransactionNameRule {
1144 pattern: LazyGlob::new("/foo/*/user/*/**".to_owned()),
1145 expiry: Utc::now() + Duration::hours(1),
1146 redaction: Default::default(),
1147 }];
1148
1149 let mut event = Annotated::<Event>::from_json(json).unwrap();
1150
1151 let mut processor = TransactionsProcessor::new_name_config(TransactionNameConfig {
1152 rules: rules.as_ref(),
1153 });
1154 process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1155
1156 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1157 assert_eq!(
1158 get_value!(event.transaction_info.source!).as_str(),
1159 "sanitized"
1160 );
1161
1162 let remarks = get_value!(event!)
1163 .transaction
1164 .meta()
1165 .iter_remarks()
1166 .collect_vec();
1167 assert_debug_snapshot!(remarks, @r###"
1168 [
1169 Remark {
1170 ty: Substituted,
1171 rule_id: "int",
1172 range: Some(
1173 (
1174 22,
1175 25,
1176 ),
1177 ),
1178 },
1179 Remark {
1180 ty: Substituted,
1181 rule_id: "/foo/*/user/*/**",
1182 range: None,
1183 },
1184 ]
1185 "###);
1186
1187 assert_eq!(
1188 get_value!(event.transaction_info.source!).as_str(),
1189 "sanitized"
1190 );
1191
1192 process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1194
1195 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1196 assert_eq!(
1197 get_value!(event.transaction_info.source!).as_str(),
1198 "sanitized"
1199 );
1200
1201 let remarks = get_value!(event!)
1202 .transaction
1203 .meta()
1204 .iter_remarks()
1205 .collect_vec();
1206 assert_debug_snapshot!(remarks, @r###"
1207 [
1208 Remark {
1209 ty: Substituted,
1210 rule_id: "int",
1211 range: Some(
1212 (
1213 22,
1214 25,
1215 ),
1216 ),
1217 },
1218 Remark {
1219 ty: Substituted,
1220 rule_id: "/foo/*/user/*/**",
1221 range: None,
1222 },
1223 ]
1224 "###);
1225
1226 assert_eq!(
1227 get_value!(event.transaction_info.source!).as_str(),
1228 "sanitized"
1229 );
1230 }
1231
1232 #[test]
1233 fn test_transaction_name_unsupported_source() {
1234 let json = r#"
1235 {
1236 "type": "transaction",
1237 "transaction": "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0",
1238 "transaction_info": {
1239 "source": "foobar"
1240 },
1241 "timestamp": "2021-04-26T08:00:00+0100",
1242 "start_timestamp": "2021-04-26T07:59:01+0100",
1243 "contexts": {
1244 "trace": {
1245 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1246 "span_id": "fa90fdead5f74053",
1247 "op": "rails.request",
1248 "status": "ok"
1249 }
1250 }
1251 }
1252 "#;
1253 let mut event = Annotated::<Event>::from_json(json).unwrap();
1254 let rule1 = TransactionNameRule {
1255 pattern: LazyGlob::new("/foo/*/**".to_owned()),
1256 expiry: Utc::now() + Duration::hours(1),
1257 redaction: Default::default(),
1258 };
1259 let rule2 = TransactionNameRule {
1261 pattern: LazyGlob::new("/*/**".to_owned()),
1262 expiry: Utc::now() + Duration::hours(1),
1263 redaction: Default::default(),
1264 };
1265 let rules = vec![rule1, rule2];
1266
1267 process_value(
1269 &mut event,
1270 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1271 rules: rules.as_ref(),
1272 }),
1273 ProcessingState::root(),
1274 )
1275 .unwrap();
1276
1277 assert_eq!(
1278 get_value!(event.transaction!),
1279 "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0"
1280 );
1281 assert!(
1282 get_value!(event!)
1283 .transaction
1284 .meta()
1285 .iter_remarks()
1286 .next()
1287 .is_none()
1288 );
1289 assert_eq!(
1290 get_value!(event.transaction_info.source!).as_str(),
1291 "foobar"
1292 );
1293 }
1294
1295 fn run_with_unknown_source(sdk: &str) -> Annotated<Event> {
1296 let json = r#"
1297 {
1298 "type": "transaction",
1299 "transaction": "/user/jane/blog/",
1300 "timestamp": "2021-04-26T08:00:00+0100",
1301 "start_timestamp": "2021-04-26T07:59:01+0100",
1302 "contexts": {
1303 "trace": {
1304 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1305 "span_id": "fa90fdead5f74053",
1306 "op": "rails.request",
1307 "status": "ok"
1308 }
1309 }
1310 }
1311 "#;
1312 let mut event = Annotated::<Event>::from_json(json).unwrap();
1313 event
1314 .value_mut()
1315 .as_mut()
1316 .unwrap()
1317 .client_sdk
1318 .set_value(Some(ClientSdkInfo {
1319 name: sdk.to_owned().into(),
1320 ..Default::default()
1321 }));
1322 let rules: Vec<TransactionNameRule> = serde_json::from_value(serde_json::json!([
1323 {"pattern": "/user/*/**", "expiry": "3021-04-26T07:59:01+0100", "redaction": {"method": "replace"}}
1324 ]))
1325 .unwrap();
1326
1327 process_value(
1328 &mut event,
1329 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1330 rules: rules.as_ref(),
1331 }),
1332 ProcessingState::root(),
1333 )
1334 .unwrap();
1335 event
1336 }
1337
1338 #[test]
1339 fn test_normalize_legacy_javascript() {
1340 let event = run_with_unknown_source("sentry.javascript.browser");
1342
1343 assert_eq!(get_value!(event.transaction!), "/user/*/blog/");
1344 assert_eq!(
1345 get_value!(event.transaction_info.source!).as_str(),
1346 "sanitized"
1347 );
1348
1349 let remarks = get_value!(event!)
1350 .transaction
1351 .meta()
1352 .iter_remarks()
1353 .collect_vec();
1354 assert_debug_snapshot!(remarks, @r###"
1355 [
1356 Remark {
1357 ty: Substituted,
1358 rule_id: "/user/*/**",
1359 range: None,
1360 },
1361 ]
1362 "###);
1363
1364 assert_eq!(
1365 get_value!(event.transaction_info.source!).as_str(),
1366 "sanitized"
1367 );
1368 }
1369
1370 #[test]
1371 fn test_normalize_legacy_python() {
1372 let event = run_with_unknown_source("sentry.python");
1375 assert_eq!(get_value!(event.transaction!), "/user/jane/blog/");
1376 assert_eq!(
1377 get_value!(event.transaction_info.source!).as_str(),
1378 "unknown"
1379 );
1380 }
1381
1382 #[test]
1383 fn test_transaction_name_rename_end_slash() {
1384 let json = r#"
1385 {
1386 "type": "transaction",
1387 "transaction": "/foo/rule-target/user",
1388 "transaction_info": {
1389 "source": "url"
1390 },
1391 "timestamp": "2021-04-26T08:00:00+0100",
1392 "start_timestamp": "2021-04-26T07:59:01+0100",
1393 "contexts": {
1394 "trace": {
1395 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1396 "span_id": "fa90fdead5f74053",
1397 "op": "rails.request",
1398 "status": "ok"
1399 }
1400 },
1401 "sdk": {"name": "sentry.ruby"},
1402 "modules": {"rack": "1.2.3"}
1403 }
1404 "#;
1405
1406 let rule = TransactionNameRule {
1407 pattern: LazyGlob::new("/foo/*/**".to_owned()),
1408 expiry: Utc::now() + Duration::hours(1),
1409 redaction: Default::default(),
1410 };
1411
1412 let mut event = Annotated::<Event>::from_json(json).unwrap();
1413
1414 process_value(
1415 &mut event,
1416 &mut TransactionsProcessor::new_name_config(TransactionNameConfig { rules: &[rule] }),
1417 ProcessingState::root(),
1418 )
1419 .unwrap();
1420
1421 assert_eq!(get_value!(event.transaction!), "/foo/*/user");
1422 assert_eq!(
1423 get_value!(event.transaction_info.source!).as_str(),
1424 "sanitized"
1425 );
1426
1427 let remarks = get_value!(event!)
1428 .transaction
1429 .meta()
1430 .iter_remarks()
1431 .collect_vec();
1432 assert_debug_snapshot!(remarks, @r###"
1433 [
1434 Remark {
1435 ty: Substituted,
1436 rule_id: "/foo/*/**",
1437 range: None,
1438 },
1439 ]
1440 "###);
1441
1442 assert_eq!(
1443 get_value!(event.transaction_info.source!).as_str(),
1444 "sanitized"
1445 );
1446 }
1447
1448 #[test]
1449 fn test_normalize_transaction_names() {
1450 let should_be_replaced = [
1451 "/aaa11111-aa11-11a1-a11a-1aaa1111a111",
1452 "/1aa111aa-11a1-11aa-a111-a1a11111aa11",
1453 "/00a00000-0000-0000-0000-000000000001",
1454 "/test/b25feeaa-ed2d-4132-bcbd-6232b7922add/url",
1455 ];
1456 let replaced = should_be_replaced.map(|s| {
1457 let mut s = Annotated::new(s.to_owned());
1458 scrub_identifiers(&mut s);
1459 s.0.unwrap()
1460 });
1461 assert_eq!(
1462 replaced,
1463 ["/*", "/*", "/*", "/test/*/url",].map(str::to_owned)
1464 )
1465 }
1466
1467 macro_rules! transaction_name_test {
1468 ($name:ident, $input:literal, $output:literal) => {
1469 #[test]
1470 fn $name() {
1471 let json = format!(
1472 r#"
1473 {{
1474 "type": "transaction",
1475 "transaction": "{}",
1476 "transaction_info": {{
1477 "source": "url"
1478 }},
1479 "timestamp": "2021-04-26T08:00:00+0100",
1480 "start_timestamp": "2021-04-26T07:59:01+0100",
1481 "contexts": {{
1482 "trace": {{
1483 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1484 "span_id": "fa90fdead5f74053",
1485 "op": "rails.request",
1486 "status": "ok"
1487 }}
1488 }}
1489 }}
1490 "#,
1491 $input
1492 );
1493
1494 let mut event = Annotated::<Event>::from_json(&json).unwrap();
1495
1496 process_value(
1497 &mut event,
1498 &mut TransactionsProcessor::default(),
1499 ProcessingState::root(),
1500 )
1501 .unwrap();
1502
1503 assert_eq!($output, event.value().unwrap().transaction.value().unwrap());
1504 }
1505 };
1506 }
1507
1508 transaction_name_test!(test_transaction_name_normalize_id, "/1234", "/*");
1509 transaction_name_test!(
1510 test_transaction_name_normalize_in_segments_1,
1511 "/user/path-with-1234/",
1512 "/user/*/"
1513 );
1514 transaction_name_test!(
1515 test_transaction_name_normalize_in_segments_2,
1516 "/testing/open-19-close/1",
1517 "/testing/*/1"
1518 );
1519 transaction_name_test!(
1520 test_transaction_name_normalize_in_segments_3,
1521 "/testing/open19close/1",
1522 "/testing/*/1"
1523 );
1524 transaction_name_test!(
1525 test_transaction_name_normalize_in_segments_4,
1526 "/testing/asdf012/asdf034/asdf056",
1527 "/testing/*/*/*"
1528 );
1529 transaction_name_test!(
1530 test_transaction_name_normalize_in_segments_5,
1531 "/foo/test%A33/1234",
1532 "/foo/test%A33/*"
1533 );
1534 transaction_name_test!(
1535 test_transaction_name_normalize_url_encode_1,
1536 "/%2Ftest%2Fopen%20and%20help%2F1%0A",
1537 "/%2Ftest%2Fopen%20and%20help%2F1%0A"
1538 );
1539 transaction_name_test!(
1540 test_transaction_name_normalize_url_encode_2,
1541 "/this/1234/%E2%9C%85/foo/bar/098123908213",
1542 "/this/*/%E2%9C%85/foo/bar/*"
1543 );
1544 transaction_name_test!(
1545 test_transaction_name_normalize_url_encode_3,
1546 "/foo/hello%20world-4711/",
1547 "/foo/*/"
1548 );
1549 transaction_name_test!(
1550 test_transaction_name_normalize_url_encode_4,
1551 "/foo/hello%20world-0xdeadbeef/",
1552 "/foo/*/"
1553 );
1554 transaction_name_test!(
1555 test_transaction_name_normalize_url_encode_5,
1556 "/foo/hello%20world-4711/",
1557 "/foo/*/"
1558 );
1559 transaction_name_test!(
1560 test_transaction_name_normalize_url_encode_6,
1561 "/foo/hello%2Fworld/",
1562 "/foo/hello%2Fworld/"
1563 );
1564 transaction_name_test!(
1565 test_transaction_name_normalize_url_encode_7,
1566 "/foo/hello%201/",
1567 "/foo/hello%201/"
1568 );
1569 transaction_name_test!(
1570 test_transaction_name_normalize_sha,
1571 "/hash/4c79f60c11214eb38604f4ae0781bfb2/diff",
1572 "/hash/*/diff"
1573 );
1574 transaction_name_test!(
1575 test_transaction_name_normalize_uuid,
1576 "/u/7b25feea-ed2d-4132-bcbd-6232b7922add/edit",
1577 "/u/*/edit"
1578 );
1579 transaction_name_test!(
1580 test_transaction_name_normalize_hex,
1581 "/u/0x3707344A4093822299F31D008/profile/123123213",
1582 "/u/*/profile/*"
1583 );
1584 transaction_name_test!(
1585 test_transaction_name_normalize_windows_path,
1586 r"C:\\\\Program Files\\1234\\Files",
1587 r"C:\\Program Files\*\Files"
1588 );
1589 transaction_name_test!(test_transaction_name_skip_replace_all, "12345", "12345");
1590 transaction_name_test!(
1591 test_transaction_name_skip_replace_all2,
1592 "open-12345-close",
1593 "open-12345-close"
1594 );
1595
1596 #[test]
1597 fn test_scrub_identifiers_before_rules() {
1598 let mut event = Annotated::<Event>::from_json(
1603 r#"{
1604 "type": "transaction",
1605 "transaction": "/remains/rule-target/1234567890",
1606 "transaction_info": {
1607 "source": "url"
1608 },
1609 "timestamp": "2021-04-26T08:00:00+0100",
1610 "start_timestamp": "2021-04-26T07:59:01+0100",
1611 "contexts": {
1612 "trace": {
1613 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1614 "span_id": "fa90fdead5f74053"
1615 }
1616 }
1617 }"#,
1618 )
1619 .unwrap();
1620
1621 process_value(
1622 &mut event,
1623 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1624 rules: &[TransactionNameRule {
1625 pattern: LazyGlob::new("/remains/*/1234567890/".to_owned()),
1626 expiry: Utc.with_ymd_and_hms(3000, 1, 1, 1, 1, 1).unwrap(),
1627 redaction: RedactionRule::default(),
1628 }],
1629 }),
1630 ProcessingState::root(),
1631 )
1632 .unwrap();
1633
1634 assert_eq!(get_value!(event.transaction!), "/remains/rule-target/*");
1635 assert_eq!(
1636 get_value!(event.transaction_info.source!).as_str(),
1637 "sanitized"
1638 );
1639
1640 let remarks = get_value!(event!)
1641 .transaction
1642 .meta()
1643 .iter_remarks()
1644 .collect_vec();
1645 assert_debug_snapshot!(remarks, @r###"
1646 [
1647 Remark {
1648 ty: Substituted,
1649 rule_id: "int",
1650 range: Some(
1651 (
1652 21,
1653 31,
1654 ),
1655 ),
1656 },
1657 ]
1658 "###);
1659 assert_eq!(
1660 get_value!(event.transaction_info.source!).as_str(),
1661 "sanitized"
1662 );
1663 }
1664
1665 #[test]
1666 fn test_scrub_identifiers_and_apply_rules() {
1667 let mut event = Annotated::<Event>::from_json(
1671 r#"{
1672 "type": "transaction",
1673 "transaction": "/remains/rule-target/1234567890",
1674 "transaction_info": {
1675 "source": "url"
1676 },
1677 "timestamp": "2021-04-26T08:00:00+0100",
1678 "start_timestamp": "2021-04-26T07:59:01+0100",
1679 "contexts": {
1680 "trace": {
1681 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1682 "span_id": "fa90fdead5f74053"
1683 }
1684 }
1685 }"#,
1686 )
1687 .unwrap();
1688
1689 process_value(
1690 &mut event,
1691 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1692 rules: &[TransactionNameRule {
1693 pattern: LazyGlob::new("/remains/*/**".to_owned()),
1694 expiry: Utc.with_ymd_and_hms(3000, 1, 1, 1, 1, 1).unwrap(),
1695 redaction: RedactionRule::default(),
1696 }],
1697 }),
1698 ProcessingState::root(),
1699 )
1700 .unwrap();
1701
1702 assert_eq!(get_value!(event.transaction!), "/remains/*/*");
1703 assert_eq!(
1704 get_value!(event.transaction_info.source!).as_str(),
1705 "sanitized"
1706 );
1707
1708 let remarks = get_value!(event!)
1709 .transaction
1710 .meta()
1711 .iter_remarks()
1712 .collect_vec();
1713 assert_debug_snapshot!(remarks, @r###"
1714 [
1715 Remark {
1716 ty: Substituted,
1717 rule_id: "int",
1718 range: Some(
1719 (
1720 21,
1721 31,
1722 ),
1723 ),
1724 },
1725 Remark {
1726 ty: Substituted,
1727 rule_id: "/remains/*/**",
1728 range: None,
1729 },
1730 ]
1731 "###);
1732 }
1733
1734 #[test]
1735 fn test_infer_span_op_default() {
1736 let span = Annotated::from_json(r#"{}"#).unwrap();
1737 let defaults: SpanOpDefaults = serde_json::from_value(json!({
1738 "rules": [{
1739 "condition": {
1740 "op": "not",
1741 "inner": {
1742 "op": "eq",
1743 "name": "span.data.messaging\\.system",
1744 "value": null,
1745 },
1746 },
1747 "value": "message"
1748 }]
1749 }
1750 ))
1751 .unwrap();
1752 let op = defaults.borrow().infer(span.value().unwrap());
1753 assert_eq!(&op, "default");
1754 }
1755
1756 #[test]
1757 fn test_infer_span_op_messaging() {
1758 let span = Annotated::from_json(
1759 r#"{
1760 "data": {
1761 "messaging.system": "activemq"
1762 }
1763 }"#,
1764 )
1765 .unwrap();
1766 let defaults: SpanOpDefaults = serde_json::from_value(json!({
1767 "rules": [{
1768 "condition": {
1769 "op": "not",
1770 "inner": {
1771 "op": "eq",
1772 "name": "span.data.messaging\\.system",
1773 "value": null,
1774 },
1775 },
1776 "value": "message"
1777 }]
1778 }
1779 ))
1780 .unwrap();
1781 let op = defaults.borrow().infer(span.value().unwrap());
1782 assert_eq!(&op, "message");
1783 }
1784}