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