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