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 /// These are metrics generated and consumed by Relay.
102 ///
103 /// SDK rate limiting behavior: ignore.
104 MetricBucket = 15,
105 /// SpanIndexed
106 ///
107 /// This is the category for spans we store in full.
108 ///
109 /// SDK rate limiting behavior: ignore.
110 SpanIndexed = 16,
111 /// ProfileDuration
112 ///
113 /// This data category is used to count the number of milliseconds per indexed profile chunk,
114 /// excluding UI profile chunks.
115 ///
116 /// SDK rate limiting behavior: apply to profile chunks.
117 ProfileDuration = 17,
118 /// ProfileChunk
119 ///
120 /// This is a count of profile chunks received. It will not be used for billing but will be
121 /// useful for customers to track what's being dropped.
122 ///
123 /// SDK rate limiting behavior: apply to profile chunks.
124 ProfileChunk = 18,
125 /// MetricSecond
126 ///
127 /// Reserved by billing to summarize the bucketed product of metric volume
128 /// and metric cardinality. Defined here so as not to clash with future
129 /// categories.
130 ///
131 /// SDK rate limiting behavior: ignore.
132 MetricSecond = 19,
133 /// Replay Video
134 ///
135 /// This is the data category for Session Replays produced via a video recording.
136 ///
137 /// SDK rate limiting behavior: ignore.
138 DoNotUseReplayVideo = 20,
139 /// This is the data category for Uptime monitors.
140 ///
141 /// SDK rate limiting behavior: ignore.
142 Uptime = 21,
143 /// Counts the number of individual attachments, as opposed to the number of bytes in an attachment.
144 ///
145 /// SDK rate limiting behavior: apply to attachments.
146 AttachmentItem = 22,
147 /// LogItem
148 ///
149 /// This is the category for logs for which we store the count log events for users for measuring
150 /// missing breadcrumbs, and count of logs for rate limiting purposes.
151 ///
152 /// SDK rate limiting behavior: apply to logs.
153 LogItem = 23,
154 /// LogByte
155 ///
156 /// This is the category for logs for which we store log event total bytes for users.
157 ///
158 /// SDK rate limiting behavior: apply to logs.
159 LogByte = 24,
160 /// Profile duration of a UI profile.
161 ///
162 /// This data category is used to count the number of milliseconds per indexed UI profile
163 /// chunk.
164 ///
165 /// See also: [`Self::ProfileDuration`]
166 ///
167 /// SDK rate limiting behavior: apply to profile chunks.
168 ProfileDurationUi = 25,
169 /// UI Profile Chunk.
170 ///
171 /// This data category is used to count the number of milliseconds per indexed UI profile
172 /// chunk.
173 ///
174 /// See also: [`Self::ProfileChunk`]
175 ///
176 /// SDK rate limiting behavior: apply to profile chunks.
177 ProfileChunkUi = 26,
178 /// This is the data category to count Seer Autofix run events.
179 ///
180 /// SDK rate limiting behavior: ignore.
181 SeerAutofix = 27,
182 /// This is the data category to count Seer Scanner run events.
183 ///
184 /// SDK rate limiting behavior: ignore.
185 SeerScanner = 28,
186 /// DEPRECATED: Use SeerUser instead.
187 ///
188 /// PreventUser
189 ///
190 /// This is the data category to count the number of assigned Prevent Users.
191 ///
192 /// SDK rate limiting behavior: ignore.
193 PreventUser = 29,
194 /// PreventReview
195 ///
196 /// This is the data category to count the number of Prevent review events.
197 ///
198 /// SDK rate limiting behavior: ignore.
199 PreventReview = 30,
200 /// Size analysis
201 ///
202 /// This is the data category to count the number of size analyses performed.
203 /// 'Size analysis' a static binary analysis of a preprod build artifact
204 /// (e.g. the .apk of an Android app or MacOS .app).
205 /// When enabled there will typically be one such analysis per uploaded artifact.
206 ///
207 /// SDK rate limiting behavior: ignore.
208 SizeAnalysis = 31,
209 /// InstallableBuild
210 ///
211 /// This is the data category to count the number of installable builds.
212 /// It counts the number of artifacts uploaded *not* the number of times the
213 /// artifacts are downloaded for installation.
214 /// When enabled there will typically be one 'InstallableBuild' per uploaded artifact.
215 ///
216 /// SDK rate limiting behavior: ignore.
217 InstallableBuild = 32,
218 /// TraceMetric
219 ///
220 /// This is the data category to count the number of trace metric items.
221 TraceMetric = 33,
222 /// SeerUser
223 ///
224 /// This is the data category to count the number of Seer users.
225 ///
226 /// SDK rate limiting behavior: ignore.
227 SeerUser = 34,
228 /// Transaction profiles for backend platforms.
229 ///
230 /// This is an extension of [`Self::Profile`], but additionally discriminates on the profile
231 /// platform, see also [`Self::ProfileUi`].
232 ///
233 /// Continuous profiling uses [`Self::ProfileChunk`] and [`Self::ProfileChunkUi`].
234 ///
235 /// SDK rate limiting behavior: optional, apply to transaction profiles on "backend platforms".
236 ProfileBackend = 35,
237 /// Transaction profiles for ui platforms.
238 ///
239 /// This is an extension of [`Self::Profile`], but additionally discriminates on the profile
240 /// platform, see also [`Self::ProfileBackend`].
241 ///
242 /// Continuous profiling uses [`Self::ProfileChunk`] and [`Self::ProfileChunkUi`].
243 ///
244 /// SDK rate limiting behavior: optional, apply to transaction profiles on "ui platforms".
245 ProfileUi = 36,
246 /// TraceMetricByte
247 ///
248 /// This is the category for trace metrics for which we store total bytes for users.
249 TraceMetricByte = 37,
250 /// Snapshot image
251 ///
252 /// Counts images accepted by the preprod snapshot upload API.
253 ///
254 /// SDK rate limiting behavior: ignore.
255 SnapshotImage = 38,
256 //
257 // IMPORTANT: After adding a new entry to DataCategory, go to the `relay-cabi` subfolder and run
258 // `make header` to regenerate the C-binding. This allows using the data category from Python.
259 // Rerun this step every time the **code name** of the variant is updated.
260 //
261 /// Any other data category not known by this Relay.
262 Unknown = -1,
263}
264
265impl DataCategory {
266 /// Returns the data category corresponding to the given name.
267 pub fn from_name(string: &str) -> Self {
268 match string {
269 "default" => Self::Default,
270 "error" => Self::Error,
271 "transaction" => Self::Transaction,
272 "security" => Self::Security,
273 "attachment" => Self::Attachment,
274 "session" => Self::Session,
275 "profile" => Self::Profile,
276 "profile_indexed" => Self::ProfileIndexed,
277 "replay" => Self::Replay,
278 "transaction_processed" => Self::TransactionProcessed,
279 "transaction_indexed" => Self::TransactionIndexed,
280 "monitor" => Self::Monitor,
281 "span" => Self::Span,
282 "log_item" => Self::LogItem,
283 "log_byte" => Self::LogByte,
284 "monitor_seat" => Self::MonitorSeat,
285 "feedback" => Self::UserReportV2,
286 "user_report_v2" => Self::UserReportV2,
287 "metric_bucket" => Self::MetricBucket,
288 "span_indexed" => Self::SpanIndexed,
289 "profile_duration" => Self::ProfileDuration,
290 "profile_duration_ui" => Self::ProfileDurationUi,
291 "profile_chunk" => Self::ProfileChunk,
292 "profile_chunk_ui" => Self::ProfileChunkUi,
293 "metric_second" => Self::MetricSecond,
294 "replay_video" => Self::DoNotUseReplayVideo,
295 "uptime" => Self::Uptime,
296 "attachment_item" => Self::AttachmentItem,
297 "seer_autofix" => Self::SeerAutofix,
298 "seer_scanner" => Self::SeerScanner,
299 "prevent_user" => Self::PreventUser,
300 "prevent_review" => Self::PreventReview,
301 "size_analysis" => Self::SizeAnalysis,
302 "installable_build" => Self::InstallableBuild,
303 "trace_metric" => Self::TraceMetric,
304 "trace_metric_byte" => Self::TraceMetricByte,
305 "snapshot_image" => Self::SnapshotImage,
306 "seer_user" => Self::SeerUser,
307 "profile_backend" => Self::ProfileBackend,
308 "profile_ui" => Self::ProfileUi,
309 _ => Self::Unknown,
310 }
311 }
312
313 /// Returns the canonical name of this data category.
314 pub fn name(self) -> &'static str {
315 match self {
316 Self::Default => "default",
317 Self::Error => "error",
318 Self::Transaction => "transaction",
319 Self::Security => "security",
320 Self::Attachment => "attachment",
321 Self::Session => "session",
322 Self::Profile => "profile",
323 Self::ProfileIndexed => "profile_indexed",
324 Self::Replay => "replay",
325 Self::DoNotUseReplayVideo => "replay_video",
326 Self::TransactionProcessed => "transaction_processed",
327 Self::TransactionIndexed => "transaction_indexed",
328 Self::Monitor => "monitor",
329 Self::Span => "span",
330 Self::LogItem => "log_item",
331 Self::LogByte => "log_byte",
332 Self::MonitorSeat => "monitor_seat",
333 Self::UserReportV2 => "feedback",
334 Self::MetricBucket => "metric_bucket",
335 Self::SpanIndexed => "span_indexed",
336 Self::ProfileDuration => "profile_duration",
337 Self::ProfileDurationUi => "profile_duration_ui",
338 Self::ProfileChunk => "profile_chunk",
339 Self::ProfileChunkUi => "profile_chunk_ui",
340 Self::MetricSecond => "metric_second",
341 Self::Uptime => "uptime",
342 Self::AttachmentItem => "attachment_item",
343 Self::SeerAutofix => "seer_autofix",
344 Self::SeerScanner => "seer_scanner",
345 Self::PreventUser => "prevent_user",
346 Self::PreventReview => "prevent_review",
347 Self::SizeAnalysis => "size_analysis",
348 Self::InstallableBuild => "installable_build",
349 Self::TraceMetric => "trace_metric",
350 Self::TraceMetricByte => "trace_metric_byte",
351 Self::SnapshotImage => "snapshot_image",
352 Self::SeerUser => "seer_user",
353 Self::ProfileBackend => "profile_backend",
354 Self::ProfileUi => "profile_ui",
355 Self::Unknown => "unknown",
356 }
357 }
358
359 /// Returns true if the DataCategory refers to an error (i.e an error event).
360 pub fn is_error(self) -> bool {
361 matches!(self, Self::Error | Self::Default | Self::Security)
362 }
363
364 /// Returns the numeric value for this outcome.
365 pub fn value(self) -> Option<u8> {
366 // negative values (Internal and Unknown) cannot be sent as
367 // outcomes (internally so!)
368 (self as i8).try_into().ok()
369 }
370
371 /// Returns a dedicated category for indexing if this data can be converted to metrics.
372 ///
373 /// This returns `None` for most data categories.
374 pub fn index_category(self) -> Option<Self> {
375 match self {
376 Self::Transaction => Some(Self::TransactionIndexed),
377 Self::Span => Some(Self::SpanIndexed),
378 Self::Profile => Some(Self::ProfileIndexed),
379 _ => None,
380 }
381 }
382
383 /// Returns `true` if this data category is an indexed data category.
384 pub fn is_indexed(self) -> bool {
385 matches!(
386 self,
387 Self::TransactionIndexed | Self::SpanIndexed | Self::ProfileIndexed
388 )
389 }
390}
391
392relay_common::impl_str_serde!(DataCategory, "a data category");
393
394impl fmt::Display for DataCategory {
395 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396 write!(f, "{}", self.name())
397 }
398}
399
400impl FromStr for DataCategory {
401 type Err = std::convert::Infallible;
402
403 fn from_str(string: &str) -> Result<Self, Self::Err> {
404 Ok(Self::from_name(string))
405 }
406}
407
408impl From<EventType> for DataCategory {
409 fn from(ty: EventType) -> Self {
410 match ty {
411 EventType::Default | EventType::Error => Self::Error,
412 EventType::Transaction => Self::Transaction,
413 EventType::Csp => Self::Security,
414 EventType::UserReportV2 => Self::UserReportV2,
415 }
416 }
417}
418
419impl TryFrom<u8> for DataCategory {
420 type Error = UnknownDataCategory;
421
422 fn try_from(value: u8) -> Result<Self, UnknownDataCategory> {
423 match value {
424 0 => Ok(Self::Default),
425 1 => Ok(Self::Error),
426 2 => Ok(Self::Transaction),
427 3 => Ok(Self::Security),
428 4 => Ok(Self::Attachment),
429 5 => Ok(Self::Session),
430 6 => Ok(Self::Profile),
431 7 => Ok(Self::Replay),
432 8 => Ok(Self::TransactionProcessed),
433 9 => Ok(Self::TransactionIndexed),
434 10 => Ok(Self::Monitor),
435 11 => Ok(Self::ProfileIndexed),
436 12 => Ok(Self::Span),
437 13 => Ok(Self::MonitorSeat),
438 14 => Ok(Self::UserReportV2),
439 15 => Ok(Self::MetricBucket),
440 16 => Ok(Self::SpanIndexed),
441 17 => Ok(Self::ProfileDuration),
442 18 => Ok(Self::ProfileChunk),
443 19 => Ok(Self::MetricSecond),
444 20 => Ok(Self::DoNotUseReplayVideo),
445 21 => Ok(Self::Uptime),
446 22 => Ok(Self::AttachmentItem),
447 23 => Ok(Self::LogItem),
448 24 => Ok(Self::LogByte),
449 25 => Ok(Self::ProfileDurationUi),
450 26 => Ok(Self::ProfileChunkUi),
451 27 => Ok(Self::SeerAutofix),
452 28 => Ok(Self::SeerScanner),
453 29 => Ok(Self::PreventUser),
454 30 => Ok(Self::PreventReview),
455 31 => Ok(Self::SizeAnalysis),
456 32 => Ok(Self::InstallableBuild),
457 33 => Ok(Self::TraceMetric),
458 34 => Ok(Self::SeerUser),
459 35 => Ok(Self::ProfileBackend),
460 36 => Ok(Self::ProfileUi),
461 37 => Ok(Self::TraceMetricByte),
462 38 => Ok(Self::SnapshotImage),
463 other => Err(UnknownDataCategory(other as u32)),
464 }
465 }
466}
467
468impl TryFrom<u32> for DataCategory {
469 type Error = UnknownDataCategory;
470
471 fn try_from(value: u32) -> Result<Self, UnknownDataCategory> {
472 let value = u8::try_from(value).map_err(|_| UnknownDataCategory(value))?;
473 value.try_into()
474 }
475}
476
477/// The unit in which a data category is measured.
478///
479/// This enum specifies how quantities for different data categories are measured,
480/// which affects how quota limits are interpreted and enforced.
481///
482/// Note: There is no `Unknown` variant. For categories without a defined unit
483/// (e.g., `DataCategory::Unknown`), methods return `Option::None`.
484//
485// IMPORTANT: After adding a new entry to CategoryUnit, go to the `relay-cabi` subfolder and run
486// `make header` to regenerate the C-binding. This allows using the category unit from Python.
487//
488#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
489#[serde(rename_all = "snake_case")]
490#[repr(i8)]
491pub enum CategoryUnit {
492 /// Counts the number of discrete items.
493 Count = 0,
494 /// Counts the number of bytes across items.
495 Bytes = 1,
496 /// Counts the accumulated time in milliseconds across items.
497 Milliseconds = 2,
498}
499
500impl CategoryUnit {
501 /// Returns the canonical name of this category unit.
502 pub fn name(self) -> &'static str {
503 match self {
504 Self::Count => "count",
505 Self::Bytes => "bytes",
506 Self::Milliseconds => "milliseconds",
507 }
508 }
509
510 /// Returns the category unit corresponding to the given name string.
511 ///
512 /// Returns `None` if the string doesn't match any known unit.
513 pub fn from_name(string: &str) -> Option<Self> {
514 match string {
515 "count" => Some(Self::Count),
516 "bytes" => Some(Self::Bytes),
517 "milliseconds" => Some(Self::Milliseconds),
518 _ => None,
519 }
520 }
521
522 /// Returns the `CategoryUnit` for the given `DataCategory`.
523 ///
524 /// Returns `None` for `DataCategory::Unknown`.
525 ///
526 /// Note: Takes a reference to avoid unnecessary copying and allow direct use with iterators.
527 pub fn from_category(category: DataCategory) -> Option<Self> {
528 match category {
529 DataCategory::Default
530 | DataCategory::Error
531 | DataCategory::Transaction
532 | DataCategory::Replay
533 | DataCategory::DoNotUseReplayVideo
534 | DataCategory::Security
535 | DataCategory::Profile
536 | DataCategory::ProfileIndexed
537 | DataCategory::TransactionProcessed
538 | DataCategory::TransactionIndexed
539 | DataCategory::LogItem
540 | DataCategory::Span
541 | DataCategory::SpanIndexed
542 | DataCategory::MonitorSeat
543 | DataCategory::Monitor
544 | DataCategory::MetricBucket
545 | DataCategory::UserReportV2
546 | DataCategory::ProfileChunk
547 | DataCategory::ProfileChunkUi
548 | DataCategory::Uptime
549 | DataCategory::MetricSecond
550 | DataCategory::AttachmentItem
551 | DataCategory::SeerAutofix
552 | DataCategory::SeerScanner
553 | DataCategory::PreventUser
554 | DataCategory::PreventReview
555 | DataCategory::Session
556 | DataCategory::SizeAnalysis
557 | DataCategory::InstallableBuild
558 | DataCategory::TraceMetric
559 | DataCategory::SeerUser
560 | DataCategory::ProfileBackend
561 | DataCategory::ProfileUi
562 | DataCategory::SnapshotImage => Some(Self::Count),
563
564 DataCategory::Attachment | DataCategory::LogByte | DataCategory::TraceMetricByte => {
565 Some(Self::Bytes)
566 }
567
568 DataCategory::ProfileDuration | DataCategory::ProfileDurationUi => {
569 Some(Self::Milliseconds)
570 }
571
572 DataCategory::Unknown => None,
573 }
574 }
575}
576
577impl fmt::Display for CategoryUnit {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 write!(f, "{}", self.name())
580 }
581}
582
583impl FromStr for CategoryUnit {
584 type Err = ();
585
586 fn from_str(string: &str) -> Result<Self, Self::Err> {
587 Self::from_name(string).ok_or(())
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 #[test]
596 fn test_last_variant_conversion() {
597 // If this test fails, update the numeric bounds so that the first assertion
598 // maps to the last variant in the enum and the second assertion produces an error
599 // that the DataCategory does not exist.
600 assert_eq!(
601 DataCategory::try_from(38u8),
602 Ok(DataCategory::SnapshotImage)
603 );
604 assert_eq!(DataCategory::try_from(39u8), Err(UnknownDataCategory(39)));
605 }
606
607 #[test]
608 fn test_data_category_alias() {
609 assert_eq!("feedback".parse(), Ok(DataCategory::UserReportV2));
610 assert_eq!("user_report_v2".parse(), Ok(DataCategory::UserReportV2));
611 assert_eq!(&DataCategory::UserReportV2.to_string(), "feedback");
612
613 assert_eq!(
614 serde_json::from_str::<DataCategory>(r#""feedback""#).unwrap(),
615 DataCategory::UserReportV2,
616 );
617 assert_eq!(
618 serde_json::from_str::<DataCategory>(r#""user_report_v2""#).unwrap(),
619 DataCategory::UserReportV2,
620 );
621 assert_eq!(
622 &serde_json::to_string(&DataCategory::UserReportV2).unwrap(),
623 r#""feedback""#
624 )
625 }
626
627 #[test]
628 fn test_category_unit_name() {
629 assert_eq!(CategoryUnit::Count.name(), "count");
630 assert_eq!(CategoryUnit::Bytes.name(), "bytes");
631 assert_eq!(CategoryUnit::Milliseconds.name(), "milliseconds");
632 }
633
634 #[test]
635 fn test_category_unit_from_name() {
636 assert_eq!(CategoryUnit::from_name("count"), Some(CategoryUnit::Count));
637 assert_eq!(CategoryUnit::from_name("bytes"), Some(CategoryUnit::Bytes));
638 assert_eq!(
639 CategoryUnit::from_name("milliseconds"),
640 Some(CategoryUnit::Milliseconds)
641 );
642 assert_eq!(CategoryUnit::from_name("unknown"), None);
643 assert_eq!(CategoryUnit::from_name(""), None);
644 }
645
646 #[test]
647 fn test_category_unit_from_category() {
648 // Count categories
649 assert_eq!(
650 CategoryUnit::from_category(DataCategory::Error),
651 Some(CategoryUnit::Count)
652 );
653 assert_eq!(
654 CategoryUnit::from_category(DataCategory::Transaction),
655 Some(CategoryUnit::Count)
656 );
657 assert_eq!(
658 CategoryUnit::from_category(DataCategory::Span),
659 Some(CategoryUnit::Count)
660 );
661 assert_eq!(
662 CategoryUnit::from_category(DataCategory::SnapshotImage),
663 Some(CategoryUnit::Count)
664 );
665
666 // Bytes categories
667 assert_eq!(
668 CategoryUnit::from_category(DataCategory::Attachment),
669 Some(CategoryUnit::Bytes)
670 );
671 assert_eq!(
672 CategoryUnit::from_category(DataCategory::LogByte),
673 Some(CategoryUnit::Bytes)
674 );
675
676 // Milliseconds categories
677 assert_eq!(
678 CategoryUnit::from_category(DataCategory::ProfileDuration),
679 Some(CategoryUnit::Milliseconds)
680 );
681 assert_eq!(
682 CategoryUnit::from_category(DataCategory::ProfileDurationUi),
683 Some(CategoryUnit::Milliseconds)
684 );
685
686 // Unknown returns None
687 assert_eq!(CategoryUnit::from_category(DataCategory::Unknown), None);
688 }
689
690 #[test]
691 fn test_category_unit_display() {
692 assert_eq!(format!("{}", CategoryUnit::Count), "count");
693 assert_eq!(format!("{}", CategoryUnit::Bytes), "bytes");
694 assert_eq!(format!("{}", CategoryUnit::Milliseconds), "milliseconds");
695 }
696
697 #[test]
698 fn test_category_unit_from_str() {
699 assert_eq!("count".parse::<CategoryUnit>(), Ok(CategoryUnit::Count));
700 assert_eq!("bytes".parse::<CategoryUnit>(), Ok(CategoryUnit::Bytes));
701 assert_eq!(
702 "milliseconds".parse::<CategoryUnit>(),
703 Ok(CategoryUnit::Milliseconds)
704 );
705 assert!("invalid".parse::<CategoryUnit>().is_err());
706 }
707
708 #[test]
709 fn test_category_unit_repr_values() {
710 // Verify the repr(i8) values are correct for FFI
711 assert_eq!(CategoryUnit::Count as i8, 0);
712 assert_eq!(CategoryUnit::Bytes as i8, 1);
713 assert_eq!(CategoryUnit::Milliseconds as i8, 2);
714 }
715}