Skip to main content

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