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