relay_event_normalization/transactions/
rules.rs

1use std::borrow::Cow;
2
3use chrono::{DateTime, Utc};
4use relay_common::glob2::LazyGlob;
5use relay_event_schema::protocol::OperationType;
6use serde::{Deserialize, Serialize};
7
8/// Object containing transaction attributes the rules must only be applied to.
9///
10/// This is part of [`SpanDescriptionRule`].
11#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)]
12pub struct SpanDescriptionRuleScope {
13    /// The span operation type to match on.
14    #[serde(skip_serializing_if = "String::is_empty")]
15    pub op: OperationType,
16}
17
18/// Default value for substitution in [`RedactionRule`].
19fn default_substitution() -> String {
20    "*".to_string()
21}
22
23/// Describes what to do with the matched pattern.
24#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
25#[serde(tag = "method", rename_all = "snake_case")]
26pub enum RedactionRule {
27    /// Replaces the matched pattern with a placeholder.
28    Replace {
29        /// The string to substitute with.
30        ///
31        /// Defaults to `"*"`.
32        #[serde(default = "default_substitution")]
33        substitution: String,
34    },
35
36    /// Unsupported redaction rule for forward compatibility.
37    #[serde(other)]
38    Unknown,
39}
40
41impl Default for RedactionRule {
42    fn default() -> Self {
43        Self::Replace {
44            substitution: default_substitution(),
45        }
46    }
47}
48
49/// The rule describes how span descriptions should be changed.
50#[derive(Debug, Serialize, Deserialize, Clone)]
51pub struct SpanDescriptionRule {
52    /// The pattern which will be applied to the span description.
53    pub pattern: LazyGlob,
54    /// Date time when the rule expires and it should not be applied anymore.
55    pub expiry: DateTime<Utc>,
56    /// Object containing transaction attributes the rules must only be applied to.
57    pub scope: SpanDescriptionRuleScope,
58    /// Object describing what to do with the matched pattern.
59    pub redaction: RedactionRule,
60}
61
62impl From<&TransactionNameRule> for SpanDescriptionRule {
63    fn from(value: &TransactionNameRule) -> Self {
64        SpanDescriptionRule {
65            pattern: LazyGlob::new(format!("**{}", value.pattern.as_str())),
66            expiry: value.expiry,
67            scope: SpanDescriptionRuleScope::default(),
68            redaction: value.redaction.clone(),
69        }
70    }
71}
72
73impl SpanDescriptionRule {
74    /// Applies the span description rule to the given string, if it matches the pattern.
75    ///
76    /// TODO(iker): we should check the rule's domain, similar to transaction name rules.
77    pub fn match_and_apply(&self, mut string: Cow<String>) -> Option<String> {
78        let slash_is_present = string.ends_with('/');
79        if !slash_is_present {
80            string.to_mut().push('/');
81        }
82        let is_matched = self.matches(&string);
83
84        if is_matched {
85            let mut result = self.apply(&string);
86            if !slash_is_present {
87                result.pop();
88            }
89            Some(result)
90        } else {
91            None
92        }
93    }
94
95    /// Returns `true` if the rule isn't expired yet and its pattern matches the given string.
96    fn matches(&self, string: &str) -> bool {
97        let now = Utc::now();
98        self.expiry > now && self.pattern.compiled().is_match(string)
99    }
100
101    /// Applies the rule to the provided value.
102    fn apply(&self, value: &str) -> String {
103        match &self.redaction {
104            RedactionRule::Replace { substitution } => self
105                .pattern
106                .compiled()
107                .replace_captures(value, substitution),
108            _ => {
109                relay_log::trace!("Replacement rule type is unsupported!");
110                value.to_owned()
111            }
112        }
113    }
114}
115
116/// The rule describes how transaction name should be changed.
117#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
118pub struct TransactionNameRule {
119    /// The pattern which will be applied to transaction name.
120    pub pattern: LazyGlob,
121    /// Date time when the rule expires and it should not be applied anymore.
122    pub expiry: DateTime<Utc>,
123    /// Object describing what to do with the matched pattern.
124    pub redaction: RedactionRule,
125}
126
127impl TransactionNameRule {
128    /// Checks is the current rule matches and tries to apply it.
129    pub fn match_and_apply(&self, mut transaction: Cow<String>) -> Option<String> {
130        let slash_is_present = transaction.ends_with('/');
131        if !slash_is_present {
132            transaction.to_mut().push('/');
133        }
134        let is_matched = self.matches(&transaction);
135
136        if is_matched {
137            let mut result = self.apply(&transaction);
138            if !slash_is_present {
139                result.pop();
140            }
141            Some(result)
142        } else {
143            None
144        }
145    }
146
147    /// Applies the rule to the provided value.
148    ///
149    /// Note: currently only `url` source for rules supported.
150    fn apply(&self, value: &str) -> String {
151        match &self.redaction {
152            RedactionRule::Replace { substitution } => self
153                .pattern
154                .compiled()
155                .replace_captures(value, substitution),
156            _ => {
157                relay_log::trace!("Replacement rule type is unsupported!");
158                value.to_owned()
159            }
160        }
161    }
162
163    /// Returns `true` if the current rule pattern matches transaction, expected transaction
164    /// source, and not expired yet.
165    fn matches(&self, transaction: &str) -> bool {
166        self.expiry > Utc::now() && self.pattern.compiled().is_match(transaction)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_rule_format() {
176        let json = r#"
177        {
178          "pattern": "/auth/login/*/**",
179          "expiry": "2022-11-30T00:00:00.000000Z",
180          "scope": {
181            "source": "url"
182          },
183          "redaction": {
184            "method": "replace",
185            "substitution": ":id"
186          }
187        }
188        "#;
189
190        let rule: TransactionNameRule = serde_json::from_str(json).unwrap();
191
192        let parsed_time = DateTime::parse_from_rfc3339("2022-11-30T00:00:00Z").unwrap();
193        let result = TransactionNameRule {
194            pattern: LazyGlob::new("/auth/login/*/**".to_string()),
195            expiry: DateTime::from_naive_utc_and_offset(parsed_time.naive_utc(), Utc),
196            redaction: RedactionRule::Replace {
197                substitution: String::from(":id"),
198            },
199        };
200
201        assert_eq!(rule, result);
202    }
203
204    #[test]
205    fn test_rule_format_defaults() {
206        let json = r#"
207        {
208          "pattern": "/auth/login/*/**",
209          "expiry": "2022-11-30T00:00:00.000000Z",
210          "redaction": {
211            "method": "replace"
212          }
213        }
214        "#;
215
216        let rule: TransactionNameRule = serde_json::from_str(json).unwrap();
217
218        let parsed_time = DateTime::parse_from_rfc3339("2022-11-30T00:00:00Z").unwrap();
219        let result = TransactionNameRule {
220            pattern: LazyGlob::new("/auth/login/*/**".to_string()),
221            expiry: DateTime::from_naive_utc_and_offset(parsed_time.naive_utc(), Utc),
222            redaction: RedactionRule::Replace {
223                substitution: default_substitution(),
224            },
225        };
226
227        assert_eq!(rule, result);
228    }
229
230    #[test]
231    fn test_rule_format_unsupported_reduction() {
232        let json = r#"
233        {
234          "pattern": "/auth/login/*/**",
235          "expiry": "2022-11-30T00:00:00.000000Z",
236          "redaction": {
237            "method": "update"
238          }
239        }
240        "#;
241
242        let rule: TransactionNameRule = serde_json::from_str(json).unwrap();
243        let result = rule.apply("/auth/login/test/");
244
245        assert_eq!(result, "/auth/login/test/".to_string());
246    }
247
248    #[test]
249    fn test_rule_format_roundtrip() {
250        let json = r#"{
251  "pattern": "/auth/login/*/**",
252  "expiry": "2022-11-30T00:00:00Z",
253  "redaction": {
254    "method": "replace",
255    "substitution": ":id"
256  }
257}"#;
258
259        let rule: TransactionNameRule = serde_json::from_str(json).unwrap();
260        let rule_json = serde_json::to_string_pretty(&rule).unwrap();
261        // Make sure that we can  serialize into the same format we receive from the wire.
262        assert_eq!(json, rule_json);
263    }
264}