relay_base_schema/
data_category.rs

1//! Defines the [`DataCategory`] type that classifies data Relay can handle.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8use crate::events::EventType;
9
10/// An error that occurs if a number cannot be converted into a [`DataCategory`].
11#[derive(Debug, PartialEq, thiserror::Error)]
12#[error("Unknown numeric data category {0} can not be converted into a DataCategory.")]
13pub struct UnknownDataCategory(pub u8);
14
15/// Classifies the type of data that is being ingested.
16#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17#[repr(i8)]
18pub enum DataCategory {
19    /// Reserved and unused.
20    ///
21    /// SDK rate limiting behavior: ignore.
22    Default = 0,
23    /// Error events and Events with an `event_type` not explicitly listed below.
24    ///
25    /// SDK rate limiting behavior: apply to the entire envelope if it contains an item type `event`.
26    Error = 1,
27    /// Transaction events.
28    ///
29    /// SDK rate limiting behavior: apply to the entire envelope if it contains an item `transaction`.
30    Transaction = 2,
31    /// Events with an event type of `csp`, `hpkp`, `expectct` and `expectstaple`.
32    ///
33    /// SDK rate limiting behavior: ignore.
34    Security = 3,
35    /// An attachment. Quantity is the size of the attachment in bytes.
36    ///
37    /// SDK rate limiting behavior: apply to all attachments.
38    Attachment = 4,
39    /// Session updates. Quantity is the number of updates in the batch.
40    ///
41    /// SDK rate limiting behavior: apply to all sessions and session aggregates.
42    Session = 5,
43    /// Profile
44    ///
45    /// This is the category for processed profiles (all profiles, whether or not we store them).
46    ///
47    /// SDK rate limiting behavior: apply to all profiles.
48    Profile = 6,
49    /// Session Replays
50    ///
51    /// SDK rate limiting behavior: apply to all Session Replay data.
52    Replay = 7,
53    /// DEPRECATED: A transaction for which metrics were extracted.
54    ///
55    /// This category is now obsolete because the `Transaction` variant will represent
56    /// processed transactions from now on.
57    ///
58    /// SDK rate limiting behavior: ignore.
59    TransactionProcessed = 8,
60    /// Indexed transaction events.
61    ///
62    /// This is the category for transaction payloads that were accepted and stored in full. In
63    /// contrast, `transaction` only guarantees that metrics have been accepted for the transaction.
64    ///
65    /// SDK rate limiting behavior: ignore.
66    TransactionIndexed = 9,
67    /// Monitor check-ins.
68    ///
69    /// SDK rate limiting behavior: apply to items of type `check_in`.
70    Monitor = 10,
71    /// Indexed Profile
72    ///
73    /// This is the category for indexed profiles that will be stored later.
74    ///
75    /// SDK rate limiting behavior: ignore.
76    ProfileIndexed = 11,
77    /// Span
78    ///
79    /// This is the category for spans from which we extracted metrics from.
80    ///
81    /// SDK rate limiting behavior: apply to spans that are not sent in a transaction.
82    Span = 12,
83    /// Monitor Seat
84    ///
85    /// Represents a monitor job that has scheduled monitor checkins. The seats are not ingested
86    /// but we define it here to prevent clashing values since this data category enumeration
87    /// is also used outside of Relay via the Python package.
88    ///
89    /// SDK rate limiting behavior: ignore.
90    MonitorSeat = 13,
91    /// User Feedback
92    ///
93    /// Represents a User Feedback processed.
94    /// Currently standardized on name UserReportV2 to avoid clashing with the old UserReport.
95    /// TODO(jferg): Rename this to UserFeedback once old UserReport is deprecated.
96    ///
97    /// SDK rate limiting behavior: apply to items of type 'feedback'.
98    UserReportV2 = 14,
99    /// Metric buckets.
100    ///
101    /// SDK rate limiting behavior: apply to `statsd` and `metrics` items.
102    MetricBucket = 15,
103    /// SpanIndexed
104    ///
105    /// This is the category for spans we store in full.
106    ///
107    /// SDK rate limiting behavior: ignore.
108    SpanIndexed = 16,
109    /// ProfileDuration
110    ///
111    /// This data category is used to count the number of milliseconds per indexed profile chunk,
112    /// excluding UI profile chunks.
113    ///
114    /// SDK rate limiting behavior: apply to profile chunks.
115    ProfileDuration = 17,
116    /// ProfileChunk
117    ///
118    /// This is a count of profile chunks received. It will not be used for billing but will be
119    /// useful for customers to track what's being dropped.
120    ///
121    /// SDK rate limiting behavior: apply to profile chunks.
122    ProfileChunk = 18,
123    /// MetricSecond
124    ///
125    /// Reserved by billing to summarize the bucketed product of metric volume
126    /// and metric cardinality. Defined here so as not to clash with future
127    /// categories.
128    ///
129    /// SDK rate limiting behavior: ignore.
130    MetricSecond = 19,
131    /// Replay Video
132    ///
133    /// This is the data category for Session Replays produced via a video recording.
134    ///
135    /// SDK rate limiting behavior: ignore.
136    DoNotUseReplayVideo = 20,
137    /// This is the data category for Uptime monitors.
138    ///
139    /// SDK rate limiting behavior: ignore.
140    Uptime = 21,
141    /// Counts the number of individual attachments, as opposed to the number of bytes in an attachment.
142    ///
143    /// SDK rate limiting behavior: apply to attachments.
144    AttachmentItem = 22,
145    /// LogItem
146    ///
147    /// This is the category for logs for which we store the count log events for users for measuring
148    /// missing breadcrumbs, and count of logs for rate limiting purposes.
149    ///
150    /// SDK rate limiting behavior: apply to logs.
151    LogItem = 23,
152    /// LogByte
153    ///
154    /// This is the category for logs for which we store log event total bytes for users.
155    ///
156    /// SDK rate limiting behavior: apply to logs.
157    LogByte = 24,
158    /// Profile duration of a UI profile.
159    ///
160    /// This data category is used to count the number of milliseconds per indexed UI profile
161    /// chunk.
162    ///
163    /// See also: [`Self::ProfileDuration`]
164    ///
165    /// SDK rate limiting behavior: apply to profile chunks.
166    ProfileDurationUi = 25,
167    /// UI Profile Chunk.
168    ///
169    /// This data category is used to count the number of milliseconds per indexed UI profile
170    /// chunk.
171    ///
172    /// See also: [`Self::ProfileChunk`]
173    ///
174    /// SDK rate limiting behavior: apply to profile chunks.
175    ProfileChunkUi = 26,
176    /// This is the data category to count Seer Autofix run events.
177    ///
178    /// SDK rate limiting behavior: ignore.
179    SeerAutofix = 27,
180    /// This is the data category to count Seer Scanner run events.
181    ///
182    /// SDK rate limiting behavior: ignore.
183    SeerScanner = 28,
184    /// DEPRECATED: Use SeerUser instead.
185    ///
186    /// PreventUser
187    ///
188    /// This is the data category to count the number of assigned Prevent Users.
189    ///
190    /// SDK rate limiting behavior: ignore.
191    PreventUser = 29,
192    /// PreventReview
193    ///
194    /// This is the data category to count the number of Prevent review events.
195    ///
196    /// SDK rate limiting behavior: ignore.
197    PreventReview = 30,
198    /// Size analysis
199    ///
200    /// This is the data category to count the number of size analyses performed.
201    /// 'Size analysis' a static binary analysis of a preprod build artifact
202    /// (e.g. the .apk of an Android app or MacOS .app).
203    /// When enabled there will typically be one such analysis per uploaded artifact.
204    ///
205    /// SDK rate limiting behavior: ignore.
206    SizeAnalysis = 31,
207    /// InstallableBuild
208    ///
209    /// This is the data category to count the number of installable builds.
210    /// It counts the number of artifacts uploaded *not* the number of times the
211    /// artifacts are downloaded for installation.
212    /// When enabled there will typically be one 'InstallableBuild' per uploaded artifact.
213    ///
214    /// SDK rate limiting behavior: ignore.
215    InstallableBuild = 32,
216    /// TraceMetric
217    ///
218    /// This is the data category to count the number of trace metric items.
219    TraceMetric = 33,
220    /// SeerUser
221    ///
222    /// This is the data category to count the number of Seer users.
223    ///
224    /// SDK rate limiting behavior: ignore.
225    SeerUser = 34,
226    //
227    // IMPORTANT: After adding a new entry to DataCategory, go to the `relay-cabi` subfolder and run
228    // `make header` to regenerate the C-binding. This allows using the data category from Python.
229    // Rerun this step every time the **code name** of the variant is updated.
230    //
231    /// Any other data category not known by this Relay.
232    Unknown = -1,
233}
234
235impl DataCategory {
236    /// Returns the data category corresponding to the given name.
237    pub fn from_name(string: &str) -> Self {
238        match string {
239            "default" => Self::Default,
240            "error" => Self::Error,
241            "transaction" => Self::Transaction,
242            "security" => Self::Security,
243            "attachment" => Self::Attachment,
244            "session" => Self::Session,
245            "profile" => Self::Profile,
246            "profile_indexed" => Self::ProfileIndexed,
247            "replay" => Self::Replay,
248            "transaction_processed" => Self::TransactionProcessed,
249            "transaction_indexed" => Self::TransactionIndexed,
250            "monitor" => Self::Monitor,
251            "span" => Self::Span,
252            "log_item" => Self::LogItem,
253            "log_byte" => Self::LogByte,
254            "monitor_seat" => Self::MonitorSeat,
255            "feedback" => Self::UserReportV2,
256            "user_report_v2" => Self::UserReportV2,
257            "metric_bucket" => Self::MetricBucket,
258            "span_indexed" => Self::SpanIndexed,
259            "profile_duration" => Self::ProfileDuration,
260            "profile_duration_ui" => Self::ProfileDurationUi,
261            "profile_chunk" => Self::ProfileChunk,
262            "profile_chunk_ui" => Self::ProfileChunkUi,
263            "metric_second" => Self::MetricSecond,
264            "replay_video" => Self::DoNotUseReplayVideo,
265            "uptime" => Self::Uptime,
266            "attachment_item" => Self::AttachmentItem,
267            "seer_autofix" => Self::SeerAutofix,
268            "seer_scanner" => Self::SeerScanner,
269            "prevent_user" => Self::PreventUser,
270            "prevent_review" => Self::PreventReview,
271            "size_analysis" => Self::SizeAnalysis,
272            "installable_build" => Self::InstallableBuild,
273            "trace_metric" => Self::TraceMetric,
274            "seer_user" => Self::SeerUser,
275            _ => Self::Unknown,
276        }
277    }
278
279    /// Returns the canonical name of this data category.
280    pub fn name(self) -> &'static str {
281        match self {
282            Self::Default => "default",
283            Self::Error => "error",
284            Self::Transaction => "transaction",
285            Self::Security => "security",
286            Self::Attachment => "attachment",
287            Self::Session => "session",
288            Self::Profile => "profile",
289            Self::ProfileIndexed => "profile_indexed",
290            Self::Replay => "replay",
291            Self::DoNotUseReplayVideo => "replay_video",
292            Self::TransactionProcessed => "transaction_processed",
293            Self::TransactionIndexed => "transaction_indexed",
294            Self::Monitor => "monitor",
295            Self::Span => "span",
296            Self::LogItem => "log_item",
297            Self::LogByte => "log_byte",
298            Self::MonitorSeat => "monitor_seat",
299            Self::UserReportV2 => "feedback",
300            Self::MetricBucket => "metric_bucket",
301            Self::SpanIndexed => "span_indexed",
302            Self::ProfileDuration => "profile_duration",
303            Self::ProfileDurationUi => "profile_duration_ui",
304            Self::ProfileChunk => "profile_chunk",
305            Self::ProfileChunkUi => "profile_chunk_ui",
306            Self::MetricSecond => "metric_second",
307            Self::Uptime => "uptime",
308            Self::AttachmentItem => "attachment_item",
309            Self::SeerAutofix => "seer_autofix",
310            Self::SeerScanner => "seer_scanner",
311            Self::PreventUser => "prevent_user",
312            Self::PreventReview => "prevent_review",
313            Self::SizeAnalysis => "size_analysis",
314            Self::InstallableBuild => "installable_build",
315            Self::TraceMetric => "trace_metric",
316            Self::SeerUser => "seer_user",
317            Self::Unknown => "unknown",
318        }
319    }
320
321    /// Returns true if the DataCategory refers to an error (i.e an error event).
322    pub fn is_error(self) -> bool {
323        matches!(self, Self::Error | Self::Default | Self::Security)
324    }
325
326    /// Returns the numeric value for this outcome.
327    pub fn value(self) -> Option<u8> {
328        // negative values (Internal and Unknown) cannot be sent as
329        // outcomes (internally so!)
330        (self as i8).try_into().ok()
331    }
332
333    /// Returns a dedicated category for indexing if this data can be converted to metrics.
334    ///
335    /// This returns `None` for most data categories.
336    pub fn index_category(self) -> Option<Self> {
337        match self {
338            Self::Transaction => Some(Self::TransactionIndexed),
339            Self::Span => Some(Self::SpanIndexed),
340            Self::Profile => Some(Self::ProfileIndexed),
341            _ => None,
342        }
343    }
344
345    /// Returns `true` if this data category is an indexed data category.
346    pub fn is_indexed(self) -> bool {
347        matches!(
348            self,
349            Self::TransactionIndexed | Self::SpanIndexed | Self::ProfileIndexed
350        )
351    }
352}
353
354relay_common::impl_str_serde!(DataCategory, "a data category");
355
356impl fmt::Display for DataCategory {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        write!(f, "{}", self.name())
359    }
360}
361
362impl FromStr for DataCategory {
363    type Err = std::convert::Infallible;
364
365    fn from_str(string: &str) -> Result<Self, Self::Err> {
366        Ok(Self::from_name(string))
367    }
368}
369
370impl From<EventType> for DataCategory {
371    fn from(ty: EventType) -> Self {
372        match ty {
373            EventType::Default | EventType::Error | EventType::Nel => Self::Error,
374            EventType::Transaction => Self::Transaction,
375            EventType::Csp | EventType::Hpkp | EventType::ExpectCt | EventType::ExpectStaple => {
376                Self::Security
377            }
378            EventType::UserReportV2 => Self::UserReportV2,
379        }
380    }
381}
382
383impl TryFrom<u8> for DataCategory {
384    type Error = UnknownDataCategory;
385
386    fn try_from(value: u8) -> Result<Self, UnknownDataCategory> {
387        match value {
388            0 => Ok(Self::Default),
389            1 => Ok(Self::Error),
390            2 => Ok(Self::Transaction),
391            3 => Ok(Self::Security),
392            4 => Ok(Self::Attachment),
393            5 => Ok(Self::Session),
394            6 => Ok(Self::Profile),
395            7 => Ok(Self::Replay),
396            8 => Ok(Self::TransactionProcessed),
397            9 => Ok(Self::TransactionIndexed),
398            10 => Ok(Self::Monitor),
399            11 => Ok(Self::ProfileIndexed),
400            12 => Ok(Self::Span),
401            13 => Ok(Self::MonitorSeat),
402            14 => Ok(Self::UserReportV2),
403            15 => Ok(Self::MetricBucket),
404            16 => Ok(Self::SpanIndexed),
405            17 => Ok(Self::ProfileDuration),
406            18 => Ok(Self::ProfileChunk),
407            19 => Ok(Self::MetricSecond),
408            20 => Ok(Self::DoNotUseReplayVideo),
409            21 => Ok(Self::Uptime),
410            22 => Ok(Self::AttachmentItem),
411            23 => Ok(Self::LogItem),
412            24 => Ok(Self::LogByte),
413            25 => Ok(Self::ProfileDurationUi),
414            26 => Ok(Self::ProfileChunkUi),
415            27 => Ok(Self::SeerAutofix),
416            28 => Ok(Self::SeerScanner),
417            29 => Ok(Self::PreventUser),
418            30 => Ok(Self::PreventReview),
419            31 => Ok(Self::SizeAnalysis),
420            32 => Ok(Self::InstallableBuild),
421            33 => Ok(Self::TraceMetric),
422            34 => Ok(Self::SeerUser),
423            other => Err(UnknownDataCategory(other)),
424        }
425    }
426}
427
428/// The unit in which a data category is measured.
429///
430/// This enum specifies how quantities for different data categories are measured,
431/// which affects how quota limits are interpreted and enforced.
432///
433/// Note: There is no `Unknown` variant. For categories without a defined unit
434/// (e.g., `DataCategory::Unknown`), methods return `Option::None`.
435//
436// IMPORTANT: After adding a new entry to CategoryUnit, go to the `relay-cabi` subfolder and run
437// `make header` to regenerate the C-binding. This allows using the category unit from Python.
438//
439#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
440#[serde(rename_all = "snake_case")]
441#[repr(i8)]
442pub enum CategoryUnit {
443    /// Counts the number of discrete items.
444    Count = 0,
445    /// Counts the number of bytes across items.
446    Bytes = 1,
447    /// Counts the accumulated time in milliseconds across items.
448    Milliseconds = 2,
449}
450
451impl CategoryUnit {
452    /// Returns the canonical name of this category unit.
453    pub fn name(self) -> &'static str {
454        match self {
455            Self::Count => "count",
456            Self::Bytes => "bytes",
457            Self::Milliseconds => "milliseconds",
458        }
459    }
460
461    /// Returns the category unit corresponding to the given name string.
462    ///
463    /// Returns `None` if the string doesn't match any known unit.
464    pub fn from_name(string: &str) -> Option<Self> {
465        match string {
466            "count" => Some(Self::Count),
467            "bytes" => Some(Self::Bytes),
468            "milliseconds" => Some(Self::Milliseconds),
469            _ => None,
470        }
471    }
472
473    /// Returns the `CategoryUnit` for the given `DataCategory`.
474    ///
475    /// Returns `None` for `DataCategory::Unknown`.
476    ///
477    /// Note: Takes a reference to avoid unnecessary copying and allow direct use with iterators.
478    pub fn from_category(category: &DataCategory) -> Option<Self> {
479        match category {
480            DataCategory::Default
481            | DataCategory::Error
482            | DataCategory::Transaction
483            | DataCategory::Replay
484            | DataCategory::DoNotUseReplayVideo
485            | DataCategory::Security
486            | DataCategory::Profile
487            | DataCategory::ProfileIndexed
488            | DataCategory::TransactionProcessed
489            | DataCategory::TransactionIndexed
490            | DataCategory::LogItem
491            | DataCategory::Span
492            | DataCategory::SpanIndexed
493            | DataCategory::MonitorSeat
494            | DataCategory::Monitor
495            | DataCategory::MetricBucket
496            | DataCategory::UserReportV2
497            | DataCategory::ProfileChunk
498            | DataCategory::ProfileChunkUi
499            | DataCategory::Uptime
500            | DataCategory::MetricSecond
501            | DataCategory::AttachmentItem
502            | DataCategory::SeerAutofix
503            | DataCategory::SeerScanner
504            | DataCategory::PreventUser
505            | DataCategory::PreventReview
506            | DataCategory::Session
507            | DataCategory::SizeAnalysis
508            | DataCategory::InstallableBuild
509            | DataCategory::TraceMetric
510            | DataCategory::SeerUser => Some(Self::Count),
511
512            DataCategory::Attachment | DataCategory::LogByte => Some(Self::Bytes),
513
514            DataCategory::ProfileDuration | DataCategory::ProfileDurationUi => {
515                Some(Self::Milliseconds)
516            }
517
518            DataCategory::Unknown => None,
519        }
520    }
521}
522
523impl fmt::Display for CategoryUnit {
524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525        write!(f, "{}", self.name())
526    }
527}
528
529impl FromStr for CategoryUnit {
530    type Err = ();
531
532    fn from_str(string: &str) -> Result<Self, Self::Err> {
533        Self::from_name(string).ok_or(())
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    #[test]
542    fn test_last_variant_conversion() {
543        // If this test fails, update the numeric bounds so that the first assertion
544        // maps to the last variant in the enum and the second assertion produces an error
545        // that the DataCategory does not exist.
546        assert_eq!(DataCategory::try_from(34), Ok(DataCategory::SeerUser));
547        assert_eq!(DataCategory::try_from(35), Err(UnknownDataCategory(35)));
548    }
549
550    #[test]
551    fn test_data_category_alias() {
552        assert_eq!("feedback".parse(), Ok(DataCategory::UserReportV2));
553        assert_eq!("user_report_v2".parse(), Ok(DataCategory::UserReportV2));
554        assert_eq!(&DataCategory::UserReportV2.to_string(), "feedback");
555
556        assert_eq!(
557            serde_json::from_str::<DataCategory>(r#""feedback""#).unwrap(),
558            DataCategory::UserReportV2,
559        );
560        assert_eq!(
561            serde_json::from_str::<DataCategory>(r#""user_report_v2""#).unwrap(),
562            DataCategory::UserReportV2,
563        );
564        assert_eq!(
565            &serde_json::to_string(&DataCategory::UserReportV2).unwrap(),
566            r#""feedback""#
567        )
568    }
569
570    #[test]
571    fn test_category_unit_name() {
572        assert_eq!(CategoryUnit::Count.name(), "count");
573        assert_eq!(CategoryUnit::Bytes.name(), "bytes");
574        assert_eq!(CategoryUnit::Milliseconds.name(), "milliseconds");
575    }
576
577    #[test]
578    fn test_category_unit_from_name() {
579        assert_eq!(CategoryUnit::from_name("count"), Some(CategoryUnit::Count));
580        assert_eq!(CategoryUnit::from_name("bytes"), Some(CategoryUnit::Bytes));
581        assert_eq!(
582            CategoryUnit::from_name("milliseconds"),
583            Some(CategoryUnit::Milliseconds)
584        );
585        assert_eq!(CategoryUnit::from_name("unknown"), None);
586        assert_eq!(CategoryUnit::from_name(""), None);
587    }
588
589    #[test]
590    fn test_category_unit_from_category() {
591        // Count categories
592        assert_eq!(
593            CategoryUnit::from_category(&DataCategory::Error),
594            Some(CategoryUnit::Count)
595        );
596        assert_eq!(
597            CategoryUnit::from_category(&DataCategory::Transaction),
598            Some(CategoryUnit::Count)
599        );
600        assert_eq!(
601            CategoryUnit::from_category(&DataCategory::Span),
602            Some(CategoryUnit::Count)
603        );
604
605        // Bytes categories
606        assert_eq!(
607            CategoryUnit::from_category(&DataCategory::Attachment),
608            Some(CategoryUnit::Bytes)
609        );
610        assert_eq!(
611            CategoryUnit::from_category(&DataCategory::LogByte),
612            Some(CategoryUnit::Bytes)
613        );
614
615        // Milliseconds categories
616        assert_eq!(
617            CategoryUnit::from_category(&DataCategory::ProfileDuration),
618            Some(CategoryUnit::Milliseconds)
619        );
620        assert_eq!(
621            CategoryUnit::from_category(&DataCategory::ProfileDurationUi),
622            Some(CategoryUnit::Milliseconds)
623        );
624
625        // Unknown returns None
626        assert_eq!(CategoryUnit::from_category(&DataCategory::Unknown), None);
627    }
628
629    #[test]
630    fn test_category_unit_display() {
631        assert_eq!(format!("{}", CategoryUnit::Count), "count");
632        assert_eq!(format!("{}", CategoryUnit::Bytes), "bytes");
633        assert_eq!(format!("{}", CategoryUnit::Milliseconds), "milliseconds");
634    }
635
636    #[test]
637    fn test_category_unit_from_str() {
638        assert_eq!("count".parse::<CategoryUnit>(), Ok(CategoryUnit::Count));
639        assert_eq!("bytes".parse::<CategoryUnit>(), Ok(CategoryUnit::Bytes));
640        assert_eq!(
641            "milliseconds".parse::<CategoryUnit>(),
642            Ok(CategoryUnit::Milliseconds)
643        );
644        assert!("invalid".parse::<CategoryUnit>().is_err());
645    }
646
647    #[test]
648    fn test_category_unit_repr_values() {
649        // Verify the repr(i8) values are correct for FFI
650        assert_eq!(CategoryUnit::Count as i8, 0);
651        assert_eq!(CategoryUnit::Bytes as i8, 1);
652        assert_eq!(CategoryUnit::Milliseconds as i8, 2);
653    }
654}