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 u32);
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    /// Transaction profiles for backend platforms.
227    ///
228    /// This is an extension of [`Self::Profile`], but additionally discriminates on the profile
229    /// platform, see also [`Self::ProfileUi`].
230    ///
231    /// Continuous profiling uses [`Self::ProfileChunk`] and [`Self::ProfileChunkUi`].
232    ///
233    /// SDK rate limiting behavior: optional, apply to transaction profiles on "backend platforms".
234    ProfileBackend = 35,
235    /// Transaction profiles for ui platforms.
236    ///
237    /// This is an extension of [`Self::Profile`], but additionally discriminates on the profile
238    /// platform, see also [`Self::ProfileBackend`].
239    ///
240    /// Continuous profiling uses [`Self::ProfileChunk`] and [`Self::ProfileChunkUi`].
241    ///
242    /// SDK rate limiting behavior: optional, apply to transaction profiles on "ui platforms".
243    ProfileUi = 36,
244    /// TraceMetricByte
245    ///
246    /// This is the category for trace metrics for which we store total bytes for users.
247    TraceMetricByte = 37,
248    //
249    // IMPORTANT: After adding a new entry to DataCategory, go to the `relay-cabi` subfolder and run
250    // `make header` to regenerate the C-binding. This allows using the data category from Python.
251    // Rerun this step every time the **code name** of the variant is updated.
252    //
253    /// Any other data category not known by this Relay.
254    Unknown = -1,
255}
256
257impl DataCategory {
258    /// Returns the data category corresponding to the given name.
259    pub fn from_name(string: &str) -> Self {
260        match string {
261            "default" => Self::Default,
262            "error" => Self::Error,
263            "transaction" => Self::Transaction,
264            "security" => Self::Security,
265            "attachment" => Self::Attachment,
266            "session" => Self::Session,
267            "profile" => Self::Profile,
268            "profile_indexed" => Self::ProfileIndexed,
269            "replay" => Self::Replay,
270            "transaction_processed" => Self::TransactionProcessed,
271            "transaction_indexed" => Self::TransactionIndexed,
272            "monitor" => Self::Monitor,
273            "span" => Self::Span,
274            "log_item" => Self::LogItem,
275            "log_byte" => Self::LogByte,
276            "monitor_seat" => Self::MonitorSeat,
277            "feedback" => Self::UserReportV2,
278            "user_report_v2" => Self::UserReportV2,
279            "metric_bucket" => Self::MetricBucket,
280            "span_indexed" => Self::SpanIndexed,
281            "profile_duration" => Self::ProfileDuration,
282            "profile_duration_ui" => Self::ProfileDurationUi,
283            "profile_chunk" => Self::ProfileChunk,
284            "profile_chunk_ui" => Self::ProfileChunkUi,
285            "metric_second" => Self::MetricSecond,
286            "replay_video" => Self::DoNotUseReplayVideo,
287            "uptime" => Self::Uptime,
288            "attachment_item" => Self::AttachmentItem,
289            "seer_autofix" => Self::SeerAutofix,
290            "seer_scanner" => Self::SeerScanner,
291            "prevent_user" => Self::PreventUser,
292            "prevent_review" => Self::PreventReview,
293            "size_analysis" => Self::SizeAnalysis,
294            "installable_build" => Self::InstallableBuild,
295            "trace_metric" => Self::TraceMetric,
296            "trace_metric_byte" => Self::TraceMetricByte,
297            "seer_user" => Self::SeerUser,
298            "profile_backend" => Self::ProfileBackend,
299            "profile_ui" => Self::ProfileUi,
300            _ => Self::Unknown,
301        }
302    }
303
304    /// Returns the canonical name of this data category.
305    pub fn name(self) -> &'static str {
306        match self {
307            Self::Default => "default",
308            Self::Error => "error",
309            Self::Transaction => "transaction",
310            Self::Security => "security",
311            Self::Attachment => "attachment",
312            Self::Session => "session",
313            Self::Profile => "profile",
314            Self::ProfileIndexed => "profile_indexed",
315            Self::Replay => "replay",
316            Self::DoNotUseReplayVideo => "replay_video",
317            Self::TransactionProcessed => "transaction_processed",
318            Self::TransactionIndexed => "transaction_indexed",
319            Self::Monitor => "monitor",
320            Self::Span => "span",
321            Self::LogItem => "log_item",
322            Self::LogByte => "log_byte",
323            Self::MonitorSeat => "monitor_seat",
324            Self::UserReportV2 => "feedback",
325            Self::MetricBucket => "metric_bucket",
326            Self::SpanIndexed => "span_indexed",
327            Self::ProfileDuration => "profile_duration",
328            Self::ProfileDurationUi => "profile_duration_ui",
329            Self::ProfileChunk => "profile_chunk",
330            Self::ProfileChunkUi => "profile_chunk_ui",
331            Self::MetricSecond => "metric_second",
332            Self::Uptime => "uptime",
333            Self::AttachmentItem => "attachment_item",
334            Self::SeerAutofix => "seer_autofix",
335            Self::SeerScanner => "seer_scanner",
336            Self::PreventUser => "prevent_user",
337            Self::PreventReview => "prevent_review",
338            Self::SizeAnalysis => "size_analysis",
339            Self::InstallableBuild => "installable_build",
340            Self::TraceMetric => "trace_metric",
341            Self::TraceMetricByte => "trace_metric_byte",
342            Self::SeerUser => "seer_user",
343            Self::ProfileBackend => "profile_backend",
344            Self::ProfileUi => "profile_ui",
345            Self::Unknown => "unknown",
346        }
347    }
348
349    /// Returns true if the DataCategory refers to an error (i.e an error event).
350    pub fn is_error(self) -> bool {
351        matches!(self, Self::Error | Self::Default | Self::Security)
352    }
353
354    /// Returns the numeric value for this outcome.
355    pub fn value(self) -> Option<u8> {
356        // negative values (Internal and Unknown) cannot be sent as
357        // outcomes (internally so!)
358        (self as i8).try_into().ok()
359    }
360
361    /// Returns a dedicated category for indexing if this data can be converted to metrics.
362    ///
363    /// This returns `None` for most data categories.
364    pub fn index_category(self) -> Option<Self> {
365        match self {
366            Self::Transaction => Some(Self::TransactionIndexed),
367            Self::Span => Some(Self::SpanIndexed),
368            Self::Profile => Some(Self::ProfileIndexed),
369            _ => None,
370        }
371    }
372
373    /// Returns `true` if this data category is an indexed data category.
374    pub fn is_indexed(self) -> bool {
375        matches!(
376            self,
377            Self::TransactionIndexed | Self::SpanIndexed | Self::ProfileIndexed
378        )
379    }
380}
381
382relay_common::impl_str_serde!(DataCategory, "a data category");
383
384impl fmt::Display for DataCategory {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        write!(f, "{}", self.name())
387    }
388}
389
390impl FromStr for DataCategory {
391    type Err = std::convert::Infallible;
392
393    fn from_str(string: &str) -> Result<Self, Self::Err> {
394        Ok(Self::from_name(string))
395    }
396}
397
398impl From<EventType> for DataCategory {
399    fn from(ty: EventType) -> Self {
400        match ty {
401            EventType::Default | EventType::Error | EventType::Nel => Self::Error,
402            EventType::Transaction => Self::Transaction,
403            EventType::Csp | EventType::Hpkp | EventType::ExpectCt | EventType::ExpectStaple => {
404                Self::Security
405            }
406            EventType::UserReportV2 => Self::UserReportV2,
407        }
408    }
409}
410
411impl TryFrom<u8> for DataCategory {
412    type Error = UnknownDataCategory;
413
414    fn try_from(value: u8) -> Result<Self, UnknownDataCategory> {
415        match value {
416            0 => Ok(Self::Default),
417            1 => Ok(Self::Error),
418            2 => Ok(Self::Transaction),
419            3 => Ok(Self::Security),
420            4 => Ok(Self::Attachment),
421            5 => Ok(Self::Session),
422            6 => Ok(Self::Profile),
423            7 => Ok(Self::Replay),
424            8 => Ok(Self::TransactionProcessed),
425            9 => Ok(Self::TransactionIndexed),
426            10 => Ok(Self::Monitor),
427            11 => Ok(Self::ProfileIndexed),
428            12 => Ok(Self::Span),
429            13 => Ok(Self::MonitorSeat),
430            14 => Ok(Self::UserReportV2),
431            15 => Ok(Self::MetricBucket),
432            16 => Ok(Self::SpanIndexed),
433            17 => Ok(Self::ProfileDuration),
434            18 => Ok(Self::ProfileChunk),
435            19 => Ok(Self::MetricSecond),
436            20 => Ok(Self::DoNotUseReplayVideo),
437            21 => Ok(Self::Uptime),
438            22 => Ok(Self::AttachmentItem),
439            23 => Ok(Self::LogItem),
440            24 => Ok(Self::LogByte),
441            25 => Ok(Self::ProfileDurationUi),
442            26 => Ok(Self::ProfileChunkUi),
443            27 => Ok(Self::SeerAutofix),
444            28 => Ok(Self::SeerScanner),
445            29 => Ok(Self::PreventUser),
446            30 => Ok(Self::PreventReview),
447            31 => Ok(Self::SizeAnalysis),
448            32 => Ok(Self::InstallableBuild),
449            33 => Ok(Self::TraceMetric),
450            34 => Ok(Self::SeerUser),
451            35 => Ok(Self::ProfileBackend),
452            36 => Ok(Self::ProfileUi),
453            37 => Ok(Self::TraceMetricByte),
454            other => Err(UnknownDataCategory(other as u32)),
455        }
456    }
457}
458
459impl TryFrom<u32> for DataCategory {
460    type Error = UnknownDataCategory;
461
462    fn try_from(value: u32) -> Result<Self, UnknownDataCategory> {
463        let value = u8::try_from(value).map_err(|_| UnknownDataCategory(value))?;
464        value.try_into()
465    }
466}
467
468/// The unit in which a data category is measured.
469///
470/// This enum specifies how quantities for different data categories are measured,
471/// which affects how quota limits are interpreted and enforced.
472///
473/// Note: There is no `Unknown` variant. For categories without a defined unit
474/// (e.g., `DataCategory::Unknown`), methods return `Option::None`.
475//
476// IMPORTANT: After adding a new entry to CategoryUnit, go to the `relay-cabi` subfolder and run
477// `make header` to regenerate the C-binding. This allows using the category unit from Python.
478//
479#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
480#[serde(rename_all = "snake_case")]
481#[repr(i8)]
482pub enum CategoryUnit {
483    /// Counts the number of discrete items.
484    Count = 0,
485    /// Counts the number of bytes across items.
486    Bytes = 1,
487    /// Counts the accumulated time in milliseconds across items.
488    Milliseconds = 2,
489}
490
491impl CategoryUnit {
492    /// Returns the canonical name of this category unit.
493    pub fn name(self) -> &'static str {
494        match self {
495            Self::Count => "count",
496            Self::Bytes => "bytes",
497            Self::Milliseconds => "milliseconds",
498        }
499    }
500
501    /// Returns the category unit corresponding to the given name string.
502    ///
503    /// Returns `None` if the string doesn't match any known unit.
504    pub fn from_name(string: &str) -> Option<Self> {
505        match string {
506            "count" => Some(Self::Count),
507            "bytes" => Some(Self::Bytes),
508            "milliseconds" => Some(Self::Milliseconds),
509            _ => None,
510        }
511    }
512
513    /// Returns the `CategoryUnit` for the given `DataCategory`.
514    ///
515    /// Returns `None` for `DataCategory::Unknown`.
516    ///
517    /// Note: Takes a reference to avoid unnecessary copying and allow direct use with iterators.
518    pub fn from_category(category: &DataCategory) -> Option<Self> {
519        match category {
520            DataCategory::Default
521            | DataCategory::Error
522            | DataCategory::Transaction
523            | DataCategory::Replay
524            | DataCategory::DoNotUseReplayVideo
525            | DataCategory::Security
526            | DataCategory::Profile
527            | DataCategory::ProfileIndexed
528            | DataCategory::TransactionProcessed
529            | DataCategory::TransactionIndexed
530            | DataCategory::LogItem
531            | DataCategory::Span
532            | DataCategory::SpanIndexed
533            | DataCategory::MonitorSeat
534            | DataCategory::Monitor
535            | DataCategory::MetricBucket
536            | DataCategory::UserReportV2
537            | DataCategory::ProfileChunk
538            | DataCategory::ProfileChunkUi
539            | DataCategory::Uptime
540            | DataCategory::MetricSecond
541            | DataCategory::AttachmentItem
542            | DataCategory::SeerAutofix
543            | DataCategory::SeerScanner
544            | DataCategory::PreventUser
545            | DataCategory::PreventReview
546            | DataCategory::Session
547            | DataCategory::SizeAnalysis
548            | DataCategory::InstallableBuild
549            | DataCategory::TraceMetric
550            | DataCategory::SeerUser
551            | DataCategory::ProfileBackend
552            | DataCategory::ProfileUi => Some(Self::Count),
553
554            DataCategory::Attachment | DataCategory::LogByte | DataCategory::TraceMetricByte => {
555                Some(Self::Bytes)
556            }
557
558            DataCategory::ProfileDuration | DataCategory::ProfileDurationUi => {
559                Some(Self::Milliseconds)
560            }
561
562            DataCategory::Unknown => None,
563        }
564    }
565}
566
567impl fmt::Display for CategoryUnit {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        write!(f, "{}", self.name())
570    }
571}
572
573impl FromStr for CategoryUnit {
574    type Err = ();
575
576    fn from_str(string: &str) -> Result<Self, Self::Err> {
577        Self::from_name(string).ok_or(())
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    #[test]
586    fn test_last_variant_conversion() {
587        // If this test fails, update the numeric bounds so that the first assertion
588        // maps to the last variant in the enum and the second assertion produces an error
589        // that the DataCategory does not exist.
590        assert_eq!(
591            DataCategory::try_from(37u8),
592            Ok(DataCategory::TraceMetricByte)
593        );
594        assert_eq!(DataCategory::try_from(38u8), Err(UnknownDataCategory(38)));
595    }
596
597    #[test]
598    fn test_data_category_alias() {
599        assert_eq!("feedback".parse(), Ok(DataCategory::UserReportV2));
600        assert_eq!("user_report_v2".parse(), Ok(DataCategory::UserReportV2));
601        assert_eq!(&DataCategory::UserReportV2.to_string(), "feedback");
602
603        assert_eq!(
604            serde_json::from_str::<DataCategory>(r#""feedback""#).unwrap(),
605            DataCategory::UserReportV2,
606        );
607        assert_eq!(
608            serde_json::from_str::<DataCategory>(r#""user_report_v2""#).unwrap(),
609            DataCategory::UserReportV2,
610        );
611        assert_eq!(
612            &serde_json::to_string(&DataCategory::UserReportV2).unwrap(),
613            r#""feedback""#
614        )
615    }
616
617    #[test]
618    fn test_category_unit_name() {
619        assert_eq!(CategoryUnit::Count.name(), "count");
620        assert_eq!(CategoryUnit::Bytes.name(), "bytes");
621        assert_eq!(CategoryUnit::Milliseconds.name(), "milliseconds");
622    }
623
624    #[test]
625    fn test_category_unit_from_name() {
626        assert_eq!(CategoryUnit::from_name("count"), Some(CategoryUnit::Count));
627        assert_eq!(CategoryUnit::from_name("bytes"), Some(CategoryUnit::Bytes));
628        assert_eq!(
629            CategoryUnit::from_name("milliseconds"),
630            Some(CategoryUnit::Milliseconds)
631        );
632        assert_eq!(CategoryUnit::from_name("unknown"), None);
633        assert_eq!(CategoryUnit::from_name(""), None);
634    }
635
636    #[test]
637    fn test_category_unit_from_category() {
638        // Count categories
639        assert_eq!(
640            CategoryUnit::from_category(&DataCategory::Error),
641            Some(CategoryUnit::Count)
642        );
643        assert_eq!(
644            CategoryUnit::from_category(&DataCategory::Transaction),
645            Some(CategoryUnit::Count)
646        );
647        assert_eq!(
648            CategoryUnit::from_category(&DataCategory::Span),
649            Some(CategoryUnit::Count)
650        );
651
652        // Bytes categories
653        assert_eq!(
654            CategoryUnit::from_category(&DataCategory::Attachment),
655            Some(CategoryUnit::Bytes)
656        );
657        assert_eq!(
658            CategoryUnit::from_category(&DataCategory::LogByte),
659            Some(CategoryUnit::Bytes)
660        );
661
662        // Milliseconds categories
663        assert_eq!(
664            CategoryUnit::from_category(&DataCategory::ProfileDuration),
665            Some(CategoryUnit::Milliseconds)
666        );
667        assert_eq!(
668            CategoryUnit::from_category(&DataCategory::ProfileDurationUi),
669            Some(CategoryUnit::Milliseconds)
670        );
671
672        // Unknown returns None
673        assert_eq!(CategoryUnit::from_category(&DataCategory::Unknown), None);
674    }
675
676    #[test]
677    fn test_category_unit_display() {
678        assert_eq!(format!("{}", CategoryUnit::Count), "count");
679        assert_eq!(format!("{}", CategoryUnit::Bytes), "bytes");
680        assert_eq!(format!("{}", CategoryUnit::Milliseconds), "milliseconds");
681    }
682
683    #[test]
684    fn test_category_unit_from_str() {
685        assert_eq!("count".parse::<CategoryUnit>(), Ok(CategoryUnit::Count));
686        assert_eq!("bytes".parse::<CategoryUnit>(), Ok(CategoryUnit::Bytes));
687        assert_eq!(
688            "milliseconds".parse::<CategoryUnit>(),
689            Ok(CategoryUnit::Milliseconds)
690        );
691        assert!("invalid".parse::<CategoryUnit>().is_err());
692    }
693
694    #[test]
695    fn test_category_unit_repr_values() {
696        // Verify the repr(i8) values are correct for FFI
697        assert_eq!(CategoryUnit::Count as i8, 0);
698        assert_eq!(CategoryUnit::Bytes as i8, 1);
699        assert_eq!(CategoryUnit::Milliseconds as i8, 2);
700    }
701}