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