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, SpanId};
408    use relay_protocol::{assert_annotated_snapshot, get_value};
409    use serde_json::json;
410
411    use crate::validation::validate_event;
412    use crate::{EventValidationConfig, RedactionRule};
413
414    use super::*;
415
416    #[test]
417    fn test_is_high_cardinality_sdk_ruby_ok() {
418        let json = r#"
419        {
420            "type": "transaction",
421            "transaction": "foo",
422            "timestamp": "2021-04-26T08:00:00+0100",
423            "start_timestamp": "2021-04-26T07:59:01+0100",
424            "contexts": {
425                "trace": {
426                    "op": "rails.request",
427                    "status": "ok"
428                }
429            },
430            "sdk": {"name": "sentry.ruby"},
431            "modules": {"rack": "1.2.3"}
432        }
433        "#;
434        let event = Annotated::<Event>::from_json(json).unwrap();
435
436        assert!(!is_high_cardinality_sdk(&event.0.unwrap()));
437    }
438
439    #[test]
440    fn test_is_high_cardinality_sdk_ruby_error() {
441        let json = r#"
442        {
443            "type": "transaction",
444            "transaction": "foo",
445            "timestamp": "2021-04-26T08:00:00+0100",
446            "start_timestamp": "2021-04-26T07:59:01+0100",
447            "contexts": {
448                "trace": {
449                    "op": "rails.request",
450                    "status": "internal_error"
451                }
452            },
453            "sdk": {"name": "sentry.ruby"},
454            "modules": {"rack": "1.2.3"}
455        }
456        "#;
457        let event = Annotated::<Event>::from_json(json).unwrap();
458        assert!(!event.meta().has_errors());
459
460        assert!(is_high_cardinality_sdk(&event.0.unwrap()));
461    }
462
463    #[test]
464    fn test_skips_non_transaction_events() {
465        let mut event = Annotated::new(Event::default());
466        process_value(
467            &mut event,
468            &mut TransactionsProcessor::default(),
469            ProcessingState::root(),
470        )
471        .unwrap();
472        assert!(event.value().is_some());
473    }
474
475    fn new_test_event() -> Annotated<Event> {
476        let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
477        let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
478        Annotated::new(Event {
479            ty: Annotated::new(EventType::Transaction),
480            transaction: Annotated::new("/".to_owned()),
481            start_timestamp: Annotated::new(start.into()),
482            timestamp: Annotated::new(end.into()),
483            contexts: {
484                let mut contexts = Contexts::new();
485                contexts.add(TraceContext {
486                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
487                    span_id: Annotated::new(SpanId("fa90fdead5f74053".into())),
488                    op: Annotated::new("http.server".to_owned()),
489                    ..Default::default()
490                });
491                Annotated::new(contexts)
492            },
493            spans: Annotated::new(vec![Annotated::new(Span {
494                start_timestamp: Annotated::new(start.into()),
495                timestamp: Annotated::new(end.into()),
496                trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
497                span_id: Annotated::new(SpanId("fa90fdead5f74053".into())),
498                op: Annotated::new("db.statement".to_owned()),
499                ..Default::default()
500            })]),
501            ..Default::default()
502        })
503    }
504
505    #[test]
506    fn test_defaults_missing_op_in_context() {
507        let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
508        let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
509
510        let mut event = Annotated::new(Event {
511            ty: Annotated::new(EventType::Transaction),
512            transaction: Annotated::new("/".to_owned()),
513            timestamp: Annotated::new(end.into()),
514            start_timestamp: Annotated::new(start.into()),
515            contexts: {
516                let mut contexts = Contexts::new();
517                contexts.add(TraceContext {
518                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
519                    span_id: Annotated::new(SpanId("fa90fdead5f74053".into())),
520                    ..Default::default()
521                });
522                Annotated::new(contexts)
523            },
524            ..Default::default()
525        });
526
527        process_value(
528            &mut event,
529            &mut TransactionsProcessor::default(),
530            ProcessingState::root(),
531        )
532        .unwrap();
533
534        let trace_context = get_value!(event.contexts)
535            .unwrap()
536            .get::<TraceContext>()
537            .unwrap();
538        let trace_op = trace_context.op.value().unwrap();
539        assert_eq!(trace_op, "default");
540    }
541
542    #[test]
543    fn test_allows_transaction_event_without_span_list() {
544        let mut event = Annotated::new(Event {
545            ty: Annotated::new(EventType::Transaction),
546            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
547            start_timestamp: Annotated::new(
548                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
549            ),
550            contexts: {
551                let mut contexts = Contexts::new();
552                contexts.add(TraceContext {
553                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
554                    span_id: Annotated::new(SpanId("fa90fdead5f74053".into())),
555                    op: Annotated::new("http.server".to_owned()),
556                    ..Default::default()
557                });
558                Annotated::new(contexts)
559            },
560            ..Default::default()
561        });
562
563        process_value(
564            &mut event,
565            &mut TransactionsProcessor::default(),
566            ProcessingState::root(),
567        )
568        .unwrap();
569        assert!(event.value().is_some());
570    }
571
572    #[test]
573    fn test_allows_transaction_event_with_empty_span_list() {
574        let mut event = Annotated::new(Event {
575            ty: Annotated::new(EventType::Transaction),
576            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
577            start_timestamp: Annotated::new(
578                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
579            ),
580            contexts: {
581                let mut contexts = Contexts::new();
582                contexts.add(TraceContext {
583                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
584                    span_id: Annotated::new(SpanId("fa90fdead5f74053".into())),
585                    op: Annotated::new("http.server".to_owned()),
586                    ..Default::default()
587                });
588                Annotated::new(contexts)
589            },
590            spans: Annotated::new(vec![]),
591            ..Default::default()
592        });
593
594        process_value(
595            &mut event,
596            &mut TransactionsProcessor::default(),
597            ProcessingState::root(),
598        )
599        .unwrap();
600        assert!(event.value().is_some());
601    }
602
603    #[test]
604    fn test_allows_transaction_event_with_null_span_list() {
605        let mut event = new_test_event();
606
607        processor::apply(&mut event, |event, _| {
608            event.spans.set_value(None);
609            Ok(())
610        })
611        .unwrap();
612
613        validate_event(&mut event, &EventValidationConfig::default()).unwrap();
614        process_value(
615            &mut event,
616            &mut TransactionsProcessor::default(),
617            ProcessingState::root(),
618        )
619        .unwrap();
620        assert!(get_value!(event.spans).unwrap().is_empty());
621    }
622
623    #[test]
624    fn test_defaults_transaction_event_with_span_with_missing_op() {
625        let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
626        let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
627
628        let mut event = Annotated::new(Event {
629            ty: Annotated::new(EventType::Transaction),
630            transaction: Annotated::new("/".to_owned()),
631            timestamp: Annotated::new(end.into()),
632            start_timestamp: Annotated::new(start.into()),
633            contexts: {
634                let mut contexts = Contexts::new();
635                contexts.add(TraceContext {
636                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
637                    span_id: Annotated::new(SpanId("fa90fdead5f74053".into())),
638                    op: Annotated::new("http.server".to_owned()),
639                    ..Default::default()
640                });
641                Annotated::new(contexts)
642            },
643            spans: Annotated::new(vec![Annotated::new(Span {
644                timestamp: Annotated::new(
645                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
646                ),
647                start_timestamp: Annotated::new(
648                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
649                ),
650                trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
651                span_id: Annotated::new(SpanId("fa90fdead5f74053".into())),
652
653                ..Default::default()
654            })]),
655            ..Default::default()
656        });
657
658        process_value(
659            &mut event,
660            &mut TransactionsProcessor::default(),
661            ProcessingState::root(),
662        )
663        .unwrap();
664
665        assert_annotated_snapshot!(event, @r#"
666        {
667          "type": "transaction",
668          "transaction": "/",
669          "transaction_info": {
670            "source": "unknown"
671          },
672          "timestamp": 946684810.0,
673          "start_timestamp": 946684800.0,
674          "contexts": {
675            "trace": {
676              "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
677              "span_id": "fa90fdead5f74053",
678              "op": "http.server",
679              "type": "trace"
680            }
681          },
682          "spans": [
683            {
684              "timestamp": 946684810.0,
685              "start_timestamp": 946684800.0,
686              "op": "default",
687              "span_id": "fa90fdead5f74053",
688              "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
689            }
690          ]
691        }
692        "#);
693    }
694
695    #[test]
696    fn test_default_transaction_source_unknown() {
697        let mut event = Annotated::<Event>::from_json(
698            r#"
699            {
700                "type": "transaction",
701                "transaction": "/",
702                "timestamp": 946684810.0,
703                "start_timestamp": 946684800.0,
704                "contexts": {
705                    "trace": {
706                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
707                    "span_id": "fa90fdead5f74053",
708                    "op": "http.server",
709                    "type": "trace"
710                    }
711                },
712                "sdk": {
713                    "name": "sentry.dart.flutter"
714                },
715                "spans": []
716            }
717            "#,
718        )
719        .unwrap();
720
721        process_value(
722            &mut event,
723            &mut TransactionsProcessor::default(),
724            ProcessingState::root(),
725        )
726        .unwrap();
727
728        let source = event
729            .value()
730            .unwrap()
731            .transaction_info
732            .value()
733            .and_then(|info| info.source.value())
734            .unwrap();
735
736        assert_eq!(source, &TransactionSource::Unknown);
737    }
738
739    #[test]
740    fn test_allows_valid_transaction_event_with_spans() {
741        let mut event = new_test_event();
742
743        assert!(
744            process_value(
745                &mut event,
746                &mut TransactionsProcessor::default(),
747                ProcessingState::root(),
748            )
749            .is_ok()
750        );
751    }
752
753    #[test]
754    fn test_defaults_transaction_name_when_missing() {
755        let mut event = new_test_event();
756
757        processor::apply(&mut event, |event, _| {
758            event.transaction.set_value(None);
759            Ok(())
760        })
761        .unwrap();
762
763        process_value(
764            &mut event,
765            &mut TransactionsProcessor::default(),
766            ProcessingState::root(),
767        )
768        .unwrap();
769
770        assert_eq!(get_value!(event.transaction!), "<unlabeled transaction>");
771    }
772
773    #[test]
774    fn test_defaults_transaction_name_when_empty() {
775        let mut event = new_test_event();
776
777        processor::apply(&mut event, |event, _| {
778            event.transaction.set_value(Some("".to_owned()));
779            Ok(())
780        })
781        .unwrap();
782
783        process_value(
784            &mut event,
785            &mut TransactionsProcessor::default(),
786            ProcessingState::root(),
787        )
788        .unwrap();
789
790        assert_eq!(get_value!(event.transaction!), "<unlabeled transaction>");
791    }
792
793    #[test]
794    fn test_transaction_name_normalize() {
795        let json = r#"
796        {
797            "type": "transaction",
798            "transaction": "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0",
799            "transaction_info": {
800              "source": "url"
801            },
802            "timestamp": "2021-04-26T08:00:00+0100",
803            "start_timestamp": "2021-04-26T07:59:01+0100",
804            "contexts": {
805                "trace": {
806                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
807                    "span_id": "fa90fdead5f74053",
808                    "op": "rails.request",
809                    "status": "ok"
810                }
811            },
812            "sdk": {"name": "sentry.ruby"},
813            "modules": {"rack": "1.2.3"}
814        }
815        "#;
816        let mut event = Annotated::<Event>::from_json(json).unwrap();
817
818        process_value(
819            &mut event,
820            &mut TransactionsProcessor::default(),
821            ProcessingState::root(),
822        )
823        .unwrap();
824
825        assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0");
826        assert_eq!(
827            get_value!(event.transaction_info.source!).as_str(),
828            "sanitized"
829        );
830
831        let remarks = get_value!(event!)
832            .transaction
833            .meta()
834            .iter_remarks()
835            .collect_vec();
836        assert_debug_snapshot!(remarks, @r#"[
837    Remark {
838        ty: Substituted,
839        rule_id: "int",
840        range: Some(
841            (
842                5,
843                45,
844            ),
845        ),
846    },
847    Remark {
848        ty: Substituted,
849        rule_id: "int",
850        range: Some(
851            (
852                51,
853                54,
854            ),
855        ),
856    },
857]"#);
858    }
859
860    /// When no identifiers are scrubbed, we should not set an original value in _meta.
861    #[test]
862    fn test_transaction_name_skip_original_value() {
863        let json = r#"
864        {
865            "type": "transaction",
866            "transaction": "/foo/static/page",
867            "transaction_info": {
868              "source": "url"
869            },
870            "timestamp": "2021-04-26T08:00:00+0100",
871            "start_timestamp": "2021-04-26T07:59:01+0100",
872            "contexts": {
873                "trace": {
874                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
875                    "span_id": "fa90fdead5f74053",
876                    "op": "rails.request",
877                    "status": "ok"
878                }
879            },
880            "sdk": {"name": "sentry.ruby"},
881            "modules": {"rack": "1.2.3"}
882        }
883        "#;
884        let mut event = Annotated::<Event>::from_json(json).unwrap();
885
886        process_value(
887            &mut event,
888            &mut TransactionsProcessor::default(),
889            ProcessingState::root(),
890        )
891        .unwrap();
892
893        assert!(event.meta().is_empty());
894    }
895
896    #[test]
897    fn test_transaction_name_normalize_mark_as_sanitized() {
898        let json = r#"
899        {
900            "type": "transaction",
901            "transaction": "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0",
902            "transaction_info": {
903              "source": "url"
904            },
905            "timestamp": "2021-04-26T08:00:00+0100",
906            "start_timestamp": "2021-04-26T07:59:01+0100",
907            "contexts": {
908                "trace": {
909                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
910                    "span_id": "fa90fdead5f74053",
911                    "op": "rails.request",
912                    "status": "ok"
913                }
914            }
915
916        }
917        "#;
918        let mut event = Annotated::<Event>::from_json(json).unwrap();
919
920        process_value(
921            &mut event,
922            &mut TransactionsProcessor::default(),
923            ProcessingState::root(),
924        )
925        .unwrap();
926
927        assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0");
928        assert_eq!(
929            get_value!(event.transaction_info.source!).as_str(),
930            "sanitized"
931        );
932    }
933
934    #[test]
935    fn test_transaction_name_rename_with_rules() {
936        let json = r#"
937        {
938            "type": "transaction",
939            "transaction": "/foo/rule-target/user/123/0/",
940            "transaction_info": {
941              "source": "url"
942            },
943            "timestamp": "2021-04-26T08:00:00+0100",
944            "start_timestamp": "2021-04-26T07:59:01+0100",
945            "contexts": {
946                "trace": {
947                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
948                    "span_id": "fa90fdead5f74053",
949                    "op": "rails.request",
950                    "status": "ok"
951                }
952            },
953            "sdk": {"name": "sentry.ruby"},
954            "modules": {"rack": "1.2.3"}
955        }
956        "#;
957
958        let rule1 = TransactionNameRule {
959            pattern: LazyGlob::new("/foo/*/user/*/**".to_string()),
960            expiry: Utc::now() + Duration::hours(1),
961            redaction: Default::default(),
962        };
963        let rule2 = TransactionNameRule {
964            pattern: LazyGlob::new("/foo/*/**".to_string()),
965            expiry: Utc::now() + Duration::hours(1),
966            redaction: Default::default(),
967        };
968        // This should not happend, such rules shouldn't be sent to relay at all.
969        let rule3 = TransactionNameRule {
970            pattern: LazyGlob::new("/*/**".to_string()),
971            expiry: Utc::now() + Duration::hours(1),
972            redaction: Default::default(),
973        };
974
975        let mut event = Annotated::<Event>::from_json(json).unwrap();
976
977        process_value(
978            &mut event,
979            &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
980                rules: &[rule1, rule2, rule3],
981            }),
982            ProcessingState::root(),
983        )
984        .unwrap();
985
986        assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
987        assert_eq!(
988            get_value!(event.transaction_info.source!).as_str(),
989            "sanitized"
990        );
991
992        let remarks = get_value!(event!)
993            .transaction
994            .meta()
995            .iter_remarks()
996            .collect_vec();
997        assert_debug_snapshot!(remarks, @r#"[
998    Remark {
999        ty: Substituted,
1000        rule_id: "int",
1001        range: Some(
1002            (
1003                22,
1004                25,
1005            ),
1006        ),
1007    },
1008    Remark {
1009        ty: Substituted,
1010        rule_id: "/foo/*/user/*/**",
1011        range: None,
1012    },
1013]"#);
1014    }
1015
1016    #[test]
1017    fn test_transaction_name_rules_skip_expired() {
1018        let json = r#"
1019        {
1020            "type": "transaction",
1021            "transaction": "/foo/rule-target/user/123/0/",
1022            "transaction_info": {
1023              "source": "url"
1024            },
1025            "timestamp": "2021-04-26T08:00:00+0100",
1026            "start_timestamp": "2021-04-26T07:59:01+0100",
1027            "contexts": {
1028                "trace": {
1029                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1030                    "span_id": "fa90fdead5f74053",
1031                    "op": "rails.request",
1032                    "status": "ok"
1033                }
1034            },
1035            "sdk": {"name": "sentry.ruby"},
1036            "modules": {"rack": "1.2.3"}
1037        }
1038        "#;
1039        let mut event = Annotated::<Event>::from_json(json).unwrap();
1040
1041        let rule1 = TransactionNameRule {
1042            pattern: LazyGlob::new("/foo/*/user/*/**".to_string()),
1043            expiry: Utc::now() - Duration::hours(1), // Expired rule
1044            redaction: Default::default(),
1045        };
1046        let rule2 = TransactionNameRule {
1047            pattern: LazyGlob::new("/foo/*/**".to_string()),
1048            expiry: Utc::now() + Duration::hours(1),
1049            redaction: Default::default(),
1050        };
1051        // This should not happend, such rules shouldn't be sent to relay at all.
1052        let rule3 = TransactionNameRule {
1053            pattern: LazyGlob::new("/*/**".to_string()),
1054            expiry: Utc::now() + Duration::hours(1),
1055            redaction: Default::default(),
1056        };
1057
1058        process_value(
1059            &mut event,
1060            &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1061                rules: &[rule1, rule2, rule3],
1062            }),
1063            ProcessingState::root(),
1064        )
1065        .unwrap();
1066
1067        assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1068        assert_eq!(
1069            get_value!(event.transaction_info.source!).as_str(),
1070            "sanitized"
1071        );
1072
1073        let remarks = get_value!(event!)
1074            .transaction
1075            .meta()
1076            .iter_remarks()
1077            .collect_vec();
1078        assert_debug_snapshot!(remarks, @r#"[
1079    Remark {
1080        ty: Substituted,
1081        rule_id: "int",
1082        range: Some(
1083            (
1084                22,
1085                25,
1086            ),
1087        ),
1088    },
1089    Remark {
1090        ty: Substituted,
1091        rule_id: "/foo/*/**",
1092        range: None,
1093    },
1094]"#);
1095    }
1096
1097    #[test]
1098    fn test_normalize_twice() {
1099        // Simulate going through a chain of relays.
1100        let json = r#"
1101        {
1102            "type": "transaction",
1103            "transaction": "/foo/rule-target/user/123/0/",
1104            "transaction_info": {
1105              "source": "url"
1106            },
1107            "timestamp": "2021-04-26T08:00:00+0100",
1108            "start_timestamp": "2021-04-26T07:59:01+0100",
1109            "contexts": {
1110                "trace": {
1111                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1112                    "span_id": "fa90fdead5f74053",
1113                    "op": "rails.request"
1114                }
1115            }
1116        }
1117        "#;
1118
1119        let rules = vec![TransactionNameRule {
1120            pattern: LazyGlob::new("/foo/*/user/*/**".to_string()),
1121            expiry: Utc::now() + Duration::hours(1),
1122            redaction: Default::default(),
1123        }];
1124
1125        let mut event = Annotated::<Event>::from_json(json).unwrap();
1126
1127        let mut processor = TransactionsProcessor::new_name_config(TransactionNameConfig {
1128            rules: rules.as_ref(),
1129        });
1130        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1131
1132        assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1133        assert_eq!(
1134            get_value!(event.transaction_info.source!).as_str(),
1135            "sanitized"
1136        );
1137
1138        let remarks = get_value!(event!)
1139            .transaction
1140            .meta()
1141            .iter_remarks()
1142            .collect_vec();
1143        assert_debug_snapshot!(remarks, @r#"[
1144    Remark {
1145        ty: Substituted,
1146        rule_id: "int",
1147        range: Some(
1148            (
1149                22,
1150                25,
1151            ),
1152        ),
1153    },
1154    Remark {
1155        ty: Substituted,
1156        rule_id: "/foo/*/user/*/**",
1157        range: None,
1158    },
1159]"#);
1160
1161        assert_eq!(
1162            get_value!(event.transaction_info.source!).as_str(),
1163            "sanitized"
1164        );
1165
1166        // Process again:
1167        process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1168
1169        assert_eq!(get_value!(event.transaction!), "/foo/*/user/*/0/");
1170        assert_eq!(
1171            get_value!(event.transaction_info.source!).as_str(),
1172            "sanitized"
1173        );
1174
1175        let remarks = get_value!(event!)
1176            .transaction
1177            .meta()
1178            .iter_remarks()
1179            .collect_vec();
1180        assert_debug_snapshot!(remarks, @r#"[
1181    Remark {
1182        ty: Substituted,
1183        rule_id: "int",
1184        range: Some(
1185            (
1186                22,
1187                25,
1188            ),
1189        ),
1190    },
1191    Remark {
1192        ty: Substituted,
1193        rule_id: "/foo/*/user/*/**",
1194        range: None,
1195    },
1196]"#);
1197
1198        assert_eq!(
1199            get_value!(event.transaction_info.source!).as_str(),
1200            "sanitized"
1201        );
1202    }
1203
1204    #[test]
1205    fn test_transaction_name_unsupported_source() {
1206        let json = r#"
1207        {
1208            "type": "transaction",
1209            "transaction": "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0",
1210            "transaction_info": {
1211              "source": "foobar"
1212            },
1213            "timestamp": "2021-04-26T08:00:00+0100",
1214            "start_timestamp": "2021-04-26T07:59:01+0100",
1215            "contexts": {
1216                "trace": {
1217                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1218                    "span_id": "fa90fdead5f74053",
1219                    "op": "rails.request",
1220                    "status": "ok"
1221                }
1222            }
1223        }
1224        "#;
1225        let mut event = Annotated::<Event>::from_json(json).unwrap();
1226        let rule1 = TransactionNameRule {
1227            pattern: LazyGlob::new("/foo/*/**".to_string()),
1228            expiry: Utc::now() + Duration::hours(1),
1229            redaction: Default::default(),
1230        };
1231        // This should not happend, such rules shouldn't be sent to relay at all.
1232        let rule2 = TransactionNameRule {
1233            pattern: LazyGlob::new("/*/**".to_string()),
1234            expiry: Utc::now() + Duration::hours(1),
1235            redaction: Default::default(),
1236        };
1237        let rules = vec![rule1, rule2];
1238
1239        // This must not normalize transaction name, since it's disabled.
1240        process_value(
1241            &mut event,
1242            &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1243                rules: rules.as_ref(),
1244            }),
1245            ProcessingState::root(),
1246        )
1247        .unwrap();
1248
1249        assert_eq!(
1250            get_value!(event.transaction!),
1251            "/foo/2fd4e1c67a2d28fced849ee1bb76e7391b93eb12/user/123/0"
1252        );
1253        assert!(
1254            get_value!(event!)
1255                .transaction
1256                .meta()
1257                .iter_remarks()
1258                .next()
1259                .is_none()
1260        );
1261        assert_eq!(
1262            get_value!(event.transaction_info.source!).as_str(),
1263            "foobar"
1264        );
1265    }
1266
1267    fn run_with_unknown_source(sdk: &str) -> Annotated<Event> {
1268        let json = r#"
1269        {
1270            "type": "transaction",
1271            "transaction": "/user/jane/blog/",
1272            "timestamp": "2021-04-26T08:00:00+0100",
1273            "start_timestamp": "2021-04-26T07:59:01+0100",
1274            "contexts": {
1275                "trace": {
1276                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1277                    "span_id": "fa90fdead5f74053",
1278                    "op": "rails.request",
1279                    "status": "ok"
1280                }
1281            }
1282        }
1283        "#;
1284        let mut event = Annotated::<Event>::from_json(json).unwrap();
1285        event
1286            .value_mut()
1287            .as_mut()
1288            .unwrap()
1289            .client_sdk
1290            .set_value(Some(ClientSdkInfo {
1291                name: sdk.to_owned().into(),
1292                ..Default::default()
1293            }));
1294        let rules: Vec<TransactionNameRule> = serde_json::from_value(serde_json::json!([
1295            {"pattern": "/user/*/**", "expiry": "3021-04-26T07:59:01+0100", "redaction": {"method": "replace"}}
1296        ]))
1297        .unwrap();
1298
1299        process_value(
1300            &mut event,
1301            &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1302                rules: rules.as_ref(),
1303            }),
1304            ProcessingState::root(),
1305        )
1306        .unwrap();
1307        event
1308    }
1309
1310    #[test]
1311    fn test_normalize_legacy_javascript() {
1312        // Javascript without source annotation gets sanitized.
1313        let event = run_with_unknown_source("sentry.javascript.browser");
1314
1315        assert_eq!(get_value!(event.transaction!), "/user/*/blog/");
1316        assert_eq!(
1317            get_value!(event.transaction_info.source!).as_str(),
1318            "sanitized"
1319        );
1320
1321        let remarks = get_value!(event!)
1322            .transaction
1323            .meta()
1324            .iter_remarks()
1325            .collect_vec();
1326        assert_debug_snapshot!(remarks, @r#"[
1327    Remark {
1328        ty: Substituted,
1329        rule_id: "/user/*/**",
1330        range: None,
1331    },
1332]"#);
1333
1334        assert_eq!(
1335            get_value!(event.transaction_info.source!).as_str(),
1336            "sanitized"
1337        );
1338    }
1339
1340    #[test]
1341    fn test_normalize_legacy_python() {
1342        // Python without source annotation does not get sanitized, because we assume it to be
1343        // low cardinality.
1344        let event = run_with_unknown_source("sentry.python");
1345        assert_eq!(get_value!(event.transaction!), "/user/jane/blog/");
1346        assert_eq!(
1347            get_value!(event.transaction_info.source!).as_str(),
1348            "unknown"
1349        );
1350    }
1351
1352    #[test]
1353    fn test_transaction_name_rename_end_slash() {
1354        let json = r#"
1355        {
1356            "type": "transaction",
1357            "transaction": "/foo/rule-target/user",
1358            "transaction_info": {
1359              "source": "url"
1360            },
1361            "timestamp": "2021-04-26T08:00:00+0100",
1362            "start_timestamp": "2021-04-26T07:59:01+0100",
1363            "contexts": {
1364                "trace": {
1365                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1366                    "span_id": "fa90fdead5f74053",
1367                    "op": "rails.request",
1368                    "status": "ok"
1369                }
1370            },
1371            "sdk": {"name": "sentry.ruby"},
1372            "modules": {"rack": "1.2.3"}
1373        }
1374        "#;
1375
1376        let rule = TransactionNameRule {
1377            pattern: LazyGlob::new("/foo/*/**".to_string()),
1378            expiry: Utc::now() + Duration::hours(1),
1379            redaction: Default::default(),
1380        };
1381
1382        let mut event = Annotated::<Event>::from_json(json).unwrap();
1383
1384        process_value(
1385            &mut event,
1386            &mut TransactionsProcessor::new_name_config(TransactionNameConfig { rules: &[rule] }),
1387            ProcessingState::root(),
1388        )
1389        .unwrap();
1390
1391        assert_eq!(get_value!(event.transaction!), "/foo/*/user");
1392        assert_eq!(
1393            get_value!(event.transaction_info.source!).as_str(),
1394            "sanitized"
1395        );
1396
1397        let remarks = get_value!(event!)
1398            .transaction
1399            .meta()
1400            .iter_remarks()
1401            .collect_vec();
1402        assert_debug_snapshot!(remarks, @r#"[
1403    Remark {
1404        ty: Substituted,
1405        rule_id: "/foo/*/**",
1406        range: None,
1407    },
1408]"#);
1409
1410        assert_eq!(
1411            get_value!(event.transaction_info.source!).as_str(),
1412            "sanitized"
1413        );
1414    }
1415
1416    #[test]
1417    fn test_normalize_transaction_names() {
1418        let should_be_replaced = [
1419            "/aaa11111-aa11-11a1-a11a-1aaa1111a111",
1420            "/1aa111aa-11a1-11aa-a111-a1a11111aa11",
1421            "/00a00000-0000-0000-0000-000000000001",
1422            "/test/b25feeaa-ed2d-4132-bcbd-6232b7922add/url",
1423        ];
1424        let replaced = should_be_replaced.map(|s| {
1425            let mut s = Annotated::new(s.to_owned());
1426            scrub_identifiers(&mut s);
1427            s.0.unwrap()
1428        });
1429        assert_eq!(
1430            replaced,
1431            ["/*", "/*", "/*", "/test/*/url",].map(str::to_owned)
1432        )
1433    }
1434
1435    macro_rules! transaction_name_test {
1436        ($name:ident, $input:literal, $output:literal) => {
1437            #[test]
1438            fn $name() {
1439                let json = format!(
1440                    r#"
1441                    {{
1442                        "type": "transaction",
1443                        "transaction": "{}",
1444                        "transaction_info": {{
1445                          "source": "url"
1446                        }},
1447                        "timestamp": "2021-04-26T08:00:00+0100",
1448                        "start_timestamp": "2021-04-26T07:59:01+0100",
1449                        "contexts": {{
1450                            "trace": {{
1451                                "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1452                                "span_id": "fa90fdead5f74053",
1453                                "op": "rails.request",
1454                                "status": "ok"
1455                            }}
1456                        }}
1457                    }}
1458                "#,
1459                    $input
1460                );
1461
1462                let mut event = Annotated::<Event>::from_json(&json).unwrap();
1463
1464                process_value(
1465                    &mut event,
1466                    &mut TransactionsProcessor::default(),
1467                    ProcessingState::root(),
1468                )
1469                .unwrap();
1470
1471                assert_eq!($output, event.value().unwrap().transaction.value().unwrap());
1472            }
1473        };
1474    }
1475
1476    transaction_name_test!(test_transaction_name_normalize_id, "/1234", "/*");
1477    transaction_name_test!(
1478        test_transaction_name_normalize_in_segments_1,
1479        "/user/path-with-1234/",
1480        "/user/*/"
1481    );
1482    transaction_name_test!(
1483        test_transaction_name_normalize_in_segments_2,
1484        "/testing/open-19-close/1",
1485        "/testing/*/1"
1486    );
1487    transaction_name_test!(
1488        test_transaction_name_normalize_in_segments_3,
1489        "/testing/open19close/1",
1490        "/testing/*/1"
1491    );
1492    transaction_name_test!(
1493        test_transaction_name_normalize_in_segments_4,
1494        "/testing/asdf012/asdf034/asdf056",
1495        "/testing/*/*/*"
1496    );
1497    transaction_name_test!(
1498        test_transaction_name_normalize_in_segments_5,
1499        "/foo/test%A33/1234",
1500        "/foo/test%A33/*"
1501    );
1502    transaction_name_test!(
1503        test_transaction_name_normalize_url_encode_1,
1504        "/%2Ftest%2Fopen%20and%20help%2F1%0A",
1505        "/%2Ftest%2Fopen%20and%20help%2F1%0A"
1506    );
1507    transaction_name_test!(
1508        test_transaction_name_normalize_url_encode_2,
1509        "/this/1234/%E2%9C%85/foo/bar/098123908213",
1510        "/this/*/%E2%9C%85/foo/bar/*"
1511    );
1512    transaction_name_test!(
1513        test_transaction_name_normalize_url_encode_3,
1514        "/foo/hello%20world-4711/",
1515        "/foo/*/"
1516    );
1517    transaction_name_test!(
1518        test_transaction_name_normalize_url_encode_4,
1519        "/foo/hello%20world-0xdeadbeef/",
1520        "/foo/*/"
1521    );
1522    transaction_name_test!(
1523        test_transaction_name_normalize_url_encode_5,
1524        "/foo/hello%20world-4711/",
1525        "/foo/*/"
1526    );
1527    transaction_name_test!(
1528        test_transaction_name_normalize_url_encode_6,
1529        "/foo/hello%2Fworld/",
1530        "/foo/hello%2Fworld/"
1531    );
1532    transaction_name_test!(
1533        test_transaction_name_normalize_url_encode_7,
1534        "/foo/hello%201/",
1535        "/foo/hello%201/"
1536    );
1537    transaction_name_test!(
1538        test_transaction_name_normalize_sha,
1539        "/hash/4c79f60c11214eb38604f4ae0781bfb2/diff",
1540        "/hash/*/diff"
1541    );
1542    transaction_name_test!(
1543        test_transaction_name_normalize_uuid,
1544        "/u/7b25feea-ed2d-4132-bcbd-6232b7922add/edit",
1545        "/u/*/edit"
1546    );
1547    transaction_name_test!(
1548        test_transaction_name_normalize_hex,
1549        "/u/0x3707344A4093822299F31D008/profile/123123213",
1550        "/u/*/profile/*"
1551    );
1552    transaction_name_test!(
1553        test_transaction_name_normalize_windows_path,
1554        r"C:\\\\Program Files\\1234\\Files",
1555        r"C:\\Program Files\*\Files"
1556    );
1557    transaction_name_test!(test_transaction_name_skip_replace_all, "12345", "12345");
1558    transaction_name_test!(
1559        test_transaction_name_skip_replace_all2,
1560        "open-12345-close",
1561        "open-12345-close"
1562    );
1563
1564    #[test]
1565    fn test_scrub_identifiers_before_rules() {
1566        // There's a rule matching the transaction name. However, the UUID
1567        // should be scrubbed first. Scrubbing the UUID makes the rule to not
1568        // match the transformed transaction name anymore.
1569
1570        let mut event = Annotated::<Event>::from_json(
1571            r#"{
1572                "type": "transaction",
1573                "transaction": "/remains/rule-target/1234567890",
1574                "transaction_info": {
1575                    "source": "url"
1576                },
1577                "timestamp": "2021-04-26T08:00:00+0100",
1578                "start_timestamp": "2021-04-26T07:59:01+0100",
1579                "contexts": {
1580                    "trace": {
1581                        "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1582                        "span_id": "fa90fdead5f74053"
1583                    }
1584                }
1585            }"#,
1586        )
1587        .unwrap();
1588
1589        process_value(
1590            &mut event,
1591            &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1592                rules: &[TransactionNameRule {
1593                    pattern: LazyGlob::new("/remains/*/1234567890/".to_owned()),
1594                    expiry: Utc.with_ymd_and_hms(3000, 1, 1, 1, 1, 1).unwrap(),
1595                    redaction: RedactionRule::default(),
1596                }],
1597            }),
1598            ProcessingState::root(),
1599        )
1600        .unwrap();
1601
1602        assert_eq!(get_value!(event.transaction!), "/remains/rule-target/*");
1603        assert_eq!(
1604            get_value!(event.transaction_info.source!).as_str(),
1605            "sanitized"
1606        );
1607
1608        let remarks = get_value!(event!)
1609            .transaction
1610            .meta()
1611            .iter_remarks()
1612            .collect_vec();
1613        assert_debug_snapshot!(remarks, @r#"[
1614    Remark {
1615        ty: Substituted,
1616        rule_id: "int",
1617        range: Some(
1618            (
1619                21,
1620                31,
1621            ),
1622        ),
1623    },
1624]"#);
1625        assert_eq!(
1626            get_value!(event.transaction_info.source!).as_str(),
1627            "sanitized"
1628        );
1629    }
1630
1631    #[test]
1632    fn test_scrub_identifiers_and_apply_rules() {
1633        // Ensure rules are applied after scrubbing identifiers. Rules are only
1634        // applied when `transaction.source="url"`, so this test ensures this
1635        // value isn't set as part of identifier scrubbing.
1636        let mut event = Annotated::<Event>::from_json(
1637            r#"{
1638                "type": "transaction",
1639                "transaction": "/remains/rule-target/1234567890",
1640                "transaction_info": {
1641                    "source": "url"
1642                },
1643                "timestamp": "2021-04-26T08:00:00+0100",
1644                "start_timestamp": "2021-04-26T07:59:01+0100",
1645                "contexts": {
1646                    "trace": {
1647                        "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1648                        "span_id": "fa90fdead5f74053"
1649                    }
1650                }
1651            }"#,
1652        )
1653        .unwrap();
1654
1655        process_value(
1656            &mut event,
1657            &mut TransactionsProcessor::new_name_config(TransactionNameConfig {
1658                rules: &[TransactionNameRule {
1659                    pattern: LazyGlob::new("/remains/*/**".to_owned()),
1660                    expiry: Utc.with_ymd_and_hms(3000, 1, 1, 1, 1, 1).unwrap(),
1661                    redaction: RedactionRule::default(),
1662                }],
1663            }),
1664            ProcessingState::root(),
1665        )
1666        .unwrap();
1667
1668        assert_eq!(get_value!(event.transaction!), "/remains/*/*");
1669        assert_eq!(
1670            get_value!(event.transaction_info.source!).as_str(),
1671            "sanitized"
1672        );
1673
1674        let remarks = get_value!(event!)
1675            .transaction
1676            .meta()
1677            .iter_remarks()
1678            .collect_vec();
1679        assert_debug_snapshot!(remarks, @r#"[
1680    Remark {
1681        ty: Substituted,
1682        rule_id: "int",
1683        range: Some(
1684            (
1685                21,
1686                31,
1687            ),
1688        ),
1689    },
1690    Remark {
1691        ty: Substituted,
1692        rule_id: "/remains/*/**",
1693        range: None,
1694    },
1695]"#);
1696    }
1697
1698    #[test]
1699    fn test_infer_span_op_default() {
1700        let span = Annotated::from_json(r#"{}"#).unwrap();
1701        let defaults: SpanOpDefaults = serde_json::from_value(json!({
1702                "rules": [{
1703                    "condition": {
1704                        "op": "not",
1705                        "inner": {
1706                            "op": "eq",
1707                            "name": "span.data.messaging\\.system",
1708                            "value": null,
1709                        },
1710                    },
1711                    "value": "message"
1712                }]
1713            }
1714        ))
1715        .unwrap();
1716        let op = defaults.borrow().infer(span.value().unwrap());
1717        assert_eq!(&op, "default");
1718    }
1719
1720    #[test]
1721    fn test_infer_span_op_messaging() {
1722        let span = Annotated::from_json(
1723            r#"{
1724            "data": {
1725                "messaging.system": "activemq"
1726            }
1727        }"#,
1728        )
1729        .unwrap();
1730        let defaults: SpanOpDefaults = serde_json::from_value(json!({
1731                "rules": [{
1732                    "condition": {
1733                        "op": "not",
1734                        "inner": {
1735                            "op": "eq",
1736                            "name": "span.data.messaging\\.system",
1737                            "value": null,
1738                        },
1739                    },
1740                    "value": "message"
1741                }]
1742            }
1743        ))
1744        .unwrap();
1745        let op = defaults.borrow().infer(span.value().unwrap());
1746        assert_eq!(&op, "message");
1747    }
1748}