relay_server/utils/
rate_limits.rs

1use std::fmt::{self, Write};
2use std::future::Future;
3use std::marker::PhantomData;
4
5use relay_profiling::ProfileType;
6use relay_quotas::{
7    DataCategory, ItemScoping, QuotaScope, RateLimit, RateLimitScope, RateLimits, ReasonCode,
8    Scoping,
9};
10use smallvec::SmallVec;
11
12use crate::envelope::{AttachmentParentType, AttachmentType, Envelope, Item, ItemType};
13use crate::integrations::Integration;
14use crate::managed::{Managed, ManagedEnvelope};
15use crate::services::outcome::Outcome;
16
17/// Name of the rate limits header.
18pub const RATE_LIMITS_HEADER: &str = "X-Sentry-Rate-Limits";
19
20/// Formats the `X-Sentry-Rate-Limits` header.
21pub fn format_rate_limits(rate_limits: &RateLimits) -> String {
22    let mut header = String::new();
23
24    for rate_limit in rate_limits {
25        if !header.is_empty() {
26            header.push_str(", ");
27        }
28
29        write!(header, "{}:", rate_limit.retry_after.remaining_seconds()).ok();
30
31        for (index, category) in rate_limit.categories.iter().enumerate() {
32            if index > 0 {
33                header.push(';');
34            }
35            write!(header, "{category}").ok();
36        }
37
38        write!(header, ":{}", rate_limit.scope.name()).ok();
39
40        if let Some(ref reason_code) = rate_limit.reason_code {
41            write!(header, ":{reason_code}").ok();
42        } else if !rate_limit.namespaces.is_empty() {
43            write!(header, ":").ok(); // delimits the empty reason code for namespaces
44        }
45
46        for (index, namespace) in rate_limit.namespaces.iter().enumerate() {
47            header.push(if index == 0 { ':' } else { ';' });
48            write!(header, "{namespace}").ok();
49        }
50    }
51
52    header
53}
54
55/// Parses the `X-Sentry-Rate-Limits` header.
56pub fn parse_rate_limits(scoping: &Scoping, string: &str) -> RateLimits {
57    let mut rate_limits = RateLimits::new();
58
59    for limit in string.split(',') {
60        let limit = limit.trim();
61        if limit.is_empty() {
62            continue;
63        }
64
65        let mut components = limit.split(':');
66
67        let retry_after = match components.next().and_then(|s| s.parse().ok()) {
68            Some(retry_after) => retry_after,
69            None => continue,
70        };
71
72        let categories = components
73            .next()
74            .unwrap_or("")
75            .split(';')
76            .filter(|category| !category.is_empty())
77            .map(DataCategory::from_name)
78            .collect();
79
80        let quota_scope = QuotaScope::from_name(components.next().unwrap_or(""));
81        let scope = RateLimitScope::for_quota(*scoping, quota_scope);
82
83        let reason_code = components
84            .next()
85            .filter(|s| !s.is_empty())
86            .map(ReasonCode::new);
87
88        let namespace = components
89            .next()
90            .unwrap_or("")
91            .split(';')
92            .filter(|s| !s.is_empty())
93            .filter_map(|s| s.parse().ok())
94            .collect();
95
96        rate_limits.add(RateLimit {
97            categories,
98            scope,
99            reason_code,
100            retry_after,
101            namespaces: namespace,
102        });
103    }
104
105    rate_limits
106}
107
108/// Infer the data category from an item.
109///
110/// Categories depend mostly on the item type, with a few special cases:
111/// - `Event`: the category is inferred from the event type. This requires the `event_type` header
112///   to be set on the event item.
113/// - `Attachment`: If the attachment creates an event (e.g. for minidumps), the category is assumed
114///   to be `Error`.
115fn infer_event_category(item: &Item) -> Option<DataCategory> {
116    match item.ty() {
117        ItemType::Event => Some(DataCategory::Error),
118        ItemType::Transaction => Some(DataCategory::Transaction),
119        ItemType::Security | ItemType::RawSecurity => Some(DataCategory::Security),
120        ItemType::UnrealReport => Some(DataCategory::Error),
121        ItemType::UserReportV2 => Some(DataCategory::UserReportV2),
122        ItemType::Attachment if item.creates_event() => Some(DataCategory::Error),
123        ItemType::Attachment => None,
124        ItemType::Session => None,
125        ItemType::Sessions => None,
126        ItemType::Statsd => None,
127        ItemType::MetricBuckets => None,
128        ItemType::FormData => None,
129        ItemType::UserReport => None,
130        ItemType::Profile => None,
131        ItemType::ReplayEvent => None,
132        ItemType::ReplayRecording => None,
133        ItemType::ReplayVideo => None,
134        ItemType::ClientReport => None,
135        ItemType::CheckIn => None,
136        ItemType::Nel => None,
137        ItemType::Log => None,
138        ItemType::TraceMetric => None,
139        ItemType::Span => None,
140        ItemType::ProfileChunk => None,
141        ItemType::Integration => None,
142        ItemType::Unknown(_) => None,
143    }
144}
145
146/// Quantity metrics for a single category of attachments.
147///
148/// Tracks both the count of attachments and size in bytes.
149#[derive(Clone, Copy, Debug, Default)]
150pub struct AttachmentQuantity {
151    /// Number of attachment items.
152    pub count: usize,
153    /// Total size of attachments in bytes.
154    pub bytes: usize,
155}
156
157impl AttachmentQuantity {
158    pub fn is_empty(&self) -> bool {
159        self.count == 0 && self.bytes == 0
160    }
161}
162
163/// Aggregated attachment quantities grouped by [`AttachmentParentType`].
164///
165/// This separation is necessary since rate limiting logic varies by [`AttachmentParentType`].
166#[derive(Clone, Copy, Debug, Default)]
167pub struct AttachmentQuantities {
168    /// Quantities of Event Attachments.
169    ///
170    /// See also: [`AttachmentParentType::Event`].
171    pub event: AttachmentQuantity,
172    /// Quantities of trace V2 Attachments.
173    pub trace: AttachmentQuantity,
174    /// Quantities of span V2 Attachments.
175    pub span: AttachmentQuantity,
176}
177
178impl AttachmentQuantities {
179    /// Returns the total count of all attachments across all parent types.
180    pub fn count(&self) -> usize {
181        let AttachmentQuantities { event, trace, span } = self;
182        event.count + trace.count + span.count
183    }
184
185    /// Returns the total size in bytes of all attachments across all parent types.
186    pub fn bytes(&self) -> usize {
187        let AttachmentQuantities { event, trace, span } = self;
188        event.bytes + trace.bytes + span.bytes
189    }
190}
191
192/// Collection of all transaction profile quantities.
193#[derive(Clone, Copy, Debug, Default)]
194pub struct ProfileQuantities {
195    /// All transaction profiles in the backend category.
196    pub backend: usize,
197    /// All transaction profiles in the ui category.
198    pub ui: usize,
199    /// All transaction profiles, includes profiles in the backend and ui categories as well as
200    /// profiles which are in neither category.
201    pub total: usize,
202}
203
204/// A summary of `Envelope` contents.
205///
206/// Summarizes the contained event, size of attachments, session updates, and whether there are
207/// plain attachments. This is used for efficient rate limiting or outcome handling.
208#[non_exhaustive]
209#[derive(Clone, Copy, Debug, Default)]
210pub struct EnvelopeSummary {
211    /// The data category of the event in the envelope. `None` if there is no event.
212    pub event_category: Option<DataCategory>,
213
214    /// The quantities of all attachments combined.
215    pub attachment_quantities: AttachmentQuantities,
216
217    /// The number of all session updates.
218    pub session_quantity: usize,
219
220    /// The number of profiles.
221    pub profile_quantity: ProfileQuantities,
222
223    /// The number of replays.
224    pub replay_quantity: usize,
225
226    /// The number of user reports (legacy item type for user feedback).
227    pub user_report_quantity: usize,
228
229    /// The number of monitor check-ins.
230    pub monitor_quantity: usize,
231
232    /// The number of log for the log product sent.
233    pub log_item_quantity: usize,
234
235    /// The number of log bytes for the log product sent, in bytes
236    pub log_byte_quantity: usize,
237
238    /// Secondary number of transactions.
239    ///
240    /// This is 0 for envelopes which contain a transaction,
241    /// only secondary transaction quantity should be tracked here,
242    /// these are for example transaction counts extracted from metrics.
243    ///
244    /// A "primary" transaction is contained within the envelope,
245    /// marking the envelope data category a [`DataCategory::Transaction`].
246    pub secondary_transaction_quantity: usize,
247
248    /// See `secondary_transaction_quantity`.
249    pub secondary_span_quantity: usize,
250
251    /// The number of standalone spans.
252    pub span_quantity: usize,
253
254    /// Indicates that the envelope contains regular attachments that do not create event payloads.
255    pub has_plain_attachments: bool,
256
257    /// The payload size of this envelope.
258    pub payload_size: usize,
259
260    /// The number of profile chunks in this envelope.
261    pub profile_chunk_quantity: usize,
262    /// The number of UI profile chunks in this envelope.
263    pub profile_chunk_ui_quantity: usize,
264
265    /// The number of trace metrics in this envelope.
266    pub trace_metric_quantity: usize,
267
268    /// The number of trace metric bytes in this envelope.
269    pub trace_metric_byte_quantity: usize,
270}
271
272impl EnvelopeSummary {
273    /// Creates an empty summary.
274    pub fn empty() -> Self {
275        Self::default()
276    }
277
278    /// Creates an envelope summary and aggregates the given envelope.
279    pub fn compute(envelope: &Envelope) -> Self {
280        Self::compute_items(envelope.items())
281    }
282
283    pub fn compute_items<'a>(items: impl IntoIterator<Item = &'a Item>) -> Self {
284        let mut summary = Self::empty();
285
286        for item in items {
287            if item.creates_event() {
288                summary.infer_category(item);
289            } else if item.ty() == &ItemType::Attachment {
290                // Plain attachments do not create events.
291                summary.has_plain_attachments = true;
292            }
293
294            // If the item has been rate limited before, the quota has been consumed and outcomes
295            // emitted. We can skip it here.
296            if item.rate_limited() {
297                continue;
298            }
299
300            if let Some(source_quantities) = item.source_quantities() {
301                summary.secondary_transaction_quantity += source_quantities.transactions;
302                summary.secondary_span_quantity += source_quantities.spans;
303            }
304
305            summary.payload_size += item.len();
306
307            summary.add_quantities(item);
308
309            // Special case since v1 and v2 share a data category.
310            // Adding this in add_quantity would include v2 in the count.
311            if item.ty() == &ItemType::UserReport {
312                summary.user_report_quantity += 1;
313            }
314        }
315
316        summary
317    }
318
319    fn add_quantities(&mut self, item: &Item) {
320        // The Nintendo switch item is a special case which should've been modelled like the
321        // `Unreal4Context` as potentially a separate item type which does not have its own data
322        // category.
323        //
324        // Currently there is no outcome category for this item, as it will be dissolved into
325        // multiple different items once processed.
326        if item.attachment_type() == Some(AttachmentType::NintendoSwitchDyingMessage) {
327            return;
328        }
329
330        for (category, quantity) in item.quantities() {
331            let target_quantity = match category {
332                DataCategory::Attachment => match item.attachment_parent_type() {
333                    AttachmentParentType::Span => &mut self.attachment_quantities.span.bytes,
334                    AttachmentParentType::Trace => &mut self.attachment_quantities.trace.bytes,
335                    AttachmentParentType::Event => &mut self.attachment_quantities.event.bytes,
336                },
337                DataCategory::AttachmentItem => match item.attachment_parent_type() {
338                    AttachmentParentType::Span => &mut self.attachment_quantities.span.count,
339                    AttachmentParentType::Trace => &mut self.attachment_quantities.trace.count,
340                    AttachmentParentType::Event => &mut self.attachment_quantities.event.count,
341                },
342                DataCategory::Session => &mut self.session_quantity,
343                DataCategory::Profile => &mut self.profile_quantity.total,
344                DataCategory::ProfileBackend => &mut self.profile_quantity.backend,
345                DataCategory::ProfileUi => &mut self.profile_quantity.ui,
346                DataCategory::Replay => &mut self.replay_quantity,
347                DataCategory::DoNotUseReplayVideo => &mut self.replay_quantity,
348                DataCategory::Monitor => &mut self.monitor_quantity,
349                DataCategory::Span => &mut self.span_quantity,
350                DataCategory::TraceMetric => &mut self.trace_metric_quantity,
351                DataCategory::TraceMetricByte => &mut self.trace_metric_byte_quantity,
352                DataCategory::LogItem => &mut self.log_item_quantity,
353                DataCategory::LogByte => &mut self.log_byte_quantity,
354                DataCategory::ProfileChunk => &mut self.profile_chunk_quantity,
355                DataCategory::ProfileChunkUi => &mut self.profile_chunk_ui_quantity,
356                // TODO: This catch-all looks dangerous
357                _ => continue,
358            };
359            *target_quantity += quantity;
360        }
361    }
362
363    /// Infers the appropriate [`DataCategory`] for the envelope [`Item`].
364    ///
365    /// The inferred category is only applied to the [`EnvelopeSummary`] if there is not yet
366    /// a category set.
367    fn infer_category(&mut self, item: &Item) {
368        if matches!(self.event_category, None | Some(DataCategory::Default))
369            && let Some(category) = infer_event_category(item)
370        {
371            self.event_category = Some(category);
372        }
373    }
374
375    /// Returns `true` if the envelope contains items that depend on spans.
376    ///
377    /// This is used to determined if we should be checking span quota, as the quota should be
378    /// checked both if there are spans or if there are span dependent items (e.g. span attachments).
379    pub fn has_span_dependent_items(&self) -> bool {
380        !self.attachment_quantities.span.is_empty()
381    }
382}
383
384/// Rate limiting information for a data category.
385#[derive(Debug, Default, PartialEq)]
386#[cfg_attr(test, derive(Clone))]
387pub struct CategoryLimit {
388    /// The limited data category.
389    category: Option<DataCategory>,
390    /// Additional and optional data categories in which outcomes will be produced.
391    extra_outcome_categories: SmallVec<[DataCategory; 1]>,
392    /// The total rate limited quantity across all items.
393    ///
394    /// This will be `0` if nothing was rate limited.
395    quantity: usize,
396    /// The reason code of the applied rate limit.
397    ///
398    /// Defaults to `None` if the quota does not declare a reason code.
399    reason_code: Option<ReasonCode>,
400}
401
402impl CategoryLimit {
403    /// Creates a new `CategoryLimit`.
404    ///
405    /// Returns an inactive limit if `rate_limit` is `None`.
406    fn new(category: DataCategory, quantity: usize, rate_limit: Option<&RateLimit>) -> Self {
407        match rate_limit {
408            Some(limit) => Self {
409                category: Some(category),
410                quantity,
411                extra_outcome_categories: Default::default(),
412                reason_code: limit.reason_code.clone(),
413            },
414            None => Self::default(),
415        }
416    }
417
418    /// Adds an additional outcome in the specified category to the limit.
419    pub fn add_outcome_category(mut self, category: DataCategory) -> Self {
420        self.extra_outcome_categories.push(category);
421        self
422    }
423
424    /// Recreates the category limit, if active, for a new category with the same reason.
425    pub fn clone_for(&self, category: DataCategory, quantity: usize) -> CategoryLimit {
426        if !self.is_active() {
427            return Self::default();
428        }
429
430        Self {
431            category: Some(category),
432            extra_outcome_categories: Default::default(),
433            quantity,
434            reason_code: self.reason_code.clone(),
435        }
436    }
437
438    /// Returns `true` if this is an active limit.
439    ///
440    /// Inactive limits are placeholders with no category set.
441    pub fn is_active(&self) -> bool {
442        self.category.is_some()
443    }
444
445    fn outcomes(self) -> impl Iterator<Item = (Outcome, DataCategory, usize)> {
446        let Self {
447            category,
448            extra_outcome_categories,
449            quantity,
450            reason_code,
451        } = self;
452
453        if category.is_none() || quantity == 0 {
454            return either::Either::Left(std::iter::empty());
455        }
456
457        let outcomes = std::iter::chain(category, extra_outcome_categories).map(move |category| {
458            (
459                Outcome::RateLimited(reason_code.clone()),
460                category,
461                quantity,
462            )
463        });
464
465        either::Either::Right(outcomes)
466    }
467}
468
469/// Rate limiting information for a single category of attachments.
470#[derive(Default, Debug)]
471#[cfg_attr(test, derive(Clone))]
472pub struct AttachmentLimits {
473    /// Rate limit applied to attachment bytes ([`DataCategory::Attachment`]).
474    pub bytes: CategoryLimit,
475    /// Rate limit applied to attachment item count ([`DataCategory::AttachmentItem`]).
476    pub count: CategoryLimit,
477}
478
479impl AttachmentLimits {
480    fn is_active(&self) -> bool {
481        self.bytes.is_active() || self.count.is_active()
482    }
483}
484
485/// Rate limiting information for attachments grouped by [`AttachmentParentType`].
486///
487/// See [`AttachmentQuantities`] for the corresponding quantity tracking.
488#[derive(Default, Debug)]
489#[cfg_attr(test, derive(Clone))]
490pub struct AttachmentsLimits {
491    /// Limits for V1 Attachments.
492    pub event: AttachmentLimits,
493    /// Limits for trace V2 Attachments.
494    pub trace: AttachmentLimits,
495    /// Limits for span V2 Attachments.
496    pub span: AttachmentLimits,
497}
498
499/// Information on the limited quantities returned by [`EnvelopeLimiter::compute`].
500#[derive(Default, Debug)]
501#[cfg_attr(test, derive(Clone))]
502pub struct Enforcement {
503    /// The event item rate limit.
504    pub event: CategoryLimit,
505    /// The rate limit for the indexed category of the event.
506    pub event_indexed: CategoryLimit,
507    /// The attachments limits
508    pub attachments_limits: AttachmentsLimits,
509    /// The combined session item rate limit.
510    pub sessions: CategoryLimit,
511    /// The combined transaction profile item rate limits, for all transaction profiles.
512    ///
513    /// This is at least the sum of [`Self::profiles_backend`] and [`Self::profiles_ui`],
514    /// potentially more if there are profiles without a known platform.
515    pub profiles: CategoryLimit,
516    /// The combined backend transaction profile item rate limit.
517    pub profiles_backend: CategoryLimit,
518    /// The combined ui transaction profile item rate limit.
519    pub profiles_ui: CategoryLimit,
520    /// The rate limit for the indexed profiles category.
521    pub profiles_indexed: CategoryLimit,
522    /// The combined replay item rate limit.
523    pub replays: CategoryLimit,
524    /// The combined check-in item rate limit.
525    pub check_ins: CategoryLimit,
526    /// The combined logs (our product logs) rate limit.
527    pub log_items: CategoryLimit,
528    /// The combined logs (our product logs) rate limit.
529    pub log_bytes: CategoryLimit,
530    /// The combined spans rate limit.
531    pub spans: CategoryLimit,
532    /// The rate limit for the indexed span category.
533    pub spans_indexed: CategoryLimit,
534    /// The rate limit for user report v1.
535    pub user_reports: CategoryLimit,
536    /// The combined profile chunk item rate limit.
537    pub profile_chunks: CategoryLimit,
538    /// The combined profile chunk ui item rate limit.
539    pub profile_chunks_ui: CategoryLimit,
540    /// The combined trace metric item rate limit.
541    pub trace_metrics: CategoryLimit,
542    /// The combined trace metric byte rate limit.
543    pub trace_metrics_bytes: CategoryLimit,
544}
545
546impl Enforcement {
547    /// Returns the `CategoryLimit` for the event.
548    ///
549    /// `None` if the event is not rate limited.
550    pub fn active_event(&self) -> Option<&CategoryLimit> {
551        if self.event.is_active() {
552            Some(&self.event)
553        } else if self.event_indexed.is_active() {
554            Some(&self.event_indexed)
555        } else {
556            None
557        }
558    }
559
560    /// Returns `true` if the event is rate limited.
561    pub fn is_event_active(&self) -> bool {
562        self.active_event().is_some()
563    }
564
565    /// Helper for `track_outcomes`.
566    fn get_outcomes(self) -> impl Iterator<Item = (Outcome, DataCategory, usize)> {
567        let Self {
568            event,
569            event_indexed,
570            attachments_limits:
571                AttachmentsLimits {
572                    event:
573                        AttachmentLimits {
574                            bytes: event_attachment_bytes,
575                            count: event_attachment_item,
576                        },
577                    trace:
578                        AttachmentLimits {
579                            bytes: trace_attachment_bytes,
580                            count: trace_attachment_item,
581                        },
582                    span:
583                        AttachmentLimits {
584                            bytes: span_attachment_bytes,
585                            count: span_attachment_item,
586                        },
587                },
588            sessions: _, // Do not report outcomes for sessions.
589            profiles,
590            profiles_backend,
591            profiles_ui,
592            profiles_indexed,
593            replays,
594            check_ins,
595            log_items,
596            log_bytes,
597            spans,
598            spans_indexed,
599            user_reports,
600            profile_chunks,
601            profile_chunks_ui,
602            trace_metrics,
603            trace_metrics_bytes,
604        } = self;
605
606        let limits = [
607            event,
608            event_indexed,
609            event_attachment_bytes,
610            event_attachment_item,
611            trace_attachment_bytes,
612            trace_attachment_item,
613            span_attachment_bytes,
614            span_attachment_item,
615            profiles,
616            profiles_backend,
617            profiles_ui,
618            profiles_indexed,
619            replays,
620            check_ins,
621            log_items,
622            log_bytes,
623            spans,
624            spans_indexed,
625            user_reports,
626            profile_chunks,
627            profile_chunks_ui,
628            trace_metrics,
629            trace_metrics_bytes,
630        ];
631
632        limits.into_iter().flat_map(|limit| limit.outcomes())
633    }
634
635    /// Applies the [`Enforcement`] on the [`Envelope`] by removing all items that were rate limited
636    /// and emits outcomes for each rate limited category.
637    ///
638    /// # Example
639    ///
640    /// ## Interaction between Events and Attachments
641    ///
642    /// An envelope with an `Error` event and an `Attachment`. Two quotas specify to drop all
643    /// attachments (reason `"a"`) and all errors (reason `"e"`). The result of enforcement will be:
644    ///
645    /// 1. All items are removed from the envelope.
646    /// 2. Enforcements report both the event and the attachment dropped with reason `"e"`, since
647    ///    dropping an event automatically drops all attachments with the same reason.
648    /// 3. Rate limits report the single event limit `"e"`, since attachment limits do not need to
649    ///    be checked in this case.
650    ///
651    /// ## Required Attachments
652    ///
653    /// An envelope with a single Minidump `Attachment`, and a single quota specifying to drop all
654    /// attachments with reason `"a"`:
655    ///
656    /// 1. Since the minidump creates an event and is required for processing, it remains in the
657    ///    envelope and is marked as `rate_limited`.
658    /// 2. Enforcements report the attachment dropped with reason `"a"`.
659    /// 3. Rate limits are empty since it is allowed to send required attachments even when rate
660    ///    limited.
661    ///
662    /// ## Previously Rate Limited Attachments
663    ///
664    /// An envelope with a single item marked as `rate_limited`, and a quota specifying to drop
665    /// everything with reason `"d"`:
666    ///
667    /// 1. The item remains in the envelope.
668    /// 2. Enforcements are empty. Rate limiting has occurred at an earlier stage in the pipeline.
669    /// 3. Rate limits are empty.
670    pub fn apply_with_outcomes(self, envelope: &mut ManagedEnvelope) {
671        envelope
672            .envelope_mut()
673            .retain_items(|item| self.retain_item(item));
674        self.track_outcomes(envelope);
675    }
676
677    /// Applies the [`Enforcement`] on the [`Envelope`] by removing all items that were rate limited
678    /// and emits outcomes for each rate limited category.
679    ///
680    /// Works exactly like [`Self::apply_with_outcomes`], but instead operates on [`Managed`]
681    /// instead of [`ManagedEnvelope`].
682    pub fn apply_to_managed(self, envelope: &mut Managed<Box<Envelope>>) {
683        envelope.modify(|envelope, records| {
684            envelope.retain_items(|item| self.retain_item(item));
685
686            // Sessions currently do not emit any outcomes, but may be dropped.
687            records.lenient(DataCategory::Session);
688            // This is an existing bug in how user reports handle rate limits and emit outcomes.
689            //
690            // User report v1 and v2 (feedback) are counting into the same category, but that is not
691            // completely consistent leading to some mismatches when emitting outcomes from rate
692            // limiting vs how outcomes are counted on the `Managed` instance.
693            //
694            // Issue: <https://github.com/getsentry/relay/issues/5524>.
695            records.lenient(DataCategory::UserReportV2);
696
697            for (outcome, category, quantity) in self.get_outcomes() {
698                records.reject_err(outcome, (category, quantity))
699            }
700        });
701    }
702
703    /// Returns `true` when an [`Item`] can be retained, `false` otherwise.
704    fn retain_item(&self, item: &mut Item) -> bool {
705        // Remove event items and all items that depend on this event
706        if self.event.is_active() && item.requires_event() {
707            return false;
708        }
709
710        // When checking limits for categories that have an indexed variant,
711        // we only have to check the more specific, the indexed, variant
712        // to determine whether an item is limited.
713        match item.ty() {
714            ItemType::Attachment => {
715                match item.attachment_parent_type() {
716                    AttachmentParentType::Span => !self.attachments_limits.span.is_active(),
717                    AttachmentParentType::Trace => !self.attachments_limits.trace.is_active(),
718                    AttachmentParentType::Event => {
719                        if !self.attachments_limits.event.is_active() {
720                            return true;
721                        }
722                        if item.creates_event() {
723                            item.set_rate_limited(true);
724                            true
725                        } else {
726                            false
727                        }
728                    }
729                }
730            }
731            ItemType::Session => !self.sessions.is_active(),
732            ItemType::Profile => {
733                if self.profiles_indexed.is_active() {
734                    false
735                } else if let Some(platform) = item.profile_type() {
736                    match platform {
737                        ProfileType::Backend => !self.profiles_backend.is_active(),
738                        ProfileType::Ui => !self.profiles_ui.is_active(),
739                    }
740                } else {
741                    true
742                }
743            }
744            ItemType::ReplayEvent => !self.replays.is_active(),
745            ItemType::ReplayVideo => !self.replays.is_active(),
746            ItemType::ReplayRecording => !self.replays.is_active(),
747            ItemType::UserReport => !self.user_reports.is_active(),
748            ItemType::CheckIn => !self.check_ins.is_active(),
749            ItemType::Log => {
750                !(self.log_items.is_active() || self.log_bytes.is_active())
751            }
752            ItemType::Span => !self.spans_indexed.is_active(),
753            ItemType::ProfileChunk => match item.profile_type() {
754                Some(ProfileType::Backend) => !self.profile_chunks.is_active(),
755                Some(ProfileType::Ui) => !self.profile_chunks_ui.is_active(),
756                None => true,
757            },
758            ItemType::TraceMetric => !(self.trace_metrics.is_active() || self.trace_metrics_bytes.is_active()),
759            ItemType::Integration => match item.integration() {
760                Some(Integration::Logs(_)) => !(self.log_items.is_active() || self.log_bytes.is_active()),
761                Some(Integration::Spans(_)) => !self.spans_indexed.is_active(),
762                None => true,
763            },
764            ItemType::Event
765            | ItemType::Transaction
766            | ItemType::Security
767            | ItemType::FormData
768            | ItemType::RawSecurity
769            | ItemType::Nel
770            | ItemType::UnrealReport
771            | ItemType::Sessions
772            | ItemType::Statsd
773            | ItemType::MetricBuckets
774            | ItemType::ClientReport
775            | ItemType::UserReportV2  // This is an event type.
776            | ItemType::Unknown(_) => true,
777        }
778    }
779
780    /// Invokes track outcome on all enforcements reported by the [`EnvelopeLimiter`].
781    ///
782    /// Relay generally does not emit outcomes for sessions, so those are skipped.
783    fn track_outcomes(self, envelope: &mut ManagedEnvelope) {
784        for (outcome, category, quantity) in self.get_outcomes() {
785            envelope.track_outcome(outcome, category, quantity)
786        }
787    }
788}
789
790/// Which limits to check with the [`EnvelopeLimiter`].
791#[derive(Debug, Copy, Clone)]
792pub enum CheckLimits {
793    /// Checks all limits except indexed categories.
794    ///
795    /// In the fast path it is necessary to apply cached rate limits but to not enforce indexed rate limits.
796    /// Because at the time of the check the decision whether an envelope is sampled or not is not yet known.
797    /// Additionally even if the item is later dropped by dynamic sampling, it must still be around to extract metrics
798    /// and cannot be dropped too early.
799    NonIndexed,
800    /// Checks all limits against the envelope.
801    All,
802}
803
804struct Check<F, E, R> {
805    limits: CheckLimits,
806    check: F,
807    _1: PhantomData<E>,
808    _2: PhantomData<R>,
809}
810
811impl<F, E, R> Check<F, E, R>
812where
813    F: FnMut(ItemScoping, usize) -> R,
814    R: Future<Output = Result<RateLimits, E>>,
815{
816    async fn apply(&mut self, scoping: ItemScoping, quantity: usize) -> Result<RateLimits, E> {
817        if matches!(self.limits, CheckLimits::NonIndexed) && scoping.category.is_indexed() {
818            return Ok(RateLimits::default());
819        }
820
821        (self.check)(scoping, quantity).await
822    }
823}
824
825/// Enforces rate limits with the given `check` function on items in the envelope.
826///
827/// The `check` function is called with the following rules:
828///  - Once for a single event, if present in the envelope.
829///  - Once for all comprised attachments, unless the event was rate limited.
830///  - Once for all comprised sessions.
831///
832/// Items violating the rate limit are removed from the envelope. This follows a set of rules:
833///  - If the event is removed, all items depending on the event are removed (e.g. attachments).
834///  - Attachments are not removed if they create events (e.g. minidumps).
835///  - Sessions are handled separately from all of the above.
836pub struct EnvelopeLimiter<F, E, R> {
837    check: Check<F, E, R>,
838    event_category: Option<DataCategory>,
839}
840
841impl<'a, F, E, R> EnvelopeLimiter<F, E, R>
842where
843    F: FnMut(ItemScoping, usize) -> R,
844    R: Future<Output = Result<RateLimits, E>>,
845{
846    /// Create a new `EnvelopeLimiter` with the given `check` function.
847    pub fn new(limits: CheckLimits, check: F) -> Self {
848        Self {
849            check: Check {
850                check,
851                limits,
852                _1: PhantomData,
853                _2: PhantomData,
854            },
855            event_category: None,
856        }
857    }
858
859    /// Assume an event with the given category, even if no item is present in the envelope.
860    ///
861    /// This ensures that rate limits for the given data category are checked even if there is no
862    /// matching item in the envelope. Other items are handled according to the rules as if the
863    /// event item were present.
864    pub fn assume_event(&mut self, category: DataCategory) {
865        self.event_category = Some(category);
866    }
867
868    /// Process rate limits for the envelope, returning applied limits.
869    ///
870    /// Returns a tuple of `Enforcement` and `RateLimits`:
871    ///
872    /// - Enforcements declare the quantities of categories that have been rate limited with the
873    ///   individual reason codes that caused rate limiting. If multiple rate limits applied to a
874    ///   category, then the longest limit is reported.
875    /// - Rate limits declare all active rate limits, regardless of whether they have been applied
876    ///   to items in the envelope. This excludes rate limits applied to required attachments, since
877    ///   clients are allowed to continue sending them.
878    pub async fn compute(
879        mut self,
880        envelope: &Envelope,
881        scoping: &'a Scoping,
882    ) -> Result<(Enforcement, RateLimits), E> {
883        let mut summary = EnvelopeSummary::compute(envelope);
884        summary.event_category = self.event_category.or(summary.event_category);
885
886        let (enforcement, rate_limits) = self.execute(&summary, scoping).await?;
887        Ok((enforcement, rate_limits))
888    }
889
890    async fn execute(
891        &mut self,
892        summary: &EnvelopeSummary,
893        scoping: &'a Scoping,
894    ) -> Result<(Enforcement, RateLimits), E> {
895        let mut rate_limits = RateLimits::new();
896        let mut enforcement = Enforcement::default();
897
898        // Handle event.
899        if let Some(category) = summary.event_category {
900            // Check the broad category for limits.
901            let mut event_limits = self.check.apply(scoping.item(category), 1).await?;
902            enforcement.event = CategoryLimit::new(category, 1, event_limits.longest());
903
904            if let Some(index_category) = category.index_category() {
905                // Check the specific/indexed category for limits only if the specific one has not already
906                // an enforced limit.
907                if event_limits.is_empty() {
908                    event_limits.merge(self.check.apply(scoping.item(index_category), 1).await?);
909                }
910
911                enforcement.event_indexed =
912                    CategoryLimit::new(index_category, 1, event_limits.longest());
913            };
914
915            rate_limits.merge(event_limits);
916        }
917
918        // Handle spans.
919        if enforcement.is_event_active() {
920            enforcement.spans = enforcement
921                .event
922                .clone_for(DataCategory::Span, summary.span_quantity);
923
924            enforcement.spans_indexed = enforcement
925                .event_indexed
926                .clone_for(DataCategory::SpanIndexed, summary.span_quantity);
927        } else if summary.span_quantity > 0 || summary.has_span_dependent_items() {
928            let mut span_limits = self
929                .check
930                .apply(scoping.item(DataCategory::Span), summary.span_quantity)
931                .await?;
932            enforcement.spans = CategoryLimit::new(
933                DataCategory::Span,
934                summary.span_quantity,
935                span_limits.longest(),
936            );
937
938            if span_limits.is_empty() {
939                span_limits.merge(
940                    self.check
941                        .apply(
942                            scoping.item(DataCategory::SpanIndexed),
943                            summary.span_quantity,
944                        )
945                        .await?,
946                );
947            }
948
949            enforcement.spans_indexed = CategoryLimit::new(
950                DataCategory::SpanIndexed,
951                summary.span_quantity,
952                span_limits.longest(),
953            );
954
955            rate_limits.merge(span_limits);
956        }
957
958        // Handle span attachments
959        if enforcement.spans_indexed.is_active() {
960            enforcement.attachments_limits.span.bytes = enforcement.spans_indexed.clone_for(
961                DataCategory::Attachment,
962                summary.attachment_quantities.span.bytes,
963            );
964            enforcement.attachments_limits.span.count = enforcement.spans_indexed.clone_for(
965                DataCategory::AttachmentItem,
966                summary.attachment_quantities.span.count,
967            );
968        } else if !summary.attachment_quantities.span.is_empty() {
969            // While we could combine this check with the check that we do for event and trace
970            // attachments, this would complicate the logic so we opted against doing that.
971            // In practice the performance impact should be negligible since different types
972            // of attachments should rarely be send together.
973            enforcement.attachments_limits.span = self
974                .check_attachment_limits(scoping, &summary.attachment_quantities.span)
975                .await?;
976        }
977
978        // Handle attachments.
979        if let Some(limit) = enforcement.active_event() {
980            let limit1 = limit.clone_for(
981                DataCategory::Attachment,
982                summary.attachment_quantities.event.bytes,
983            );
984            let limit2 = limit.clone_for(
985                DataCategory::AttachmentItem,
986                summary.attachment_quantities.event.count,
987            );
988
989            enforcement.attachments_limits.event.bytes = limit1;
990            enforcement.attachments_limits.event.count = limit2;
991        } else {
992            let mut attachment_limits = RateLimits::new();
993            if summary.attachment_quantities.event.bytes > 0 {
994                let item_scoping = scoping.item(DataCategory::Attachment);
995
996                let attachment_byte_limits = self
997                    .check
998                    .apply(item_scoping, summary.attachment_quantities.event.bytes)
999                    .await?;
1000
1001                enforcement.attachments_limits.event.bytes = CategoryLimit::new(
1002                    DataCategory::Attachment,
1003                    summary.attachment_quantities.event.bytes,
1004                    attachment_byte_limits.longest(),
1005                );
1006                enforcement.attachments_limits.event.count =
1007                    enforcement.attachments_limits.event.bytes.clone_for(
1008                        DataCategory::AttachmentItem,
1009                        summary.attachment_quantities.event.count,
1010                    );
1011                attachment_limits.merge(attachment_byte_limits);
1012            }
1013            if !attachment_limits.is_limited() && summary.attachment_quantities.event.count > 0 {
1014                let item_scoping = scoping.item(DataCategory::AttachmentItem);
1015
1016                let attachment_item_limits = self
1017                    .check
1018                    .apply(item_scoping, summary.attachment_quantities.event.count)
1019                    .await?;
1020
1021                enforcement.attachments_limits.event.count = CategoryLimit::new(
1022                    DataCategory::AttachmentItem,
1023                    summary.attachment_quantities.event.count,
1024                    attachment_item_limits.longest(),
1025                );
1026                enforcement.attachments_limits.event.bytes =
1027                    enforcement.attachments_limits.event.count.clone_for(
1028                        DataCategory::Attachment,
1029                        summary.attachment_quantities.event.bytes,
1030                    );
1031                attachment_limits.merge(attachment_item_limits);
1032            }
1033
1034            // Only record rate limits for plain attachments. For all other attachments, it's
1035            // perfectly "legal" to send them. They will still be discarded in Sentry, but clients
1036            // can continue to send them.
1037            if summary.has_plain_attachments {
1038                rate_limits.merge(attachment_limits);
1039            }
1040        }
1041
1042        // Handle trace attachments.
1043        if !summary.attachment_quantities.trace.is_empty() {
1044            enforcement.attachments_limits.trace = self
1045                .check_attachment_limits(scoping, &summary.attachment_quantities.trace)
1046                .await?;
1047        }
1048
1049        // Handle sessions.
1050        if summary.session_quantity > 0 {
1051            let item_scoping = scoping.item(DataCategory::Session);
1052            let session_limits = self
1053                .check
1054                .apply(item_scoping, summary.session_quantity)
1055                .await?;
1056            enforcement.sessions = CategoryLimit::new(
1057                DataCategory::Session,
1058                summary.session_quantity,
1059                session_limits.longest(),
1060            );
1061            rate_limits.merge(session_limits);
1062        }
1063
1064        // Handle trace metrics.
1065        let mut trace_metric_limits = RateLimits::new();
1066        if summary.trace_metric_quantity > 0 {
1067            let item_scoping = scoping.item(DataCategory::TraceMetric);
1068            trace_metric_limits = self
1069                .check
1070                .apply(item_scoping, summary.trace_metric_quantity)
1071                .await?;
1072            enforcement.trace_metrics = CategoryLimit::new(
1073                DataCategory::TraceMetric,
1074                summary.trace_metric_quantity,
1075                trace_metric_limits.longest(),
1076            );
1077            enforcement.trace_metrics_bytes = CategoryLimit::new(
1078                DataCategory::TraceMetricByte,
1079                summary.trace_metric_byte_quantity,
1080                trace_metric_limits.longest(),
1081            );
1082        }
1083        if !trace_metric_limits.is_limited() && summary.trace_metric_byte_quantity > 0 {
1084            let item_scoping = scoping.item(DataCategory::TraceMetricByte);
1085            trace_metric_limits = self
1086                .check
1087                .apply(item_scoping, summary.trace_metric_byte_quantity)
1088                .await?;
1089            enforcement.trace_metrics = CategoryLimit::new(
1090                DataCategory::TraceMetric,
1091                summary.trace_metric_quantity,
1092                trace_metric_limits.longest(),
1093            );
1094            enforcement.trace_metrics_bytes = CategoryLimit::new(
1095                DataCategory::TraceMetricByte,
1096                summary.trace_metric_byte_quantity,
1097                trace_metric_limits.longest(),
1098            );
1099        }
1100        rate_limits.merge(trace_metric_limits);
1101
1102        // Handle logs.
1103        let mut log_limits = RateLimits::new();
1104        if summary.log_item_quantity > 0 {
1105            let item_scoping = scoping.item(DataCategory::LogItem);
1106            log_limits = self
1107                .check
1108                .apply(item_scoping, summary.log_item_quantity)
1109                .await?;
1110            enforcement.log_bytes = CategoryLimit::new(
1111                DataCategory::LogByte,
1112                summary.log_byte_quantity,
1113                log_limits.longest(),
1114            );
1115            enforcement.log_items = CategoryLimit::new(
1116                DataCategory::LogItem,
1117                summary.log_item_quantity,
1118                log_limits.longest(),
1119            );
1120        }
1121        if !log_limits.is_limited() && summary.log_byte_quantity > 0 {
1122            let item_scoping = scoping.item(DataCategory::LogByte);
1123            log_limits = self
1124                .check
1125                .apply(item_scoping, summary.log_byte_quantity)
1126                .await?;
1127            enforcement.log_bytes = CategoryLimit::new(
1128                DataCategory::LogByte,
1129                summary.log_byte_quantity,
1130                log_limits.longest(),
1131            );
1132            enforcement.log_items = CategoryLimit::new(
1133                DataCategory::LogItem,
1134                summary.log_item_quantity,
1135                log_limits.longest(),
1136            );
1137        }
1138        rate_limits.merge(log_limits);
1139
1140        // Handle profiles.
1141        if enforcement.is_event_active() {
1142            enforcement.profiles = enforcement
1143                .event
1144                .clone_for(DataCategory::Profile, summary.profile_quantity.total);
1145            enforcement.profiles_indexed = enforcement
1146                .event_indexed
1147                .clone_for(DataCategory::ProfileIndexed, summary.profile_quantity.total);
1148
1149            enforcement.profiles_backend = enforcement.event.clone_for(
1150                DataCategory::ProfileBackend,
1151                summary.profile_quantity.backend,
1152            );
1153            enforcement.profiles_ui = enforcement
1154                .event
1155                .clone_for(DataCategory::ProfileUi, summary.profile_quantity.ui);
1156        } else if summary.profile_quantity.total > 0 {
1157            let mut profile_limits = self
1158                .check
1159                .apply(
1160                    scoping.item(DataCategory::Profile),
1161                    summary.profile_quantity.total,
1162                )
1163                .await?;
1164
1165            // Profiles can persist in envelopes without transaction if the transaction item
1166            // was dropped by dynamic sampling.
1167            if profile_limits.is_empty() && summary.event_category.is_none() {
1168                profile_limits = self
1169                    .check
1170                    .apply(scoping.item(DataCategory::Transaction), 0)
1171                    .await?;
1172            }
1173
1174            enforcement.profiles = CategoryLimit::new(
1175                DataCategory::Profile,
1176                summary.profile_quantity.total,
1177                profile_limits.longest(),
1178            );
1179
1180            if enforcement.profiles.quantity == 0 {
1181                if summary.profile_quantity.backend > 0 {
1182                    let limit = self
1183                        .check
1184                        .apply(
1185                            scoping.item(DataCategory::ProfileBackend),
1186                            summary.profile_quantity.backend,
1187                        )
1188                        .await?;
1189
1190                    enforcement.profiles_backend = CategoryLimit::new(
1191                        DataCategory::ProfileBackend,
1192                        summary.profile_quantity.backend,
1193                        limit.longest(),
1194                    )
1195                    .add_outcome_category(DataCategory::Profile);
1196
1197                    profile_limits.merge(limit);
1198                }
1199                if summary.profile_quantity.ui > 0 {
1200                    let limit = self
1201                        .check
1202                        .apply(
1203                            scoping.item(DataCategory::ProfileUi),
1204                            summary.profile_quantity.ui,
1205                        )
1206                        .await?;
1207
1208                    enforcement.profiles_ui = CategoryLimit::new(
1209                        DataCategory::ProfileUi,
1210                        summary.profile_quantity.ui,
1211                        limit.longest(),
1212                    )
1213                    .add_outcome_category(DataCategory::Profile);
1214
1215                    profile_limits.merge(limit);
1216                }
1217            } else {
1218                enforcement.profiles_backend = CategoryLimit::new(
1219                    DataCategory::ProfileBackend,
1220                    summary.profile_quantity.backend,
1221                    profile_limits.longest(),
1222                );
1223                enforcement.profiles_ui = CategoryLimit::new(
1224                    DataCategory::ProfileUi,
1225                    summary.profile_quantity.ui,
1226                    profile_limits.longest(),
1227                );
1228            }
1229
1230            if enforcement.profiles.quantity > 0 {
1231                enforcement.profiles_indexed = enforcement
1232                    .profiles
1233                    .clone_for(DataCategory::ProfileIndexed, summary.profile_quantity.total);
1234            } else {
1235                let limit = self
1236                    .check
1237                    .apply(
1238                        scoping.item(DataCategory::ProfileIndexed),
1239                        summary.profile_quantity.total,
1240                    )
1241                    .await?;
1242
1243                if !limit.is_empty() {
1244                    enforcement.profiles_indexed = CategoryLimit::new(
1245                        DataCategory::ProfileIndexed,
1246                        summary.profile_quantity.total,
1247                        limit.longest(),
1248                    );
1249
1250                    profile_limits.merge(limit);
1251                } else {
1252                    enforcement.profiles_backend = enforcement
1253                        .profiles_backend
1254                        .add_outcome_category(DataCategory::ProfileIndexed);
1255                    enforcement.profiles_ui = enforcement
1256                        .profiles_ui
1257                        .add_outcome_category(DataCategory::ProfileIndexed);
1258                }
1259            }
1260
1261            rate_limits.merge(profile_limits);
1262        }
1263
1264        // Handle replays.
1265        if summary.replay_quantity > 0 {
1266            let item_scoping = scoping.item(DataCategory::Replay);
1267            let replay_limits = self
1268                .check
1269                .apply(item_scoping, summary.replay_quantity)
1270                .await?;
1271            enforcement.replays = CategoryLimit::new(
1272                DataCategory::Replay,
1273                summary.replay_quantity,
1274                replay_limits.longest(),
1275            );
1276            rate_limits.merge(replay_limits);
1277        }
1278
1279        // Handle user report v1s, which share limits with v2.
1280        if summary.user_report_quantity > 0 {
1281            let item_scoping = scoping.item(DataCategory::UserReportV2);
1282            let user_report_v2_limits = self
1283                .check
1284                .apply(item_scoping, summary.user_report_quantity)
1285                .await?;
1286            enforcement.user_reports = CategoryLimit::new(
1287                DataCategory::UserReportV2,
1288                summary.user_report_quantity,
1289                user_report_v2_limits.longest(),
1290            );
1291            rate_limits.merge(user_report_v2_limits);
1292        }
1293
1294        // Handle monitor checkins.
1295        if summary.monitor_quantity > 0 {
1296            let item_scoping = scoping.item(DataCategory::Monitor);
1297            let checkin_limits = self
1298                .check
1299                .apply(item_scoping, summary.monitor_quantity)
1300                .await?;
1301            enforcement.check_ins = CategoryLimit::new(
1302                DataCategory::Monitor,
1303                summary.monitor_quantity,
1304                checkin_limits.longest(),
1305            );
1306            rate_limits.merge(checkin_limits);
1307        }
1308
1309        // Handle profile chunks.
1310        if summary.profile_chunk_quantity > 0 {
1311            let item_scoping = scoping.item(DataCategory::ProfileChunk);
1312            let limits = self
1313                .check
1314                .apply(item_scoping, summary.profile_chunk_quantity)
1315                .await?;
1316            enforcement.profile_chunks = CategoryLimit::new(
1317                DataCategory::ProfileChunk,
1318                summary.profile_chunk_quantity,
1319                limits.longest(),
1320            );
1321            rate_limits.merge(limits);
1322        }
1323
1324        if summary.profile_chunk_ui_quantity > 0 {
1325            let item_scoping = scoping.item(DataCategory::ProfileChunkUi);
1326            let limits = self
1327                .check
1328                .apply(item_scoping, summary.profile_chunk_ui_quantity)
1329                .await?;
1330            enforcement.profile_chunks_ui = CategoryLimit::new(
1331                DataCategory::ProfileChunkUi,
1332                summary.profile_chunk_ui_quantity,
1333                limits.longest(),
1334            );
1335            rate_limits.merge(limits);
1336        }
1337
1338        Ok((enforcement, rate_limits))
1339    }
1340
1341    async fn check_attachment_limits(
1342        &mut self,
1343        scoping: &Scoping,
1344        quantities: &AttachmentQuantity,
1345    ) -> Result<AttachmentLimits, E> {
1346        let mut attachment_limits = self
1347            .check
1348            .apply(scoping.item(DataCategory::Attachment), quantities.bytes)
1349            .await?;
1350
1351        // Note: The check here is taken from the attachments logic for consistency I think just
1352        // checking `is_empty` should be fine?
1353        if !attachment_limits.is_limited() && quantities.count > 0 {
1354            attachment_limits.merge(
1355                self.check
1356                    .apply(scoping.item(DataCategory::AttachmentItem), quantities.count)
1357                    .await?,
1358            );
1359        }
1360
1361        Ok(AttachmentLimits {
1362            bytes: CategoryLimit::new(
1363                DataCategory::Attachment,
1364                quantities.bytes,
1365                attachment_limits.longest(),
1366            ),
1367            count: CategoryLimit::new(
1368                DataCategory::AttachmentItem,
1369                quantities.count,
1370                attachment_limits.longest(),
1371            ),
1372        })
1373    }
1374}
1375
1376impl<F, E, R> fmt::Debug for EnvelopeLimiter<F, E, R> {
1377    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1378        f.debug_struct("EnvelopeLimiter")
1379            .field("event_category", &self.event_category)
1380            .finish()
1381    }
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386
1387    use std::collections::{BTreeMap, BTreeSet};
1388    use std::sync::Arc;
1389
1390    use relay_base_schema::organization::OrganizationId;
1391    use relay_base_schema::project::{ProjectId, ProjectKey};
1392    use relay_metrics::MetricNamespace;
1393    use relay_quotas::RetryAfter;
1394    use relay_system::Addr;
1395    use smallvec::smallvec;
1396    use tokio::sync::Mutex;
1397
1398    use super::*;
1399    use crate::envelope::ParentId;
1400    use crate::{
1401        envelope::{AttachmentType, ContentType, SourceQuantities},
1402        extractors::RequestMeta,
1403    };
1404
1405    struct RateLimitTestCase {
1406        name: &'static str,
1407        denied_categories: &'static [DataCategory],
1408        expect_attachment_limit_active: bool,
1409        expected_limiter_calls: &'static [(DataCategory, usize)],
1410        expected_outcomes: &'static [(DataCategory, usize)],
1411    }
1412
1413    #[tokio::test]
1414    async fn test_format_rate_limits() {
1415        let mut rate_limits = RateLimits::new();
1416
1417        // Add a generic rate limit for all categories.
1418        rate_limits.add(RateLimit {
1419            categories: Default::default(),
1420            scope: RateLimitScope::Organization(OrganizationId::new(42)),
1421            reason_code: Some(ReasonCode::new("my_limit")),
1422            retry_after: RetryAfter::from_secs(42),
1423            namespaces: smallvec![],
1424        });
1425
1426        // Add a more specific rate limit for just one category.
1427        rate_limits.add(RateLimit {
1428            categories: [DataCategory::Transaction, DataCategory::Security].into(),
1429            scope: RateLimitScope::Project(ProjectId::new(21)),
1430            reason_code: None,
1431            retry_after: RetryAfter::from_secs(4711),
1432            namespaces: smallvec![],
1433        });
1434
1435        let formatted = format_rate_limits(&rate_limits);
1436        let expected = "42::organization:my_limit, 4711:transaction;security:project";
1437        assert_eq!(formatted, expected);
1438    }
1439
1440    #[tokio::test]
1441    async fn test_format_rate_limits_namespace() {
1442        let mut rate_limits = RateLimits::new();
1443
1444        // Rate limit with reason code and namespace.
1445        rate_limits.add(RateLimit {
1446            categories: [DataCategory::MetricBucket].into(),
1447            scope: RateLimitScope::Organization(OrganizationId::new(42)),
1448            reason_code: Some(ReasonCode::new("my_limit")),
1449            retry_after: RetryAfter::from_secs(42),
1450            namespaces: smallvec![MetricNamespace::Custom, MetricNamespace::Spans],
1451        });
1452
1453        // Rate limit without reason code.
1454        rate_limits.add(RateLimit {
1455            categories: [DataCategory::MetricBucket].into(),
1456            scope: RateLimitScope::Organization(OrganizationId::new(42)),
1457            reason_code: None,
1458            retry_after: RetryAfter::from_secs(42),
1459            namespaces: smallvec![MetricNamespace::Spans],
1460        });
1461
1462        let formatted = format_rate_limits(&rate_limits);
1463        let expected = "42:metric_bucket:organization:my_limit:custom;spans, 42:metric_bucket:organization::spans";
1464        assert_eq!(formatted, expected);
1465    }
1466
1467    #[tokio::test]
1468    async fn test_parse_invalid_rate_limits() {
1469        let scoping = Scoping {
1470            organization_id: OrganizationId::new(42),
1471            project_id: ProjectId::new(21),
1472            project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1473            key_id: Some(17),
1474        };
1475
1476        assert!(parse_rate_limits(&scoping, "").is_ok());
1477        assert!(parse_rate_limits(&scoping, "invalid").is_ok());
1478        assert!(parse_rate_limits(&scoping, ",,,").is_ok());
1479    }
1480
1481    #[tokio::test]
1482    async fn test_parse_rate_limits() {
1483        let scoping = Scoping {
1484            organization_id: OrganizationId::new(42),
1485            project_id: ProjectId::new(21),
1486            project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1487            key_id: Some(17),
1488        };
1489
1490        // contains "foobar", an unknown scope that should be mapped to Unknown
1491        let formatted =
1492            "42::organization:my_limit, invalid, 4711:foobar;transaction;security:project";
1493        let rate_limits: Vec<RateLimit> =
1494            parse_rate_limits(&scoping, formatted).into_iter().collect();
1495
1496        assert_eq!(
1497            rate_limits,
1498            vec![
1499                RateLimit {
1500                    categories: Default::default(),
1501                    scope: RateLimitScope::Organization(OrganizationId::new(42)),
1502                    reason_code: Some(ReasonCode::new("my_limit")),
1503                    retry_after: rate_limits[0].retry_after,
1504                    namespaces: smallvec![],
1505                },
1506                RateLimit {
1507                    categories: [
1508                        DataCategory::Unknown,
1509                        DataCategory::Transaction,
1510                        DataCategory::Security,
1511                    ]
1512                    .into(),
1513                    scope: RateLimitScope::Project(ProjectId::new(21)),
1514                    reason_code: None,
1515                    retry_after: rate_limits[1].retry_after,
1516                    namespaces: smallvec![],
1517                }
1518            ]
1519        );
1520
1521        assert_eq!(42, rate_limits[0].retry_after.remaining_seconds());
1522        assert_eq!(4711, rate_limits[1].retry_after.remaining_seconds());
1523    }
1524
1525    #[tokio::test]
1526    async fn test_parse_rate_limits_namespace() {
1527        let scoping = Scoping {
1528            organization_id: OrganizationId::new(42),
1529            project_id: ProjectId::new(21),
1530            project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1531            key_id: Some(17),
1532        };
1533
1534        let formatted = "42:metric_bucket:organization::custom;spans";
1535        let rate_limits: Vec<RateLimit> =
1536            parse_rate_limits(&scoping, formatted).into_iter().collect();
1537
1538        assert_eq!(
1539            rate_limits,
1540            vec![RateLimit {
1541                categories: [DataCategory::MetricBucket].into(),
1542                scope: RateLimitScope::Organization(OrganizationId::new(42)),
1543                reason_code: None,
1544                retry_after: rate_limits[0].retry_after,
1545                namespaces: smallvec![MetricNamespace::Custom, MetricNamespace::Spans],
1546            }]
1547        );
1548    }
1549
1550    #[tokio::test]
1551    async fn test_parse_rate_limits_empty_namespace() {
1552        let scoping = Scoping {
1553            organization_id: OrganizationId::new(42),
1554            project_id: ProjectId::new(21),
1555            project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1556            key_id: Some(17),
1557        };
1558
1559        // notice the trailing colon
1560        let formatted = "42:metric_bucket:organization:some_reason:";
1561        let rate_limits: Vec<RateLimit> =
1562            parse_rate_limits(&scoping, formatted).into_iter().collect();
1563
1564        assert_eq!(
1565            rate_limits,
1566            vec![RateLimit {
1567                categories: [DataCategory::MetricBucket].into(),
1568                scope: RateLimitScope::Organization(OrganizationId::new(42)),
1569                reason_code: Some(ReasonCode::new("some_reason")),
1570                retry_after: rate_limits[0].retry_after,
1571                namespaces: smallvec![],
1572            }]
1573        );
1574    }
1575
1576    #[tokio::test]
1577    async fn test_parse_rate_limits_only_unknown() {
1578        let scoping = Scoping {
1579            organization_id: OrganizationId::new(42),
1580            project_id: ProjectId::new(21),
1581            project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
1582            key_id: Some(17),
1583        };
1584
1585        let formatted = "42:foo;bar:organization";
1586        let rate_limits: Vec<RateLimit> =
1587            parse_rate_limits(&scoping, formatted).into_iter().collect();
1588
1589        assert_eq!(
1590            rate_limits,
1591            vec![RateLimit {
1592                categories: [DataCategory::Unknown, DataCategory::Unknown].into(),
1593                scope: RateLimitScope::Organization(OrganizationId::new(42)),
1594                reason_code: None,
1595                retry_after: rate_limits[0].retry_after,
1596                namespaces: smallvec![],
1597            },]
1598        );
1599    }
1600
1601    macro_rules! envelope {
1602        ($( $item_type:ident $( :: $attachment_type:ident )? ),*) => {{
1603            let bytes = "{\"dsn\":\"https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42\"}";
1604            #[allow(unused_mut)]
1605            let mut envelope = Envelope::parse_bytes(bytes.into()).unwrap();
1606            $(
1607                let mut item = Item::new(ItemType::$item_type);
1608                item.set_payload(ContentType::OctetStream, "0123456789");
1609                $( item.set_attachment_type(AttachmentType::$attachment_type); )?
1610                envelope.add_item(item);
1611            )*
1612
1613            let (outcome_aggregator, _) = Addr::custom();
1614
1615            ManagedEnvelope::new(
1616                envelope,
1617                outcome_aggregator,
1618            )
1619        }}
1620    }
1621
1622    fn set_extracted(envelope: &mut Envelope, ty: ItemType) {
1623        envelope
1624            .get_item_by_mut(|item| *item.ty() == ty)
1625            .unwrap()
1626            .set_metrics_extracted(true);
1627    }
1628
1629    fn rate_limit(category: DataCategory) -> RateLimit {
1630        RateLimit {
1631            categories: [category].into(),
1632            scope: RateLimitScope::Organization(OrganizationId::new(42)),
1633            reason_code: None,
1634            retry_after: RetryAfter::from_secs(60),
1635            namespaces: smallvec![],
1636        }
1637    }
1638
1639    fn trace_attachment_item(bytes: usize, parent_id: Option<ParentId>) -> Item {
1640        let mut item = Item::new(ItemType::Attachment);
1641        item.set_payload(ContentType::TraceAttachment, "0".repeat(bytes));
1642        item.set_parent_id(parent_id);
1643        item
1644    }
1645
1646    #[derive(Debug, Default)]
1647    struct MockLimiter {
1648        denied: Vec<DataCategory>,
1649        called: BTreeMap<DataCategory, usize>,
1650        checked: BTreeSet<DataCategory>,
1651    }
1652
1653    impl MockLimiter {
1654        pub fn deny(mut self, category: DataCategory) -> Self {
1655            self.denied.push(category);
1656            self
1657        }
1658
1659        pub fn check(&mut self, scoping: ItemScoping, quantity: usize) -> Result<RateLimits, ()> {
1660            let cat = scoping.category;
1661            let previous = self.called.insert(cat, quantity);
1662            assert!(previous.is_none(), "rate limiter invoked twice for {cat}");
1663
1664            let mut limits = RateLimits::new();
1665            if self.denied.contains(&cat) {
1666                limits.add(rate_limit(cat));
1667            }
1668            Ok(limits)
1669        }
1670
1671        #[track_caller]
1672        pub fn assert_call(&mut self, category: DataCategory, expected: usize) {
1673            self.checked.insert(category);
1674
1675            let quantity = self.called.get(&category).copied();
1676            assert_eq!(
1677                quantity,
1678                Some(expected),
1679                "Expected quantity `{expected}` for data category `{category}`, got {quantity:?}."
1680            );
1681        }
1682    }
1683
1684    impl Drop for MockLimiter {
1685        fn drop(&mut self) {
1686            if std::thread::panicking() {
1687                return;
1688            }
1689
1690            for checked in &self.checked {
1691                self.called.remove(checked);
1692            }
1693
1694            if self.called.is_empty() {
1695                return;
1696            }
1697
1698            let not_asserted = self
1699                .called
1700                .iter()
1701                .map(|(k, v)| format!("- {k}: {v}"))
1702                .collect::<Vec<_>>()
1703                .join("\n");
1704
1705            panic!("Following calls to the limiter were not asserted:\n{not_asserted}");
1706        }
1707    }
1708
1709    async fn enforce_and_apply(
1710        mock: Arc<Mutex<MockLimiter>>,
1711        envelope: &mut ManagedEnvelope,
1712        #[allow(unused_variables)] assume_event: Option<DataCategory>,
1713    ) -> (Enforcement, RateLimits) {
1714        let scoping = envelope.scoping();
1715
1716        #[allow(unused_mut)]
1717        let mut limiter = EnvelopeLimiter::new(CheckLimits::All, move |s, q| {
1718            let mock = mock.clone();
1719            async move {
1720                let mut mock = mock.lock().await;
1721                mock.check(s, q)
1722            }
1723        });
1724        #[cfg(feature = "processing")]
1725        if let Some(assume_event) = assume_event {
1726            limiter.assume_event(assume_event);
1727        }
1728
1729        let (enforcement, limits) = limiter
1730            .compute(envelope.envelope_mut(), &scoping)
1731            .await
1732            .unwrap();
1733
1734        // We implemented `clone` only for tests because we don't want to make `apply_with_outcomes`
1735        // &self because we want move semantics to prevent double tracking.
1736        enforcement.clone().apply_with_outcomes(envelope);
1737
1738        (enforcement, limits)
1739    }
1740
1741    fn mock_limiter(categories: &[DataCategory]) -> Arc<Mutex<MockLimiter>> {
1742        let mut mock = MockLimiter::default();
1743        for &category in categories {
1744            mock = mock.deny(category);
1745        }
1746
1747        Arc::new(Mutex::new(mock))
1748    }
1749
1750    #[tokio::test]
1751    async fn test_enforce_pass_empty() {
1752        let mut envelope = envelope![];
1753
1754        let mock = mock_limiter(&[]);
1755        let (_, limits) = enforce_and_apply(mock, &mut envelope, None).await;
1756
1757        assert!(!limits.is_limited());
1758        assert!(envelope.envelope().is_empty());
1759    }
1760
1761    #[tokio::test]
1762    async fn test_enforce_limit_error_event() {
1763        let mut envelope = envelope![Event];
1764
1765        let mock = mock_limiter(&[DataCategory::Error]);
1766        let (_, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1767
1768        assert!(limits.is_limited());
1769        assert!(envelope.envelope().is_empty());
1770        mock.lock().await.assert_call(DataCategory::Error, 1);
1771    }
1772
1773    #[tokio::test]
1774    async fn test_enforce_limit_error_with_attachments() {
1775        let mut envelope = envelope![Event, Attachment];
1776
1777        let mock = mock_limiter(&[DataCategory::Error]);
1778        let (_, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1779
1780        assert!(limits.is_limited());
1781        assert!(envelope.envelope().is_empty());
1782        mock.lock().await.assert_call(DataCategory::Error, 1);
1783    }
1784
1785    #[tokio::test]
1786    async fn test_enforce_limit_minidump() {
1787        let mut envelope = envelope![Attachment::Minidump];
1788
1789        let mock = mock_limiter(&[DataCategory::Error]);
1790        let (_, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1791
1792        assert!(limits.is_limited());
1793        assert!(envelope.envelope().is_empty());
1794        mock.lock().await.assert_call(DataCategory::Error, 1);
1795    }
1796
1797    #[tokio::test]
1798    async fn test_enforce_limit_attachments() {
1799        let mut envelope = envelope![Attachment::Minidump, Attachment];
1800
1801        let mock = mock_limiter(&[DataCategory::Attachment]);
1802        let (_, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1803
1804        // Attachments would be limited, but crash reports create events and are thus allowed.
1805        assert!(limits.is_limited());
1806        assert_eq!(envelope.envelope().len(), 1);
1807        mock.lock().await.assert_call(DataCategory::Error, 1);
1808        mock.lock().await.assert_call(DataCategory::Attachment, 20);
1809    }
1810
1811    /// Limit stand-alone profiles.
1812    #[tokio::test]
1813    async fn test_enforce_limit_profiles() {
1814        let mut envelope = envelope![Profile, Profile];
1815
1816        let mock = mock_limiter(&[DataCategory::Profile]);
1817        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1818
1819        assert!(limits.is_limited());
1820        assert_eq!(envelope.envelope().len(), 0);
1821        mock.lock().await.assert_call(DataCategory::Profile, 2);
1822
1823        assert_eq!(
1824            get_outcomes(enforcement),
1825            vec![
1826                (DataCategory::Profile, 2),
1827                (DataCategory::ProfileIndexed, 2)
1828            ]
1829        );
1830    }
1831
1832    /// Limit profile chunks.
1833    #[tokio::test]
1834    async fn test_enforce_limit_profile_chunks_no_profile_type() {
1835        // In this test we have profile chunks which have not yet been classified, which means they
1836        // should not be rate limited.
1837        let mut envelope = envelope![ProfileChunk, ProfileChunk];
1838
1839        let mock = mock_limiter(&[DataCategory::ProfileChunk]);
1840        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1841        assert!(!limits.is_limited());
1842        assert_eq!(get_outcomes(enforcement), vec![]);
1843
1844        let mock = mock_limiter(&[DataCategory::ProfileChunkUi]);
1845        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1846        assert!(!limits.is_limited());
1847        assert_eq!(get_outcomes(enforcement), vec![]);
1848
1849        assert_eq!(envelope.envelope().len(), 2);
1850    }
1851
1852    #[tokio::test]
1853    async fn test_enforce_limit_profile_chunks_ui() {
1854        let mut envelope = envelope![];
1855
1856        let mut item = Item::new(ItemType::ProfileChunk);
1857        item.set_platform("python".to_owned());
1858        envelope.envelope_mut().add_item(item);
1859        let mut item = Item::new(ItemType::ProfileChunk);
1860        item.set_platform("javascript".to_owned());
1861        envelope.envelope_mut().add_item(item);
1862
1863        let mock = mock_limiter(&[DataCategory::ProfileChunkUi]);
1864        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1865
1866        assert!(limits.is_limited());
1867        assert_eq!(envelope.envelope().len(), 1);
1868        mock.lock()
1869            .await
1870            .assert_call(DataCategory::ProfileChunkUi, 1);
1871        mock.lock().await.assert_call(DataCategory::ProfileChunk, 1);
1872
1873        assert_eq!(
1874            get_outcomes(enforcement),
1875            vec![(DataCategory::ProfileChunkUi, 1)]
1876        );
1877    }
1878
1879    #[tokio::test]
1880    async fn test_enforce_limit_profile_chunks_backend() {
1881        let mut envelope = envelope![];
1882
1883        let mut item = Item::new(ItemType::ProfileChunk);
1884        item.set_platform("python".to_owned());
1885        envelope.envelope_mut().add_item(item);
1886        let mut item = Item::new(ItemType::ProfileChunk);
1887        item.set_platform("javascript".to_owned());
1888        envelope.envelope_mut().add_item(item);
1889
1890        let mock = mock_limiter(&[DataCategory::ProfileChunk]);
1891        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1892
1893        assert!(limits.is_limited());
1894        assert_eq!(envelope.envelope().len(), 1);
1895        mock.lock()
1896            .await
1897            .assert_call(DataCategory::ProfileChunkUi, 1);
1898        mock.lock().await.assert_call(DataCategory::ProfileChunk, 1);
1899
1900        assert_eq!(
1901            get_outcomes(enforcement),
1902            vec![(DataCategory::ProfileChunk, 1)]
1903        );
1904    }
1905
1906    /// Limit replays.
1907    #[tokio::test]
1908    async fn test_enforce_limit_replays() {
1909        let mut envelope = envelope![ReplayEvent, ReplayRecording, ReplayVideo];
1910
1911        let mock = mock_limiter(&[DataCategory::Replay]);
1912        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1913
1914        assert!(limits.is_limited());
1915        assert_eq!(envelope.envelope().len(), 0);
1916        mock.lock().await.assert_call(DataCategory::Replay, 3);
1917
1918        assert_eq!(get_outcomes(enforcement), vec![(DataCategory::Replay, 3),]);
1919    }
1920
1921    /// Limit monitor checkins.
1922    #[tokio::test]
1923    async fn test_enforce_limit_monitor_checkins() {
1924        let mut envelope = envelope![CheckIn];
1925
1926        let mock = mock_limiter(&[DataCategory::Monitor]);
1927        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1928
1929        assert!(limits.is_limited());
1930        assert_eq!(envelope.envelope().len(), 0);
1931        mock.lock().await.assert_call(DataCategory::Monitor, 1);
1932
1933        assert_eq!(get_outcomes(enforcement), vec![(DataCategory::Monitor, 1)])
1934    }
1935
1936    #[tokio::test]
1937    async fn test_enforce_pass_minidump() {
1938        let mut envelope = envelope![Attachment::Minidump];
1939
1940        let mock = mock_limiter(&[DataCategory::Attachment]);
1941        let (_, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1942
1943        // If only crash report attachments are present, we don't emit a rate limit.
1944        assert!(!limits.is_limited());
1945        assert_eq!(envelope.envelope().len(), 1);
1946        mock.lock().await.assert_call(DataCategory::Error, 1);
1947        mock.lock().await.assert_call(DataCategory::Attachment, 10);
1948    }
1949
1950    #[tokio::test]
1951    async fn test_enforce_skip_rate_limited() {
1952        let mut envelope = envelope![];
1953
1954        let mut item = Item::new(ItemType::Attachment);
1955        item.set_payload(ContentType::OctetStream, "0123456789");
1956        item.set_rate_limited(true);
1957        envelope.envelope_mut().add_item(item);
1958
1959        let mock = mock_limiter(&[DataCategory::Error]);
1960        let (_, limits) = enforce_and_apply(mock, &mut envelope, None).await;
1961
1962        assert!(!limits.is_limited()); // No new rate limits applied.
1963        assert_eq!(envelope.envelope().len(), 1); // The item was retained
1964    }
1965
1966    #[tokio::test]
1967    async fn test_enforce_pass_sessions() {
1968        let mut envelope = envelope![Session, Session, Session];
1969
1970        let mock = mock_limiter(&[DataCategory::Error]);
1971        let (_, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1972
1973        // If only crash report attachments are present, we don't emit a rate limit.
1974        assert!(!limits.is_limited());
1975        assert_eq!(envelope.envelope().len(), 3);
1976        mock.lock().await.assert_call(DataCategory::Session, 3);
1977    }
1978
1979    #[tokio::test]
1980    async fn test_enforce_limit_sessions() {
1981        let mut envelope = envelope![Session, Session, Event];
1982
1983        let mock = mock_limiter(&[DataCategory::Session]);
1984        let (_, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
1985
1986        // If only crash report attachments are present, we don't emit a rate limit.
1987        assert!(limits.is_limited());
1988        assert_eq!(envelope.envelope().len(), 1);
1989        mock.lock().await.assert_call(DataCategory::Error, 1);
1990        mock.lock().await.assert_call(DataCategory::Session, 2);
1991    }
1992
1993    #[tokio::test]
1994    #[cfg(feature = "processing")]
1995    async fn test_enforce_limit_assumed_event() {
1996        let mut envelope = envelope![];
1997
1998        let mock = mock_limiter(&[DataCategory::Transaction]);
1999        let (_, limits) =
2000            enforce_and_apply(mock.clone(), &mut envelope, Some(DataCategory::Transaction)).await;
2001
2002        assert!(limits.is_limited());
2003        assert!(envelope.envelope().is_empty()); // obviously
2004        mock.lock().await.assert_call(DataCategory::Transaction, 1);
2005    }
2006
2007    #[tokio::test]
2008    #[cfg(feature = "processing")]
2009    async fn test_enforce_limit_assumed_attachments() {
2010        let mut envelope = envelope![Attachment, Attachment];
2011
2012        let mock = mock_limiter(&[DataCategory::Error]);
2013        let (_, limits) =
2014            enforce_and_apply(mock.clone(), &mut envelope, Some(DataCategory::Error)).await;
2015
2016        assert!(limits.is_limited());
2017        assert!(envelope.envelope().is_empty());
2018        mock.lock().await.assert_call(DataCategory::Error, 1);
2019    }
2020
2021    #[tokio::test]
2022    async fn test_enforce_transaction() {
2023        let mut envelope = envelope![Transaction];
2024
2025        let mock = mock_limiter(&[DataCategory::Transaction]);
2026        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2027
2028        assert!(limits.is_limited());
2029        assert!(enforcement.event_indexed.is_active());
2030        assert!(enforcement.event.is_active());
2031        mock.lock().await.assert_call(DataCategory::Transaction, 1);
2032
2033        assert_eq!(
2034            get_outcomes(enforcement),
2035            vec![
2036                (DataCategory::Transaction, 1),
2037                (DataCategory::TransactionIndexed, 1),
2038                (DataCategory::Span, 1),
2039                (DataCategory::SpanIndexed, 1),
2040            ]
2041        );
2042    }
2043
2044    #[tokio::test]
2045    async fn test_enforce_transaction_non_indexed() {
2046        let mut envelope = envelope![Transaction, Profile];
2047        let scoping = envelope.scoping();
2048
2049        let mock = mock_limiter(&[DataCategory::TransactionIndexed]);
2050
2051        let mock_clone = mock.clone();
2052        let limiter = EnvelopeLimiter::new(CheckLimits::NonIndexed, move |s, q| {
2053            let mock_clone = mock_clone.clone();
2054            async move {
2055                let mut mock = mock_clone.lock().await;
2056                mock.check(s, q)
2057            }
2058        });
2059        let (enforcement, limits) = limiter
2060            .compute(envelope.envelope_mut(), &scoping)
2061            .await
2062            .unwrap();
2063        enforcement.clone().apply_with_outcomes(&mut envelope);
2064
2065        assert!(!limits.is_limited());
2066        assert!(!enforcement.event_indexed.is_active());
2067        assert!(!enforcement.event.is_active());
2068        assert!(!enforcement.profiles_indexed.is_active());
2069        assert!(!enforcement.profiles.is_active());
2070        assert!(!enforcement.spans.is_active());
2071        assert!(!enforcement.spans_indexed.is_active());
2072        mock.lock().await.assert_call(DataCategory::Transaction, 1);
2073        mock.lock().await.assert_call(DataCategory::Profile, 1);
2074        mock.lock().await.assert_call(DataCategory::Span, 1);
2075    }
2076
2077    #[tokio::test]
2078    async fn test_enforce_transaction_no_indexing_quota() {
2079        let mut envelope = envelope![Transaction];
2080
2081        let mock = mock_limiter(&[DataCategory::TransactionIndexed]);
2082        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2083
2084        assert!(limits.is_limited());
2085        assert!(enforcement.event_indexed.is_active());
2086        assert!(!enforcement.event.is_active());
2087        mock.lock().await.assert_call(DataCategory::Transaction, 1);
2088        mock.lock()
2089            .await
2090            .assert_call(DataCategory::TransactionIndexed, 1);
2091    }
2092
2093    #[tokio::test]
2094    async fn test_enforce_transaction_attachment_enforced() {
2095        let mut envelope = envelope![Transaction, Attachment];
2096
2097        let mock = mock_limiter(&[DataCategory::Transaction]);
2098        let (enforcement, _) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2099
2100        assert!(enforcement.event.is_active());
2101        assert!(enforcement.attachments_limits.event.is_active());
2102        mock.lock().await.assert_call(DataCategory::Transaction, 1);
2103    }
2104
2105    fn get_outcomes(enforcement: Enforcement) -> Vec<(DataCategory, usize)> {
2106        enforcement
2107            .get_outcomes()
2108            .map(|(_, data_category, quantity)| (data_category, quantity))
2109            .collect::<Vec<_>>()
2110    }
2111
2112    #[tokio::test]
2113    async fn test_enforce_transaction_profile_enforced() {
2114        let mut envelope = envelope![Transaction, Profile];
2115
2116        let mock = mock_limiter(&[DataCategory::Transaction]);
2117        let (enforcement, _) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2118
2119        assert!(enforcement.event.is_active());
2120        assert!(enforcement.profiles.is_active());
2121        mock.lock().await.assert_call(DataCategory::Transaction, 1);
2122
2123        assert_eq!(
2124            get_outcomes(enforcement),
2125            vec![
2126                (DataCategory::Transaction, 1),
2127                (DataCategory::TransactionIndexed, 1),
2128                (DataCategory::Profile, 1),
2129                (DataCategory::ProfileIndexed, 1),
2130                (DataCategory::Span, 1),
2131                (DataCategory::SpanIndexed, 1),
2132            ]
2133        );
2134    }
2135
2136    #[tokio::test]
2137    async fn test_enforce_transaction_standalone_profile_enforced() {
2138        // When the transaction is sampled, the profile survives as standalone.
2139        let mut envelope = envelope![Profile];
2140
2141        let mock = mock_limiter(&[DataCategory::Transaction]);
2142        let (enforcement, _) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2143
2144        assert!(enforcement.profiles.is_active());
2145        mock.lock().await.assert_call(DataCategory::Profile, 1);
2146        mock.lock().await.assert_call(DataCategory::Transaction, 0);
2147
2148        assert_eq!(
2149            get_outcomes(enforcement),
2150            vec![
2151                (DataCategory::Profile, 1),
2152                (DataCategory::ProfileIndexed, 1),
2153            ]
2154        );
2155    }
2156
2157    #[tokio::test]
2158    async fn test_enforce_transaction_attachment_enforced_indexing_quota() {
2159        let mut envelope = envelope![Transaction, Attachment];
2160        set_extracted(envelope.envelope_mut(), ItemType::Transaction);
2161
2162        let mock = mock_limiter(&[DataCategory::TransactionIndexed]);
2163        let (enforcement, _) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2164
2165        assert!(!enforcement.event.is_active());
2166        assert!(enforcement.event_indexed.is_active());
2167        assert!(enforcement.attachments_limits.event.is_active());
2168        mock.lock().await.assert_call(DataCategory::Transaction, 1);
2169        mock.lock()
2170            .await
2171            .assert_call(DataCategory::TransactionIndexed, 1);
2172
2173        assert_eq!(
2174            get_outcomes(enforcement),
2175            vec![
2176                (DataCategory::TransactionIndexed, 1),
2177                (DataCategory::Attachment, 10),
2178                (DataCategory::AttachmentItem, 1),
2179                (DataCategory::SpanIndexed, 1),
2180            ]
2181        );
2182    }
2183
2184    #[tokio::test]
2185    async fn test_enforce_span() {
2186        let mut envelope = envelope![Span, Span];
2187
2188        let mock = mock_limiter(&[DataCategory::Span]);
2189        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2190
2191        assert!(limits.is_limited());
2192        assert!(enforcement.spans_indexed.is_active());
2193        assert!(enforcement.spans.is_active());
2194        mock.lock().await.assert_call(DataCategory::Span, 2);
2195
2196        assert_eq!(
2197            get_outcomes(enforcement),
2198            vec![(DataCategory::Span, 2), (DataCategory::SpanIndexed, 2)]
2199        );
2200    }
2201
2202    #[tokio::test]
2203    async fn test_enforce_span_no_indexing_quota() {
2204        let mut envelope = envelope![Span, Span];
2205
2206        let mock = mock_limiter(&[DataCategory::SpanIndexed]);
2207        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2208
2209        assert!(limits.is_limited());
2210        assert!(enforcement.spans_indexed.is_active());
2211        assert!(!enforcement.spans.is_active());
2212        mock.lock().await.assert_call(DataCategory::Span, 2);
2213        mock.lock().await.assert_call(DataCategory::SpanIndexed, 2);
2214
2215        assert_eq!(
2216            get_outcomes(enforcement),
2217            vec![(DataCategory::SpanIndexed, 2)]
2218        );
2219    }
2220
2221    #[tokio::test]
2222    async fn test_enforce_span_metrics_extracted_no_indexing_quota() {
2223        let mut envelope = envelope![Span, Span];
2224        set_extracted(envelope.envelope_mut(), ItemType::Span);
2225
2226        let mock = mock_limiter(&[DataCategory::SpanIndexed]);
2227        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2228
2229        assert!(limits.is_limited());
2230        assert!(enforcement.spans_indexed.is_active());
2231        assert!(!enforcement.spans.is_active());
2232        mock.lock().await.assert_call(DataCategory::Span, 2);
2233        mock.lock().await.assert_call(DataCategory::SpanIndexed, 2);
2234
2235        assert_eq!(
2236            get_outcomes(enforcement),
2237            vec![(DataCategory::SpanIndexed, 2)]
2238        );
2239    }
2240
2241    #[test]
2242    fn test_source_quantity_for_total_quantity() {
2243        let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
2244            .parse()
2245            .unwrap();
2246        let request_meta = RequestMeta::new(dsn);
2247
2248        let mut envelope = Envelope::from_request(None, request_meta);
2249
2250        let mut item = Item::new(ItemType::MetricBuckets);
2251        item.set_source_quantities(SourceQuantities {
2252            transactions: 5,
2253            spans: 0,
2254            buckets: 5,
2255        });
2256        envelope.add_item(item);
2257
2258        let mut item = Item::new(ItemType::MetricBuckets);
2259        item.set_source_quantities(SourceQuantities {
2260            transactions: 2,
2261            spans: 0,
2262            buckets: 3,
2263        });
2264        envelope.add_item(item);
2265
2266        let summary = EnvelopeSummary::compute(&envelope);
2267
2268        assert_eq!(summary.secondary_transaction_quantity, 7);
2269    }
2270
2271    #[tokio::test]
2272    async fn test_enforce_limit_logs_count() {
2273        let mut envelope = envelope![Log, Log];
2274
2275        let mock = mock_limiter(&[DataCategory::LogItem]);
2276        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2277
2278        assert!(limits.is_limited());
2279        assert_eq!(envelope.envelope().len(), 0);
2280        mock.lock().await.assert_call(DataCategory::LogItem, 2);
2281
2282        assert_eq!(
2283            get_outcomes(enforcement),
2284            vec![(DataCategory::LogItem, 2), (DataCategory::LogByte, 20)]
2285        );
2286    }
2287
2288    #[tokio::test]
2289    async fn test_enforce_limit_logs_bytes() {
2290        let mut envelope = envelope![Log, Log];
2291
2292        let mock = mock_limiter(&[DataCategory::LogByte]);
2293        let (enforcement, limits) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2294
2295        assert!(limits.is_limited());
2296        assert_eq!(envelope.envelope().len(), 0);
2297        mock.lock().await.assert_call(DataCategory::LogItem, 2);
2298        mock.lock().await.assert_call(DataCategory::LogByte, 20);
2299
2300        assert_eq!(
2301            get_outcomes(enforcement),
2302            vec![(DataCategory::LogItem, 2), (DataCategory::LogByte, 20)]
2303        );
2304    }
2305
2306    #[tokio::test]
2307    async fn test_enforce_standalone_span_attachment() {
2308        let test_cases = &[
2309            RateLimitTestCase {
2310                name: "span_limit",
2311                denied_categories: &[DataCategory::Span],
2312                expect_attachment_limit_active: true,
2313                expected_limiter_calls: &[(DataCategory::Span, 0)],
2314                expected_outcomes: &[
2315                    (DataCategory::Attachment, 7),
2316                    (DataCategory::AttachmentItem, 1),
2317                ],
2318            },
2319            RateLimitTestCase {
2320                name: "span_indexed_limit",
2321                denied_categories: &[DataCategory::SpanIndexed],
2322                expect_attachment_limit_active: true,
2323                expected_limiter_calls: &[(DataCategory::Span, 0), (DataCategory::SpanIndexed, 0)],
2324                expected_outcomes: &[
2325                    (DataCategory::Attachment, 7),
2326                    (DataCategory::AttachmentItem, 1),
2327                ],
2328            },
2329            RateLimitTestCase {
2330                name: "attachment_limit",
2331                denied_categories: &[DataCategory::Attachment],
2332                expect_attachment_limit_active: true,
2333                expected_limiter_calls: &[
2334                    (DataCategory::Span, 0),
2335                    (DataCategory::SpanIndexed, 0),
2336                    (DataCategory::Attachment, 7),
2337                ],
2338                expected_outcomes: &[
2339                    (DataCategory::Attachment, 7),
2340                    (DataCategory::AttachmentItem, 1),
2341                ],
2342            },
2343            RateLimitTestCase {
2344                name: "attachment_indexed_limit",
2345                denied_categories: &[DataCategory::AttachmentItem],
2346                expect_attachment_limit_active: true,
2347                expected_limiter_calls: &[
2348                    (DataCategory::Span, 0),
2349                    (DataCategory::SpanIndexed, 0),
2350                    (DataCategory::Attachment, 7),
2351                    (DataCategory::AttachmentItem, 1),
2352                ],
2353                expected_outcomes: &[
2354                    (DataCategory::Attachment, 7),
2355                    (DataCategory::AttachmentItem, 1),
2356                ],
2357            },
2358            RateLimitTestCase {
2359                name: "transaction_limit",
2360                denied_categories: &[DataCategory::Transaction],
2361                expect_attachment_limit_active: false,
2362                expected_limiter_calls: &[
2363                    (DataCategory::Span, 0),
2364                    (DataCategory::SpanIndexed, 0),
2365                    (DataCategory::Attachment, 7),
2366                    (DataCategory::AttachmentItem, 1),
2367                ],
2368                expected_outcomes: &[],
2369            },
2370            RateLimitTestCase {
2371                name: "error_limit",
2372                denied_categories: &[DataCategory::Error],
2373                expect_attachment_limit_active: false,
2374                expected_limiter_calls: &[
2375                    (DataCategory::Span, 0),
2376                    (DataCategory::SpanIndexed, 0),
2377                    (DataCategory::Attachment, 7),
2378                    (DataCategory::AttachmentItem, 1),
2379                ],
2380                expected_outcomes: &[],
2381            },
2382            RateLimitTestCase {
2383                name: "no_limits",
2384                denied_categories: &[],
2385                expect_attachment_limit_active: false,
2386                expected_limiter_calls: &[
2387                    (DataCategory::Span, 0),
2388                    (DataCategory::SpanIndexed, 0),
2389                    (DataCategory::Attachment, 7),
2390                    (DataCategory::AttachmentItem, 1),
2391                ],
2392                expected_outcomes: &[],
2393            },
2394        ];
2395
2396        for RateLimitTestCase {
2397            name,
2398            denied_categories,
2399            expect_attachment_limit_active,
2400            expected_limiter_calls,
2401            expected_outcomes,
2402        } in test_cases
2403        {
2404            let mut envelope = envelope![];
2405            envelope
2406                .envelope_mut()
2407                .add_item(trace_attachment_item(7, Some(ParentId::SpanId(None))));
2408
2409            let mock = mock_limiter(denied_categories);
2410            let (enforcement, _) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2411
2412            for &(category, quantity) in *expected_limiter_calls {
2413                mock.lock().await.assert_call(category, quantity);
2414            }
2415
2416            assert_eq!(
2417                enforcement.attachments_limits.span.bytes.is_active(),
2418                *expect_attachment_limit_active,
2419                "{name}: span_attachment byte limit mismatch"
2420            );
2421            assert_eq!(
2422                enforcement.attachments_limits.span.count.is_active(),
2423                *expect_attachment_limit_active,
2424                "{name}: span_attachment count limit mismatch"
2425            );
2426
2427            assert_eq!(
2428                get_outcomes(enforcement),
2429                *expected_outcomes,
2430                "{name}: outcome mismatch"
2431            );
2432        }
2433    }
2434
2435    #[tokio::test]
2436    async fn test_enforce_span_with_span_attachment() {
2437        let test_cases = &[
2438            RateLimitTestCase {
2439                name: "span_limit",
2440                denied_categories: &[DataCategory::Span, DataCategory::Attachment], // Attachment here has no effect
2441                expect_attachment_limit_active: true,
2442                expected_limiter_calls: &[(DataCategory::Span, 1)],
2443                expected_outcomes: &[
2444                    (DataCategory::Attachment, 7),
2445                    (DataCategory::AttachmentItem, 1),
2446                    (DataCategory::Span, 1),
2447                    (DataCategory::SpanIndexed, 1),
2448                ],
2449            },
2450            RateLimitTestCase {
2451                name: "span_indexed_limit",
2452                denied_categories: &[DataCategory::SpanIndexed, DataCategory::Attachment],
2453                expect_attachment_limit_active: true,
2454                expected_limiter_calls: &[(DataCategory::Span, 1), (DataCategory::SpanIndexed, 1)],
2455                expected_outcomes: &[
2456                    (DataCategory::Attachment, 7),
2457                    (DataCategory::AttachmentItem, 1),
2458                    (DataCategory::SpanIndexed, 1),
2459                ],
2460            },
2461            RateLimitTestCase {
2462                name: "attachment_limit",
2463                denied_categories: &[DataCategory::Attachment],
2464                expect_attachment_limit_active: true,
2465                expected_limiter_calls: &[
2466                    (DataCategory::Span, 1),
2467                    (DataCategory::SpanIndexed, 1),
2468                    (DataCategory::Attachment, 7),
2469                ],
2470                expected_outcomes: &[
2471                    (DataCategory::Attachment, 7),
2472                    (DataCategory::AttachmentItem, 1),
2473                ],
2474            },
2475            RateLimitTestCase {
2476                name: "attachment_indexed_limit",
2477                denied_categories: &[DataCategory::AttachmentItem],
2478                expect_attachment_limit_active: true,
2479                expected_limiter_calls: &[
2480                    (DataCategory::Span, 1),
2481                    (DataCategory::SpanIndexed, 1),
2482                    (DataCategory::Attachment, 7),
2483                    (DataCategory::AttachmentItem, 1),
2484                ],
2485                expected_outcomes: &[
2486                    (DataCategory::Attachment, 7),
2487                    (DataCategory::AttachmentItem, 1),
2488                ],
2489            },
2490            RateLimitTestCase {
2491                name: "transaction_limit",
2492                denied_categories: &[DataCategory::Transaction],
2493                expect_attachment_limit_active: false,
2494                expected_limiter_calls: &[
2495                    (DataCategory::Span, 1),
2496                    (DataCategory::SpanIndexed, 1),
2497                    (DataCategory::Attachment, 7),
2498                    (DataCategory::AttachmentItem, 1),
2499                ],
2500                expected_outcomes: &[],
2501            },
2502            RateLimitTestCase {
2503                name: "error_limit",
2504                denied_categories: &[DataCategory::Error],
2505                expect_attachment_limit_active: false,
2506                expected_limiter_calls: &[
2507                    (DataCategory::Span, 1),
2508                    (DataCategory::SpanIndexed, 1),
2509                    (DataCategory::Attachment, 7),
2510                    (DataCategory::AttachmentItem, 1),
2511                ],
2512                expected_outcomes: &[],
2513            },
2514            RateLimitTestCase {
2515                name: "no_limits",
2516                denied_categories: &[],
2517                expect_attachment_limit_active: false,
2518                expected_limiter_calls: &[
2519                    (DataCategory::Span, 1),
2520                    (DataCategory::SpanIndexed, 1),
2521                    (DataCategory::Attachment, 7),
2522                    (DataCategory::AttachmentItem, 1),
2523                ],
2524                expected_outcomes: &[],
2525            },
2526        ];
2527
2528        for RateLimitTestCase {
2529            name,
2530            denied_categories,
2531            expect_attachment_limit_active,
2532            expected_limiter_calls,
2533            expected_outcomes,
2534        } in test_cases
2535        {
2536            let mut envelope = envelope![Span];
2537            envelope
2538                .envelope_mut()
2539                .add_item(trace_attachment_item(7, Some(ParentId::SpanId(None))));
2540
2541            let mock = mock_limiter(denied_categories);
2542            let (enforcement, _) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2543
2544            for &(category, quantity) in *expected_limiter_calls {
2545                mock.lock().await.assert_call(category, quantity);
2546            }
2547
2548            assert_eq!(
2549                enforcement.attachments_limits.span.bytes.is_active(),
2550                *expect_attachment_limit_active,
2551                "{name}: span_attachment byte limit mismatch"
2552            );
2553            assert_eq!(
2554                enforcement.attachments_limits.span.count.is_active(),
2555                *expect_attachment_limit_active,
2556                "{name}: span_attachment count limit mismatch"
2557            );
2558
2559            assert_eq!(
2560                get_outcomes(enforcement),
2561                *expected_outcomes,
2562                "{name}: outcome mismatch"
2563            );
2564        }
2565    }
2566
2567    #[tokio::test]
2568    async fn test_enforce_transaction_span_attachment() {
2569        let test_cases = &[
2570            RateLimitTestCase {
2571                name: "span_limit",
2572                denied_categories: &[DataCategory::Span],
2573                expect_attachment_limit_active: true,
2574                expected_limiter_calls: &[
2575                    (DataCategory::Transaction, 1),
2576                    (DataCategory::TransactionIndexed, 1),
2577                    (DataCategory::Span, 1),
2578                ],
2579                expected_outcomes: &[
2580                    (DataCategory::Attachment, 7),
2581                    (DataCategory::AttachmentItem, 1),
2582                    (DataCategory::Span, 1),
2583                    (DataCategory::SpanIndexed, 1),
2584                ],
2585            },
2586            RateLimitTestCase {
2587                name: "transaction_limit",
2588                denied_categories: &[DataCategory::Transaction],
2589                expect_attachment_limit_active: true,
2590                expected_limiter_calls: &[(DataCategory::Transaction, 1)],
2591                expected_outcomes: &[
2592                    (DataCategory::Transaction, 1),
2593                    (DataCategory::TransactionIndexed, 1),
2594                    (DataCategory::Attachment, 7),
2595                    (DataCategory::AttachmentItem, 1),
2596                    (DataCategory::Span, 1),
2597                    (DataCategory::SpanIndexed, 1),
2598                ],
2599            },
2600            RateLimitTestCase {
2601                name: "error_limit",
2602                denied_categories: &[DataCategory::Error],
2603                expect_attachment_limit_active: false,
2604                expected_limiter_calls: &[
2605                    (DataCategory::Transaction, 1),
2606                    (DataCategory::TransactionIndexed, 1),
2607                    (DataCategory::Span, 1),
2608                    (DataCategory::SpanIndexed, 1),
2609                    (DataCategory::Attachment, 7),
2610                    (DataCategory::AttachmentItem, 1),
2611                ],
2612                expected_outcomes: &[],
2613            },
2614            RateLimitTestCase {
2615                name: "no_limits",
2616                denied_categories: &[],
2617                expect_attachment_limit_active: false,
2618                expected_limiter_calls: &[
2619                    (DataCategory::Transaction, 1),
2620                    (DataCategory::TransactionIndexed, 1),
2621                    (DataCategory::Span, 1),
2622                    (DataCategory::SpanIndexed, 1),
2623                    (DataCategory::Attachment, 7),
2624                    (DataCategory::AttachmentItem, 1),
2625                ],
2626                expected_outcomes: &[],
2627            },
2628        ];
2629
2630        for RateLimitTestCase {
2631            name,
2632            denied_categories,
2633            expect_attachment_limit_active,
2634            expected_limiter_calls,
2635            expected_outcomes,
2636        } in test_cases
2637        {
2638            let mut envelope = envelope![Transaction];
2639            envelope
2640                .envelope_mut()
2641                .add_item(trace_attachment_item(7, Some(ParentId::SpanId(None))));
2642
2643            let mock = mock_limiter(denied_categories);
2644            let (enforcement, _) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2645
2646            for &(category, quantity) in *expected_limiter_calls {
2647                mock.lock().await.assert_call(category, quantity);
2648            }
2649
2650            assert_eq!(
2651                enforcement.attachments_limits.span.bytes.is_active(),
2652                *expect_attachment_limit_active,
2653                "{name}: span_attachment byte limit mismatch"
2654            );
2655            assert_eq!(
2656                enforcement.attachments_limits.span.count.is_active(),
2657                *expect_attachment_limit_active,
2658                "{name}: span_attachment count limit mismatch"
2659            );
2660
2661            assert_eq!(
2662                get_outcomes(enforcement),
2663                *expected_outcomes,
2664                "{name}: outcome mismatch"
2665            );
2666        }
2667    }
2668
2669    #[tokio::test]
2670    async fn test_enforce_standalone_trace_attachment() {
2671        let test_cases = &[
2672            RateLimitTestCase {
2673                name: "attachment_limit",
2674                denied_categories: &[DataCategory::Attachment],
2675                expect_attachment_limit_active: true,
2676                expected_limiter_calls: &[(DataCategory::Attachment, 7)],
2677                expected_outcomes: &[
2678                    (DataCategory::Attachment, 7),
2679                    (DataCategory::AttachmentItem, 1),
2680                ],
2681            },
2682            RateLimitTestCase {
2683                name: "attachment_limit_and_attachment_item_limit",
2684                denied_categories: &[DataCategory::Attachment, DataCategory::AttachmentItem],
2685                expect_attachment_limit_active: true,
2686                expected_limiter_calls: &[(DataCategory::Attachment, 7)],
2687                expected_outcomes: &[
2688                    (DataCategory::Attachment, 7),
2689                    (DataCategory::AttachmentItem, 1),
2690                ],
2691            },
2692            RateLimitTestCase {
2693                name: "attachment_item_limit",
2694                denied_categories: &[DataCategory::AttachmentItem],
2695                expect_attachment_limit_active: true,
2696                expected_limiter_calls: &[
2697                    (DataCategory::Attachment, 7),
2698                    (DataCategory::AttachmentItem, 1),
2699                ],
2700                expected_outcomes: &[
2701                    (DataCategory::Attachment, 7),
2702                    (DataCategory::AttachmentItem, 1),
2703                ],
2704            },
2705            RateLimitTestCase {
2706                name: "no_limits",
2707                denied_categories: &[],
2708                expect_attachment_limit_active: false,
2709                expected_limiter_calls: &[
2710                    (DataCategory::Attachment, 7),
2711                    (DataCategory::AttachmentItem, 1),
2712                ],
2713                expected_outcomes: &[],
2714            },
2715        ];
2716
2717        for RateLimitTestCase {
2718            name,
2719            denied_categories,
2720            expect_attachment_limit_active,
2721            expected_limiter_calls,
2722            expected_outcomes,
2723        } in test_cases
2724        {
2725            let mut envelope = envelope![];
2726            envelope
2727                .envelope_mut()
2728                .add_item(trace_attachment_item(7, None));
2729
2730            let mock = mock_limiter(denied_categories);
2731            let (enforcement, _) = enforce_and_apply(mock.clone(), &mut envelope, None).await;
2732
2733            for &(category, quantity) in *expected_limiter_calls {
2734                mock.lock().await.assert_call(category, quantity);
2735            }
2736
2737            assert_eq!(
2738                enforcement.attachments_limits.trace.bytes.is_active(),
2739                *expect_attachment_limit_active,
2740                "{name}: trace_attachment byte limit mismatch"
2741            );
2742            assert_eq!(
2743                enforcement.attachments_limits.trace.count.is_active(),
2744                *expect_attachment_limit_active,
2745                "{name}: trace_attachment count limit mismatch"
2746            );
2747
2748            assert_eq!(
2749                get_outcomes(enforcement),
2750                *expected_outcomes,
2751                "{name}: outcome mismatch"
2752            );
2753        }
2754    }
2755}