relay_filter/
transaction_name.rs

1//! Implements event filtering based on whether the endpoint called is a healthcheck endpoint.
2//!
3//! If this filter is enabled transactions from healthcheck endpoints will be filtered out.
4
5use relay_pattern::Patterns;
6
7use crate::{FilterStatKey, Filterable, IgnoreTransactionsFilterConfig};
8
9fn matches(transaction: Option<&str>, patterns: &Patterns) -> bool {
10    transaction.is_some_and(|transaction| patterns.is_match(transaction))
11}
12
13/// Filters [Transaction](relay_event_schema::protocol::EventType::Transaction) events based on a list of provided transaction
14/// name globs.
15pub fn should_filter<F: Filterable>(
16    item: &F,
17    config: &IgnoreTransactionsFilterConfig,
18) -> Result<(), FilterStatKey> {
19    if config.is_empty() {
20        return Ok(());
21    }
22
23    if matches(item.transaction(), &config.patterns) {
24        return Err(FilterStatKey::FilteredTransactions);
25    }
26    Ok(())
27}
28
29#[cfg(test)]
30mod tests {
31    use relay_event_schema::protocol::{Event, EventType};
32    use relay_pattern::TypedPatterns;
33    use relay_protocol::Annotated;
34
35    use super::*;
36
37    fn _get_config() -> IgnoreTransactionsFilterConfig {
38        let patterns_raw = [
39            "*healthcheck*",
40            "*healthy*",
41            "live",
42            "live[z/-]*",
43            "*[/-]live",
44            "*[/-]live[z/-]*",
45            "ready",
46            "ready[z/-]*",
47            "*[/-]ready",
48            "*[/-]ready[z/-]*",
49            "*heartbeat*",
50            "*/health",
51            "*/healthz",
52            "*/ping",
53            "*/up",
54        ]
55        .map(|val| val.to_string())
56        .to_vec();
57
58        IgnoreTransactionsFilterConfig {
59            patterns: TypedPatterns::from(patterns_raw),
60            is_enabled: true,
61        }
62    }
63
64    /// tests matching for various transactions
65    #[test]
66    fn test_matches() {
67        let config = _get_config();
68
69        let transaction_names = [
70            "a/b/healthcheck/c",
71            "a_HEALTHCHECK_b",
72            "healthcheck",
73            "/health",
74            "a/HEALTH",
75            "/health",
76            "a/HEALTH",
77            "live",
78            "livez",
79            "live/123",
80            "live-33",
81            "x-live",
82            "abc/live",
83            "abc/live-23",
84            "ready",
85            "123/ready",
86            "readyz",
87            "ready/123",
88            "abc/ready/1234",
89            "abcheartbeat123",
90            "123/health",
91            "123/healthz",
92            "123/ping",
93            "/up",
94            "123/up",
95        ];
96
97        for name in transaction_names {
98            let event = Event {
99                transaction: Annotated::new(name.into()),
100                ty: Annotated::new(EventType::Transaction),
101                ..Event::default()
102            };
103            assert!(
104                should_filter(&event, &config).is_err(),
105                "Did not match `{name}`"
106            )
107        }
108    }
109    /// tests non matching transactions transactions
110    #[test]
111    fn test_does_not_match() {
112        let transaction_names = [
113            "bad",
114            "/bad",
115            "a/b/c",
116            "health",
117            "healthz",
118            "/health/",
119            "/healthz/",
120            "/healthx",
121            "/healthzx",
122            "service-delivery",
123            "delivery",
124            "notready",
125            "already",
126            "/upload",
127        ];
128        let config = _get_config();
129
130        for name in transaction_names {
131            let event = Event {
132                transaction: Annotated::new(name.into()),
133                ty: Annotated::new(EventType::Transaction),
134                ..Event::default()
135            };
136            assert!(
137                should_filter(&event, &config).is_ok(),
138                "Did match `{name}` but it shouldn't have."
139            )
140        }
141    }
142
143    // test it doesn't match when the transaction name is missing
144    #[test]
145    fn test_does_not_match_missing_transaction() {
146        let event = Event {
147            ty: Annotated::new(EventType::Transaction),
148            ..Event::default()
149        };
150        let config = _get_config();
151        assert!(
152            should_filter(&event, &config).is_ok(),
153            "Did match with empty transaction but it shouldn't have."
154        )
155    }
156
157    #[test]
158    fn test_filters_when_matching() {
159        let event = Event {
160            transaction: Annotated::new("/health".into()),
161            ty: Annotated::new(EventType::Transaction),
162            ..Event::default()
163        };
164        let config = _get_config();
165
166        let filter_result = should_filter(&event, &config);
167        assert_eq!(
168            filter_result,
169            Err(FilterStatKey::FilteredTransactions),
170            "Event was not filtered "
171        )
172    }
173
174    #[test]
175    fn test_does_not_filter_when_disabled() {
176        let event = Event {
177            transaction: Annotated::new("/health".into()),
178            ty: Annotated::new(EventType::Transaction),
179            ..Event::default()
180        };
181        let filter_result = should_filter(
182            &event,
183            &IgnoreTransactionsFilterConfig {
184                patterns: TypedPatterns::default(),
185                is_enabled: true,
186            },
187        );
188        assert_eq!(
189            filter_result,
190            Ok(()),
191            "Event filtered although filter should have been disabled"
192        )
193    }
194    #[test]
195    // Tests that is_enabled flag disables the transaction name filter
196    fn test_does_not_filter_when_disabled_with_flag() {
197        let event = Event {
198            transaction: Annotated::new("/health".into()),
199            ty: Annotated::new(EventType::Transaction),
200            ..Event::default()
201        };
202        let mut config = _get_config();
203        config.is_enabled = false;
204        let filter_result = should_filter(&event, &config);
205        assert_eq!(
206            filter_result,
207            Ok(()),
208            "Event filtered although filter should have been disabled"
209        )
210    }
211
212    #[test]
213    fn test_does_not_filter_when_not_matching() {
214        let event = Event {
215            transaction: Annotated::new("/a/b/c".into()),
216            ty: Annotated::new(EventType::Transaction),
217            ..Event::default()
218        };
219        let filter_result = should_filter(&event, &_get_config());
220        assert_eq!(
221            filter_result,
222            Ok(()),
223            "Event filtered although filter should have not matched"
224        )
225    }
226
227    #[test]
228    fn test_only_filters_transactions_not_anything_else() {
229        let config = _get_config();
230
231        for event_type in [EventType::Transaction, EventType::Error, EventType::Csp] {
232            let expect_to_filter = event_type == EventType::Transaction;
233            let event = Event {
234                transaction: Annotated::new("/health".into()),
235                ty: Annotated::new(event_type),
236                ..Event::default()
237            };
238            let filter_result = should_filter(&event, &config);
239
240            if expect_to_filter {
241                assert_eq!(
242                    filter_result,
243                    Err(FilterStatKey::FilteredTransactions),
244                    "Event was not filtered "
245                );
246            } else {
247                assert_eq!(
248                    filter_result,
249                    Ok(()),
250                    "Event filtered for event_type={} although filter should have not matched",
251                    event_type
252                )
253            }
254        }
255    }
256}