1use std::iter::FusedIterator;
8
9use crate::{FilterStatKey, GenericFilterConfig, GenericFiltersConfig, GenericFiltersMap};
10
11use relay_protocol::{Getter, RuleCondition};
12
13const MAX_SUPPORTED_VERSION: u16 = 1;
17
18pub fn are_generic_filters_supported(
20 global_filters_version: Option<u16>,
21 project_filters_version: u16,
22) -> bool {
23 global_filters_version.is_none_or(|v| v <= MAX_SUPPORTED_VERSION)
24 && project_filters_version <= MAX_SUPPORTED_VERSION
25}
26
27fn matches<F: Getter>(item: &F, condition: Option<&RuleCondition>) -> bool {
29 condition.is_some_and(|condition| condition.matches(item))
32}
33
34pub(crate) fn should_filter<F: Getter>(
40 item: &F,
41 project_filters: &GenericFiltersConfig,
42 global_filters: Option<&GenericFiltersConfig>,
43) -> Result<(), FilterStatKey> {
44 let filters = merge_generic_filters(
45 project_filters,
46 global_filters,
47 #[cfg(test)]
48 MAX_SUPPORTED_VERSION,
49 );
50
51 for filter_config in filters {
52 if filter_config.is_enabled && matches(item, filter_config.condition) {
53 return Err(FilterStatKey::GenericFilter(filter_config.id.to_owned()));
54 }
55 }
56
57 Ok(())
58}
59
60fn merge_generic_filters<'a>(
77 project: &'a GenericFiltersConfig,
78 global: Option<&'a GenericFiltersConfig>,
79 #[cfg(test)] max_supported_version: u16,
80) -> impl Iterator<Item = GenericFilterConfigRef<'a>> {
81 #[cfg(not(test))]
82 let max_supported_version = MAX_SUPPORTED_VERSION;
83
84 let is_supported = project.version <= max_supported_version
85 && global.is_none_or(|gf| gf.version <= max_supported_version);
86
87 is_supported
88 .then(|| {
89 DynamicGenericFiltersConfigIter::new(&project.filters, global.map(|gc| &gc.filters))
90 })
91 .into_iter()
92 .flatten()
93}
94
95struct DynamicGenericFiltersConfigIter<'a> {
97 project: &'a GenericFiltersMap,
99 project_index: usize,
101 global: Option<&'a GenericFiltersMap>,
103 global_index: usize,
105}
106
107impl<'a> DynamicGenericFiltersConfigIter<'a> {
108 pub fn new(project: &'a GenericFiltersMap, global: Option<&'a GenericFiltersMap>) -> Self {
109 DynamicGenericFiltersConfigIter {
110 project,
111 project_index: 0,
112 global,
113 global_index: 0,
114 }
115 }
116}
117
118impl<'a> Iterator for DynamicGenericFiltersConfigIter<'a> {
119 type Item = GenericFilterConfigRef<'a>;
120
121 fn next(&mut self) -> Option<Self::Item> {
122 if let Some((id, filter)) = self.project.get_index(self.project_index) {
123 self.project_index += 1;
124 let merged = merge_filters(filter, self.global.and_then(|gf| gf.get(id)));
125 return Some(merged);
126 }
127
128 loop {
129 let (id, filter) = self.global?.get_index(self.global_index)?;
130 self.global_index += 1;
131 if !self.project.contains_key(id) {
132 return Some(filter.into());
133 }
134 }
135 }
136}
137
138impl FusedIterator for DynamicGenericFiltersConfigIter<'_> {}
139
140fn merge_filters<'a>(
145 primary: &'a GenericFilterConfig,
146 secondary: Option<&'a GenericFilterConfig>,
147) -> GenericFilterConfigRef<'a> {
148 GenericFilterConfigRef {
149 id: primary.id.as_str(),
150 is_enabled: primary.is_enabled,
151 condition: primary
152 .condition
153 .as_ref()
154 .or(secondary.and_then(|filter| filter.condition.as_ref())),
155 }
156}
157
158#[derive(Debug, Default, PartialEq)]
159struct GenericFilterConfigRef<'a> {
160 id: &'a str,
161 is_enabled: bool,
162 condition: Option<&'a RuleCondition>,
163}
164
165impl<'a> From<&'a GenericFilterConfig> for GenericFilterConfigRef<'a> {
166 fn from(value: &'a GenericFilterConfig) -> Self {
167 GenericFilterConfigRef {
168 id: value.id.as_str(),
169 is_enabled: value.is_enabled,
170 condition: value.condition.as_ref(),
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177
178 use super::*;
179
180 use relay_event_schema::protocol::{Event, LenientString};
181 use relay_protocol::{Annotated, FromValue as _};
182
183 fn mock_filters() -> GenericFiltersMap {
184 vec![
185 GenericFilterConfig {
186 id: "firstReleases".to_owned(),
187 is_enabled: true,
188 condition: Some(RuleCondition::eq("event.release", "1.0")),
189 },
190 GenericFilterConfig {
191 id: "helloTransactions".to_owned(),
192 is_enabled: true,
193 condition: Some(RuleCondition::eq("event.transaction", "/hello")),
194 },
195 ]
196 .into()
197 }
198
199 #[test]
200 fn test_should_filter_match_rules() {
201 let config = GenericFiltersConfig {
202 version: 1,
203 filters: mock_filters(),
204 };
205
206 let event = Event {
208 release: Annotated::new(LenientString("1.0".to_owned())),
209 ..Default::default()
210 };
211 assert_eq!(
212 should_filter(&event, &config, None),
213 Err(FilterStatKey::GenericFilter("firstReleases".to_owned()))
214 );
215
216 let event = Event {
218 transaction: Annotated::new("/hello".to_owned()),
219 ..Default::default()
220 };
221 assert_eq!(
222 should_filter(&event, &config, None),
223 Err(FilterStatKey::GenericFilter("helloTransactions".to_owned()))
224 );
225 }
226
227 #[test]
228 fn test_should_filter_fifo_match_rules() {
229 let config = GenericFiltersConfig {
230 version: 1,
231 filters: mock_filters(),
232 };
233
234 let event = Event {
236 release: Annotated::new(LenientString("1.0".to_owned())),
237 transaction: Annotated::new("/hello".to_owned()),
238 ..Default::default()
239 };
240 assert_eq!(
241 should_filter(&event, &config, None),
242 Err(FilterStatKey::GenericFilter("firstReleases".to_owned()))
243 );
244 }
245
246 #[test]
247 fn test_should_filter_match_no_rules() {
248 let config = GenericFiltersConfig {
249 version: 1,
250 filters: mock_filters(),
251 };
252
253 let event = Event {
255 transaction: Annotated::new("/world".to_owned()),
256 ..Default::default()
257 };
258 assert_eq!(should_filter(&event, &config, None), Ok(()));
259 }
260
261 #[test]
262 fn test_should_filter_with_higher_config_version() {
263 let config = GenericFiltersConfig {
264 version: MAX_SUPPORTED_VERSION + 1,
266 filters: mock_filters(),
267 };
268
269 let event = Event {
270 release: Annotated::new(LenientString("1.0".to_owned())),
271 transaction: Annotated::new("/hello".to_owned()),
272 ..Default::default()
273 };
274 assert_eq!(should_filter(&event, &config, None), Ok(()));
275 }
276
277 #[test]
278 fn test_should_filter_from_global_filters() {
279 let project = GenericFiltersConfig {
280 version: 1,
281 filters: vec![GenericFilterConfig {
282 id: "firstReleases".to_owned(),
283 is_enabled: true,
284 condition: Some(RuleCondition::eq("event.release", "1.0")),
285 }]
286 .into(),
287 };
288
289 let global = GenericFiltersConfig {
290 version: 1,
291 filters: vec![GenericFilterConfig {
292 id: "helloTransactions".to_owned(),
293 is_enabled: true,
294 condition: Some(RuleCondition::eq("event.transaction", "/hello")),
295 }]
296 .into(),
297 };
298
299 let event = Event {
300 transaction: Annotated::new("/hello".to_owned()),
301 ..Default::default()
302 };
303
304 assert_eq!(
305 should_filter(&event, &project, Some(&global)),
306 Err(FilterStatKey::GenericFilter("helloTransactions".to_owned()))
307 );
308 }
309
310 fn empty_filter() -> GenericFiltersConfig {
311 GenericFiltersConfig {
312 version: 1,
313 filters: GenericFiltersMap::new(),
314 }
315 }
316
317 fn enabled_filter(id: &str) -> GenericFiltersConfig {
319 GenericFiltersConfig {
320 version: 1,
321 filters: vec![GenericFilterConfig {
322 id: id.to_owned(),
323 is_enabled: true,
324 condition: Some(RuleCondition::eq("event.exceptions", "myError")),
325 }]
326 .into(),
327 }
328 }
329
330 fn enabled_flag_filter(id: &str) -> GenericFiltersConfig {
332 GenericFiltersConfig {
333 version: 1,
334 filters: vec![GenericFilterConfig {
335 id: id.to_owned(),
336 is_enabled: true,
337 condition: None,
338 }]
339 .into(),
340 }
341 }
342
343 fn disabled_filter(id: &str) -> GenericFiltersConfig {
345 GenericFiltersConfig {
346 version: 1,
347 filters: vec![GenericFilterConfig {
348 id: id.to_owned(),
349 is_enabled: false,
350 condition: Some(RuleCondition::eq("event.exceptions", "myError")),
351 }]
352 .into(),
353 }
354 }
355
356 fn disabled_flag_filter(id: &str) -> GenericFiltersConfig {
358 GenericFiltersConfig {
359 version: 1,
360 filters: vec![GenericFilterConfig {
361 id: id.to_owned(),
362 is_enabled: false,
363 condition: None,
364 }]
365 .into(),
366 }
367 }
368
369 #[test]
370 fn test_no_combined_when_unsupported_project_version() {
371 let mut project = enabled_filter("unsupported-project");
372 project.version = 2;
373 let global = enabled_filter("supported-global");
374 assert!(merge_generic_filters(&project, Some(&global), 1).eq(None.into_iter()));
375 }
376
377 #[test]
378 fn test_no_combined_when_unsupported_project_version_no_global() {
379 let mut project = enabled_filter("unsupported-project");
380 project.version = 2;
381 assert!(merge_generic_filters(&project, None, 1).eq(None.into_iter()));
382 }
383
384 #[test]
385 fn test_no_combined_when_unsupported_global_version() {
386 let project = enabled_filter("supported-project");
387 let mut global = enabled_filter("unsupported-global");
388 global.version = 2;
389 assert!(merge_generic_filters(&project, Some(&global), 1).eq(None.into_iter()));
390 }
391
392 #[test]
393 fn test_no_combined_when_unsupported_project_and_global_version() {
394 let mut project = enabled_filter("unsupported-project");
395 project.version = 2;
396 let mut global = enabled_filter("unsupported-global");
397 global.version = 2;
398 assert!(merge_generic_filters(&project, Some(&global), 1).eq(None.into_iter()));
399 }
400
401 #[test]
402 fn test_both_combined_when_supported_project_and_global_version() {
403 let project = enabled_filter("supported-project");
404 let global = enabled_filter("supported-global");
405 assert!(
406 merge_generic_filters(&project, Some(&global), 1).eq([
407 project.filters.first().unwrap().1.into(),
408 global.filters.first().unwrap().1.into()
409 ]
410 .into_iter())
411 );
412 }
413
414 #[test]
415 fn test_project_combined_when_duplicated_filter_project_and_global() {
416 let project = enabled_filter("filter");
417 let global = enabled_filter("filter");
418 assert!(
419 merge_generic_filters(&project, Some(&global), 1).eq([project
420 .filters
421 .first()
422 .unwrap()
423 .1
424 .into()]
425 .into_iter())
426 );
427 }
428
429 #[test]
430 fn test_no_combined_when_empty_project_and_global() {
431 let project = empty_filter();
432 let global = empty_filter();
433 assert!(merge_generic_filters(&project, Some(&global), 1).eq(None.into_iter()));
434 }
435
436 #[test]
437 fn test_global_combined_when_empty_project_disabled_global_filter() {
438 let project = empty_filter();
439 let global = disabled_filter("disabled-global");
440 assert!(
441 merge_generic_filters(&project, Some(&global), 1).eq([global
442 .filters
443 .first()
444 .unwrap()
445 .1
446 .into()]
447 .into_iter())
448 );
449 }
450
451 #[test]
452 fn test_global_combined_when_empty_project_enabled_global_filters() {
453 let project = empty_filter();
454 let global = enabled_filter("enabled-global");
455 assert!(
456 merge_generic_filters(&project, Some(&global), 1).eq([global
457 .filters
458 .first()
459 .unwrap()
460 .1
461 .into()]
462 .into_iter())
463 );
464 }
465
466 #[test]
467 fn test_global_combined_when_empty_project_enabled_flag_global() {
468 let project = empty_filter();
469 let global = enabled_flag_filter("skip");
470 assert!(
471 merge_generic_filters(&project, Some(&global), 1).eq([global
472 .filters
473 .first()
474 .unwrap()
475 .1
476 .into()])
477 );
478 }
479
480 #[test]
481 fn test_project_combined_when_disabled_project_empty_global() {
482 let project = disabled_filter("disabled-project");
483 let global = empty_filter();
484 assert!(
485 merge_generic_filters(&project, Some(&global), 1).eq([project
486 .filters
487 .first()
488 .unwrap()
489 .1
490 .into()]
491 .into_iter())
492 );
493 }
494
495 #[test]
496 fn test_project_combined_when_disabled_project_missing_global() {
497 let project = disabled_filter("disabled-project");
498 assert!(
499 merge_generic_filters(&project, None, 1).eq([project
500 .filters
501 .first()
502 .unwrap()
503 .1
504 .into(),]
505 .into_iter())
506 );
507 }
508
509 #[test]
510 fn test_both_combined_when_different_disabled_project_and_global() {
511 let project = disabled_filter("disabled-project");
512 let global = disabled_filter("disabled-global");
513 assert!(merge_generic_filters(&project, Some(&global), 1).eq([
514 project.filters.first().unwrap().1.into(),
515 global.filters.first().unwrap().1.into()
516 ]));
517 }
518
519 #[test]
520 fn test_project_combined_duplicated_disabled_project_and_global() {
521 let project = disabled_filter("filter");
522 let global = disabled_filter("filter");
523 assert!(
524 merge_generic_filters(&project, Some(&global), 1).eq([project
525 .filters
526 .first()
527 .unwrap()
528 .1
529 .into()])
530 );
531 }
532
533 #[test]
534 fn test_merged_combined_when_disabled_project_enabled_global() {
535 let project = disabled_filter("filter");
536 let global = enabled_filter("filter");
537 let expected = &GenericFilterConfig {
538 id: "filter".to_owned(),
539 is_enabled: false,
540 condition: global.filters.first().unwrap().1.condition.clone(),
541 };
542 assert!(
543 merge_generic_filters(&project, Some(&global), 1).eq([expected.into()].into_iter())
544 );
545 }
546
547 #[test]
548 fn test_no_combined_when_enabled_flag_project_empty_global() {
549 let project = enabled_flag_filter("filter");
550 let global = empty_filter();
551 assert!(
552 merge_generic_filters(&project, Some(&global), 1).eq([project
553 .filters
554 .first()
555 .unwrap()
556 .1
557 .into()]
558 .into_iter())
559 );
560 }
561
562 #[test]
563 fn test_project_combined_when_enabled_flag_project_missing_global() {
564 let project = enabled_flag_filter("filter");
565 assert!(
566 merge_generic_filters(&project, None, 1).eq([project
567 .filters
568 .first()
569 .unwrap()
570 .1
571 .into()]
572 .into_iter())
573 );
574 }
575
576 #[test]
577 fn test_project_combined_when_disabled_flag_project_empty_global() {
578 let project = disabled_flag_filter("filter");
579 let global = empty_filter();
580 assert!(
581 merge_generic_filters(&project, Some(&global), 1).eq([project
582 .filters
583 .first()
584 .unwrap()
585 .1
586 .into()])
587 );
588 }
589
590 #[test]
591 fn test_project_combined_when_disabled_flag_project_missing_global() {
592 let project = disabled_flag_filter("filter");
593 assert!(
594 merge_generic_filters(&project, None, 1).eq([project
595 .filters
596 .first()
597 .unwrap()
598 .1
599 .into()])
600 );
601 }
602
603 #[test]
604 fn test_project_combined_when_enabled_project_empty_global() {
605 let project = enabled_filter("enabled-project");
606 let global = empty_filter();
607 assert!(
608 merge_generic_filters(&project, Some(&global), 1).eq([project
609 .filters
610 .first()
611 .unwrap()
612 .1
613 .into()]
614 .into_iter())
615 );
616 }
617
618 #[test]
619 fn test_project_combined_when_enabled_project_missing_global() {
620 let project = enabled_filter("enabled-project");
621 assert!(
622 merge_generic_filters(&project, None, 1).eq([project
623 .filters
624 .first()
625 .unwrap()
626 .1
627 .into()]
628 .into_iter())
629 );
630 }
631
632 #[test]
633 fn test_merged_combined_when_enabled_flag_project_disabled_global() {
634 let project = enabled_flag_filter("filter");
635 let global = disabled_filter("filter");
636 let expected = &GenericFilterConfig {
637 id: "filter".to_owned(),
638 is_enabled: true,
639 condition: global.filters.first().unwrap().1.condition.clone(),
640 };
641 assert!(
642 merge_generic_filters(&project, Some(&global), 1).eq([expected.into()].into_iter())
643 );
644 }
645
646 #[test]
647 fn test_global_combined_when_disabled_flag_project_disabled_global() {
648 let project = disabled_flag_filter("filter");
649 let global = disabled_filter("filter");
650 assert!(
651 merge_generic_filters(&project, Some(&global), 1).eq([global
652 .filters
653 .first()
654 .unwrap()
655 .1
656 .into()])
657 );
658 }
659
660 #[test]
661 fn test_project_combined_when_enabled_project_disabled_global() {
662 let project = enabled_filter("filter");
663 let global = disabled_filter("filter");
664 assert!(
665 merge_generic_filters(&project, Some(&global), 1).eq([project
666 .filters
667 .first()
668 .unwrap()
669 .1
670 .into()]
671 .into_iter())
672 );
673 }
674
675 #[test]
676 fn test_global_combined_when_enabled_flag_project_enabled_global() {
677 let project = enabled_flag_filter("filter");
678 let global = enabled_filter("filter");
679 assert!(
680 merge_generic_filters(&project, Some(&global), 1).eq([global
681 .filters
682 .first()
683 .unwrap()
684 .1
685 .into()]
686 .into_iter())
687 );
688 }
689
690 #[test]
691 fn test_merged_combined_when_disabled_flag_project_enabled_global() {
692 let project = disabled_flag_filter("filter");
693 let global = enabled_filter("filter");
694 let expected = &GenericFilterConfig {
695 id: "filter".to_owned(),
696 is_enabled: false,
697 condition: global.filters.first().unwrap().1.condition.clone(),
698 };
699 assert!(
700 merge_generic_filters(&project, Some(&global), 1).eq([expected.into()].into_iter())
701 );
702 }
703
704 #[test]
705 fn test_project_combined_when_enabled_project_enabled_flag_global() {
706 let project = enabled_filter("filter");
707 let global = enabled_flag_filter("filter");
708 assert!(
709 merge_generic_filters(&project, Some(&global), 1).eq([project
710 .filters
711 .first()
712 .unwrap()
713 .1
714 .into()]
715 .into_iter())
716 );
717 }
718
719 #[test]
720 fn test_project_combined_when_enabled_flags_project_and_global() {
721 let project = enabled_flag_filter("filter");
722 let global = enabled_flag_filter("filter");
723 assert!(
724 merge_generic_filters(&project, Some(&global), 1).eq([project
725 .filters
726 .first()
727 .unwrap()
728 .1
729 .into()]
730 .into_iter())
731 );
732 }
733
734 #[test]
735 fn test_multiple_combined_filters() {
736 let project = GenericFiltersConfig {
737 version: 1,
738 filters: vec![
739 GenericFilterConfig {
740 id: "0".to_owned(),
741 is_enabled: true,
742 condition: Some(RuleCondition::eq("event.exceptions", "myError")),
743 },
744 GenericFilterConfig {
745 id: "1".to_owned(),
746 is_enabled: true,
747 condition: None,
748 },
749 GenericFilterConfig {
750 id: "2".to_owned(),
751 is_enabled: true,
752 condition: Some(RuleCondition::eq("event.exceptions", "myError")),
753 },
754 ]
755 .into(),
756 };
757 let global = GenericFiltersConfig {
758 version: 1,
759 filters: vec![
760 GenericFilterConfig {
761 id: "1".to_owned(),
762 is_enabled: false,
763 condition: Some(RuleCondition::eq("event.exceptions", "myOtherError")),
764 },
765 GenericFilterConfig {
766 id: "3".to_owned(),
767 is_enabled: false,
768 condition: Some(RuleCondition::eq("event.exceptions", "myLastError")),
769 },
770 ]
771 .into(),
772 };
773
774 let expected0 = &project.filters[0];
775 let expected1 = &GenericFilterConfig {
776 id: "1".to_owned(),
777 is_enabled: true,
778 condition: Some(RuleCondition::eq("event.exceptions", "myOtherError")),
779 };
780 let expected2 = &project.filters[2];
781 let expected3 = &global.filters[1];
782
783 assert!(
784 merge_generic_filters(&project, Some(&global), 1).eq([
785 expected0.into(),
786 expected1.into(),
787 expected2.into(),
788 expected3.into()
789 ]
790 .into_iter())
791 );
792 }
793
794 #[test]
795 fn test_os_name_not_filter() {
796 let config = GenericFiltersConfig {
797 version: 1,
798 filters: vec![GenericFilterConfig {
799 id: "os_name".to_owned(),
800 is_enabled: true,
801 condition: Some(RuleCondition::eq("event.contexts.os.name", "fooBar").negate()),
802 }]
803 .into(),
804 };
805
806 let cases = [("fooBar", false), ("foobar", true), ("other", true)];
807 for (name, filters) in cases {
808 let event = Event::from_value(
809 serde_json::json!({
810 "contexts": {
811 "os": {
812 "name": name,
813 },
814 },
815 })
816 .into(),
817 );
818
819 let expected = if filters {
820 Err(FilterStatKey::GenericFilter("os_name".to_owned()))
821 } else {
822 Ok(())
823 };
824
825 assert_eq!(
826 should_filter(event.value().unwrap(), &config, None),
827 expected
828 );
829 }
830 }
831}