1use std::fmt;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use relay_base_schema::metrics::MetricNamespace;
6use relay_base_schema::organization::OrganizationId;
7use relay_base_schema::project::{ProjectId, ProjectKey};
8use serde::{Deserialize, Serialize};
9use smallvec::SmallVec;
10
11#[doc(inline)]
12pub use relay_base_schema::data_category::{CategoryUnit, DataCategory};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct Scoping {
21 pub organization_id: OrganizationId,
23
24 pub project_id: ProjectId,
26
27 pub project_key: ProjectKey,
29
30 pub key_id: Option<u64>,
32}
33
34impl Scoping {
35 pub fn item(&self, category: DataCategory) -> ItemScoping {
41 ItemScoping {
42 category,
43 scoping: *self,
44 namespace: MetricNamespaceScoping::None,
45 }
46 }
47
48 pub fn metric_bucket(&self, namespace: MetricNamespace) -> ItemScoping {
54 ItemScoping {
55 category: DataCategory::MetricBucket,
56 scoping: *self,
57 namespace: MetricNamespaceScoping::Some(namespace),
58 }
59 }
60}
61
62#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, PartialOrd)]
67pub enum MetricNamespaceScoping {
68 #[default]
72 None,
73
74 Some(MetricNamespace),
76
77 Any,
82}
83
84impl MetricNamespaceScoping {
85 pub fn matches(&self, namespace: MetricNamespace) -> bool {
91 match self {
92 Self::None => false,
93 Self::Some(ns) => *ns == namespace,
94 Self::Any => true,
95 }
96 }
97}
98
99impl From<MetricNamespace> for MetricNamespaceScoping {
100 fn from(namespace: MetricNamespace) -> Self {
101 Self::Some(namespace)
102 }
103}
104
105#[derive(Debug, Copy, Clone, Eq, PartialEq)]
110pub struct ItemScoping {
111 pub category: DataCategory,
113
114 pub scoping: Scoping,
116
117 pub namespace: MetricNamespaceScoping,
119}
120
121impl std::ops::Deref for ItemScoping {
122 type Target = Scoping;
123
124 fn deref(&self) -> &Self::Target {
125 &self.scoping
126 }
127}
128
129impl ItemScoping {
130 pub fn scope_id(&self, scope: QuotaScope) -> Option<u64> {
135 match scope {
136 QuotaScope::Global => None,
137 QuotaScope::Organization => Some(self.organization_id.value()),
138 QuotaScope::Project => Some(self.project_id.value()),
139 QuotaScope::Key => self.key_id,
140 QuotaScope::Unknown => None,
141 }
142 }
143
144 pub(crate) fn matches_categories(&self, categories: &[DataCategory]) -> bool {
146 categories.is_empty() || categories.contains(&self.category)
151 }
152
153 pub(crate) fn matches_namespaces<'a, I>(&self, namespaces: I) -> bool
167 where
168 I: IntoIterator<Item = &'a MetricNamespace>,
169 {
170 let mut iter = namespaces.into_iter().peekable();
171 iter.peek().is_none() || iter.any(|ns| self.namespace.matches(*ns))
172 }
173}
174
175#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)]
179#[serde(transparent)]
180pub struct DataCategories(Arc<[DataCategory]>);
181
182impl DataCategories {
183 pub fn new() -> Self {
185 Default::default()
186 }
187
188 fn new_sort_and_dedup<const N: usize>(mut s: SmallVec<[DataCategory; N]>) -> Self {
192 s.sort_unstable();
193 s.dedup();
194 Self(s.as_slice().into())
195 }
196
197 pub fn add(&self, category: DataCategory) -> Option<Self> {
202 if self.0.contains(&category) {
205 return None;
206 }
207
208 let mut new = SmallVec::<[DataCategory; 12]>::from(&*self.0);
209 new.push(category);
210 Some(new.into())
211 }
212}
213
214impl std::ops::Deref for DataCategories {
215 type Target = [DataCategory];
216
217 fn deref(&self) -> &Self::Target {
218 &self.0
219 }
220}
221
222impl<'de> Deserialize<'de> for DataCategories {
223 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224 where
225 D: serde::Deserializer<'de>,
226 {
227 SmallVec::<[DataCategory; 12]>::deserialize(deserializer).map(Self::new_sort_and_dedup)
228 }
229}
230
231impl<const N: usize> From<SmallVec<[DataCategory; N]>> for DataCategories {
232 fn from(categories: SmallVec<[DataCategory; N]>) -> Self {
233 Self::new_sort_and_dedup(categories)
234 }
235}
236
237impl<const N: usize> From<[DataCategory; N]> for DataCategories {
238 fn from(categories: [DataCategory; N]) -> Self {
239 Self::new_sort_and_dedup(SmallVec::from_buf(categories))
240 }
241}
242
243impl FromIterator<DataCategory> for DataCategories {
244 fn from_iter<T: IntoIterator<Item = DataCategory>>(iter: T) -> Self {
245 let v: SmallVec<[DataCategory; 12]> = iter.into_iter().collect();
246 Self::new_sort_and_dedup(v)
247 }
248}
249
250#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
259#[serde(rename_all = "lowercase")]
260pub enum QuotaScope {
261 Global,
263 Organization,
267
268 Project,
272
273 Key,
277
278 #[serde(other)]
280 Unknown,
281}
282
283impl QuotaScope {
284 pub fn from_name(string: &str) -> Self {
288 match string {
289 "global" => Self::Global,
290 "organization" => Self::Organization,
291 "project" => Self::Project,
292 "key" => Self::Key,
293 _ => Self::Unknown,
294 }
295 }
296
297 pub fn name(self) -> &'static str {
301 match self {
302 Self::Global => "global",
303 Self::Key => "key",
304 Self::Project => "project",
305 Self::Organization => "organization",
306 Self::Unknown => "unknown",
307 }
308 }
309}
310
311impl fmt::Display for QuotaScope {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 write!(f, "{}", self.name())
314 }
315}
316
317impl FromStr for QuotaScope {
318 type Err = ();
319
320 fn from_str(string: &str) -> Result<Self, Self::Err> {
321 Ok(Self::from_name(string))
322 }
323}
324
325fn default_scope() -> QuotaScope {
326 QuotaScope::Organization
327}
328
329#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
334pub struct ReasonCode(Arc<str>);
335
336impl ReasonCode {
337 pub fn new<S: Into<Arc<str>>>(code: S) -> Self {
343 Self(code.into())
344 }
345
346 pub fn as_str(&self) -> &str {
348 &self.0
349 }
350}
351
352impl fmt::Display for ReasonCode {
353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354 self.0.fmt(f)
355 }
356}
357
358#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
371#[serde(rename_all = "camelCase")]
372pub struct Quota {
373 #[serde(default)]
377 pub id: Option<Arc<str>>,
378
379 #[serde(default)]
383 pub categories: DataCategories,
384
385 #[serde(default = "default_scope")]
390 pub scope: QuotaScope,
391
392 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub scope_id: Option<Arc<str>>,
398
399 #[serde(default)]
408 pub limit: Option<u64>,
409
410 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub window: Option<u64>,
416
417 pub namespace: Option<MetricNamespace>,
421
422 #[serde(default, skip_serializing_if = "Option::is_none")]
427 pub reason_code: Option<ReasonCode>,
428}
429
430impl Quota {
431 pub fn is_valid(&self) -> bool {
438 if self.namespace == Some(MetricNamespace::Unsupported) {
439 return false;
440 }
441
442 let mut units = self
443 .categories
444 .iter()
445 .filter_map(CategoryUnit::from_category);
446
447 match units.next() {
448 None if !self.categories.is_empty() => false,
450 _ if self.limit == Some(0) => true,
452 None => false,
454 Some(unit) => units.all(|u| u == unit),
456 }
457 }
458
459 fn matches_scope(&self, scoping: ItemScoping) -> bool {
466 if self.scope == QuotaScope::Global {
467 return true;
468 }
469
470 let Some(scope_id) = self.scope_id.as_ref() else {
474 return true;
475 };
476
477 let Ok(parsed) = scope_id.parse::<u64>() else {
480 return false;
481 };
482
483 scoping.scope_id(self.scope) == Some(parsed)
485 }
486
487 pub fn matches(&self, scoping: ItemScoping) -> bool {
492 self.matches_scope(scoping)
493 && scoping.matches_categories(&self.categories)
494 && scoping.matches_namespaces(&self.namespace)
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn test_parse_quota_reject_all() {
504 let json = r#"{
505 "limit": 0,
506 "reasonCode": "not_yet"
507 }"#;
508
509 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
510
511 insta::assert_ron_snapshot!(quota, @r###"
512 Quota(
513 id: None,
514 categories: [],
515 scope: organization,
516 limit: Some(0),
517 namespace: None,
518 reasonCode: Some(ReasonCode("not_yet")),
519 )
520 "###);
521 }
522
523 #[test]
524 fn test_parse_quota_reject_transactions() {
525 let json = r#"{
526 "limit": 0,
527 "categories": ["transaction"],
528 "reasonCode": "not_yet"
529 }"#;
530
531 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
532
533 insta::assert_ron_snapshot!(quota, @r#"
534 Quota(
535 id: None,
536 categories: [
537 "transaction",
538 ],
539 scope: organization,
540 limit: Some(0),
541 namespace: None,
542 reasonCode: Some(ReasonCode("not_yet")),
543 )
544 "#);
545 }
546
547 #[test]
548 fn test_parse_quota_limited() {
549 let json = r#"{
550 "id": "o",
551 "limit": 4711,
552 "window": 42,
553 "reasonCode": "not_so_fast"
554 }"#;
555
556 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
557
558 insta::assert_ron_snapshot!(quota, @r###"
559 Quota(
560 id: Some("o"),
561 categories: [],
562 scope: organization,
563 limit: Some(4711),
564 window: Some(42),
565 namespace: None,
566 reasonCode: Some(ReasonCode("not_so_fast")),
567 )
568 "###);
569 }
570
571 #[test]
572 fn test_parse_quota_project() {
573 let json = r#"{
574 "id": "p",
575 "scope": "project",
576 "scopeId": "1",
577 "limit": 4711,
578 "window": 42,
579 "reasonCode": "not_so_fast"
580 }"#;
581
582 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
583
584 insta::assert_ron_snapshot!(quota, @r###"
585 Quota(
586 id: Some("p"),
587 categories: [],
588 scope: project,
589 scopeId: Some("1"),
590 limit: Some(4711),
591 window: Some(42),
592 namespace: None,
593 reasonCode: Some(ReasonCode("not_so_fast")),
594 )
595 "###);
596 }
597
598 #[test]
599 fn test_parse_quota_project_large() {
600 let json = r#"{
601 "id": "p",
602 "scope": "project",
603 "scopeId": "1",
604 "limit": 4294967296,
605 "window": 42,
606 "reasonCode": "not_so_fast"
607 }"#;
608
609 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
610
611 insta::assert_ron_snapshot!(quota, @r###"
612 Quota(
613 id: Some("p"),
614 categories: [],
615 scope: project,
616 scopeId: Some("1"),
617 limit: Some(4294967296),
618 window: Some(42),
619 namespace: None,
620 reasonCode: Some(ReasonCode("not_so_fast")),
621 )
622 "###);
623 }
624
625 #[test]
626 fn test_parse_quota_key() {
627 let json = r#"{
628 "id": "k",
629 "scope": "key",
630 "scopeId": "1",
631 "limit": 4711,
632 "window": 42,
633 "reasonCode": "not_so_fast"
634 }"#;
635
636 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
637
638 insta::assert_ron_snapshot!(quota, @r###"
639 Quota(
640 id: Some("k"),
641 categories: [],
642 scope: key,
643 scopeId: Some("1"),
644 limit: Some(4711),
645 window: Some(42),
646 namespace: None,
647 reasonCode: Some(ReasonCode("not_so_fast")),
648 )
649 "###);
650 }
651
652 #[test]
653 fn test_parse_quota_unknown_variants() {
654 let json = r#"{
655 "id": "f",
656 "categories": ["future"],
657 "scope": "future",
658 "scopeId": "1",
659 "limit": 4711,
660 "window": 42,
661 "reasonCode": "not_so_fast"
662 }"#;
663
664 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
665
666 insta::assert_ron_snapshot!(quota, @r#"
667 Quota(
668 id: Some("f"),
669 categories: [
670 "unknown",
671 ],
672 scope: unknown,
673 scopeId: Some("1"),
674 limit: Some(4711),
675 window: Some(42),
676 namespace: None,
677 reasonCode: Some(ReasonCode("not_so_fast")),
678 )
679 "#);
680 }
681
682 #[test]
683 fn test_parse_quota_unlimited() {
684 let json = r#"{
685 "id": "o",
686 "window": 42
687 }"#;
688
689 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
690
691 insta::assert_ron_snapshot!(quota, @r###"
692 Quota(
693 id: Some("o"),
694 categories: [],
695 scope: organization,
696 limit: None,
697 window: Some(42),
698 namespace: None,
699 )
700 "###);
701 }
702
703 #[test]
704 fn test_quota_valid_reject_all() {
705 let quota = Quota {
706 id: None,
707 categories: Default::default(),
708 scope: QuotaScope::Organization,
709 scope_id: None,
710 limit: Some(0),
711 window: None,
712 reason_code: None,
713 namespace: None,
714 };
715
716 assert!(quota.is_valid());
717 }
718
719 #[test]
720 fn test_quota_invalid_only_unknown() {
721 let quota = Quota {
722 id: None,
723 categories: [DataCategory::Unknown, DataCategory::Unknown].into(),
724 scope: QuotaScope::Organization,
725 scope_id: None,
726 limit: Some(0),
727 window: None,
728 reason_code: None,
729 namespace: None,
730 };
731
732 assert!(!quota.is_valid());
733 }
734
735 #[test]
736 fn test_quota_valid_reject_all_mixed() {
737 let quota = Quota {
738 id: None,
739 categories: [DataCategory::Error, DataCategory::Attachment].into(),
740 scope: QuotaScope::Organization,
741 scope_id: None,
742 limit: Some(0),
743 window: None,
744 reason_code: None,
745 namespace: None,
746 };
747
748 assert!(quota.is_valid());
749 }
750
751 #[test]
752 fn test_quota_invalid_limited_mixed() {
753 let quota = Quota {
754 id: None,
755 categories: [DataCategory::Error, DataCategory::Attachment].into(),
756 scope: QuotaScope::Organization,
757 scope_id: None,
758 limit: Some(1000),
759 window: None,
760 reason_code: None,
761 namespace: None,
762 };
763
764 assert!(!quota.is_valid());
766 }
767
768 #[test]
769 fn test_quota_invalid_unlimited_mixed() {
770 let quota = Quota {
771 id: None,
772 categories: [DataCategory::Error, DataCategory::Attachment].into(),
773 scope: QuotaScope::Organization,
774 scope_id: None,
775 limit: None,
776 window: None,
777 reason_code: None,
778 namespace: None,
779 };
780
781 assert!(!quota.is_valid());
783 }
784
785 #[test]
786 fn test_quota_matches_no_categories() {
787 let quota = Quota {
788 id: None,
789 categories: Default::default(),
790 scope: QuotaScope::Organization,
791 scope_id: None,
792 limit: None,
793 window: None,
794 reason_code: None,
795 namespace: None,
796 };
797
798 assert!(quota.matches(ItemScoping {
799 category: DataCategory::Error,
800 scoping: Scoping {
801 organization_id: OrganizationId::new(42),
802 project_id: ProjectId::new(21),
803 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
804 key_id: Some(17),
805 },
806 namespace: MetricNamespaceScoping::None,
807 }));
808 }
809
810 #[test]
811 fn test_quota_matches_unknown_category() {
812 let quota = Quota {
813 id: None,
814 categories: [DataCategory::Unknown].into(),
815 scope: QuotaScope::Organization,
816 scope_id: None,
817 limit: None,
818 window: None,
819 reason_code: None,
820 namespace: None,
821 };
822
823 assert!(!quota.matches(ItemScoping {
824 category: DataCategory::Error,
825 scoping: Scoping {
826 organization_id: OrganizationId::new(42),
827 project_id: ProjectId::new(21),
828 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
829 key_id: Some(17),
830 },
831 namespace: MetricNamespaceScoping::None,
832 }));
833 }
834
835 #[test]
836 fn test_quota_matches_multiple_categores() {
837 let quota = Quota {
838 id: None,
839 categories: [DataCategory::Unknown, DataCategory::Error].into(),
840 scope: QuotaScope::Organization,
841 scope_id: None,
842 limit: None,
843 window: None,
844 reason_code: None,
845 namespace: None,
846 };
847
848 assert!(quota.matches(ItemScoping {
849 category: DataCategory::Error,
850 scoping: Scoping {
851 organization_id: OrganizationId::new(42),
852 project_id: ProjectId::new(21),
853 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
854 key_id: Some(17),
855 },
856 namespace: MetricNamespaceScoping::None,
857 }));
858
859 assert!(!quota.matches(ItemScoping {
860 category: DataCategory::Transaction,
861 scoping: Scoping {
862 organization_id: OrganizationId::new(42),
863 project_id: ProjectId::new(21),
864 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
865 key_id: Some(17),
866 },
867 namespace: MetricNamespaceScoping::None,
868 }));
869 }
870
871 #[test]
872 fn test_quota_matches_no_invalid_scope() {
873 let quota = Quota {
874 id: None,
875 categories: Default::default(),
876 scope: QuotaScope::Organization,
877 scope_id: Some("not_a_number".into()),
878 limit: None,
879 window: None,
880 reason_code: None,
881 namespace: None,
882 };
883
884 assert!(!quota.matches(ItemScoping {
885 category: DataCategory::Error,
886 scoping: Scoping {
887 organization_id: OrganizationId::new(42),
888 project_id: ProjectId::new(21),
889 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
890 key_id: Some(17),
891 },
892 namespace: MetricNamespaceScoping::None,
893 }));
894 }
895
896 #[test]
897 fn test_quota_matches_organization_scope() {
898 let quota = Quota {
899 id: None,
900 categories: Default::default(),
901 scope: QuotaScope::Organization,
902 scope_id: Some("42".into()),
903 limit: None,
904 window: None,
905 reason_code: None,
906 namespace: None,
907 };
908
909 assert!(quota.matches(ItemScoping {
910 category: DataCategory::Error,
911 scoping: Scoping {
912 organization_id: OrganizationId::new(42),
913 project_id: ProjectId::new(21),
914 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
915 key_id: Some(17),
916 },
917 namespace: MetricNamespaceScoping::None,
918 }));
919
920 assert!(!quota.matches(ItemScoping {
921 category: DataCategory::Error,
922 scoping: Scoping {
923 organization_id: OrganizationId::new(0),
924 project_id: ProjectId::new(21),
925 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
926 key_id: Some(17),
927 },
928 namespace: MetricNamespaceScoping::None,
929 }));
930 }
931
932 #[test]
933 fn test_quota_matches_project_scope() {
934 let quota = Quota {
935 id: None,
936 categories: Default::default(),
937 scope: QuotaScope::Project,
938 scope_id: Some("21".into()),
939 limit: None,
940 window: None,
941 reason_code: None,
942 namespace: None,
943 };
944
945 assert!(quota.matches(ItemScoping {
946 category: DataCategory::Error,
947 scoping: Scoping {
948 organization_id: OrganizationId::new(42),
949 project_id: ProjectId::new(21),
950 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
951 key_id: Some(17),
952 },
953 namespace: MetricNamespaceScoping::None,
954 }));
955
956 assert!(!quota.matches(ItemScoping {
957 category: DataCategory::Error,
958 scoping: Scoping {
959 organization_id: OrganizationId::new(42),
960 project_id: ProjectId::new(0),
961 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
962 key_id: Some(17),
963 },
964 namespace: MetricNamespaceScoping::None,
965 }));
966 }
967
968 #[test]
969 fn test_quota_matches_key_scope() {
970 let quota = Quota {
971 id: None,
972 categories: Default::default(),
973 scope: QuotaScope::Key,
974 scope_id: Some("17".into()),
975 limit: None,
976 window: None,
977 reason_code: None,
978 namespace: None,
979 };
980
981 assert!(quota.matches(ItemScoping {
982 category: DataCategory::Error,
983 scoping: Scoping {
984 organization_id: OrganizationId::new(42),
985 project_id: ProjectId::new(21),
986 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
987 key_id: Some(17),
988 },
989 namespace: MetricNamespaceScoping::None,
990 }));
991
992 assert!(!quota.matches(ItemScoping {
993 category: DataCategory::Error,
994 scoping: Scoping {
995 organization_id: OrganizationId::new(42),
996 project_id: ProjectId::new(21),
997 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
998 key_id: Some(0),
999 },
1000 namespace: MetricNamespaceScoping::None,
1001 }));
1002
1003 assert!(!quota.matches(ItemScoping {
1004 category: DataCategory::Error,
1005 scoping: Scoping {
1006 organization_id: OrganizationId::new(42),
1007 project_id: ProjectId::new(21),
1008 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1009 key_id: None,
1010 },
1011 namespace: MetricNamespaceScoping::None,
1012 }));
1013 }
1014
1015 #[test]
1016 fn test_data_categories_sorted_deduplicated() {
1017 let a = DataCategories::from([
1018 DataCategory::Transaction,
1019 DataCategory::Span,
1020 DataCategory::Transaction,
1021 ]);
1022 let b = DataCategories::from([
1023 DataCategory::Span,
1024 DataCategory::Transaction,
1025 DataCategory::Span,
1026 ]);
1027 let c = DataCategories::from([DataCategory::Span, DataCategory::Transaction]);
1028
1029 assert_eq!(a, b);
1030 assert_eq!(b, c);
1031 assert_eq!(a, c);
1032 }
1033
1034 #[test]
1035 fn test_data_categories_serde() {
1036 let s: DataCategories = serde_json::from_str(r#"["span", "transaction", "span"]"#).unwrap();
1037 insta::assert_json_snapshot!(s, @r#"
1038 [
1039 "transaction",
1040 "span"
1041 ]
1042 "#);
1043 }
1044
1045 #[test]
1046 fn test_data_categories_add() {
1047 let c = DataCategories::new();
1048 let c = c.add(DataCategory::Span).unwrap();
1049 assert!(c.add(DataCategory::Span).is_none());
1050 let c = c.add(DataCategory::Transaction).unwrap();
1051 assert_eq!(c, [DataCategory::Span, DataCategory::Transaction].into());
1052 }
1053}