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_string(),
187 is_enabled: true,
188 condition: Some(RuleCondition::eq("event.release", "1.0")),
189 },
190 GenericFilterConfig {
191 id: "helloTransactions".to_string(),
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_string())),
209 ..Default::default()
210 };
211 assert_eq!(
212 should_filter(&event, &config, None),
213 Err(FilterStatKey::GenericFilter("firstReleases".to_string()))
214 );
215
216 let event = Event {
218 transaction: Annotated::new("/hello".to_string()),
219 ..Default::default()
220 };
221 assert_eq!(
222 should_filter(&event, &config, None),
223 Err(FilterStatKey::GenericFilter(
224 "helloTransactions".to_string()
225 ))
226 );
227 }
228
229 #[test]
230 fn test_should_filter_fifo_match_rules() {
231 let config = GenericFiltersConfig {
232 version: 1,
233 filters: mock_filters(),
234 };
235
236 let event = Event {
238 release: Annotated::new(LenientString("1.0".to_string())),
239 transaction: Annotated::new("/hello".to_string()),
240 ..Default::default()
241 };
242 assert_eq!(
243 should_filter(&event, &config, None),
244 Err(FilterStatKey::GenericFilter("firstReleases".to_string()))
245 );
246 }
247
248 #[test]
249 fn test_should_filter_match_no_rules() {
250 let config = GenericFiltersConfig {
251 version: 1,
252 filters: mock_filters(),
253 };
254
255 let event = Event {
257 transaction: Annotated::new("/world".to_string()),
258 ..Default::default()
259 };
260 assert_eq!(should_filter(&event, &config, None), Ok(()));
261 }
262
263 #[test]
264 fn test_should_filter_with_higher_config_version() {
265 let config = GenericFiltersConfig {
266 version: MAX_SUPPORTED_VERSION + 1,
268 filters: mock_filters(),
269 };
270
271 let event = Event {
272 release: Annotated::new(LenientString("1.0".to_string())),
273 transaction: Annotated::new("/hello".to_string()),
274 ..Default::default()
275 };
276 assert_eq!(should_filter(&event, &config, None), Ok(()));
277 }
278
279 #[test]
280 fn test_should_filter_from_global_filters() {
281 let project = GenericFiltersConfig {
282 version: 1,
283 filters: vec![GenericFilterConfig {
284 id: "firstReleases".to_string(),
285 is_enabled: true,
286 condition: Some(RuleCondition::eq("event.release", "1.0")),
287 }]
288 .into(),
289 };
290
291 let global = GenericFiltersConfig {
292 version: 1,
293 filters: vec![GenericFilterConfig {
294 id: "helloTransactions".to_string(),
295 is_enabled: true,
296 condition: Some(RuleCondition::eq("event.transaction", "/hello")),
297 }]
298 .into(),
299 };
300
301 let event = Event {
302 transaction: Annotated::new("/hello".to_string()),
303 ..Default::default()
304 };
305
306 assert_eq!(
307 should_filter(&event, &project, Some(&global)),
308 Err(FilterStatKey::GenericFilter(
309 "helloTransactions".to_string()
310 ))
311 );
312 }
313
314 fn empty_filter() -> GenericFiltersConfig {
315 GenericFiltersConfig {
316 version: 1,
317 filters: GenericFiltersMap::new(),
318 }
319 }
320
321 fn enabled_filter(id: &str) -> GenericFiltersConfig {
323 GenericFiltersConfig {
324 version: 1,
325 filters: vec![GenericFilterConfig {
326 id: id.to_owned(),
327 is_enabled: true,
328 condition: Some(RuleCondition::eq("event.exceptions", "myError")),
329 }]
330 .into(),
331 }
332 }
333
334 fn enabled_flag_filter(id: &str) -> GenericFiltersConfig {
336 GenericFiltersConfig {
337 version: 1,
338 filters: vec![GenericFilterConfig {
339 id: id.to_owned(),
340 is_enabled: true,
341 condition: None,
342 }]
343 .into(),
344 }
345 }
346
347 fn disabled_filter(id: &str) -> GenericFiltersConfig {
349 GenericFiltersConfig {
350 version: 1,
351 filters: vec![GenericFilterConfig {
352 id: id.to_owned(),
353 is_enabled: false,
354 condition: Some(RuleCondition::eq("event.exceptions", "myError")),
355 }]
356 .into(),
357 }
358 }
359
360 fn disabled_flag_filter(id: &str) -> GenericFiltersConfig {
362 GenericFiltersConfig {
363 version: 1,
364 filters: vec![GenericFilterConfig {
365 id: id.to_owned(),
366 is_enabled: false,
367 condition: None,
368 }]
369 .into(),
370 }
371 }
372
373 #[test]
374 fn test_no_combined_when_unsupported_project_version() {
375 let mut project = enabled_filter("unsupported-project");
376 project.version = 2;
377 let global = enabled_filter("supported-global");
378 assert!(merge_generic_filters(&project, Some(&global), 1).eq(None.into_iter()));
379 }
380
381 #[test]
382 fn test_no_combined_when_unsupported_project_version_no_global() {
383 let mut project = enabled_filter("unsupported-project");
384 project.version = 2;
385 assert!(merge_generic_filters(&project, None, 1).eq(None.into_iter()));
386 }
387
388 #[test]
389 fn test_no_combined_when_unsupported_global_version() {
390 let project = enabled_filter("supported-project");
391 let mut global = enabled_filter("unsupported-global");
392 global.version = 2;
393 assert!(merge_generic_filters(&project, Some(&global), 1).eq(None.into_iter()));
394 }
395
396 #[test]
397 fn test_no_combined_when_unsupported_project_and_global_version() {
398 let mut project = enabled_filter("unsupported-project");
399 project.version = 2;
400 let mut global = enabled_filter("unsupported-global");
401 global.version = 2;
402 assert!(merge_generic_filters(&project, Some(&global), 1).eq(None.into_iter()));
403 }
404
405 #[test]
406 fn test_both_combined_when_supported_project_and_global_version() {
407 let project = enabled_filter("supported-project");
408 let global = enabled_filter("supported-global");
409 assert!(
410 merge_generic_filters(&project, Some(&global), 1).eq([
411 project.filters.first().unwrap().1.into(),
412 global.filters.first().unwrap().1.into()
413 ]
414 .into_iter())
415 );
416 }
417
418 #[test]
419 fn test_project_combined_when_duplicated_filter_project_and_global() {
420 let project = enabled_filter("filter");
421 let global = enabled_filter("filter");
422 assert!(
423 merge_generic_filters(&project, Some(&global), 1).eq([project
424 .filters
425 .first()
426 .unwrap()
427 .1
428 .into()]
429 .into_iter())
430 );
431 }
432
433 #[test]
434 fn test_no_combined_when_empty_project_and_global() {
435 let project = empty_filter();
436 let global = empty_filter();
437 assert!(merge_generic_filters(&project, Some(&global), 1).eq(None.into_iter()));
438 }
439
440 #[test]
441 fn test_global_combined_when_empty_project_disabled_global_filter() {
442 let project = empty_filter();
443 let global = disabled_filter("disabled-global");
444 assert!(
445 merge_generic_filters(&project, Some(&global), 1).eq([global
446 .filters
447 .first()
448 .unwrap()
449 .1
450 .into()]
451 .into_iter())
452 );
453 }
454
455 #[test]
456 fn test_global_combined_when_empty_project_enabled_global_filters() {
457 let project = empty_filter();
458 let global = enabled_filter("enabled-global");
459 assert!(
460 merge_generic_filters(&project, Some(&global), 1).eq([global
461 .filters
462 .first()
463 .unwrap()
464 .1
465 .into()]
466 .into_iter())
467 );
468 }
469
470 #[test]
471 fn test_global_combined_when_empty_project_enabled_flag_global() {
472 let project = empty_filter();
473 let global = enabled_flag_filter("skip");
474 assert!(
475 merge_generic_filters(&project, Some(&global), 1).eq([global
476 .filters
477 .first()
478 .unwrap()
479 .1
480 .into()])
481 );
482 }
483
484 #[test]
485 fn test_project_combined_when_disabled_project_empty_global() {
486 let project = disabled_filter("disabled-project");
487 let global = empty_filter();
488 assert!(
489 merge_generic_filters(&project, Some(&global), 1).eq([project
490 .filters
491 .first()
492 .unwrap()
493 .1
494 .into()]
495 .into_iter())
496 );
497 }
498
499 #[test]
500 fn test_project_combined_when_disabled_project_missing_global() {
501 let project = disabled_filter("disabled-project");
502 assert!(
503 merge_generic_filters(&project, None, 1).eq([project
504 .filters
505 .first()
506 .unwrap()
507 .1
508 .into(),]
509 .into_iter())
510 );
511 }
512
513 #[test]
514 fn test_both_combined_when_different_disabled_project_and_global() {
515 let project = disabled_filter("disabled-project");
516 let global = disabled_filter("disabled-global");
517 assert!(merge_generic_filters(&project, Some(&global), 1).eq([
518 project.filters.first().unwrap().1.into(),
519 global.filters.first().unwrap().1.into()
520 ]));
521 }
522
523 #[test]
524 fn test_project_combined_duplicated_disabled_project_and_global() {
525 let project = disabled_filter("filter");
526 let global = disabled_filter("filter");
527 assert!(
528 merge_generic_filters(&project, Some(&global), 1).eq([project
529 .filters
530 .first()
531 .unwrap()
532 .1
533 .into()])
534 );
535 }
536
537 #[test]
538 fn test_merged_combined_when_disabled_project_enabled_global() {
539 let project = disabled_filter("filter");
540 let global = enabled_filter("filter");
541 let expected = &GenericFilterConfig {
542 id: "filter".to_owned(),
543 is_enabled: false,
544 condition: global.filters.first().unwrap().1.condition.clone(),
545 };
546 assert!(
547 merge_generic_filters(&project, Some(&global), 1).eq([expected.into()].into_iter())
548 );
549 }
550
551 #[test]
552 fn test_no_combined_when_enabled_flag_project_empty_global() {
553 let project = enabled_flag_filter("filter");
554 let global = empty_filter();
555 assert!(
556 merge_generic_filters(&project, Some(&global), 1).eq([project
557 .filters
558 .first()
559 .unwrap()
560 .1
561 .into()]
562 .into_iter())
563 );
564 }
565
566 #[test]
567 fn test_project_combined_when_enabled_flag_project_missing_global() {
568 let project = enabled_flag_filter("filter");
569 assert!(
570 merge_generic_filters(&project, None, 1).eq([project
571 .filters
572 .first()
573 .unwrap()
574 .1
575 .into()]
576 .into_iter())
577 );
578 }
579
580 #[test]
581 fn test_project_combined_when_disabled_flag_project_empty_global() {
582 let project = disabled_flag_filter("filter");
583 let global = empty_filter();
584 assert!(
585 merge_generic_filters(&project, Some(&global), 1).eq([project
586 .filters
587 .first()
588 .unwrap()
589 .1
590 .into()])
591 );
592 }
593
594 #[test]
595 fn test_project_combined_when_disabled_flag_project_missing_global() {
596 let project = disabled_flag_filter("filter");
597 assert!(
598 merge_generic_filters(&project, None, 1).eq([project
599 .filters
600 .first()
601 .unwrap()
602 .1
603 .into()])
604 );
605 }
606
607 #[test]
608 fn test_project_combined_when_enabled_project_empty_global() {
609 let project = enabled_filter("enabled-project");
610 let global = empty_filter();
611 assert!(
612 merge_generic_filters(&project, Some(&global), 1).eq([project
613 .filters
614 .first()
615 .unwrap()
616 .1
617 .into()]
618 .into_iter())
619 );
620 }
621
622 #[test]
623 fn test_project_combined_when_enabled_project_missing_global() {
624 let project = enabled_filter("enabled-project");
625 assert!(
626 merge_generic_filters(&project, None, 1).eq([project
627 .filters
628 .first()
629 .unwrap()
630 .1
631 .into()]
632 .into_iter())
633 );
634 }
635
636 #[test]
637 fn test_merged_combined_when_enabled_flag_project_disabled_global() {
638 let project = enabled_flag_filter("filter");
639 let global = disabled_filter("filter");
640 let expected = &GenericFilterConfig {
641 id: "filter".to_owned(),
642 is_enabled: true,
643 condition: global.filters.first().unwrap().1.condition.clone(),
644 };
645 assert!(
646 merge_generic_filters(&project, Some(&global), 1).eq([expected.into()].into_iter())
647 );
648 }
649
650 #[test]
651 fn test_global_combined_when_disabled_flag_project_disabled_global() {
652 let project = disabled_flag_filter("filter");
653 let global = disabled_filter("filter");
654 assert!(
655 merge_generic_filters(&project, Some(&global), 1).eq([global
656 .filters
657 .first()
658 .unwrap()
659 .1
660 .into()])
661 );
662 }
663
664 #[test]
665 fn test_project_combined_when_enabled_project_disabled_global() {
666 let project = enabled_filter("filter");
667 let global = disabled_filter("filter");
668 assert!(
669 merge_generic_filters(&project, Some(&global), 1).eq([project
670 .filters
671 .first()
672 .unwrap()
673 .1
674 .into()]
675 .into_iter())
676 );
677 }
678
679 #[test]
680 fn test_global_combined_when_enabled_flag_project_enabled_global() {
681 let project = enabled_flag_filter("filter");
682 let global = enabled_filter("filter");
683 assert!(
684 merge_generic_filters(&project, Some(&global), 1).eq([global
685 .filters
686 .first()
687 .unwrap()
688 .1
689 .into()]
690 .into_iter())
691 );
692 }
693
694 #[test]
695 fn test_merged_combined_when_disabled_flag_project_enabled_global() {
696 let project = disabled_flag_filter("filter");
697 let global = enabled_filter("filter");
698 let expected = &GenericFilterConfig {
699 id: "filter".to_owned(),
700 is_enabled: false,
701 condition: global.filters.first().unwrap().1.condition.clone(),
702 };
703 assert!(
704 merge_generic_filters(&project, Some(&global), 1).eq([expected.into()].into_iter())
705 );
706 }
707
708 #[test]
709 fn test_project_combined_when_enabled_project_enabled_flag_global() {
710 let project = enabled_filter("filter");
711 let global = enabled_flag_filter("filter");
712 assert!(
713 merge_generic_filters(&project, Some(&global), 1).eq([project
714 .filters
715 .first()
716 .unwrap()
717 .1
718 .into()]
719 .into_iter())
720 );
721 }
722
723 #[test]
724 fn test_project_combined_when_enabled_flags_project_and_global() {
725 let project = enabled_flag_filter("filter");
726 let global = enabled_flag_filter("filter");
727 assert!(
728 merge_generic_filters(&project, Some(&global), 1).eq([project
729 .filters
730 .first()
731 .unwrap()
732 .1
733 .into()]
734 .into_iter())
735 );
736 }
737
738 #[test]
739 fn test_multiple_combined_filters() {
740 let project = GenericFiltersConfig {
741 version: 1,
742 filters: vec![
743 GenericFilterConfig {
744 id: "0".to_owned(),
745 is_enabled: true,
746 condition: Some(RuleCondition::eq("event.exceptions", "myError")),
747 },
748 GenericFilterConfig {
749 id: "1".to_owned(),
750 is_enabled: true,
751 condition: None,
752 },
753 GenericFilterConfig {
754 id: "2".to_owned(),
755 is_enabled: true,
756 condition: Some(RuleCondition::eq("event.exceptions", "myError")),
757 },
758 ]
759 .into(),
760 };
761 let global = GenericFiltersConfig {
762 version: 1,
763 filters: vec![
764 GenericFilterConfig {
765 id: "1".to_owned(),
766 is_enabled: false,
767 condition: Some(RuleCondition::eq("event.exceptions", "myOtherError")),
768 },
769 GenericFilterConfig {
770 id: "3".to_owned(),
771 is_enabled: false,
772 condition: Some(RuleCondition::eq("event.exceptions", "myLastError")),
773 },
774 ]
775 .into(),
776 };
777
778 let expected0 = &project.filters[0];
779 let expected1 = &GenericFilterConfig {
780 id: "1".to_owned(),
781 is_enabled: true,
782 condition: Some(RuleCondition::eq("event.exceptions", "myOtherError")),
783 };
784 let expected2 = &project.filters[2];
785 let expected3 = &global.filters[1];
786
787 assert!(
788 merge_generic_filters(&project, Some(&global), 1).eq([
789 expected0.into(),
790 expected1.into(),
791 expected2.into(),
792 expected3.into()
793 ]
794 .into_iter())
795 );
796 }
797
798 #[test]
799 fn test_os_name_not_filter() {
800 let config = GenericFiltersConfig {
801 version: 1,
802 filters: vec![GenericFilterConfig {
803 id: "os_name".to_owned(),
804 is_enabled: true,
805 condition: Some(RuleCondition::eq("event.contexts.os.name", "fooBar").negate()),
806 }]
807 .into(),
808 };
809
810 let cases = [("fooBar", false), ("foobar", true), ("other", true)];
811 for (name, filters) in cases {
812 let event = Event::from_value(
813 serde_json::json!({
814 "contexts": {
815 "os": {
816 "name": name,
817 },
818 },
819 })
820 .into(),
821 );
822
823 let expected = if filters {
824 Err(FilterStatKey::GenericFilter("os_name".to_string()))
825 } else {
826 Ok(())
827 };
828
829 assert_eq!(
830 should_filter(event.value().unwrap(), &config, None),
831 expected
832 );
833 }
834 }
835}