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}