Skip to main content

relay_event_normalization/transactions/
processor.rs

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