diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5701063..91c85e858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### New Features +- Added [`EnvelopeFilter`](https://docs.rs/sentry-types/latest/sentry_types/protocol/v7/trait.EnvelopeFilter.html) and [`EnvelopeFilterCallbacks`](https://docs.rs/sentry-types/latest/sentry_types/protocol/v7/struct.EnvelopeFilterCallbacks.html), which let callers observe envelope items removed by [`Envelope::filter`](https://docs.rs/sentry-types/latest/sentry_types/protocol/v7/struct.Envelope.html#method.filter), including attachments removed after their event or transaction is filtered out. Existing `Envelope::filter` closure callers continue to work, although some closures may require an explicit item type annotation ([#1182](https://github.com/getsentry/sentry-rust/pull/1182)). - Added [`TransportFactory::create_transport_with_options`](https://docs.rs/sentry-core/latest/sentry_core/trait.TransportFactory.html#method.create_transport_with_options), which constructs transports from [`TransportOptions`](https://docs.rs/sentry-core/latest/sentry_core/struct.TransportOptions.html) instead of full [`ClientOptions`](https://docs.rs/sentry-core/latest/sentry_core/struct.ClientOptions.html) ([#1142](https://github.com/getsentry/sentry-rust/pull/1142)). - Added transport-specific options types and `with_options` constructors for built-in HTTP transports, including `ReqwestHttpTransportOptions`, `CurlHttpTransportOptions`, `UreqHttpTransportOptions`, and `EmbeddedSVCHttpTransportOptions` ([#1142](https://github.com/getsentry/sentry-rust/pull/1142)). - Added transport worker options types and deprecated the legacy constructors for built-in background transport workers, including `StdTransportThreadOptions` and `TokioTransportThreadOptions` ([#1142](https://github.com/getsentry/sentry-rust/pull/1142)). diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 7463ecdfe..1b434a05d 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -1,3 +1,4 @@ +use std::mem; use std::{borrow::Cow, io::Write, path::Path, time::SystemTime}; use serde::{Deserialize, Serialize}; @@ -367,6 +368,46 @@ impl Items { } } +/// A trait for types which can filter items in envelopes. +/// +/// This trait is used by [`Envelope::filter`]. +pub trait EnvelopeFilter: private::Sealed { + /// The function used to filter the envelopes. + /// + /// A return value of `true` indicates that the item should be kept in the envelope, `false` + /// will filter the value out. + /// + /// A value of `true` does not guarantee the item is kept; in particular, [`Envelope::filter`] + /// removes attachments if the corresponding event or transaction item is removed from the + /// envelope, as it no longer makes sense to send them in this case. + fn filter(&mut self, item: &EnvelopeItem) -> bool; + + /// A callback which is called with all items removed by filtering, including items for which + /// [`Self::filter`] had returned `true`, such as no-longer-applicable attachments. + fn on_filtered(&mut self, item: EnvelopeItem) { + let _ = item; + } +} + +/// A container for callbacks that can be passed to [`Envelope::filter`]. +pub struct EnvelopeFilterCallbacks { + filter: F, + on_filtered: C, +} + +impl EnvelopeFilterCallbacks { + /// Create a new [`EnvelopeFilterCallbacks`]. + /// + /// `filter` will be called to determine whether the envelope items should be kept. + /// `on_filtered` will be called on all envelope items which are then dropped. + pub fn new(filter: F, on_filtered: C) -> Self { + Self { + filter, + on_filtered, + } + } +} + /// A Sentry Envelope. /// /// An Envelope is the data format that Sentry uses for Ingestion. It can contain @@ -473,22 +514,25 @@ impl Envelope { /// contains an [`EnvelopeItem::Event`] or [`EnvelopeItem::Transaction`]. /// /// [`None`] is returned if no items remain in the Envelope after filtering. - pub fn filter

(self, mut predicate: P) -> Option + pub fn filter(self, mut filter: F) -> Option where - P: FnMut(&EnvelopeItem) -> bool, + F: EnvelopeFilter, { let Items::EnvelopeItems(items) = self.items else { - return if predicate(&EnvelopeItem::Raw) { + return if filter.filter(&EnvelopeItem::Raw) { Some(self) } else { + filter.on_filtered(EnvelopeItem::Raw); None }; }; let mut filtered = Envelope::new(); for item in items { - if predicate(&item) { + if filter.filter(&item) { filtered.add_item(item); + } else { + filter.on_filtered(item); } } @@ -496,7 +540,17 @@ impl Envelope { // an event/transaction if filtered.uuid().is_none() { if let Items::EnvelopeItems(ref mut items) = filtered.items { - items.retain(|item| !matches!(item, EnvelopeItem::Attachment(..))) + let old_items = mem::take(items); + *items = old_items + .into_iter() + .filter_map(|item| match item { + EnvelopeItem::Attachment(..) => { + filter.on_filtered(item); + None + } + _ => Some(item), + }) + .collect(); } } @@ -783,6 +837,44 @@ where } } +impl EnvelopeFilter for EnvelopeFilterCallbacks +where + F: FnMut(&EnvelopeItem) -> bool, + C: FnMut(EnvelopeItem), +{ + fn filter(&mut self, item: &EnvelopeItem) -> bool { + (self.filter)(item) + } + + fn on_filtered(&mut self, item: EnvelopeItem) { + (self.on_filtered)(item); + } +} + +impl EnvelopeFilter for F +where + F: FnMut(&EnvelopeItem) -> bool, +{ + fn filter(&mut self, item: &EnvelopeItem) -> bool { + self(item) + } +} + +mod private { + use super::*; + + pub trait Sealed {} + + impl Sealed for EnvelopeFilterCallbacks + where + F: FnMut(&EnvelopeItem) -> bool, + C: FnMut(EnvelopeItem), + { + } + + impl Sealed for F where F: FnMut(&EnvelopeItem) -> bool {} +} + #[cfg(test)] mod test { use std::str::FromStr; diff --git a/sentry/src/transports/ratelimit.rs b/sentry/src/transports/ratelimit.rs index 176f07fcc..683ce949c 100644 --- a/sentry/src/transports/ratelimit.rs +++ b/sentry/src/transports/ratelimit.rs @@ -111,7 +111,7 @@ impl RateLimiter { /// /// Returns [`None`] if all the envelope items were filtered out. pub fn filter_envelope(&self, envelope: Envelope) -> Option { - envelope.filter(|item| { + envelope.filter(|item: &_| { self.is_enabled(match item { EnvelopeItem::Event(_) => RateLimitingCategory::Error, EnvelopeItem::SessionUpdate(_) | EnvelopeItem::SessionAggregates(_) => {