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