1use std::borrow::Cow;
4use std::collections::BTreeSet;
5use std::convert::Infallible;
6use std::fmt;
7use std::ops::Deref;
8use std::str::FromStr;
9
10use indexmap::IndexMap;
11use relay_pattern::{CaseInsensitive, TypedPatterns};
12use relay_protocol::RuleCondition;
13use serde::ser::SerializeSeq;
14use serde::{Deserialize, Serialize, Serializer, de};
15
16#[derive(Clone, Debug, Default, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct FilterConfig {
20 pub is_enabled: bool,
22}
23
24impl FilterConfig {
25 pub fn is_empty(&self) -> bool {
27 !self.is_enabled
28 }
29}
30
31#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
33pub enum LegacyBrowser {
34 Default,
36 IePre9,
38 Ie9,
40 Ie10,
42 Ie11,
44 OperaPre15,
46 OperaMiniPre8,
48 AndroidPre4,
50 SafariPre6,
52 EdgePre79,
54 Ie,
56 Safari,
58 Opera,
60 OperaMini,
62 Android,
64 Firefox,
66 Chrome,
68 Edge,
70 Unknown(String),
72}
73
74impl FromStr for LegacyBrowser {
75 type Err = Infallible;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 let v = match s {
79 "default" => LegacyBrowser::Default,
80 "ie_pre_9" => LegacyBrowser::IePre9,
81 "ie9" => LegacyBrowser::Ie9,
82 "ie10" => LegacyBrowser::Ie10,
83 "ie11" => LegacyBrowser::Ie11,
84 "opera_pre_15" => LegacyBrowser::OperaPre15,
85 "opera_mini_pre_8" => LegacyBrowser::OperaMiniPre8,
86 "android_pre_4" => LegacyBrowser::AndroidPre4,
87 "safari_pre_6" => LegacyBrowser::SafariPre6,
88 "edge_pre_79" => LegacyBrowser::EdgePre79,
89 "ie" => LegacyBrowser::Ie,
90 "safari" => LegacyBrowser::Safari,
91 "opera" => LegacyBrowser::Opera,
92 "opera_mini" => LegacyBrowser::OperaMini,
93 "android" => LegacyBrowser::Android,
94 "firefox" => LegacyBrowser::Firefox,
95 "chrome" => LegacyBrowser::Chrome,
96 "edge" => LegacyBrowser::Edge,
97 _ => LegacyBrowser::Unknown(s.to_owned()),
98 };
99 Ok(v)
100 }
101}
102
103impl<'de> Deserialize<'de> for LegacyBrowser {
104 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105 where
106 D: serde::de::Deserializer<'de>,
107 {
108 let s = Cow::<str>::deserialize(deserializer)?;
109 Ok(LegacyBrowser::from_str(s.as_ref()).unwrap())
110 }
111}
112
113impl Serialize for LegacyBrowser {
114 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
115 where
116 S: serde::ser::Serializer,
117 {
118 serializer.serialize_str(match self {
119 LegacyBrowser::Default => "default",
120 LegacyBrowser::IePre9 => "ie_pre_9",
121 LegacyBrowser::Ie9 => "ie9",
122 LegacyBrowser::Ie10 => "ie10",
123 LegacyBrowser::Ie11 => "ie11",
124 LegacyBrowser::OperaPre15 => "opera_pre_15",
125 LegacyBrowser::OperaMiniPre8 => "opera_mini_pre_8",
126 LegacyBrowser::AndroidPre4 => "android_pre_4",
127 LegacyBrowser::SafariPre6 => "safari_pre_6",
128 LegacyBrowser::EdgePre79 => "edge_pre_79",
129 LegacyBrowser::Ie => "ie",
130 LegacyBrowser::Safari => "safari",
131 LegacyBrowser::Opera => "opera",
132 LegacyBrowser::OperaMini => "opera_mini",
133 LegacyBrowser::Android => "android",
134 LegacyBrowser::Firefox => "firefox",
135 LegacyBrowser::Chrome => "chrome",
136 LegacyBrowser::Edge => "edge",
137 LegacyBrowser::Unknown(string) => string,
138 })
139 }
140}
141
142#[derive(Clone, Debug, Default, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct ClientIpsFilterConfig {
146 pub blacklisted_ips: Vec<String>,
148}
149
150impl ClientIpsFilterConfig {
151 pub fn is_empty(&self) -> bool {
153 self.blacklisted_ips.is_empty()
154 }
155}
156
157#[derive(Clone, Debug, Default, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct CspFilterConfig {
161 pub disallowed_sources: Vec<String>,
163}
164
165impl CspFilterConfig {
166 pub fn is_empty(&self) -> bool {
168 self.disallowed_sources.is_empty()
169 }
170}
171
172#[derive(Clone, Debug, Default, Serialize, Deserialize)]
174pub struct ErrorMessagesFilterConfig {
175 pub patterns: TypedPatterns<CaseInsensitive>,
177}
178
179#[derive(Clone, Debug, Default, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct IgnoreTransactionsFilterConfig {
183 pub patterns: TypedPatterns<CaseInsensitive>,
185 #[serde(default)]
187 pub is_enabled: bool,
188}
189
190impl IgnoreTransactionsFilterConfig {
191 pub fn is_empty(&self) -> bool {
193 self.patterns.is_empty() || !self.is_enabled
194 }
195}
196
197impl ErrorMessagesFilterConfig {
198 pub fn is_empty(&self) -> bool {
200 self.patterns.is_empty()
201 }
202}
203
204#[derive(Clone, Debug, Default, Serialize, Deserialize)]
206pub struct ReleasesFilterConfig {
207 pub releases: TypedPatterns<CaseInsensitive>,
209}
210
211impl ReleasesFilterConfig {
212 pub fn is_empty(&self) -> bool {
214 self.releases.is_empty()
215 }
216}
217
218#[derive(Clone, Debug, Default, Serialize, Deserialize)]
220#[serde(rename_all = "camelCase")]
221pub struct LegacyBrowsersFilterConfig {
222 pub is_enabled: bool,
224 #[serde(default, rename = "options")]
226 pub browsers: BTreeSet<LegacyBrowser>,
227}
228
229impl LegacyBrowsersFilterConfig {
230 pub fn is_empty(&self) -> bool {
232 !self.is_enabled && self.browsers.is_empty()
233 }
234}
235
236#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
238#[serde(rename_all = "camelCase")]
239pub struct GenericFilterConfig {
240 pub id: String,
242 pub is_enabled: bool,
244 pub condition: Option<RuleCondition>,
246}
247
248impl GenericFilterConfig {
249 pub fn is_empty(&self) -> bool {
251 !self.is_enabled || self.condition.is_none()
252 }
253}
254
255#[derive(Clone, Debug, Default, Serialize, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct GenericFiltersConfig {
406 pub version: u16,
408 #[serde(default, skip_serializing_if = "GenericFiltersMap::is_empty")]
413 pub filters: GenericFiltersMap,
414}
415
416impl GenericFiltersConfig {
417 pub fn is_empty(&self) -> bool {
419 self.filters.is_empty()
420 }
421}
422
423#[derive(Clone, Debug, Default)]
425pub struct GenericFiltersMap(IndexMap<String, GenericFilterConfig>);
426
427impl GenericFiltersMap {
428 pub fn new() -> Self {
430 GenericFiltersMap(IndexMap::new())
431 }
432
433 pub fn is_empty(&self) -> bool {
435 self.0.is_empty()
436 }
437}
438
439impl From<Vec<GenericFilterConfig>> for GenericFiltersMap {
440 fn from(filters: Vec<GenericFilterConfig>) -> Self {
441 let mut map = IndexMap::with_capacity(filters.len());
442 for filter in filters {
443 if !map.contains_key(&filter.id) {
444 map.insert(filter.id.clone(), filter);
445 }
446 }
447 GenericFiltersMap(map)
448 }
449}
450
451impl Deref for GenericFiltersMap {
452 type Target = IndexMap<String, GenericFilterConfig>;
453
454 fn deref(&self) -> &Self::Target {
455 &self.0
456 }
457}
458
459impl<'de> Deserialize<'de> for GenericFiltersMap {
460 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
461 where
462 D: de::Deserializer<'de>,
463 {
464 struct GenericFiltersVisitor;
465
466 impl<'de> serde::de::Visitor<'de> for GenericFiltersVisitor {
467 type Value = GenericFiltersMap;
468
469 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
470 formatter.write_str("a vector of filters: Vec<GenericFilterConfig>")
471 }
472
473 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
474 where
475 A: de::SeqAccess<'de>,
476 {
477 let mut filters = IndexMap::with_capacity(seq.size_hint().unwrap_or(0));
478 while let Some(filter) = seq.next_element::<GenericFilterConfig>()? {
479 if !filters.contains_key(&filter.id) {
480 filters.insert(filter.id.clone(), filter);
481 }
482 }
483 Ok(GenericFiltersMap(filters))
484 }
485 }
486
487 deserializer.deserialize_seq(GenericFiltersVisitor)
488 }
489}
490
491impl Serialize for GenericFiltersMap {
492 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
493 where
494 S: Serializer,
495 {
496 let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
497 for filter in self.0.values() {
498 seq.serialize_element(filter)?;
499 }
500 seq.end()
501 }
502}
503
504#[derive(Clone, Debug, Default, Serialize, Deserialize)]
506#[serde(rename_all = "camelCase")]
507pub struct ProjectFiltersConfig {
508 #[serde(default, skip_serializing_if = "FilterConfig::is_empty")]
510 pub browser_extensions: FilterConfig,
511
512 #[serde(default, skip_serializing_if = "ClientIpsFilterConfig::is_empty")]
514 pub client_ips: ClientIpsFilterConfig,
515
516 #[serde(default, skip_serializing_if = "FilterConfig::is_empty")]
518 pub web_crawlers: FilterConfig,
519
520 #[serde(default, skip_serializing_if = "CspFilterConfig::is_empty")]
522 pub csp: CspFilterConfig,
523
524 #[serde(default, skip_serializing_if = "ErrorMessagesFilterConfig::is_empty")]
526 pub error_messages: ErrorMessagesFilterConfig,
527
528 #[serde(default, skip_serializing_if = "LegacyBrowsersFilterConfig::is_empty")]
530 pub legacy_browsers: LegacyBrowsersFilterConfig,
531
532 #[serde(default, skip_serializing_if = "FilterConfig::is_empty")]
534 pub localhost: FilterConfig,
535
536 #[serde(default, skip_serializing_if = "ReleasesFilterConfig::is_empty")]
538 pub releases: ReleasesFilterConfig,
539
540 #[serde(
542 default,
543 skip_serializing_if = "IgnoreTransactionsFilterConfig::is_empty"
544 )]
545 pub ignore_transactions: IgnoreTransactionsFilterConfig,
546
547 #[serde(default, skip_serializing_if = "GenericFiltersConfig::is_empty")]
549 pub generic: GenericFiltersConfig,
550}
551
552impl ProjectFiltersConfig {
553 pub fn is_empty(&self) -> bool {
555 self.browser_extensions.is_empty()
556 && self.client_ips.is_empty()
557 && self.web_crawlers.is_empty()
558 && self.csp.is_empty()
559 && self.error_messages.is_empty()
560 && self.legacy_browsers.is_empty()
561 && self.localhost.is_empty()
562 && self.releases.is_empty()
563 && self.ignore_transactions.is_empty()
564 && self.generic.is_empty()
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
573 fn test_empty_config() -> Result<(), serde_json::Error> {
574 let filters_config = serde_json::from_str::<ProjectFiltersConfig>("{}")?;
575 insta::assert_debug_snapshot!(filters_config, @r###"
576 ProjectFiltersConfig {
577 browser_extensions: FilterConfig {
578 is_enabled: false,
579 },
580 client_ips: ClientIpsFilterConfig {
581 blacklisted_ips: [],
582 },
583 web_crawlers: FilterConfig {
584 is_enabled: false,
585 },
586 csp: CspFilterConfig {
587 disallowed_sources: [],
588 },
589 error_messages: ErrorMessagesFilterConfig {
590 patterns: [],
591 },
592 legacy_browsers: LegacyBrowsersFilterConfig {
593 is_enabled: false,
594 browsers: {},
595 },
596 localhost: FilterConfig {
597 is_enabled: false,
598 },
599 releases: ReleasesFilterConfig {
600 releases: [],
601 },
602 ignore_transactions: IgnoreTransactionsFilterConfig {
603 patterns: [],
604 is_enabled: false,
605 },
606 generic: GenericFiltersConfig {
607 version: 0,
608 filters: GenericFiltersMap(
609 {},
610 ),
611 },
612 }
613 "###);
614 Ok(())
615 }
616
617 #[test]
618 fn test_serialize_empty() {
619 let filters_config = ProjectFiltersConfig::default();
620 insta::assert_json_snapshot!(filters_config, @"{}");
621 }
622
623 #[test]
624 fn test_serialize_full() {
625 let filters_config = ProjectFiltersConfig {
626 browser_extensions: FilterConfig { is_enabled: true },
627 client_ips: ClientIpsFilterConfig {
628 blacklisted_ips: vec!["127.0.0.1".to_string()],
629 },
630 web_crawlers: FilterConfig { is_enabled: true },
631 csp: CspFilterConfig {
632 disallowed_sources: vec!["https://*".to_string()],
633 },
634 error_messages: ErrorMessagesFilterConfig {
635 patterns: TypedPatterns::from(["Panic".to_owned()]),
636 },
637 legacy_browsers: LegacyBrowsersFilterConfig {
638 is_enabled: false,
639 browsers: [LegacyBrowser::Ie9, LegacyBrowser::EdgePre79]
640 .iter()
641 .cloned()
642 .collect(),
643 },
644 localhost: FilterConfig { is_enabled: true },
645 releases: ReleasesFilterConfig {
646 releases: TypedPatterns::from(["1.2.3".to_owned()]),
647 },
648 ignore_transactions: IgnoreTransactionsFilterConfig {
649 patterns: TypedPatterns::from(["*health*".to_owned()]),
650 is_enabled: true,
651 },
652 generic: GenericFiltersConfig {
653 version: 1,
654 filters: vec![GenericFilterConfig {
655 id: "hydrationError".to_owned(),
656 is_enabled: true,
657 condition: Some(RuleCondition::eq("event.exceptions", "HydrationError")),
658 }]
659 .into(),
660 },
661 };
662
663 insta::assert_json_snapshot!(filters_config, @r#"
664 {
665 "browserExtensions": {
666 "isEnabled": true
667 },
668 "clientIps": {
669 "blacklistedIps": [
670 "127.0.0.1"
671 ]
672 },
673 "webCrawlers": {
674 "isEnabled": true
675 },
676 "csp": {
677 "disallowedSources": [
678 "https://*"
679 ]
680 },
681 "errorMessages": {
682 "patterns": [
683 "Panic"
684 ]
685 },
686 "legacyBrowsers": {
687 "isEnabled": false,
688 "options": [
689 "ie9",
690 "edge_pre_79"
691 ]
692 },
693 "localhost": {
694 "isEnabled": true
695 },
696 "releases": {
697 "releases": [
698 "1.2.3"
699 ]
700 },
701 "ignoreTransactions": {
702 "patterns": [
703 "*health*"
704 ],
705 "isEnabled": true
706 },
707 "generic": {
708 "version": 1,
709 "filters": [
710 {
711 "id": "hydrationError",
712 "isEnabled": true,
713 "condition": {
714 "op": "eq",
715 "name": "event.exceptions",
716 "value": "HydrationError"
717 }
718 }
719 ]
720 }
721 }
722 "#);
723 }
724
725 #[test]
726 fn test_regression_legacy_browser_missing_options() {
727 let json = r#"{"isEnabled":false}"#;
728 let config = serde_json::from_str::<LegacyBrowsersFilterConfig>(json).unwrap();
729 insta::assert_debug_snapshot!(config, @r###"
730 LegacyBrowsersFilterConfig {
731 is_enabled: false,
732 browsers: {},
733 }
734 "###);
735 }
736
737 #[test]
738 fn test_deserialize_generic_filters() {
739 let json = r#"{
740 "version": 1,
741 "filters": [
742 {
743 "id": "hydrationError",
744 "isEnabled": true,
745 "condition": {
746 "op": "eq",
747 "name": "event.exceptions",
748 "value": "HydrationError"
749 }
750 },
751 {
752 "id": "chunkLoadError",
753 "isEnabled": false
754 }
755 ]
756 }"#;
757 let config = serde_json::from_str::<GenericFiltersConfig>(json).unwrap();
758 insta::assert_debug_snapshot!(config, @r###"
759 GenericFiltersConfig {
760 version: 1,
761 filters: GenericFiltersMap(
762 {
763 "hydrationError": GenericFilterConfig {
764 id: "hydrationError",
765 is_enabled: true,
766 condition: Some(
767 Eq(
768 EqCondition {
769 name: "event.exceptions",
770 value: String("HydrationError"),
771 options: EqCondOptions {
772 ignore_case: false,
773 },
774 },
775 ),
776 ),
777 },
778 "chunkLoadError": GenericFilterConfig {
779 id: "chunkLoadError",
780 is_enabled: false,
781 condition: None,
782 },
783 },
784 ),
785 }
786 "###);
787 }
788}