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::Organization => Some(self.organization_id.value()),
137 QuotaScope::Project => Some(self.project_id.value()),
138 QuotaScope::Key => self.key_id,
139 QuotaScope::Unknown => None,
140 }
141 }
142
143 pub(crate) fn matches_categories(&self, categories: &[DataCategory]) -> bool {
145 categories.is_empty() || categories.contains(&self.category)
150 }
151
152 pub(crate) fn matches_namespaces<'a, I>(&self, namespaces: I) -> bool
166 where
167 I: IntoIterator<Item = &'a MetricNamespace>,
168 {
169 let mut iter = namespaces.into_iter().peekable();
170 iter.peek().is_none() || iter.any(|ns| self.namespace.matches(*ns))
171 }
172}
173
174#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)]
178#[serde(transparent)]
179pub struct DataCategories(Arc<[DataCategory]>);
180
181impl DataCategories {
182 pub fn new() -> Self {
184 Default::default()
185 }
186
187 fn new_sort_and_dedup<const N: usize>(mut s: SmallVec<[DataCategory; N]>) -> Self {
191 s.sort_unstable();
192 s.dedup();
193 Self(s.as_slice().into())
194 }
195
196 pub fn add(&self, category: DataCategory) -> Option<Self> {
201 if self.0.contains(&category) {
204 return None;
205 }
206
207 let mut new = SmallVec::<[DataCategory; 12]>::from(&*self.0);
208 new.push(category);
209 Some(new.into())
210 }
211}
212
213impl std::ops::Deref for DataCategories {
214 type Target = [DataCategory];
215
216 fn deref(&self) -> &Self::Target {
217 &self.0
218 }
219}
220
221impl<'de> Deserialize<'de> for DataCategories {
222 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
223 where
224 D: serde::Deserializer<'de>,
225 {
226 SmallVec::<[DataCategory; 12]>::deserialize(deserializer).map(Self::new_sort_and_dedup)
227 }
228}
229
230impl<const N: usize> From<SmallVec<[DataCategory; N]>> for DataCategories {
231 fn from(categories: SmallVec<[DataCategory; N]>) -> Self {
232 Self::new_sort_and_dedup(categories)
233 }
234}
235
236impl<const N: usize> From<[DataCategory; N]> for DataCategories {
237 fn from(categories: [DataCategory; N]) -> Self {
238 Self::new_sort_and_dedup(SmallVec::from_buf(categories))
239 }
240}
241
242impl FromIterator<DataCategory> for DataCategories {
243 fn from_iter<T: IntoIterator<Item = DataCategory>>(iter: T) -> Self {
244 let v: SmallVec<[DataCategory; 12]> = iter.into_iter().collect();
245 Self::new_sort_and_dedup(v)
246 }
247}
248
249#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
258#[serde(rename_all = "lowercase")]
259pub enum QuotaScope {
260 Organization,
264
265 Project,
269
270 Key,
274
275 #[serde(other)]
277 Unknown,
278}
279
280impl QuotaScope {
281 pub fn from_name(string: &str) -> Self {
285 match string {
286 "organization" => Self::Organization,
287 "project" => Self::Project,
288 "key" => Self::Key,
289 _ => Self::Unknown,
290 }
291 }
292
293 pub fn name(self) -> &'static str {
297 match self {
298 Self::Key => "key",
299 Self::Project => "project",
300 Self::Organization => "organization",
301 Self::Unknown => "unknown",
302 }
303 }
304}
305
306impl fmt::Display for QuotaScope {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 write!(f, "{}", self.name())
309 }
310}
311
312impl FromStr for QuotaScope {
313 type Err = ();
314
315 fn from_str(string: &str) -> Result<Self, Self::Err> {
316 Ok(Self::from_name(string))
317 }
318}
319
320fn default_scope() -> QuotaScope {
321 QuotaScope::Organization
322}
323
324#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
329pub struct ReasonCode(Arc<str>);
330
331impl ReasonCode {
332 pub fn new<S: Into<Arc<str>>>(code: S) -> Self {
338 Self(code.into())
339 }
340
341 pub fn as_str(&self) -> &str {
343 &self.0
344 }
345}
346
347impl fmt::Display for ReasonCode {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 self.0.fmt(f)
350 }
351}
352
353#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
366#[serde(rename_all = "camelCase")]
367pub struct Quota {
368 #[serde(default)]
372 pub id: Option<Arc<str>>,
373
374 #[serde(default)]
378 pub categories: DataCategories,
379
380 #[serde(default = "default_scope")]
385 pub scope: QuotaScope,
386
387 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub scope_id: Option<Arc<str>>,
393
394 #[serde(default)]
403 pub limit: Option<u64>,
404
405 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub window: Option<u64>,
411
412 pub namespace: Option<MetricNamespace>,
416
417 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub reason_code: Option<ReasonCode>,
423}
424
425impl Quota {
426 pub fn is_valid(&self) -> bool {
433 if self.namespace == Some(MetricNamespace::Unsupported) {
434 return false;
435 }
436
437 let mut units = self
438 .categories
439 .iter()
440 .filter_map(CategoryUnit::from_category);
441
442 match units.next() {
443 None if !self.categories.is_empty() => false,
445 _ if self.limit == Some(0) => true,
447 None => false,
449 Some(unit) => units.all(|u| u == unit),
451 }
452 }
453
454 fn matches_scope(&self, scoping: ItemScoping) -> bool {
461 let Some(scope_id) = self.scope_id.as_ref() else {
465 return true;
466 };
467
468 let Ok(parsed) = scope_id.parse::<u64>() else {
471 return false;
472 };
473
474 scoping.scope_id(self.scope) == Some(parsed)
476 }
477
478 pub fn matches(&self, scoping: ItemScoping) -> bool {
483 self.matches_scope(scoping)
484 && scoping.matches_categories(&self.categories)
485 && scoping.matches_namespaces(&self.namespace)
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn test_parse_quota_reject_all() {
495 let json = r#"{
496 "limit": 0,
497 "reasonCode": "not_yet"
498 }"#;
499
500 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
501
502 insta::assert_ron_snapshot!(quota, @r###"
503 Quota(
504 id: None,
505 categories: [],
506 scope: organization,
507 limit: Some(0),
508 namespace: None,
509 reasonCode: Some(ReasonCode("not_yet")),
510 )
511 "###);
512 }
513
514 #[test]
515 fn test_parse_quota_reject_transactions() {
516 let json = r#"{
517 "limit": 0,
518 "categories": ["transaction"],
519 "reasonCode": "not_yet"
520 }"#;
521
522 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
523
524 insta::assert_ron_snapshot!(quota, @r#"
525 Quota(
526 id: None,
527 categories: [
528 "transaction",
529 ],
530 scope: organization,
531 limit: Some(0),
532 namespace: None,
533 reasonCode: Some(ReasonCode("not_yet")),
534 )
535 "#);
536 }
537
538 #[test]
539 fn test_parse_quota_limited() {
540 let json = r#"{
541 "id": "o",
542 "limit": 4711,
543 "window": 42,
544 "reasonCode": "not_so_fast"
545 }"#;
546
547 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
548
549 insta::assert_ron_snapshot!(quota, @r###"
550 Quota(
551 id: Some("o"),
552 categories: [],
553 scope: organization,
554 limit: Some(4711),
555 window: Some(42),
556 namespace: None,
557 reasonCode: Some(ReasonCode("not_so_fast")),
558 )
559 "###);
560 }
561
562 #[test]
563 fn test_parse_quota_project() {
564 let json = r#"{
565 "id": "p",
566 "scope": "project",
567 "scopeId": "1",
568 "limit": 4711,
569 "window": 42,
570 "reasonCode": "not_so_fast"
571 }"#;
572
573 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
574
575 insta::assert_ron_snapshot!(quota, @r###"
576 Quota(
577 id: Some("p"),
578 categories: [],
579 scope: project,
580 scopeId: Some("1"),
581 limit: Some(4711),
582 window: Some(42),
583 namespace: None,
584 reasonCode: Some(ReasonCode("not_so_fast")),
585 )
586 "###);
587 }
588
589 #[test]
590 fn test_parse_quota_project_large() {
591 let json = r#"{
592 "id": "p",
593 "scope": "project",
594 "scopeId": "1",
595 "limit": 4294967296,
596 "window": 42,
597 "reasonCode": "not_so_fast"
598 }"#;
599
600 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
601
602 insta::assert_ron_snapshot!(quota, @r###"
603 Quota(
604 id: Some("p"),
605 categories: [],
606 scope: project,
607 scopeId: Some("1"),
608 limit: Some(4294967296),
609 window: Some(42),
610 namespace: None,
611 reasonCode: Some(ReasonCode("not_so_fast")),
612 )
613 "###);
614 }
615
616 #[test]
617 fn test_parse_quota_key() {
618 let json = r#"{
619 "id": "k",
620 "scope": "key",
621 "scopeId": "1",
622 "limit": 4711,
623 "window": 42,
624 "reasonCode": "not_so_fast"
625 }"#;
626
627 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
628
629 insta::assert_ron_snapshot!(quota, @r###"
630 Quota(
631 id: Some("k"),
632 categories: [],
633 scope: key,
634 scopeId: Some("1"),
635 limit: Some(4711),
636 window: Some(42),
637 namespace: None,
638 reasonCode: Some(ReasonCode("not_so_fast")),
639 )
640 "###);
641 }
642
643 #[test]
644 fn test_parse_quota_unknown_variants() {
645 let json = r#"{
646 "id": "f",
647 "categories": ["future"],
648 "scope": "future",
649 "scopeId": "1",
650 "limit": 4711,
651 "window": 42,
652 "reasonCode": "not_so_fast"
653 }"#;
654
655 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
656
657 insta::assert_ron_snapshot!(quota, @r#"
658 Quota(
659 id: Some("f"),
660 categories: [
661 "unknown",
662 ],
663 scope: unknown,
664 scopeId: Some("1"),
665 limit: Some(4711),
666 window: Some(42),
667 namespace: None,
668 reasonCode: Some(ReasonCode("not_so_fast")),
669 )
670 "#);
671 }
672
673 #[test]
674 fn test_parse_quota_unlimited() {
675 let json = r#"{
676 "id": "o",
677 "window": 42
678 }"#;
679
680 let quota = serde_json::from_str::<Quota>(json).expect("parse quota");
681
682 insta::assert_ron_snapshot!(quota, @r###"
683 Quota(
684 id: Some("o"),
685 categories: [],
686 scope: organization,
687 limit: None,
688 window: Some(42),
689 namespace: None,
690 )
691 "###);
692 }
693
694 #[test]
695 fn test_quota_valid_reject_all() {
696 let quota = Quota {
697 id: None,
698 categories: Default::default(),
699 scope: QuotaScope::Organization,
700 scope_id: None,
701 limit: Some(0),
702 window: None,
703 reason_code: None,
704 namespace: None,
705 };
706
707 assert!(quota.is_valid());
708 }
709
710 #[test]
711 fn test_quota_invalid_only_unknown() {
712 let quota = Quota {
713 id: None,
714 categories: [DataCategory::Unknown, DataCategory::Unknown].into(),
715 scope: QuotaScope::Organization,
716 scope_id: None,
717 limit: Some(0),
718 window: None,
719 reason_code: None,
720 namespace: None,
721 };
722
723 assert!(!quota.is_valid());
724 }
725
726 #[test]
727 fn test_quota_valid_reject_all_mixed() {
728 let quota = Quota {
729 id: None,
730 categories: [DataCategory::Error, DataCategory::Attachment].into(),
731 scope: QuotaScope::Organization,
732 scope_id: None,
733 limit: Some(0),
734 window: None,
735 reason_code: None,
736 namespace: None,
737 };
738
739 assert!(quota.is_valid());
740 }
741
742 #[test]
743 fn test_quota_invalid_limited_mixed() {
744 let quota = Quota {
745 id: None,
746 categories: [DataCategory::Error, DataCategory::Attachment].into(),
747 scope: QuotaScope::Organization,
748 scope_id: None,
749 limit: Some(1000),
750 window: None,
751 reason_code: None,
752 namespace: None,
753 };
754
755 assert!(!quota.is_valid());
757 }
758
759 #[test]
760 fn test_quota_invalid_unlimited_mixed() {
761 let quota = Quota {
762 id: None,
763 categories: [DataCategory::Error, DataCategory::Attachment].into(),
764 scope: QuotaScope::Organization,
765 scope_id: None,
766 limit: None,
767 window: None,
768 reason_code: None,
769 namespace: None,
770 };
771
772 assert!(!quota.is_valid());
774 }
775
776 #[test]
777 fn test_quota_matches_no_categories() {
778 let quota = Quota {
779 id: None,
780 categories: Default::default(),
781 scope: QuotaScope::Organization,
782 scope_id: None,
783 limit: None,
784 window: None,
785 reason_code: None,
786 namespace: None,
787 };
788
789 assert!(quota.matches(ItemScoping {
790 category: DataCategory::Error,
791 scoping: Scoping {
792 organization_id: OrganizationId::new(42),
793 project_id: ProjectId::new(21),
794 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
795 key_id: Some(17),
796 },
797 namespace: MetricNamespaceScoping::None,
798 }));
799 }
800
801 #[test]
802 fn test_quota_matches_unknown_category() {
803 let quota = Quota {
804 id: None,
805 categories: [DataCategory::Unknown].into(),
806 scope: QuotaScope::Organization,
807 scope_id: None,
808 limit: None,
809 window: None,
810 reason_code: None,
811 namespace: None,
812 };
813
814 assert!(!quota.matches(ItemScoping {
815 category: DataCategory::Error,
816 scoping: Scoping {
817 organization_id: OrganizationId::new(42),
818 project_id: ProjectId::new(21),
819 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
820 key_id: Some(17),
821 },
822 namespace: MetricNamespaceScoping::None,
823 }));
824 }
825
826 #[test]
827 fn test_quota_matches_multiple_categores() {
828 let quota = Quota {
829 id: None,
830 categories: [DataCategory::Unknown, DataCategory::Error].into(),
831 scope: QuotaScope::Organization,
832 scope_id: None,
833 limit: None,
834 window: None,
835 reason_code: None,
836 namespace: None,
837 };
838
839 assert!(quota.matches(ItemScoping {
840 category: DataCategory::Error,
841 scoping: Scoping {
842 organization_id: OrganizationId::new(42),
843 project_id: ProjectId::new(21),
844 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
845 key_id: Some(17),
846 },
847 namespace: MetricNamespaceScoping::None,
848 }));
849
850 assert!(!quota.matches(ItemScoping {
851 category: DataCategory::Transaction,
852 scoping: Scoping {
853 organization_id: OrganizationId::new(42),
854 project_id: ProjectId::new(21),
855 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
856 key_id: Some(17),
857 },
858 namespace: MetricNamespaceScoping::None,
859 }));
860 }
861
862 #[test]
863 fn test_quota_matches_no_invalid_scope() {
864 let quota = Quota {
865 id: None,
866 categories: Default::default(),
867 scope: QuotaScope::Organization,
868 scope_id: Some("not_a_number".into()),
869 limit: None,
870 window: None,
871 reason_code: None,
872 namespace: None,
873 };
874
875 assert!(!quota.matches(ItemScoping {
876 category: DataCategory::Error,
877 scoping: Scoping {
878 organization_id: OrganizationId::new(42),
879 project_id: ProjectId::new(21),
880 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
881 key_id: Some(17),
882 },
883 namespace: MetricNamespaceScoping::None,
884 }));
885 }
886
887 #[test]
888 fn test_quota_matches_organization_scope() {
889 let quota = Quota {
890 id: None,
891 categories: Default::default(),
892 scope: QuotaScope::Organization,
893 scope_id: Some("42".into()),
894 limit: None,
895 window: None,
896 reason_code: None,
897 namespace: None,
898 };
899
900 assert!(quota.matches(ItemScoping {
901 category: DataCategory::Error,
902 scoping: Scoping {
903 organization_id: OrganizationId::new(42),
904 project_id: ProjectId::new(21),
905 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
906 key_id: Some(17),
907 },
908 namespace: MetricNamespaceScoping::None,
909 }));
910
911 assert!(!quota.matches(ItemScoping {
912 category: DataCategory::Error,
913 scoping: Scoping {
914 organization_id: OrganizationId::new(0),
915 project_id: ProjectId::new(21),
916 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
917 key_id: Some(17),
918 },
919 namespace: MetricNamespaceScoping::None,
920 }));
921 }
922
923 #[test]
924 fn test_quota_matches_project_scope() {
925 let quota = Quota {
926 id: None,
927 categories: Default::default(),
928 scope: QuotaScope::Project,
929 scope_id: Some("21".into()),
930 limit: None,
931 window: None,
932 reason_code: None,
933 namespace: None,
934 };
935
936 assert!(quota.matches(ItemScoping {
937 category: DataCategory::Error,
938 scoping: Scoping {
939 organization_id: OrganizationId::new(42),
940 project_id: ProjectId::new(21),
941 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
942 key_id: Some(17),
943 },
944 namespace: MetricNamespaceScoping::None,
945 }));
946
947 assert!(!quota.matches(ItemScoping {
948 category: DataCategory::Error,
949 scoping: Scoping {
950 organization_id: OrganizationId::new(42),
951 project_id: ProjectId::new(0),
952 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
953 key_id: Some(17),
954 },
955 namespace: MetricNamespaceScoping::None,
956 }));
957 }
958
959 #[test]
960 fn test_quota_matches_key_scope() {
961 let quota = Quota {
962 id: None,
963 categories: Default::default(),
964 scope: QuotaScope::Key,
965 scope_id: Some("17".into()),
966 limit: None,
967 window: None,
968 reason_code: None,
969 namespace: None,
970 };
971
972 assert!(quota.matches(ItemScoping {
973 category: DataCategory::Error,
974 scoping: Scoping {
975 organization_id: OrganizationId::new(42),
976 project_id: ProjectId::new(21),
977 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
978 key_id: Some(17),
979 },
980 namespace: MetricNamespaceScoping::None,
981 }));
982
983 assert!(!quota.matches(ItemScoping {
984 category: DataCategory::Error,
985 scoping: Scoping {
986 organization_id: OrganizationId::new(42),
987 project_id: ProjectId::new(21),
988 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
989 key_id: Some(0),
990 },
991 namespace: MetricNamespaceScoping::None,
992 }));
993
994 assert!(!quota.matches(ItemScoping {
995 category: DataCategory::Error,
996 scoping: Scoping {
997 organization_id: OrganizationId::new(42),
998 project_id: ProjectId::new(21),
999 project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1000 key_id: None,
1001 },
1002 namespace: MetricNamespaceScoping::None,
1003 }));
1004 }
1005
1006 #[test]
1007 fn test_data_categories_sorted_deduplicated() {
1008 let a = DataCategories::from([
1009 DataCategory::Transaction,
1010 DataCategory::Span,
1011 DataCategory::Transaction,
1012 ]);
1013 let b = DataCategories::from([
1014 DataCategory::Span,
1015 DataCategory::Transaction,
1016 DataCategory::Span,
1017 ]);
1018 let c = DataCategories::from([DataCategory::Span, DataCategory::Transaction]);
1019
1020 assert_eq!(a, b);
1021 assert_eq!(b, c);
1022 assert_eq!(a, c);
1023 }
1024
1025 #[test]
1026 fn test_data_categories_serde() {
1027 let s: DataCategories = serde_json::from_str(r#"["span", "transaction", "span"]"#).unwrap();
1028 insta::assert_json_snapshot!(s, @r#"
1029 [
1030 "transaction",
1031 "span"
1032 ]
1033 "#);
1034 }
1035
1036 #[test]
1037 fn test_data_categories_add() {
1038 let c = DataCategories::new();
1039 let c = c.add(DataCategory::Span).unwrap();
1040 assert!(c.add(DataCategory::Span).is_none());
1041 let c = c.add(DataCategory::Transaction).unwrap();
1042 assert_eq!(c, [DataCategory::Span, DataCategory::Transaction].into());
1043 }
1044}