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