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, SpanId};
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(SpanId("fa90fdead5f74053".into())),
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(SpanId("fa90fdead5f74053".into())),
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(SpanId("fa90fdead5f74053".into())),
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(SpanId("fa90fdead5f74053".into())),
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(SpanId("fa90fdead5f74053".into())),
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(SpanId("fa90fdead5f74053".into())),
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(SpanId("fa90fdead5f74053".into())),
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 Remark {
838 ty: Substituted,
839 rule_id: "int",
840 range: Some(
841 (
842 5,
843 45,
844 ),
845 ),
846 },
847 Remark {
848 ty: Substituted,
849 rule_id: "int",
850 range: Some(
851 (
852 51,
853 54,
854 ),
855 ),
856 },
857]"#);
858 }
859
860 #[test]
862 fn test_transaction_name_skip_original_value() {
863 let json = r#"
864 {
865 "type": "transaction",
866 "transaction": "/foo/static/page",
867 "transaction_info": {
868 "source": "url"
869 },
870 "timestamp": "2021-04-26T08:00:00+0100",
871 "start_timestamp": "2021-04-26T07:59:01+0100",
872 "contexts": {
873 "trace": {
874 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
875 "span_id": "fa90fdead5f74053",
876 "op": "rails.request",
877 "status": "ok"
878 }
879 },
880 "sdk": {"name": "sentry.ruby"},
881 "modules": {"rack": "1.2.3"}
882 }
883 "#;
884 let mut event = Annotated::<Event>::from_json(json).unwrap();
885
886 process_value(
887 &mut event,
888 &mut TransactionsProcessor::default(),
889 ProcessingState::root(),
890 )
891 .unwrap();
892
893 assert!(event.meta().is_empty());
894 }
895
896 #[test]
897 fn test_transaction_name_normalize_mark_as_sanitized() {
898 let json = r#"
899 {
900 "type": "transaction",
901 "transaction": "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0",
902 "transaction_info": {
903 "source": "url"
904 },
905 "timestamp": "2021-04-26T08:00:00+0100",
906 "start_timestamp": "2021-04-26T07:59:01+0100",
907 "contexts": {
908 "trace": {
909 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
910 "span_id": "fa90fdead5f74053",
911 "op": "rails.request",
912 "status": "ok"
913 }
914 }
915
916 }
917 "#;
918 let mut event = Annotated::<Event>::from_json(json).unwrap();
919
920 process_value(
921 &mut event,
922 &mut TransactionsProcessor::default(),
923 ProcessingState::root(),
924 )
925 .unwrap();
926
927 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0");
928 assert_eq!(
929 get_value!(event.transaction_info.source!).as_str(),
930 "sanitized"
931 );
932 }
933
934 #[test]
935 fn test_transaction_name_rename_with_rules() {
936 let json = r#"
937 {
938 "type": "transaction",
939 "transaction": "/foo/rule-target/user/123/0/",
940 "transaction_info": {
941 "source": "url"
942 },
943 "timestamp": "2021-04-26T08:00:00+0100",
944 "start_timestamp": "2021-04-26T07:59:01+0100",
945 "contexts": {
946 "trace": {
947 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
948 "span_id": "fa90fdead5f74053",
949 "op": "rails.request",
950 "status": "ok"
951 }
952 },
953 "sdk": {"name": "sentry.ruby"},
954 "modules": {"rack": "1.2.3"}
955 }
956 "#;
957
958 let rule1 = TransactionNameRule {
959 pattern: LazyGlob::new("/foo/*/user/*/**".to_string()),
960 expiry: Utc::now() + Duration::hours(1),
961 redaction: Default::default(),
962 };
963 let rule2 = TransactionNameRule {
964 pattern: LazyGlob::new("/foo/*/**".to_string()),
965 expiry: Utc::now() + Duration::hours(1),
966 redaction: Default::default(),
967 };
968 let rule3 = TransactionNameRule {
970 pattern: LazyGlob::new("/*/**".to_string()),
971 expiry: Utc::now() + Duration::hours(1),
972 redaction: Default::default(),
973 };
974
975 let mut event = Annotated::<Event>::from_json(json).unwrap();
976
977 process_value(
978 &mut event,
979 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
980 rules: &[rule1, rule2, rule3],
981 }),
982 ProcessingState::root(),
983 )
984 .unwrap();
985
986 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
987 assert_eq!(
988 get_value!(event.transaction_info.source!).as_str(),
989 "sanitized"
990 );
991
992 let remarks = get_value!(event!)
993 .transaction
994 .meta()
995 .iter_remarks()
996 .collect_vec();
997 assert_debug_snapshot!(remarks, @r#"[
998 Remark {
999 ty: Substituted,
1000 rule_id: "int",
1001 range: Some(
1002 (
1003 22,
1004 25,
1005 ),
1006 ),
1007 },
1008 Remark {
1009 ty: Substituted,
1010 rule_id: "/foo/*/user/*/**",
1011 range: None,
1012 },
1013]"#);
1014 }
1015
1016 #[test]
1017 fn test_transaction_name_rules_skip_expired() {
1018 let json = r#"
1019 {
1020 "type": "transaction",
1021 "transaction": "/foo/rule-target/user/123/0/",
1022 "transaction_info": {
1023 "source": "url"
1024 },
1025 "timestamp": "2021-04-26T08:00:00+0100",
1026 "start_timestamp": "2021-04-26T07:59:01+0100",
1027 "contexts": {
1028 "trace": {
1029 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1030 "span_id": "fa90fdead5f74053",
1031 "op": "rails.request",
1032 "status": "ok"
1033 }
1034 },
1035 "sdk": {"name": "sentry.ruby"},
1036 "modules": {"rack": "1.2.3"}
1037 }
1038 "#;
1039 let mut event = Annotated::<Event>::from_json(json).unwrap();
1040
1041 let rule1 = TransactionNameRule {
1042 pattern: LazyGlob::new("/foo/*/user/*/**".to_string()),
1043 expiry: Utc::now() - Duration::hours(1), redaction: Default::default(),
1045 };
1046 let rule2 = TransactionNameRule {
1047 pattern: LazyGlob::new("/foo/*/**".to_string()),
1048 expiry: Utc::now() + Duration::hours(1),
1049 redaction: Default::default(),
1050 };
1051 let rule3 = TransactionNameRule {
1053 pattern: LazyGlob::new("/*/**".to_string()),
1054 expiry: Utc::now() + Duration::hours(1),
1055 redaction: Default::default(),
1056 };
1057
1058 process_value(
1059 &mut event,
1060 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1061 rules: &[rule1, rule2, rule3],
1062 }),
1063 ProcessingState::root(),
1064 )
1065 .unwrap();
1066
1067 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1068 assert_eq!(
1069 get_value!(event.transaction_info.source!).as_str(),
1070 "sanitized"
1071 );
1072
1073 let remarks = get_value!(event!)
1074 .transaction
1075 .meta()
1076 .iter_remarks()
1077 .collect_vec();
1078 assert_debug_snapshot!(remarks, @r#"[
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 #[test]
1098 fn test_normalize_twice() {
1099 let json = r#"
1101 {
1102 "type": "transaction",
1103 "transaction": "/foo/rule-target/user/123/0/",
1104 "transaction_info": {
1105 "source": "url"
1106 },
1107 "timestamp": "2021-04-26T08:00:00+0100",
1108 "start_timestamp": "2021-04-26T07:59:01+0100",
1109 "contexts": {
1110 "trace": {
1111 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1112 "span_id": "fa90fdead5f74053",
1113 "op": "rails.request"
1114 }
1115 }
1116 }
1117 "#;
1118
1119 let rules = vec![TransactionNameRule {
1120 pattern: LazyGlob::new("/foo/*/user/*/**".to_string()),
1121 expiry: Utc::now() + Duration::hours(1),
1122 redaction: Default::default(),
1123 }];
1124
1125 let mut event = Annotated::<Event>::from_json(json).unwrap();
1126
1127 let mut processor = TransactionsProcessor::new_name_config(TransactionNameConfig {
1128 rules: rules.as_ref(),
1129 });
1130 process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1131
1132 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1133 assert_eq!(
1134 get_value!(event.transaction_info.source!).as_str(),
1135 "sanitized"
1136 );
1137
1138 let remarks = get_value!(event!)
1139 .transaction
1140 .meta()
1141 .iter_remarks()
1142 .collect_vec();
1143 assert_debug_snapshot!(remarks, @r#"[
1144 Remark {
1145 ty: Substituted,
1146 rule_id: "int",
1147 range: Some(
1148 (
1149 22,
1150 25,
1151 ),
1152 ),
1153 },
1154 Remark {
1155 ty: Substituted,
1156 rule_id: "/foo/*/user/*/**",
1157 range: None,
1158 },
1159]"#);
1160
1161 assert_eq!(
1162 get_value!(event.transaction_info.source!).as_str(),
1163 "sanitized"
1164 );
1165
1166 process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1168
1169 assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1170 assert_eq!(
1171 get_value!(event.transaction_info.source!).as_str(),
1172 "sanitized"
1173 );
1174
1175 let remarks = get_value!(event!)
1176 .transaction
1177 .meta()
1178 .iter_remarks()
1179 .collect_vec();
1180 assert_debug_snapshot!(remarks, @r#"[
1181 Remark {
1182 ty: Substituted,
1183 rule_id: "int",
1184 range: Some(
1185 (
1186 22,
1187 25,
1188 ),
1189 ),
1190 },
1191 Remark {
1192 ty: Substituted,
1193 rule_id: "/foo/*/user/*/**",
1194 range: None,
1195 },
1196]"#);
1197
1198 assert_eq!(
1199 get_value!(event.transaction_info.source!).as_str(),
1200 "sanitized"
1201 );
1202 }
1203
1204 #[test]
1205 fn test_transaction_name_unsupported_source() {
1206 let json = r#"
1207 {
1208 "type": "transaction",
1209 "transaction": "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0",
1210 "transaction_info": {
1211 "source": "foobar"
1212 },
1213 "timestamp": "2021-04-26T08:00:00+0100",
1214 "start_timestamp": "2021-04-26T07:59:01+0100",
1215 "contexts": {
1216 "trace": {
1217 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1218 "span_id": "fa90fdead5f74053",
1219 "op": "rails.request",
1220 "status": "ok"
1221 }
1222 }
1223 }
1224 "#;
1225 let mut event = Annotated::<Event>::from_json(json).unwrap();
1226 let rule1 = TransactionNameRule {
1227 pattern: LazyGlob::new("/foo/*/**".to_string()),
1228 expiry: Utc::now() + Duration::hours(1),
1229 redaction: Default::default(),
1230 };
1231 let rule2 = TransactionNameRule {
1233 pattern: LazyGlob::new("/*/**".to_string()),
1234 expiry: Utc::now() + Duration::hours(1),
1235 redaction: Default::default(),
1236 };
1237 let rules = vec![rule1, rule2];
1238
1239 process_value(
1241 &mut event,
1242 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1243 rules: rules.as_ref(),
1244 }),
1245 ProcessingState::root(),
1246 )
1247 .unwrap();
1248
1249 assert_eq!(
1250 get_value!(event.transaction!),
1251 "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0"
1252 );
1253 assert!(
1254 get_value!(event!)
1255 .transaction
1256 .meta()
1257 .iter_remarks()
1258 .next()
1259 .is_none()
1260 );
1261 assert_eq!(
1262 get_value!(event.transaction_info.source!).as_str(),
1263 "foobar"
1264 );
1265 }
1266
1267 fn run_with_unknown_source(sdk: &str) -> Annotated<Event> {
1268 let json = r#"
1269 {
1270 "type": "transaction",
1271 "transaction": "/user/jane/blog/",
1272 "timestamp": "2021-04-26T08:00:00+0100",
1273 "start_timestamp": "2021-04-26T07:59:01+0100",
1274 "contexts": {
1275 "trace": {
1276 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1277 "span_id": "fa90fdead5f74053",
1278 "op": "rails.request",
1279 "status": "ok"
1280 }
1281 }
1282 }
1283 "#;
1284 let mut event = Annotated::<Event>::from_json(json).unwrap();
1285 event
1286 .value_mut()
1287 .as_mut()
1288 .unwrap()
1289 .client_sdk
1290 .set_value(Some(ClientSdkInfo {
1291 name: sdk.to_owned().into(),
1292 ..Default::default()
1293 }));
1294 let rules: Vec<TransactionNameRule> = serde_json::from_value(serde_json::json!([
1295 {"pattern": "/user/*/**", "expiry": "3021-04-26T07:59:01+0100", "redaction": {"method": "replace"}}
1296 ]))
1297 .unwrap();
1298
1299 process_value(
1300 &mut event,
1301 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1302 rules: rules.as_ref(),
1303 }),
1304 ProcessingState::root(),
1305 )
1306 .unwrap();
1307 event
1308 }
1309
1310 #[test]
1311 fn test_normalize_legacy_javascript() {
1312 let event = run_with_unknown_source("sentry.javascript.browser");
1314
1315 assert_eq!(get_value!(event.transaction!), "/user/*/blog/");
1316 assert_eq!(
1317 get_value!(event.transaction_info.source!).as_str(),
1318 "sanitized"
1319 );
1320
1321 let remarks = get_value!(event!)
1322 .transaction
1323 .meta()
1324 .iter_remarks()
1325 .collect_vec();
1326 assert_debug_snapshot!(remarks, @r#"[
1327 Remark {
1328 ty: Substituted,
1329 rule_id: "/user/*/**",
1330 range: None,
1331 },
1332]"#);
1333
1334 assert_eq!(
1335 get_value!(event.transaction_info.source!).as_str(),
1336 "sanitized"
1337 );
1338 }
1339
1340 #[test]
1341 fn test_normalize_legacy_python() {
1342 let event = run_with_unknown_source("sentry.python");
1345 assert_eq!(get_value!(event.transaction!), "/user/jane/blog/");
1346 assert_eq!(
1347 get_value!(event.transaction_info.source!).as_str(),
1348 "unknown"
1349 );
1350 }
1351
1352 #[test]
1353 fn test_transaction_name_rename_end_slash() {
1354 let json = r#"
1355 {
1356 "type": "transaction",
1357 "transaction": "/foo/rule-target/user",
1358 "transaction_info": {
1359 "source": "url"
1360 },
1361 "timestamp": "2021-04-26T08:00:00+0100",
1362 "start_timestamp": "2021-04-26T07:59:01+0100",
1363 "contexts": {
1364 "trace": {
1365 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1366 "span_id": "fa90fdead5f74053",
1367 "op": "rails.request",
1368 "status": "ok"
1369 }
1370 },
1371 "sdk": {"name": "sentry.ruby"},
1372 "modules": {"rack": "1.2.3"}
1373 }
1374 "#;
1375
1376 let rule = TransactionNameRule {
1377 pattern: LazyGlob::new("/foo/*/**".to_string()),
1378 expiry: Utc::now() + Duration::hours(1),
1379 redaction: Default::default(),
1380 };
1381
1382 let mut event = Annotated::<Event>::from_json(json).unwrap();
1383
1384 process_value(
1385 &mut event,
1386 &mut TransactionsProcessor::new_name_config(TransactionNameConfig { rules: &[rule] }),
1387 ProcessingState::root(),
1388 )
1389 .unwrap();
1390
1391 assert_eq!(get_value!(event.transaction!), "/foo/*/user");
1392 assert_eq!(
1393 get_value!(event.transaction_info.source!).as_str(),
1394 "sanitized"
1395 );
1396
1397 let remarks = get_value!(event!)
1398 .transaction
1399 .meta()
1400 .iter_remarks()
1401 .collect_vec();
1402 assert_debug_snapshot!(remarks, @r#"[
1403 Remark {
1404 ty: Substituted,
1405 rule_id: "/foo/*/**",
1406 range: None,
1407 },
1408]"#);
1409
1410 assert_eq!(
1411 get_value!(event.transaction_info.source!).as_str(),
1412 "sanitized"
1413 );
1414 }
1415
1416 #[test]
1417 fn test_normalize_transaction_names() {
1418 let should_be_replaced = [
1419 "/aaa11111-aa11-11a1-a11a-1aaa1111a111",
1420 "/1aa111aa-11a1-11aa-a111-a1a11111aa11",
1421 "/00a00000-0000-0000-0000-000000000001",
1422 "/test/b25feeaa-ed2d-4132-bcbd-6232b7922add/url",
1423 ];
1424 let replaced = should_be_replaced.map(|s| {
1425 let mut s = Annotated::new(s.to_owned());
1426 scrub_identifiers(&mut s);
1427 s.0.unwrap()
1428 });
1429 assert_eq!(
1430 replaced,
1431 ["/*", "/*", "/*", "/test/*/url",].map(str::to_owned)
1432 )
1433 }
1434
1435 macro_rules! transaction_name_test {
1436 ($name:ident, $input:literal, $output:literal) => {
1437 #[test]
1438 fn $name() {
1439 let json = format!(
1440 r#"
1441 {{
1442 "type": "transaction",
1443 "transaction": "{}",
1444 "transaction_info": {{
1445 "source": "url"
1446 }},
1447 "timestamp": "2021-04-26T08:00:00+0100",
1448 "start_timestamp": "2021-04-26T07:59:01+0100",
1449 "contexts": {{
1450 "trace": {{
1451 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1452 "span_id": "fa90fdead5f74053",
1453 "op": "rails.request",
1454 "status": "ok"
1455 }}
1456 }}
1457 }}
1458 "#,
1459 $input
1460 );
1461
1462 let mut event = Annotated::<Event>::from_json(&json).unwrap();
1463
1464 process_value(
1465 &mut event,
1466 &mut TransactionsProcessor::default(),
1467 ProcessingState::root(),
1468 )
1469 .unwrap();
1470
1471 assert_eq!($output, event.value().unwrap().transaction.value().unwrap());
1472 }
1473 };
1474 }
1475
1476 transaction_name_test!(test_transaction_name_normalize_id, "/1234", "/*");
1477 transaction_name_test!(
1478 test_transaction_name_normalize_in_segments_1,
1479 "/user/path-with-1234/",
1480 "/user/*/"
1481 );
1482 transaction_name_test!(
1483 test_transaction_name_normalize_in_segments_2,
1484 "/testing/open-19-close/1",
1485 "/testing/*/1"
1486 );
1487 transaction_name_test!(
1488 test_transaction_name_normalize_in_segments_3,
1489 "/testing/open19close/1",
1490 "/testing/*/1"
1491 );
1492 transaction_name_test!(
1493 test_transaction_name_normalize_in_segments_4,
1494 "/testing/asdf012/asdf034/asdf056",
1495 "/testing/*/*/*"
1496 );
1497 transaction_name_test!(
1498 test_transaction_name_normalize_in_segments_5,
1499 "/foo/test%A33/1234",
1500 "/foo/test%A33/*"
1501 );
1502 transaction_name_test!(
1503 test_transaction_name_normalize_url_encode_1,
1504 "/%2Ftest%2Fopen%20and%20help%2F1%0A",
1505 "/%2Ftest%2Fopen%20and%20help%2F1%0A"
1506 );
1507 transaction_name_test!(
1508 test_transaction_name_normalize_url_encode_2,
1509 "/this/1234/%E2%9C%85/foo/bar/098123908213",
1510 "/this/*/%E2%9C%85/foo/bar/*"
1511 );
1512 transaction_name_test!(
1513 test_transaction_name_normalize_url_encode_3,
1514 "/foo/hello%20world-4711/",
1515 "/foo/*/"
1516 );
1517 transaction_name_test!(
1518 test_transaction_name_normalize_url_encode_4,
1519 "/foo/hello%20world-0xdeadbeef/",
1520 "/foo/*/"
1521 );
1522 transaction_name_test!(
1523 test_transaction_name_normalize_url_encode_5,
1524 "/foo/hello%20world-4711/",
1525 "/foo/*/"
1526 );
1527 transaction_name_test!(
1528 test_transaction_name_normalize_url_encode_6,
1529 "/foo/hello%2Fworld/",
1530 "/foo/hello%2Fworld/"
1531 );
1532 transaction_name_test!(
1533 test_transaction_name_normalize_url_encode_7,
1534 "/foo/hello%201/",
1535 "/foo/hello%201/"
1536 );
1537 transaction_name_test!(
1538 test_transaction_name_normalize_sha,
1539 "/hash/4c79f60c11214eb38604f4ae0781bfb2/diff",
1540 "/hash/*/diff"
1541 );
1542 transaction_name_test!(
1543 test_transaction_name_normalize_uuid,
1544 "/u/7b25feea-ed2d-4132-bcbd-6232b7922add/edit",
1545 "/u/*/edit"
1546 );
1547 transaction_name_test!(
1548 test_transaction_name_normalize_hex,
1549 "/u/0x3707344A4093822299F31D008/profile/123123213",
1550 "/u/*/profile/*"
1551 );
1552 transaction_name_test!(
1553 test_transaction_name_normalize_windows_path,
1554 r"C:\\\\Program Files\\1234\\Files",
1555 r"C:\\Program Files\*\Files"
1556 );
1557 transaction_name_test!(test_transaction_name_skip_replace_all, "12345", "12345");
1558 transaction_name_test!(
1559 test_transaction_name_skip_replace_all2,
1560 "open-12345-close",
1561 "open-12345-close"
1562 );
1563
1564 #[test]
1565 fn test_scrub_identifiers_before_rules() {
1566 let mut event = Annotated::<Event>::from_json(
1571 r#"{
1572 "type": "transaction",
1573 "transaction": "/remains/rule-target/1234567890",
1574 "transaction_info": {
1575 "source": "url"
1576 },
1577 "timestamp": "2021-04-26T08:00:00+0100",
1578 "start_timestamp": "2021-04-26T07:59:01+0100",
1579 "contexts": {
1580 "trace": {
1581 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1582 "span_id": "fa90fdead5f74053"
1583 }
1584 }
1585 }"#,
1586 )
1587 .unwrap();
1588
1589 process_value(
1590 &mut event,
1591 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1592 rules: &[TransactionNameRule {
1593 pattern: LazyGlob::new("/remains/*/1234567890/".to_owned()),
1594 expiry: Utc.with_ymd_and_hms(3000, 1, 1, 1, 1, 1).unwrap(),
1595 redaction: RedactionRule::default(),
1596 }],
1597 }),
1598 ProcessingState::root(),
1599 )
1600 .unwrap();
1601
1602 assert_eq!(get_value!(event.transaction!), "/remains/rule-target/*");
1603 assert_eq!(
1604 get_value!(event.transaction_info.source!).as_str(),
1605 "sanitized"
1606 );
1607
1608 let remarks = get_value!(event!)
1609 .transaction
1610 .meta()
1611 .iter_remarks()
1612 .collect_vec();
1613 assert_debug_snapshot!(remarks, @r#"[
1614 Remark {
1615 ty: Substituted,
1616 rule_id: "int",
1617 range: Some(
1618 (
1619 21,
1620 31,
1621 ),
1622 ),
1623 },
1624]"#);
1625 assert_eq!(
1626 get_value!(event.transaction_info.source!).as_str(),
1627 "sanitized"
1628 );
1629 }
1630
1631 #[test]
1632 fn test_scrub_identifiers_and_apply_rules() {
1633 let mut event = Annotated::<Event>::from_json(
1637 r#"{
1638 "type": "transaction",
1639 "transaction": "/remains/rule-target/1234567890",
1640 "transaction_info": {
1641 "source": "url"
1642 },
1643 "timestamp": "2021-04-26T08:00:00+0100",
1644 "start_timestamp": "2021-04-26T07:59:01+0100",
1645 "contexts": {
1646 "trace": {
1647 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1648 "span_id": "fa90fdead5f74053"
1649 }
1650 }
1651 }"#,
1652 )
1653 .unwrap();
1654
1655 process_value(
1656 &mut event,
1657 &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1658 rules: &[TransactionNameRule {
1659 pattern: LazyGlob::new("/remains/*/**".to_owned()),
1660 expiry: Utc.with_ymd_and_hms(3000, 1, 1, 1, 1, 1).unwrap(),
1661 redaction: RedactionRule::default(),
1662 }],
1663 }),
1664 ProcessingState::root(),
1665 )
1666 .unwrap();
1667
1668 assert_eq!(get_value!(event.transaction!), "/remains/*/*");
1669 assert_eq!(
1670 get_value!(event.transaction_info.source!).as_str(),
1671 "sanitized"
1672 );
1673
1674 let remarks = get_value!(event!)
1675 .transaction
1676 .meta()
1677 .iter_remarks()
1678 .collect_vec();
1679 assert_debug_snapshot!(remarks, @r#"[
1680 Remark {
1681 ty: Substituted,
1682 rule_id: "int",
1683 range: Some(
1684 (
1685 21,
1686 31,
1687 ),
1688 ),
1689 },
1690 Remark {
1691 ty: Substituted,
1692 rule_id: "/remains/*/**",
1693 range: None,
1694 },
1695]"#);
1696 }
1697
1698 #[test]
1699 fn test_infer_span_op_default() {
1700 let span = Annotated::from_json(r#"{}"#).unwrap();
1701 let defaults: SpanOpDefaults = serde_json::from_value(json!({
1702 "rules": [{
1703 "condition": {
1704 "op": "not",
1705 "inner": {
1706 "op": "eq",
1707 "name": "span.data.messaging\\.system",
1708 "value": null,
1709 },
1710 },
1711 "value": "message"
1712 }]
1713 }
1714 ))
1715 .unwrap();
1716 let op = defaults.borrow().infer(span.value().unwrap());
1717 assert_eq!(&op, "default");
1718 }
1719
1720 #[test]
1721 fn test_infer_span_op_messaging() {
1722 let span = Annotated::from_json(
1723 r#"{
1724 "data": {
1725 "messaging.system": "activemq"
1726 }
1727 }"#,
1728 )
1729 .unwrap();
1730 let defaults: SpanOpDefaults = serde_json::from_value(json!({
1731 "rules": [{
1732 "condition": {
1733 "op": "not",
1734 "inner": {
1735 "op": "eq",
1736 "name": "span.data.messaging\\.system",
1737 "value": null,
1738 },
1739 },
1740 "value": "message"
1741 }]
1742 }
1743 ))
1744 .unwrap();
1745 let op = defaults.borrow().infer(span.value().unwrap());
1746 assert_eq!(&op, "message");
1747 }
1748}