relay_event_normalization/transactions/
processor.rs

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/// Configuration for sanitizing unparameterized transaction names.
17#[derive(Clone, Copy, Debug, Default)]
18pub struct TransactionNameConfig<'r> {
19    /// Rules for identifier replacement that were discovered by Sentry's transaction clusterer.
20    pub rules: &'r [TransactionNameRule],
21}
22
23/// Apply parametrization to transaction.
24pub fn normalize_transaction_name(
25    transaction: &mut Annotated<String>,
26    rules: &[TransactionNameRule],
27) {
28    // Normalize transaction names for URLs and Sanitized transaction sources.
29    // This in addition to renaming rules can catch some high cardinality parts.
30    scrub_identifiers(transaction);
31
32    // Apply rules discovered by the transaction clusterer in sentry.
33    if !rules.is_empty() {
34        apply_transaction_rename_rules(transaction, rules);
35    }
36}
37
38/// Applies the rule if any found to the transaction name.
39///
40/// It find the first rule matching the criteria:
41/// - source matchining the one provided in the rule sorce
42/// - rule hasn't epired yet
43/// - glob pattern matches the transaction name
44///
45/// Note: we add `/` at the end of the transaction name if there isn't one, to make sure that
46/// patterns like `/<something>/*/**` where we have `**` at the end are a match.
47pub 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 another rule was applied before, we don't want to
60                // rename the transaction name to keep the original one.
61                // We do want to continue adding remarks though, in
62                // order to keep track of all rules applied.
63                if meta.original_value().is_none() {
64                    meta.set_original_value(Some(transaction.clone()));
65                }
66                // add also the rule which was applied to the transaction name
67                meta.add_remark(Remark::new(RemarkType::Substituted, rule));
68                *transaction = result;
69            }
70        }
71
72        Ok(())
73    });
74}
75
76/// Rejects transactions based on required fields.
77#[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    /// Creates a new `TransactionsProcessor` instance.
85    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    /// Returns `true` if the given transaction name should be treated as a URL.
104    ///
105    /// We treat a transaction as URL if one of the following conditions apply:
106    ///
107    /// 1. It is marked with `source:url`
108    /// 2. It is marked with `source:sanitized`, in which case we run normalization again.
109    /// 3. It has no source attribute because it's from an old SDK version,
110    ///    but it contains slashes and we expect it to be high-cardinality
111    ///    based on the SDK information (see [`set_default_transaction_source`]).
112    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            // Always mark URL transactions as sanitized, even if no modification were made by
129            // clusterer rules or regex matchers. This has the consequence that the transaction name
130            // is always extracted as a tag on transaction metrics.
131            // Instead of changing the source to "sanitized", we could have changed metrics extraction
132            // to also extract the transaction name for URL transactions. But this is the safer way,
133            // because the product currently uses queries that assume that `source:url` is equivalent
134            // to `transaction:<< unparameterized >>`.
135            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        // The transaction name is expected to be non-empty by downstream services (e.g. Snuba), but
156        // Relay doesn't reject events missing the transaction name. Instead, a default transaction
157        // name is given, similar to how Sentry gives an "<unlabeled event>" title to error events.
158        // SDKs should avoid sending empty transaction names, setting a more contextual default
159        // value when possible.
160        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/// Rules used to infer `span.op` from other span fields.
192#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
193pub struct SpanOpDefaults {
194    /// List of rules to apply. First match wins.
195    pub rules: Vec<SpanOpDefaultRule>,
196}
197
198impl SpanOpDefaults {
199    /// Gets a borrowed version of this config.
200    pub fn borrow(&self) -> BorrowedSpanOpDefaults {
201        BorrowedSpanOpDefaults {
202            rules: self.rules.as_slice(),
203        }
204    }
205}
206
207/// Borrowed version of [`SpanOpDefaults`].
208#[derive(Clone, Copy, Debug, Default)]
209pub struct BorrowedSpanOpDefaults<'a> {
210    rules: &'a [SpanOpDefaultRule],
211}
212
213impl BorrowedSpanOpDefaults<'_> {
214    /// Infer the span op from a set of rules.
215    ///
216    /// The first matching rule determines the span op.
217    /// If no rule matches, `"default"` is returned.
218    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/// A rule to infer [`Span::op`] from other span fields.
229#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
230pub struct SpanOpDefaultRule {
231    /// When to set the given value.
232    pub condition: RuleCondition,
233    /// Value for the [`Span::op`]. Only set if omitted by the SDK.
234    pub value: String,
235}
236
237/// Span status codes for the Ruby Rack integration that indicate raw URLs being sent as
238/// transaction names. These cases are considered as high-cardinality.
239///
240/// See <https://github.com/getsentry/sentry-ruby/blob/ad4828f6d8d60e98217b2edb1ab003fb627d6bdb/sentry-ruby/lib/sentry/span.rb#L7-L19>
241const 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
255/// List of SDKs which we assume to produce high cardinality transaction names, such as
256/// "/user/123134/login".
257const 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
270/// Returns `true` if the event's transaction name is known to contain unsanitized values.
271///
272/// Newer SDK send the [`TransactionSource`] attribute, which we can rely on to determine
273/// cardinality. If the source is missing, this function gives an indication whether the transaction
274/// name should be sanitized.
275pub 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
314/// Set a default transaction source if it is missing, but only if the transaction name was
315/// extracted as a metrics tag.
316/// This behavior makes it possible to identify transactions for which the transaction name was
317/// not extracted as a tag on the corresponding metrics, because
318///     source == null <=> transaction name == null
319/// See `relay_server::metrics_extraction::transactions::get_transaction_name`.
320pub 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        // Assume low cardinality, set transaction source "Unknown" to signal that the transaction
328        // tag can be safely added to transaction metrics.
329        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    // We treat transactions from legacy SDKs as URLs if they contain slashes.
339    // Otherwise, we assume low cardinality.
340    transaction.contains('/') && is_high_cardinality_sdk(event)
341}
342
343/// Normalize the given string.
344///
345/// Replaces UUIDs, SHAs and numerical IDs in transaction names by placeholders.
346/// Returns `Ok(true)` if the name was changed.
347pub(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        // Collect all the remarks if anything matches.
361        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            // Nothing to do for this transaction.
377            return Ok(());
378        }
379
380        // Sort by the capture end position.
381        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    /// When no identifiers are scrubbed, we should not set an original value in _meta.
863    #[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        // This should not happend, such rules shouldn't be sent to relay at all.
971        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), // Expired rule
1048            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        // This should not happend, such rules shouldn't be sent to relay at all.
1056        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        // Simulate going through a chain of relays.
1106        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 again:
1175        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        // This should not happend, such rules shouldn't be sent to relay at all.
1242        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        // This must not normalize transaction name, since it's disabled.
1250        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        // Javascript without source annotation gets sanitized.
1323        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        // Python without source annotation does not get sanitized, because we assume it to be
1355        // low cardinality.
1356        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        // There's a rule matching the transaction name. However, the UUID
1581        // should be scrubbed first. Scrubbing the UUID makes the rule to not
1582        // match the transformed transaction name anymore.
1583
1584        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        // Ensure rules are applied after scrubbing identifiers. Rules are only
1650        // applied when `transaction.source="url"`, so this test ensures this
1651        // value isn't set as part of identifier scrubbing.
1652        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}