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}