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