Skip to main content

relay_server/services/
processor.rs

1use std::borrow::Cow;
2use std::collections::{BTreeMap, BTreeSet, HashMap};
3use std::error::Error;
4use std::fmt::Debug;
5use std::future::Future;
6use std::io::Write;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::time::Duration;
10
11use anyhow::Context;
12use brotli::CompressorWriter as BrotliEncoder;
13use bytes::Bytes;
14use chrono::{DateTime, Utc};
15use flate2::Compression;
16use flate2::write::{GzEncoder, ZlibEncoder};
17use futures::future::BoxFuture;
18use relay_base_schema::project::{ProjectId, ProjectKey};
19use relay_cogs::{AppFeature, Cogs, FeatureWeights, ResourceId, Token};
20use relay_common::time::UnixTimestamp;
21use relay_config::{Config, EmitOutcomes, HttpEncoding, UpstreamDescriptor};
22use relay_event_normalization::{ClockDriftProcessor, GeoIpLookup};
23use relay_event_schema::processor::ProcessingAction;
24use relay_event_schema::protocol::ClientReport;
25use relay_filter::FilterStatKey;
26use relay_log::sentry::SentryFutureExt;
27use relay_metrics::{Bucket, BucketMetadata, BucketView, BucketsView, MetricNamespace};
28use relay_quotas::{RateLimits, Scoping};
29use relay_sampling::evaluation::SamplingDecision;
30use relay_statsd::metric;
31use relay_system::{Addr, FromMessage, NoResponse, Service};
32use reqwest::header;
33use zstd::stream::Encoder as ZstdEncoder;
34
35use crate::envelope::{self, ContentType, Envelope, EnvelopeError, Item, ItemType};
36use crate::extractors::{PartialDsn, RequestMeta, RequestTrust};
37use crate::managed::ManagedEnvelope;
38use crate::metrics::{MetricOutcomes, MetricsLimiter, MinimalTrackableBucket};
39use crate::metrics_extraction::ExtractedMetrics;
40use crate::processing::errors::SwitchProcessingError;
41use crate::processing::relay::RelayProcessor;
42use crate::processing::{Forward as _, Output, Outputs, QuotaRateLimiter};
43use crate::service::ServiceError;
44use crate::services::global_config::GlobalConfigHandle;
45use crate::services::metrics::{Aggregator, FlushBuckets, MergeBuckets, ProjectBuckets};
46use crate::services::outcome::{self, DiscardItemType, DiscardReason, Outcome, TrackOutcome};
47use crate::services::projects::cache::ProjectCacheHandle;
48use crate::services::projects::project::{ProjectInfo, ProjectState};
49use crate::services::upstream::{
50    SendRequest, Sign, SignatureType, UpstreamRelay, UpstreamRequest, UpstreamRequestError,
51};
52use crate::statsd::{RelayCounters, RelayDistributions, RelayTimers};
53use crate::utils;
54use crate::{http, processing};
55use relay_threading::AsyncPool;
56use symbolic_unreal::{Unreal4Error, Unreal4ErrorKind};
57#[cfg(feature = "processing")]
58use {
59    crate::services::objectstore::Objectstore,
60    crate::services::store::Store,
61    itertools::Itertools,
62    relay_dynamic_config::GlobalConfig,
63    relay_quotas::{Quota, RateLimitingError, RedisRateLimiter},
64    relay_redis::RedisClients,
65    std::time::Instant,
66};
67
68mod metrics;
69
70/// The minimum clock drift for correction to apply.
71pub const MINIMUM_CLOCK_DRIFT: Duration = Duration::from_secs(55 * 60);
72
73/// An error returned when handling [`ProcessEnvelope`].
74#[derive(Debug, thiserror::Error)]
75pub enum ProcessingError {
76    #[error("invalid json in event")]
77    InvalidJson(#[source] serde_json::Error),
78
79    #[error("invalid message pack event payload")]
80    InvalidMsgpack(#[from] rmp_serde::decode::Error),
81
82    #[error("event data too deeply nested")]
83    NestingTooDeep,
84
85    #[error("invalid unreal crash report")]
86    InvalidUnrealReport(#[source] Unreal4Error),
87
88    #[error("event payload too large")]
89    PayloadTooLarge(DiscardItemType),
90
91    #[error("invalid transaction event")]
92    InvalidTransaction,
93
94    #[error("the item is not allowed/supported in this envelope")]
95    UnsupportedItem,
96
97    #[error("envelope processor failed")]
98    ProcessingFailed(#[from] ProcessingAction),
99
100    #[error("duplicate {0} in event")]
101    DuplicateItem(ItemType),
102
103    #[error("failed to extract event payload")]
104    NoEventPayload,
105
106    #[error("invalid security report type: {0:?}")]
107    InvalidSecurityType(Bytes),
108
109    #[error("unsupported security report type")]
110    UnsupportedSecurityType,
111
112    #[error("invalid security report")]
113    InvalidSecurityReport(#[source] serde_json::Error),
114
115    #[error("event filtered with reason: {0:?}")]
116    EventFiltered(FilterStatKey),
117
118    #[error("could not serialize event payload")]
119    SerializeFailed(#[source] serde_json::Error),
120
121    #[cfg(feature = "processing")]
122    #[error("failed to apply quotas")]
123    QuotasFailed(#[from] RateLimitingError),
124
125    #[error("nintendo switch dying message processing failed {0:?}")]
126    InvalidNintendoDyingMessage(#[source] SwitchProcessingError),
127
128    #[cfg(all(sentry, feature = "processing"))]
129    #[error("playstation dump processing failed: {0}")]
130    InvalidPlaystationDump(String),
131
132    #[cfg(feature = "processing")]
133    #[error("invalid attachment reference")]
134    InvalidAttachmentRef,
135}
136
137impl ProcessingError {
138    pub fn to_outcome(&self) -> Option<Outcome> {
139        match self {
140            Self::PayloadTooLarge(payload_type) => {
141                Some(Outcome::Invalid(DiscardReason::ItemTooLarge(*payload_type)))
142            }
143            Self::InvalidJson(_) => Some(Outcome::Invalid(DiscardReason::InvalidJson)),
144            Self::InvalidMsgpack(_) => Some(Outcome::Invalid(DiscardReason::InvalidMsgpack)),
145            Self::NestingTooDeep => Some(Outcome::Invalid(DiscardReason::NestingTooDeep)),
146            Self::InvalidSecurityType(_) => {
147                Some(Outcome::Invalid(DiscardReason::SecurityReportType))
148            }
149            Self::UnsupportedItem => Some(Outcome::Invalid(DiscardReason::InvalidEnvelope)),
150            Self::InvalidSecurityReport(_) => Some(Outcome::Invalid(DiscardReason::SecurityReport)),
151            Self::UnsupportedSecurityType => Some(Outcome::Filtered(FilterStatKey::InvalidCsp)),
152            Self::InvalidTransaction => Some(Outcome::Invalid(DiscardReason::InvalidTransaction)),
153            Self::DuplicateItem(_) => Some(Outcome::Invalid(DiscardReason::DuplicateItem)),
154            Self::NoEventPayload => Some(Outcome::Invalid(DiscardReason::NoEventPayload)),
155            Self::InvalidNintendoDyingMessage(_) => Some(Outcome::Invalid(DiscardReason::Payload)),
156            #[cfg(all(sentry, feature = "processing"))]
157            Self::InvalidPlaystationDump(_) => Some(Outcome::Invalid(DiscardReason::Payload)),
158            Self::InvalidUnrealReport(err) if err.kind() == Unreal4ErrorKind::BadCompression => {
159                Some(Outcome::Invalid(DiscardReason::InvalidCompression))
160            }
161            Self::InvalidUnrealReport(_) => Some(Outcome::Invalid(DiscardReason::ProcessUnreal)),
162            Self::SerializeFailed(_) | Self::ProcessingFailed(_) => {
163                Some(Outcome::Invalid(DiscardReason::Internal))
164            }
165            #[cfg(feature = "processing")]
166            Self::QuotasFailed(_) => Some(Outcome::Invalid(DiscardReason::Internal)),
167            Self::EventFiltered(key) => Some(Outcome::Filtered(key.clone())),
168
169            #[cfg(feature = "processing")]
170            Self::InvalidAttachmentRef => {
171                Some(Outcome::Invalid(DiscardReason::InvalidAttachmentRef))
172            }
173        }
174    }
175}
176
177impl From<Unreal4Error> for ProcessingError {
178    fn from(err: Unreal4Error) -> Self {
179        match err.kind() {
180            Unreal4ErrorKind::TooLarge => Self::PayloadTooLarge(ItemType::UnrealReport.into()),
181            _ => ProcessingError::InvalidUnrealReport(err),
182        }
183    }
184}
185
186/// A container for extracted metrics during processing.
187///
188/// The container enforces that the extracted metrics are correctly tagged
189/// with the dynamic sampling decision.
190#[derive(Debug)]
191pub struct ProcessingExtractedMetrics {
192    metrics: ExtractedMetrics,
193}
194
195impl ProcessingExtractedMetrics {
196    pub fn new() -> Self {
197        Self {
198            metrics: ExtractedMetrics::default(),
199        }
200    }
201
202    pub fn into_inner(self) -> ExtractedMetrics {
203        self.metrics
204    }
205
206    /// Extends the contained metrics with [`ExtractedMetrics`].
207    pub fn extend(
208        &mut self,
209        extracted: ExtractedMetrics,
210        sampling_decision: Option<SamplingDecision>,
211    ) {
212        self.extend_project_metrics(extracted.project_metrics, sampling_decision);
213        self.extend_sampling_metrics(extracted.sampling_metrics, sampling_decision);
214    }
215
216    /// Extends the contained project metrics.
217    pub fn extend_project_metrics<I>(
218        &mut self,
219        buckets: I,
220        sampling_decision: Option<SamplingDecision>,
221    ) where
222        I: IntoIterator<Item = Bucket>,
223    {
224        self.metrics
225            .project_metrics
226            .extend(buckets.into_iter().map(|mut bucket| {
227                bucket.metadata.extracted_from_indexed =
228                    sampling_decision == Some(SamplingDecision::Keep);
229                bucket
230            }));
231    }
232
233    /// Extends the contained sampling metrics.
234    pub fn extend_sampling_metrics<I>(
235        &mut self,
236        buckets: I,
237        sampling_decision: Option<SamplingDecision>,
238    ) where
239        I: IntoIterator<Item = Bucket>,
240    {
241        self.metrics
242            .sampling_metrics
243            .extend(buckets.into_iter().map(|mut bucket| {
244                bucket.metadata.extracted_from_indexed =
245                    sampling_decision == Some(SamplingDecision::Keep);
246                bucket
247            }));
248    }
249}
250
251fn send_metrics(
252    metrics: ExtractedMetrics,
253    project_key: ProjectKey,
254    sampling_key: Option<ProjectKey>,
255    aggregator: &Addr<Aggregator>,
256) {
257    let ExtractedMetrics {
258        project_metrics,
259        sampling_metrics,
260    } = metrics;
261
262    if !project_metrics.is_empty() {
263        aggregator.send(MergeBuckets {
264            project_key,
265            buckets: project_metrics,
266        });
267    }
268
269    if !sampling_metrics.is_empty() {
270        // If no sampling project state is available, we associate the sampling
271        // metrics with the current project.
272        //
273        // project_without_tracing         -> metrics goes to self
274        // dependent_project_with_tracing  -> metrics goes to root
275        // root_project_with_tracing       -> metrics goes to root == self
276        let sampling_project_key = sampling_key.unwrap_or(project_key);
277        aggregator.send(MergeBuckets {
278            project_key: sampling_project_key,
279            buckets: sampling_metrics,
280        });
281    }
282}
283
284/// Applies processing to all contents of the given envelope.
285///
286/// Depending on the contents of the envelope and Relay's mode, this includes:
287///
288///  - Basic normalization and validation for all item types.
289///  - Clock drift correction if the required `sent_at` header is present.
290///  - Expansion of certain item types (e.g. unreal).
291///  - Store normalization for event payloads in processing mode.
292///  - Rate limiters and inbound filters on events in processing mode.
293#[derive(Debug)]
294pub struct ProcessEnvelope {
295    /// Envelope to process.
296    pub envelope: ManagedEnvelope,
297    /// The project info.
298    pub project_info: Arc<ProjectInfo>,
299    /// Currently active cached rate limits for this project.
300    pub rate_limits: Arc<RateLimits>,
301    /// Root sampling project info.
302    pub sampling_project_info: Option<Arc<ProjectInfo>>,
303}
304
305/// Parses a list of metrics or metric buckets and pushes them to the project's aggregator.
306///
307/// This parses and validates the metrics:
308///  - For [`Metrics`](ItemType::Statsd), each metric is parsed separately, and invalid metrics are
309///    ignored independently.
310///  - For [`MetricBuckets`](ItemType::MetricBuckets), the entire list of buckets is parsed and
311///    dropped together on parsing failure.
312///  - Other envelope items will be ignored with an error message.
313///
314/// Additionally, processing applies clock drift correction using the system clock of this Relay, if
315/// the Envelope specifies the [`sent_at`](Envelope::sent_at) header.
316#[derive(Debug)]
317pub struct ProcessMetrics {
318    /// A list of metric items.
319    pub data: MetricData,
320    /// The target project.
321    pub project_key: ProjectKey,
322    /// Whether to keep or reset the metric metadata.
323    pub source: BucketSource,
324    /// The wall clock time at which the request was received.
325    pub received_at: DateTime<Utc>,
326    /// The value of the Envelope's [`sent_at`](Envelope::sent_at) header for clock drift
327    /// correction.
328    pub sent_at: Option<DateTime<Utc>>,
329}
330
331/// Raw unparsed metric data.
332#[derive(Debug)]
333pub enum MetricData {
334    /// Raw data, unparsed envelope items.
335    Raw(Vec<Item>),
336    /// Already parsed buckets but unprocessed.
337    Parsed(Vec<Bucket>),
338}
339
340impl MetricData {
341    /// Consumes the metric data and parses the contained buckets.
342    ///
343    /// If the contained data is already parsed the buckets are returned unchanged.
344    /// Raw buckets are parsed and created with the passed `timestamp`.
345    fn into_buckets(self, timestamp: UnixTimestamp) -> Vec<Bucket> {
346        let items = match self {
347            Self::Parsed(buckets) => return buckets,
348            Self::Raw(items) => items,
349        };
350
351        let mut buckets = Vec::new();
352        for item in items {
353            let payload = item.payload();
354            if item.ty() == &ItemType::Statsd {
355                for bucket_result in Bucket::parse_all(&payload, timestamp) {
356                    match bucket_result {
357                        Ok(bucket) => buckets.push(bucket),
358                        Err(error) => relay_log::debug!(
359                            error = &error as &dyn Error,
360                            "failed to parse metric bucket from statsd format",
361                        ),
362                    }
363                }
364            } else if item.ty() == &ItemType::MetricBuckets {
365                match serde_json::from_slice::<Vec<Bucket>>(&payload) {
366                    Ok(parsed_buckets) => {
367                        // Re-use the allocation of `b` if possible.
368                        if buckets.is_empty() {
369                            buckets = parsed_buckets;
370                        } else {
371                            buckets.extend(parsed_buckets);
372                        }
373                    }
374                    Err(error) => {
375                        relay_log::debug!(
376                            error = &error as &dyn Error,
377                            "failed to parse metric bucket",
378                        );
379                        metric!(counter(RelayCounters::MetricBucketsParsingFailed) += 1);
380                    }
381                }
382            } else {
383                relay_log::error!(
384                    "invalid item of type {} passed to ProcessMetrics",
385                    item.ty()
386                );
387            }
388        }
389        buckets
390    }
391}
392
393#[derive(Debug)]
394pub struct ProcessBatchedMetrics {
395    /// Metrics payload in JSON format.
396    pub payload: Bytes,
397    /// Whether to keep or reset the metric metadata.
398    pub source: BucketSource,
399    /// The wall clock time at which the request was received.
400    pub received_at: DateTime<Utc>,
401    /// The wall clock time at which the request was received.
402    pub sent_at: Option<DateTime<Utc>>,
403}
404
405/// Source information where a metric bucket originates from.
406#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
407pub enum BucketSource {
408    /// The metric bucket originated from an internal Relay use case.
409    ///
410    /// The metric bucket originates either from within the same Relay
411    /// or was accepted coming from another Relay which is registered as
412    /// an internal Relay via Relay's configuration.
413    Internal,
414    /// The bucket source originated from an untrusted source.
415    ///
416    /// Managed Relays sending extracted metrics are considered external,
417    /// it's a project use case but it comes from an untrusted source.
418    External,
419}
420
421impl BucketSource {
422    /// Infers the bucket source from [`RequestMeta::request_trust`].
423    pub fn from_meta(meta: &RequestMeta) -> Self {
424        match meta.request_trust() {
425            RequestTrust::Trusted => Self::Internal,
426            RequestTrust::Untrusted => Self::External,
427        }
428    }
429}
430
431/// Sends a client report to the upstream.
432#[derive(Debug)]
433pub struct SubmitClientReports {
434    /// The client report to be sent.
435    pub client_reports: Vec<ClientReport>,
436    /// Scoping information for the client report.
437    pub scoping: Scoping,
438}
439
440/// CPU-intensive processing tasks for envelopes.
441#[derive(Debug)]
442pub enum EnvelopeProcessor {
443    ProcessEnvelope(Box<ProcessEnvelope>),
444    ProcessProjectMetrics(Box<ProcessMetrics>),
445    ProcessBatchedMetrics(Box<ProcessBatchedMetrics>),
446    FlushBuckets(Box<FlushBuckets>),
447    SubmitClientReports(Box<SubmitClientReports>),
448}
449
450impl EnvelopeProcessor {
451    /// Returns the name of the message variant.
452    pub fn variant(&self) -> &'static str {
453        match self {
454            EnvelopeProcessor::ProcessEnvelope(_) => "ProcessEnvelope",
455            EnvelopeProcessor::ProcessProjectMetrics(_) => "ProcessProjectMetrics",
456            EnvelopeProcessor::ProcessBatchedMetrics(_) => "ProcessBatchedMetrics",
457            EnvelopeProcessor::FlushBuckets(_) => "FlushBuckets",
458            EnvelopeProcessor::SubmitClientReports(_) => "SubmitClientReports",
459        }
460    }
461}
462
463impl relay_system::Interface for EnvelopeProcessor {}
464
465impl FromMessage<ProcessEnvelope> for EnvelopeProcessor {
466    type Response = relay_system::NoResponse;
467
468    fn from_message(message: ProcessEnvelope, _sender: ()) -> Self {
469        Self::ProcessEnvelope(Box::new(message))
470    }
471}
472
473impl FromMessage<ProcessMetrics> for EnvelopeProcessor {
474    type Response = NoResponse;
475
476    fn from_message(message: ProcessMetrics, _: ()) -> Self {
477        Self::ProcessProjectMetrics(Box::new(message))
478    }
479}
480
481impl FromMessage<ProcessBatchedMetrics> for EnvelopeProcessor {
482    type Response = NoResponse;
483
484    fn from_message(message: ProcessBatchedMetrics, _: ()) -> Self {
485        Self::ProcessBatchedMetrics(Box::new(message))
486    }
487}
488
489impl FromMessage<FlushBuckets> for EnvelopeProcessor {
490    type Response = NoResponse;
491
492    fn from_message(message: FlushBuckets, _: ()) -> Self {
493        Self::FlushBuckets(Box::new(message))
494    }
495}
496
497impl FromMessage<SubmitClientReports> for EnvelopeProcessor {
498    type Response = NoResponse;
499
500    fn from_message(message: SubmitClientReports, _: ()) -> Self {
501        Self::SubmitClientReports(Box::new(message))
502    }
503}
504
505/// The asynchronous thread pool used for scheduling processing tasks in the processor.
506pub type EnvelopeProcessorServicePool = AsyncPool<BoxFuture<'static, ()>>;
507
508/// Service implementing the [`EnvelopeProcessor`] interface.
509///
510/// This service handles messages in a worker pool with configurable concurrency.
511#[derive(Clone)]
512pub struct EnvelopeProcessorService {
513    inner: Arc<InnerProcessor>,
514}
515
516/// Contains the addresses of services that the processor publishes to.
517pub struct Addrs {
518    pub outcome_aggregator: Addr<TrackOutcome>,
519    pub upstream_relay: Addr<UpstreamRelay>,
520    #[cfg(feature = "processing")]
521    pub objectstore: Option<Addr<Objectstore>>,
522    #[cfg(feature = "processing")]
523    pub store_forwarder: Option<Addr<Store>>,
524    pub aggregator: Addr<Aggregator>,
525}
526
527impl Default for Addrs {
528    fn default() -> Self {
529        Addrs {
530            outcome_aggregator: Addr::dummy(),
531            upstream_relay: Addr::dummy(),
532            #[cfg(feature = "processing")]
533            objectstore: None,
534            #[cfg(feature = "processing")]
535            store_forwarder: None,
536            aggregator: Addr::dummy(),
537        }
538    }
539}
540
541struct InnerProcessor {
542    pool: EnvelopeProcessorServicePool,
543    config: Arc<Config>,
544    global_config: GlobalConfigHandle,
545    project_cache: ProjectCacheHandle,
546    cogs: Cogs,
547    addrs: Addrs,
548    #[cfg(feature = "processing")]
549    rate_limiter: Option<Arc<RedisRateLimiter>>,
550    metric_outcomes: MetricOutcomes,
551    processor: RelayProcessor,
552}
553
554impl EnvelopeProcessorService {
555    /// Creates a multi-threaded envelope processor.
556    #[cfg_attr(feature = "processing", expect(clippy::too_many_arguments))]
557    pub fn new(
558        pool: EnvelopeProcessorServicePool,
559        config: Arc<Config>,
560        global_config: GlobalConfigHandle,
561        project_cache: ProjectCacheHandle,
562        cogs: Cogs,
563        #[cfg(feature = "processing")] redis: Option<RedisClients>,
564        addrs: Addrs,
565        metric_outcomes: MetricOutcomes,
566    ) -> Self {
567        let geoip_lookup = config
568            .geoip_path()
569            .and_then(
570                |p| match GeoIpLookup::open(p).context(ServiceError::GeoIp) {
571                    Ok(geoip) => Some(geoip),
572                    Err(err) => {
573                        relay_log::error!("failed to open GeoIP db {p:?}: {err:?}");
574                        None
575                    }
576                },
577            )
578            .unwrap_or_else(GeoIpLookup::empty);
579
580        if let Some(build_epoch) = geoip_lookup.build_epoch() {
581            relay_log::info!("Loaded GeoIP database (build: {build_epoch})");
582        }
583
584        #[cfg(feature = "processing")]
585        let rate_limiter = redis.map(|redis| {
586            RedisRateLimiter::new(redis.quotas)
587                .max_limit(config.max_rate_limit())
588                .cache(config.quota_cache_ratio(), config.quota_cache_max())
589        });
590
591        let quota_limiter = Arc::new(QuotaRateLimiter::new(
592            #[cfg(feature = "processing")]
593            project_cache.clone(),
594            #[cfg(feature = "processing")]
595            rate_limiter.clone(),
596        ));
597        #[cfg(feature = "processing")]
598        let rate_limiter = rate_limiter.map(Arc::new);
599        let inner = InnerProcessor {
600            pool,
601            global_config,
602            project_cache,
603            #[cfg(feature = "processing")]
604            rate_limiter,
605            processor: RelayProcessor::new(
606                cogs.clone(),
607                &quota_limiter,
608                &geoip_lookup,
609                addrs.outcome_aggregator.clone(),
610            ),
611            cogs,
612            addrs,
613            metric_outcomes,
614            config,
615        };
616
617        Self {
618            inner: Arc::new(inner),
619        }
620    }
621
622    async fn process_envelope(
623        &self,
624        project_id: ProjectId,
625        mut envelope: ManagedEnvelope,
626        ctx: processing::Context<'_>,
627    ) -> Vec<Output<Outputs>> {
628        // Pre-process the envelope headers.
629        if let Some(sampling_state) = ctx.sampling_project_info {
630            // Both transactions and standalone span envelopes need a normalized DSC header
631            // to make sampling rules based on the segment/transaction name work correctly.
632            envelope
633                .envelope_mut()
634                .parametrize_dsc_transaction(&sampling_state.config.tx_name_rules);
635        }
636
637        // Set the event retention. Effectively, this value will only be available in processing
638        // mode when the full project config is queried from the upstream.
639        if let Some(retention) = ctx.project_info.config.event_retention {
640            envelope.envelope_mut().set_retention(retention);
641        }
642
643        // Ensure the project ID is updated to the stored instance for this project cache. This can
644        // differ in two cases:
645        //  1. The envelope was sent to the legacy `/store/` endpoint without a project ID.
646        //  2. The DSN was moved and the envelope sent to the old project ID.
647        envelope
648            .envelope_mut()
649            .meta_mut()
650            .set_project_id(project_id);
651
652        self.inner.processor.run(envelope, ctx).await
653    }
654
655    /// Processes the envelope and returns the processed envelope back.
656    ///
657    /// Returns `Some` if the envelope passed inbound filtering and rate limiting. Invalid items are
658    /// removed from the envelope. Otherwise, if the envelope is empty or the entire envelope needs
659    /// to be dropped, this is `None`.
660    async fn process<'a>(
661        &self,
662        mut envelope: ManagedEnvelope,
663        ctx: processing::Context<'a>,
664    ) -> Vec<Output<Outputs>> {
665        // Prefer the project's project ID, and fall back to the stated project id from the
666        // envelope. The project ID is available in all modes, other than in proxy mode, where
667        // envelopes for unknown projects are forwarded blindly.
668        //
669        // Neither ID can be available in proxy mode on the /store/ endpoint. This is not supported,
670        // since we cannot process an envelope without project ID, so drop it.
671        let Some(project_id) = ctx
672            .project_info
673            .project_id
674            .or_else(|| envelope.envelope().meta().project_id())
675        else {
676            relay_log::error!(
677                tags.project_key = %envelope.envelope().meta().public_key(),
678                "project info does not contain project id"
679            );
680            envelope.reject(Outcome::Invalid(DiscardReason::Internal));
681            return Vec::new();
682        };
683
684        relay_log::configure_scope(|scope| {
685            scope.set_tag("project_id", project_id);
686        });
687
688        self.process_envelope(project_id, envelope, ctx).await
689    }
690
691    async fn handle_process_envelope(&self, cogs: &mut Token, message: ProcessEnvelope) {
692        let wait_time = message.envelope.age();
693        metric!(timer(RelayTimers::EnvelopeWaitTime) = wait_time);
694
695        // This COGS handling may need an overhaul in the future:
696        // Cancel the passed in token, to start individual measurements per processor instead.
697        cogs.cancel();
698
699        let global_config = self.inner.global_config.current().unwrap_or_default();
700
701        let ctx = processing::Context {
702            config: &self.inner.config,
703            global_config: &global_config,
704            project_info: &message.project_info,
705            sampling_project_info: message.sampling_project_info.as_deref(),
706            rate_limits: &message.rate_limits,
707        };
708
709        let project_key = message.envelope.meta().public_key();
710        // Only allow sending to the sampling key, if we successfully loaded a sampling project
711        // info relating to it. This filters out unknown/invalid project keys as well as project
712        // keys from different organizations.
713        let sampling_key = ctx
714            .sampling_project_info
715            .and_then(|p| p.get_public_key_config())
716            .map(|pkc| pkc.public_key);
717
718        relay_log::configure_scope(|scope| {
719            scope.set_tag("project_key", project_key);
720            if let Some(sampling_key) = sampling_key {
721                scope.set_tag("sampling_key", sampling_key);
722            }
723            let meta = message.envelope.envelope().meta();
724            scope.set_tag("sdk_name", meta.client_name());
725            if let Some(client) = meta.client() {
726                scope.set_tag("sdk", client);
727            }
728            if let Some(user_agent) = meta.user_agent() {
729                scope.set_extra("user_agent", user_agent.into());
730            }
731        });
732
733        let outputs = metric!(timer(RelayTimers::EnvelopeProcessingTime), {
734            self.process(message.envelope, ctx).await
735        });
736
737        let ctx = ctx.to_forward();
738        for Output { main, metrics } in outputs {
739            if let Some(metrics) = metrics {
740                let agg = &self.inner.addrs.aggregator;
741                metrics.accept(|metrics| {
742                    send_metrics(metrics, project_key, sampling_key, agg);
743                });
744            }
745
746            if let Some(output) = main {
747                // Only counting processing time for COGS at the moment.
748                self.submit_upstream(&mut Token::noop(), output, ctx);
749            }
750        }
751    }
752
753    fn handle_process_metrics(&self, cogs: &mut Token, message: ProcessMetrics) {
754        let ProcessMetrics {
755            data,
756            project_key,
757            received_at,
758            sent_at,
759            source,
760        } = message;
761
762        let received_timestamp =
763            UnixTimestamp::from_datetime(received_at).unwrap_or(UnixTimestamp::now());
764
765        let mut buckets = data.into_buckets(received_timestamp);
766        if buckets.is_empty() {
767            return;
768        };
769        cogs.update(relay_metrics::cogs::BySize(&buckets));
770
771        let clock_drift_processor =
772            ClockDriftProcessor::new(sent_at, received_at).at_least(MINIMUM_CLOCK_DRIFT);
773
774        buckets.retain_mut(|bucket| {
775            if let Err(error) = relay_metrics::normalize_bucket(bucket) {
776                relay_log::debug!(error = &error as &dyn Error, "dropping bucket {bucket:?}");
777                return false;
778            }
779
780            if !self::metrics::is_valid_namespace(bucket, source) {
781                relay_log::debug!("dropping bucket in invalid namespace {bucket:?}");
782                return false;
783            }
784
785            clock_drift_processor.process_timestamp(&mut bucket.timestamp);
786
787            if !matches!(source, BucketSource::Internal) {
788                bucket.metadata = BucketMetadata::new(received_timestamp);
789            }
790
791            true
792        });
793
794        let project = self.inner.project_cache.get(project_key);
795
796        // Best effort check to filter and rate limit buckets, if there is no project state
797        // available at the current time, we will check again after flushing.
798        let buckets = match project.state() {
799            ProjectState::Enabled(project_info) => {
800                let rate_limits = project.rate_limits().current_limits();
801                self.check_buckets(project_key, project_info, &rate_limits, buckets)
802            }
803            _ => buckets,
804        };
805
806        relay_log::trace!("merging metric buckets into the aggregator");
807        self.inner
808            .addrs
809            .aggregator
810            .send(MergeBuckets::new(project_key, buckets));
811    }
812
813    fn handle_process_batched_metrics(&self, cogs: &mut Token, message: ProcessBatchedMetrics) {
814        let ProcessBatchedMetrics {
815            payload,
816            source,
817            received_at,
818            sent_at,
819        } = message;
820
821        #[derive(serde::Deserialize)]
822        struct Wrapper {
823            buckets: HashMap<ProjectKey, Vec<Bucket>>,
824        }
825
826        let buckets = match serde_json::from_slice(&payload) {
827            Ok(Wrapper { buckets }) => buckets,
828            Err(error) => {
829                relay_log::debug!(
830                    error = &error as &dyn Error,
831                    "failed to parse batched metrics",
832                );
833                metric!(counter(RelayCounters::MetricBucketsParsingFailed) += 1);
834                return;
835            }
836        };
837
838        for (project_key, buckets) in buckets {
839            self.handle_process_metrics(
840                cogs,
841                ProcessMetrics {
842                    data: MetricData::Parsed(buckets),
843                    project_key,
844                    source,
845                    received_at,
846                    sent_at,
847                },
848            )
849        }
850    }
851
852    /// Submits a processor [`Output`] to the appropriate upstream.
853    ///
854    /// If processing is enabled, the upstream is Kafka.
855    fn submit_upstream(
856        &self,
857        cogs: &mut Token,
858        output: Outputs,
859        ctx: processing::ForwardContext<'_>,
860    ) {
861        let _submit = cogs.start_category("submit");
862
863        #[cfg(feature = "processing")]
864        if ctx.config.processing_enabled()
865            && let Some(store_forwarder) = &self.inner.addrs.store_forwarder
866        {
867            use crate::processing::StoreHandle;
868
869            let objectstore = self.inner.addrs.objectstore.as_ref();
870            let handle = StoreHandle::new(store_forwarder, objectstore, ctx.global_config);
871
872            output
873                .forward_store(handle, ctx)
874                .unwrap_or_else(|err| err.into_inner());
875
876            return;
877        }
878
879        match output.serialize_envelope(ctx) {
880            Ok(envelope) => {
881                let envelope = ManagedEnvelope::from(envelope);
882                self.submit_envelope_upstream(envelope, ctx.project_info.upstream.clone());
883            }
884            Err(_) => relay_log::error!("failed to serialize output to an envelope"),
885        };
886    }
887
888    fn submit_envelope_upstream(
889        &self,
890        mut envelope: ManagedEnvelope,
891        // Currently allowed to be optional as code is migrated to respect the upstream override
892        // provided from the project config. Eventually must be available and is required.
893        upstream: Option<UpstreamDescriptor>,
894    ) {
895        if envelope.envelope_mut().is_empty() {
896            envelope.accept();
897            return;
898        }
899
900        // No code path should hit this.
901        //
902        // Any item which is produced by processing is handled in `submit_upstream`,
903        // metrics are sent to the store directly and outcomes must be produced to Kafka
904        // instead of being sent onward as client report.
905        if self.inner.config.processing_enabled() {
906            relay_log::error!(
907                "attempt to forward envelope to http upstream when processing is enabled"
908            );
909            return;
910        }
911
912        // Override the `sent_at` timestamp. Since the envelope went through basic
913        // normalization, all timestamps have been corrected. We propagate the new
914        // `sent_at` to allow the next Relay to double-check this timestamp and
915        // potentially apply correction again. This is done as close to sending as
916        // possible so that we avoid internal delays.
917        envelope.envelope_mut().set_sent_at(Utc::now());
918
919        relay_log::trace!("sending envelope to sentry endpoint");
920        let http_encoding = self.inner.config.http_encoding();
921        let result = envelope.envelope().to_vec().and_then(|v| {
922            encode_payload(&v.into(), http_encoding).map_err(EnvelopeError::PayloadIoFailed)
923        });
924
925        match result {
926            Ok(body) => {
927                self.inner
928                    .addrs
929                    .upstream_relay
930                    .send(SendRequest(SendEnvelope {
931                        upstream,
932                        envelope,
933                        body,
934                        http_encoding,
935                        project_cache: self.inner.project_cache.clone(),
936                    }));
937            }
938            Err(error) => {
939                // Errors are only logged for what we consider an internal discard reason. These
940                // indicate errors in the infrastructure or implementation bugs.
941                relay_log::error!(
942                    error = &error as &dyn Error,
943                    tags.project_key = %envelope.scoping().project_key,
944                    "failed to serialize envelope payload"
945                );
946
947                envelope.reject(Outcome::Invalid(DiscardReason::Internal));
948            }
949        }
950    }
951
952    fn handle_submit_client_reports(&self, message: SubmitClientReports) {
953        let SubmitClientReports {
954            client_reports,
955            scoping,
956        } = message;
957
958        relay_log::trace!(
959            "sending {} client report(s) to project id {}",
960            client_reports.len(),
961            scoping.project_id
962        );
963
964        if client_reports.is_empty() {
965            return;
966        }
967
968        let upstream = self.inner.config.upstream();
969        let dsn = PartialDsn::outbound(&scoping, upstream);
970
971        let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn));
972        for client_report in client_reports {
973            match client_report.serialize() {
974                Ok(payload) => {
975                    let mut item = Item::new(ItemType::ClientReport);
976                    item.set_payload(ContentType::Json, payload);
977                    envelope.add_item(item);
978                }
979                Err(error) => {
980                    relay_log::error!(
981                        error = &error as &dyn std::error::Error,
982                        "failed to serialize client report"
983                    );
984                }
985            }
986        }
987
988        let envelope = ManagedEnvelope::new(envelope, self.inner.addrs.outcome_aggregator.clone());
989        self.submit_envelope_upstream(envelope, None);
990    }
991
992    fn check_buckets(
993        &self,
994        project_key: ProjectKey,
995        project_info: &ProjectInfo,
996        rate_limits: &RateLimits,
997        buckets: Vec<Bucket>,
998    ) -> Vec<Bucket> {
999        let Some(scoping) = project_info.scoping(project_key) else {
1000            relay_log::error!(
1001                tags.project_key = project_key.as_str(),
1002                "there is no scoping: dropping {} buckets",
1003                buckets.len(),
1004            );
1005            return Vec::new();
1006        };
1007
1008        let mut buckets =
1009            self::metrics::remove_invalid_namespaces(buckets, &self.inner.metric_outcomes, scoping);
1010
1011        let mut namespaces: BTreeSet<MetricNamespace> = buckets
1012            .iter()
1013            .filter_map(|bucket| bucket.name.try_namespace())
1014            .collect();
1015
1016        // Never rate limit outcomes.
1017        namespaces.remove(&MetricNamespace::Outcomes);
1018
1019        for namespace in namespaces {
1020            let limits = rate_limits
1021                .check_with_quotas(project_info.get_quotas(), scoping.metric_bucket(namespace));
1022
1023            if limits.is_limited() {
1024                let rejected;
1025                (buckets, rejected) = utils::split_off(buckets, |bucket| {
1026                    bucket.name.try_namespace() == Some(namespace)
1027                });
1028
1029                let reason_code = limits.longest().and_then(|limit| limit.reason_code.clone());
1030                self.inner.metric_outcomes.track(
1031                    scoping,
1032                    &rejected,
1033                    Outcome::RateLimited(reason_code),
1034                );
1035            }
1036        }
1037
1038        let quotas = project_info.config.quotas.clone();
1039        match MetricsLimiter::create(buckets, quotas, scoping) {
1040            Ok(mut bucket_limiter) => {
1041                bucket_limiter.enforce_limits(rate_limits, &self.inner.metric_outcomes);
1042                bucket_limiter.into_buckets()
1043            }
1044            Err(buckets) => buckets,
1045        }
1046    }
1047
1048    #[cfg(feature = "processing")]
1049    async fn rate_limit_buckets(
1050        &self,
1051        scoping: Scoping,
1052        project_info: &ProjectInfo,
1053        mut buckets: Vec<Bucket>,
1054    ) -> Vec<Bucket> {
1055        let Some(rate_limiter) = &self.inner.rate_limiter else {
1056            return buckets;
1057        };
1058
1059        let global_config = self.inner.global_config.current().unwrap_or_default();
1060        let mut namespaces = buckets
1061            .iter()
1062            .filter_map(|bucket| bucket.name.try_namespace())
1063            .counts();
1064
1065        // Never rate limit outcomes.
1066        namespaces.remove(&MetricNamespace::Outcomes);
1067
1068        let quotas = CombinedQuotas::new(&global_config, project_info.get_quotas());
1069
1070        for (namespace, quantity) in namespaces {
1071            let item_scoping = scoping.metric_bucket(namespace);
1072
1073            let limits = match rate_limiter
1074                .is_rate_limited(quotas, item_scoping, quantity, false)
1075                .await
1076            {
1077                Ok(limits) => limits,
1078                Err(err) => {
1079                    relay_log::error!(
1080                        error = &err as &dyn std::error::Error,
1081                        "failed to check redis rate limits"
1082                    );
1083                    break;
1084                }
1085            };
1086
1087            if limits.is_limited() {
1088                let rejected;
1089                (buckets, rejected) = utils::split_off(buckets, |bucket| {
1090                    bucket.name.try_namespace() == Some(namespace)
1091                });
1092
1093                let reason_code = limits.longest().and_then(|limit| limit.reason_code.clone());
1094                self.inner.metric_outcomes.track(
1095                    scoping,
1096                    &rejected,
1097                    Outcome::RateLimited(reason_code),
1098                );
1099
1100                self.inner
1101                    .project_cache
1102                    .get(item_scoping.scoping.project_key)
1103                    .rate_limits()
1104                    .merge(limits);
1105            }
1106        }
1107
1108        match MetricsLimiter::create(buckets, project_info.config.quotas.clone(), scoping) {
1109            Err(buckets) => buckets,
1110            Ok(bucket_limiter) => self.apply_other_rate_limits(bucket_limiter).await,
1111        }
1112    }
1113
1114    /// Check and apply rate limits to metrics buckets for transactions and spans.
1115    #[cfg(feature = "processing")]
1116    async fn apply_other_rate_limits(&self, mut bucket_limiter: MetricsLimiter) -> Vec<Bucket> {
1117        relay_log::trace!("handle_rate_limit_buckets");
1118
1119        let scoping = *bucket_limiter.scoping();
1120
1121        if let Some(rate_limiter) = self.inner.rate_limiter.as_ref() {
1122            let global_config = self.inner.global_config.current().unwrap_or_default();
1123            let quotas = CombinedQuotas::new(&global_config, bucket_limiter.quotas());
1124
1125            // We set over_accept_once such that the limit is actually reached, which allows subsequent
1126            // calls with quantity=0 to be rate limited.
1127            let over_accept_once = true;
1128            let mut rate_limits = RateLimits::new();
1129
1130            let (category, count) = bucket_limiter.count();
1131
1132            let timer = Instant::now();
1133            let mut is_limited = false;
1134
1135            if let Some(count) = count {
1136                match rate_limiter
1137                    .is_rate_limited(quotas, scoping.item(category), count, over_accept_once)
1138                    .await
1139                {
1140                    Ok(limits) => {
1141                        is_limited = limits.is_limited();
1142                        rate_limits.merge(limits)
1143                    }
1144                    Err(e) => {
1145                        relay_log::error!(error = &e as &dyn Error, "rate limiting error")
1146                    }
1147                }
1148            }
1149
1150            relay_statsd::metric!(
1151                timer(RelayTimers::RateLimitBucketsDuration) = timer.elapsed(),
1152                category = category.name(),
1153                limited = if is_limited { "true" } else { "false" },
1154                count = match count {
1155                    None => "none",
1156                    Some(0) => "0",
1157                    Some(1) => "1",
1158                    Some(1..=10) => "10",
1159                    Some(1..=25) => "25",
1160                    Some(1..=50) => "50",
1161                    Some(51..=100) => "100",
1162                    Some(101..=500) => "500",
1163                    _ => "> 500",
1164                },
1165            );
1166
1167            if rate_limits.is_limited() {
1168                let was_enforced =
1169                    bucket_limiter.enforce_limits(&rate_limits, &self.inner.metric_outcomes);
1170
1171                if was_enforced {
1172                    // Update the rate limits in the project cache.
1173                    self.inner
1174                        .project_cache
1175                        .get(scoping.project_key)
1176                        .rate_limits()
1177                        .merge(rate_limits);
1178                }
1179            }
1180        }
1181
1182        bucket_limiter.into_buckets()
1183    }
1184
1185    /// Processes metric buckets and sends them to Kafka.
1186    ///
1187    /// This function runs the following steps:
1188    ///  - rate limiting
1189    ///  - emit billing outcomes
1190    ///  - submit to `StoreForwarder`
1191    #[cfg(feature = "processing")]
1192    async fn encode_metrics_processing(
1193        &self,
1194        message: FlushBuckets,
1195        store_forwarder: &Addr<Store>,
1196    ) {
1197        use crate::constants::DEFAULT_EVENT_RETENTION;
1198        use crate::services::store::StoreMetrics;
1199
1200        for ProjectBuckets {
1201            buckets,
1202            scoping,
1203            project_info,
1204            ..
1205        } in message.buckets.into_values()
1206        {
1207            let mut buckets = self
1208                .rate_limit_buckets(scoping, &project_info, buckets)
1209                .await;
1210
1211            if buckets.is_empty() {
1212                continue;
1213            }
1214
1215            // Emit metric billing outcomes.
1216            self.inner
1217                .metric_outcomes
1218                .track_accepted_outcome(scoping, &mut buckets);
1219
1220            let retention = project_info
1221                .config
1222                .event_retention
1223                .unwrap_or(DEFAULT_EVENT_RETENTION);
1224
1225            // The store forwarder takes care of bucket splitting internally, so we can submit the
1226            // entire list of buckets. There is no batching needed here.
1227            store_forwarder.send(StoreMetrics {
1228                buckets,
1229                scoping,
1230                retention,
1231            });
1232        }
1233    }
1234
1235    /// Serializes metric buckets to JSON and sends them to the upstream.
1236    ///
1237    /// This function runs the following steps:
1238    ///  - partitioning
1239    ///  - batching by configured size limit
1240    ///  - serialize to JSON and pack in an envelope
1241    ///
1242    /// Rate limiting runs only in processing Relays as it requires access to the central Redis instance.
1243    /// Cached rate limits are applied in the project cache already.
1244    fn encode_metrics_envelope(&self, message: FlushBuckets) {
1245        let FlushBuckets {
1246            partition_key,
1247            buckets,
1248        } = message;
1249
1250        let batch_size = self.inner.config.metrics_max_batch_size_bytes();
1251        let upstream = self.inner.config.upstream();
1252
1253        for ProjectBuckets {
1254            buckets,
1255            scoping,
1256            project_info,
1257            ..
1258        } in buckets.values()
1259        {
1260            let dsn = PartialDsn::outbound(scoping, upstream);
1261
1262            relay_statsd::metric!(
1263                distribution(RelayDistributions::PartitionKeys) = u64::from(partition_key)
1264            );
1265
1266            let mut num_batches = 0;
1267            for batch in BucketsView::from(buckets).by_size(batch_size) {
1268                let mut envelope = Envelope::from_request(None, RequestMeta::outbound(dsn.clone()));
1269
1270                let mut item = Item::new(ItemType::MetricBuckets);
1271                item.set_source_quantities(crate::metrics::extract_quantities(batch));
1272                item.set_payload(ContentType::Json, serde_json::to_vec(&buckets).unwrap());
1273                envelope.add_item(item);
1274
1275                let mut envelope =
1276                    ManagedEnvelope::new(envelope, self.inner.addrs.outcome_aggregator.clone());
1277                envelope
1278                    .set_partition_key(Some(partition_key))
1279                    .scope(*scoping);
1280
1281                relay_statsd::metric!(
1282                    distribution(RelayDistributions::BucketsPerBatch) = batch.len() as u64
1283                );
1284
1285                self.submit_envelope_upstream(envelope, project_info.upstream.clone());
1286                num_batches += 1;
1287            }
1288
1289            relay_statsd::metric!(
1290                distribution(RelayDistributions::BatchesPerPartition) = num_batches
1291            );
1292        }
1293    }
1294
1295    /// Creates a [`SendMetricsRequest`] and sends it to the upstream relay.
1296    fn send_global_partition(
1297        &self,
1298        upstream: Option<UpstreamDescriptor>,
1299        partition_key: u32,
1300        partition: &mut Partition<'_>,
1301    ) {
1302        if partition.is_empty() {
1303            return;
1304        }
1305
1306        let (unencoded, project_info) = partition.take();
1307        let http_encoding = self.inner.config.http_encoding();
1308        let encoded = match encode_payload(&unencoded, http_encoding) {
1309            Ok(payload) => payload,
1310            Err(error) => {
1311                let error = &error as &dyn std::error::Error;
1312                relay_log::error!(error, "failed to encode metrics payload");
1313                return;
1314            }
1315        };
1316
1317        let request = SendMetricsRequest {
1318            upstream,
1319            partition_key: partition_key.to_string(),
1320            unencoded,
1321            encoded,
1322            project_info,
1323            http_encoding,
1324            metric_outcomes: self.inner.metric_outcomes.clone(),
1325        };
1326
1327        self.inner.addrs.upstream_relay.send(SendRequest(request));
1328    }
1329
1330    /// Serializes metric buckets to JSON and sends them to the upstream via the global endpoint.
1331    ///
1332    /// This function is similar to [`Self::encode_metrics_envelope`], but sends a global batched
1333    /// payload directly instead of per-project Envelopes.
1334    ///
1335    /// This function runs the following steps:
1336    ///  - partitioning
1337    ///  - batching by configured size limit
1338    ///  - serialize to JSON
1339    ///  - submit directly to the upstream
1340    fn encode_metrics_global(&self, message: FlushBuckets) {
1341        let FlushBuckets {
1342            partition_key,
1343            buckets,
1344        } = message;
1345
1346        let batch_size = self.inner.config.metrics_max_batch_size_bytes();
1347        let mut partitions = BTreeMap::new();
1348        let mut partition_splits = 0;
1349
1350        for ProjectBuckets {
1351            buckets,
1352            scoping,
1353            project_info,
1354            ..
1355        } in buckets.values()
1356        {
1357            let partition = match partitions.get_mut(&project_info.upstream) {
1358                Some(partition) => partition,
1359                None => partitions
1360                    .entry(project_info.upstream.clone())
1361                    .or_insert_with(|| Partition::new(batch_size)),
1362            };
1363
1364            for bucket in buckets {
1365                let mut remaining = Some(BucketView::new(bucket));
1366
1367                while let Some(bucket) = remaining.take() {
1368                    if let Some(next) = partition.insert(bucket, *scoping) {
1369                        // A part of the bucket could not be inserted. Take the partition and submit
1370                        // it immediately. Repeat until the final part was inserted. This should
1371                        // always result in a request, otherwise we would enter an endless loop.
1372                        self.send_global_partition(
1373                            project_info.upstream.clone(),
1374                            partition_key,
1375                            partition,
1376                        );
1377                        remaining = Some(next);
1378                        partition_splits += 1;
1379                    }
1380                }
1381            }
1382        }
1383
1384        if partition_splits > 0 {
1385            metric!(distribution(RelayDistributions::PartitionSplits) = partition_splits);
1386        }
1387
1388        for (upstream, mut partition) in partitions {
1389            self.send_global_partition(upstream, partition_key, &mut partition);
1390        }
1391    }
1392
1393    /// Removes all outcome metrics from `message` and sends them as client reports.
1394    ///
1395    /// Returns a new [`FlushBuckets`] message, without any outcome metrics remaining.
1396    fn encode_metrics_client_reports(&self, mut message: FlushBuckets) -> FlushBuckets {
1397        for ProjectBuckets {
1398            buckets, scoping, ..
1399        } in message.buckets.values_mut()
1400        {
1401            let client_reports = outcome::metric::extract_client_reports(buckets).collect();
1402
1403            self.handle_submit_client_reports(SubmitClientReports {
1404                client_reports,
1405                scoping: *scoping,
1406            });
1407        }
1408
1409        message
1410    }
1411
1412    async fn handle_flush_buckets(&self, mut message: FlushBuckets) {
1413        for (project_key, pb) in message.buckets.iter_mut() {
1414            let buckets = std::mem::take(&mut pb.buckets);
1415            pb.buckets =
1416                self.check_buckets(*project_key, &pb.project_info, &pb.rate_limits, buckets);
1417        }
1418
1419        #[cfg(feature = "processing")]
1420        if self.inner.config.processing_enabled()
1421            && let Some(ref store_forwarder) = self.inner.addrs.store_forwarder
1422        {
1423            return self
1424                .encode_metrics_processing(message, store_forwarder)
1425                .await;
1426        }
1427
1428        // Processing Relays never send outcomes as client reports, which is why this check is after
1429        // the processing check.
1430        if self.inner.config.emit_outcomes() == EmitOutcomes::AsClientReports {
1431            // Remove client reports from metrics to be sent, if configured as client reports
1432            // and send them separately.
1433            message = self.encode_metrics_client_reports(message);
1434        }
1435
1436        if self.inner.config.http_global_metrics() {
1437            self.encode_metrics_global(message)
1438        } else {
1439            self.encode_metrics_envelope(message)
1440        }
1441    }
1442
1443    #[cfg(all(test, feature = "processing"))]
1444    fn redis_rate_limiter_enabled(&self) -> bool {
1445        self.inner.rate_limiter.is_some()
1446    }
1447
1448    async fn handle_message(self, message: EnvelopeProcessor) {
1449        let ty = message.variant();
1450        let feature_weights = self.feature_weights(&message);
1451
1452        metric!(timer(RelayTimers::ProcessMessageDuration), message = ty, {
1453            let mut cogs = self.inner.cogs.timed(ResourceId::Relay, feature_weights);
1454
1455            match message {
1456                EnvelopeProcessor::ProcessEnvelope(m) => {
1457                    self.handle_process_envelope(&mut cogs, *m).await
1458                }
1459                EnvelopeProcessor::ProcessProjectMetrics(m) => {
1460                    self.handle_process_metrics(&mut cogs, *m)
1461                }
1462                EnvelopeProcessor::ProcessBatchedMetrics(m) => {
1463                    self.handle_process_batched_metrics(&mut cogs, *m)
1464                }
1465                EnvelopeProcessor::FlushBuckets(m) => self.handle_flush_buckets(*m).await,
1466                EnvelopeProcessor::SubmitClientReports(m) => self.handle_submit_client_reports(*m),
1467            }
1468        });
1469    }
1470
1471    fn feature_weights(&self, message: &EnvelopeProcessor) -> FeatureWeights {
1472        match message {
1473            // Envelope is split later and tokens are attributed then.
1474            EnvelopeProcessor::ProcessEnvelope(_) => AppFeature::Unattributed.into(),
1475            EnvelopeProcessor::ProcessProjectMetrics(_) => AppFeature::Unattributed.into(),
1476            EnvelopeProcessor::ProcessBatchedMetrics(_) => AppFeature::Unattributed.into(),
1477            EnvelopeProcessor::FlushBuckets(v) => v
1478                .buckets
1479                .values()
1480                .map(|s| {
1481                    if self.inner.config.processing_enabled() {
1482                        // Processing does not encode the metrics but instead rate limit the metrics,
1483                        // which scales by count and not size.
1484                        relay_metrics::cogs::ByCount(&s.buckets).into()
1485                    } else {
1486                        relay_metrics::cogs::BySize(&s.buckets).into()
1487                    }
1488                })
1489                .fold(FeatureWeights::none(), FeatureWeights::merge),
1490            EnvelopeProcessor::SubmitClientReports(_) => AppFeature::ClientReports.into(),
1491        }
1492    }
1493}
1494
1495impl Service for EnvelopeProcessorService {
1496    type Interface = EnvelopeProcessor;
1497
1498    async fn run(self, mut rx: relay_system::Receiver<Self::Interface>) {
1499        while let Some(message) = rx.recv().await {
1500            let service = self.clone();
1501            // Create a new hub to prevent sentry scopes from bleeding to other tasks.
1502            let hub = relay_log::Hub::new_from_top(relay_log::Hub::current());
1503
1504            self.inner
1505                .pool
1506                .spawn_async(Box::pin(service.handle_message(message).bind_hub(hub)))
1507                .await;
1508        }
1509    }
1510}
1511
1512pub fn encode_payload(body: &Bytes, http_encoding: HttpEncoding) -> Result<Bytes, std::io::Error> {
1513    let envelope_body: Vec<u8> = match http_encoding {
1514        HttpEncoding::Identity => return Ok(body.clone()),
1515        HttpEncoding::Deflate => {
1516            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1517            encoder.write_all(body.as_ref())?;
1518            encoder.finish()?
1519        }
1520        HttpEncoding::Gzip => {
1521            let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1522            encoder.write_all(body.as_ref())?;
1523            encoder.finish()?
1524        }
1525        HttpEncoding::Br => {
1526            // Use default buffer size (via 0), medium quality (5), and the default lgwin (22).
1527            let mut encoder = BrotliEncoder::new(Vec::new(), 0, 5, 22);
1528            encoder.write_all(body.as_ref())?;
1529            encoder.into_inner()
1530        }
1531        HttpEncoding::Zstd => {
1532            // Use the fastest compression level, our main objective here is to get the best
1533            // compression ratio for least amount of time spent.
1534            let mut encoder = ZstdEncoder::new(Vec::new(), 1)?;
1535            encoder.write_all(body.as_ref())?;
1536            encoder.finish()?
1537        }
1538    };
1539
1540    Ok(envelope_body.into())
1541}
1542
1543/// An upstream request that submits an envelope via HTTP.
1544#[derive(Debug)]
1545pub struct SendEnvelope {
1546    pub upstream: Option<UpstreamDescriptor>,
1547    pub envelope: ManagedEnvelope,
1548    pub body: Bytes,
1549    pub http_encoding: HttpEncoding,
1550    pub project_cache: ProjectCacheHandle,
1551}
1552
1553impl UpstreamRequest for SendEnvelope {
1554    fn upstream(&self) -> Option<&UpstreamDescriptor> {
1555        self.upstream.as_ref()
1556    }
1557
1558    fn method(&self) -> reqwest::Method {
1559        reqwest::Method::POST
1560    }
1561
1562    fn path(&self) -> Cow<'_, str> {
1563        format!("/api/{}/envelope/", self.envelope.scoping().project_id).into()
1564    }
1565
1566    fn route(&self) -> &'static str {
1567        "envelope"
1568    }
1569
1570    fn build(&mut self, builder: &mut http::RequestBuilder) -> Result<(), http::HttpError> {
1571        let envelope_body = self.body.clone();
1572
1573        let meta = &self.envelope.meta();
1574        let shard = self.envelope.partition_key().map(|p| p.to_string());
1575        builder
1576            .content_encoding(self.http_encoding)
1577            .header_opt("Origin", meta.origin().map(|url| url.as_str()))
1578            .header_opt("User-Agent", meta.user_agent())
1579            .header("X-Sentry-Auth", meta.auth_header())
1580            .header("X-Forwarded-For", meta.forwarded_for())
1581            .header("Content-Type", envelope::CONTENT_TYPE)
1582            .header_opt("X-Sentry-Relay-Shard", shard)
1583            .body(envelope_body);
1584
1585        Ok(())
1586    }
1587
1588    fn sign(&mut self) -> Option<Sign> {
1589        Some(Sign::Optional(SignatureType::RequestSign))
1590    }
1591
1592    fn respond(
1593        self: Box<Self>,
1594        result: Result<http::Response, UpstreamRequestError>,
1595    ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> {
1596        Box::pin(async move {
1597            let result = match result {
1598                Ok(mut response) => response.consume().await.map_err(UpstreamRequestError::Http),
1599                Err(error) => Err(error),
1600            };
1601
1602            match result {
1603                Ok(()) => self.envelope.accept(),
1604                Err(error) if error.is_received() => {
1605                    let scoping = self.envelope.scoping();
1606                    self.envelope.accept();
1607
1608                    if let UpstreamRequestError::RateLimited(limits) = error {
1609                        self.project_cache
1610                            .get(scoping.project_key)
1611                            .rate_limits()
1612                            .merge(limits.scope(&scoping));
1613                    }
1614                }
1615                Err(error) => {
1616                    // Errors are only logged for what we consider an internal discard reason. These
1617                    // indicate errors in the infrastructure or implementation bugs.
1618                    let mut envelope = self.envelope;
1619                    envelope.reject(Outcome::Invalid(DiscardReason::Internal));
1620                    relay_log::error!(
1621                        error = &error as &dyn Error,
1622                        tags.project_key = %envelope.scoping().project_key,
1623                        "error sending envelope"
1624                    );
1625                }
1626            }
1627        })
1628    }
1629}
1630
1631/// A container for metric buckets from multiple projects.
1632///
1633/// This container is used to send metrics to the upstream in global batches as part of the
1634/// [`FlushBuckets`] message if the `http.global_metrics` option is enabled. The container monitors
1635/// the size of all metrics and allows to split them into multiple batches. See
1636/// [`insert`](Self::insert) for more information.
1637#[derive(Debug)]
1638struct Partition<'a> {
1639    max_size: usize,
1640    remaining: usize,
1641    views: HashMap<ProjectKey, Vec<BucketView<'a>>>,
1642    project_info: HashMap<ProjectKey, Scoping>,
1643}
1644
1645impl<'a> Partition<'a> {
1646    /// Creates a new partition with the given maximum size in bytes.
1647    pub fn new(size: usize) -> Self {
1648        Self {
1649            max_size: size,
1650            remaining: size,
1651            views: HashMap::new(),
1652            project_info: HashMap::new(),
1653        }
1654    }
1655
1656    /// Inserts a bucket into the partition, splitting it if necessary.
1657    ///
1658    /// This function attempts to add the bucket to this partition. If the bucket does not fit
1659    /// entirely into the partition given its maximum size, the remaining part of the bucket is
1660    /// returned from this function call.
1661    ///
1662    /// If this function returns `Some(_)`, the partition is full and should be submitted to the
1663    /// upstream immediately. Use [`Self::take`] to retrieve the contents of the
1664    /// partition. Afterwards, the caller is responsible to call this function again with the
1665    /// remaining bucket until it is fully inserted.
1666    pub fn insert(&mut self, bucket: BucketView<'a>, scoping: Scoping) -> Option<BucketView<'a>> {
1667        let (current, next) = bucket.split(self.remaining, Some(self.max_size));
1668
1669        if let Some(current) = current {
1670            self.remaining = self.remaining.saturating_sub(current.estimated_size());
1671            self.views
1672                .entry(scoping.project_key)
1673                .or_default()
1674                .push(current);
1675
1676            self.project_info
1677                .entry(scoping.project_key)
1678                .or_insert(scoping);
1679        }
1680
1681        next
1682    }
1683
1684    /// Returns `true` if the partition does not hold any data.
1685    fn is_empty(&self) -> bool {
1686        self.views.is_empty()
1687    }
1688
1689    /// Returns the serialized buckets for this partition.
1690    ///
1691    /// This empties the partition, so that it can be reused.
1692    fn take(&mut self) -> (Bytes, HashMap<ProjectKey, Scoping>) {
1693        #[derive(serde::Serialize)]
1694        struct Wrapper<'a> {
1695            buckets: &'a HashMap<ProjectKey, Vec<BucketView<'a>>>,
1696        }
1697
1698        let buckets = &self.views;
1699        let payload = serde_json::to_vec(&Wrapper { buckets }).unwrap().into();
1700
1701        let scopings = std::mem::take(&mut self.project_info);
1702
1703        self.views.clear();
1704        self.remaining = self.max_size;
1705
1706        (payload, scopings)
1707    }
1708}
1709
1710/// An upstream request that submits metric buckets via HTTP.
1711///
1712/// This request is not awaited. It automatically tracks outcomes if the request is not received.
1713#[derive(Debug)]
1714struct SendMetricsRequest {
1715    /// Optional upstream override where the request will be sent to.
1716    upstream: Option<UpstreamDescriptor>,
1717    /// If the partition key is set, the request is marked with `X-Sentry-Relay-Shard`.
1718    partition_key: String,
1719    /// Serialized metric buckets without encoding applied, used for signing.
1720    unencoded: Bytes,
1721    /// Serialized metric buckets with the stated HTTP encoding applied.
1722    encoded: Bytes,
1723    /// Mapping of all contained project keys to their scoping and extraction mode.
1724    ///
1725    /// Used to track outcomes for transmission failures.
1726    project_info: HashMap<ProjectKey, Scoping>,
1727    /// Encoding (compression) of the payload.
1728    http_encoding: HttpEncoding,
1729    /// Metric outcomes instance to send outcomes on error.
1730    metric_outcomes: MetricOutcomes,
1731}
1732
1733impl SendMetricsRequest {
1734    fn create_error_outcomes(self) {
1735        #[derive(serde::Deserialize)]
1736        struct Wrapper {
1737            buckets: HashMap<ProjectKey, Vec<MinimalTrackableBucket>>,
1738        }
1739
1740        let buckets = match serde_json::from_slice(&self.unencoded) {
1741            Ok(Wrapper { buckets }) => buckets,
1742            Err(err) => {
1743                relay_log::error!(
1744                    error = &err as &dyn std::error::Error,
1745                    "failed to parse buckets from failed transmission"
1746                );
1747                return;
1748            }
1749        };
1750
1751        for (key, buckets) in buckets {
1752            let Some(&scoping) = self.project_info.get(&key) else {
1753                relay_log::error!("missing scoping for project key");
1754                continue;
1755            };
1756
1757            self.metric_outcomes.track(
1758                scoping,
1759                &buckets,
1760                Outcome::Invalid(DiscardReason::Internal),
1761            );
1762        }
1763    }
1764}
1765
1766impl UpstreamRequest for SendMetricsRequest {
1767    fn upstream(&self) -> Option<&UpstreamDescriptor> {
1768        self.upstream.as_ref()
1769    }
1770
1771    fn set_relay_id(&self) -> bool {
1772        true
1773    }
1774
1775    fn sign(&mut self) -> Option<Sign> {
1776        Some(Sign::Required(SignatureType::Body(self.unencoded.clone())))
1777    }
1778
1779    fn method(&self) -> reqwest::Method {
1780        reqwest::Method::POST
1781    }
1782
1783    fn path(&self) -> Cow<'_, str> {
1784        "/api/0/relays/metrics/".into()
1785    }
1786
1787    fn route(&self) -> &'static str {
1788        "global_metrics"
1789    }
1790
1791    fn build(&mut self, builder: &mut http::RequestBuilder) -> Result<(), http::HttpError> {
1792        builder
1793            .content_encoding(self.http_encoding)
1794            .header("X-Sentry-Relay-Shard", self.partition_key.as_bytes())
1795            .header(header::CONTENT_TYPE, b"application/json")
1796            .body(self.encoded.clone());
1797
1798        Ok(())
1799    }
1800
1801    fn respond(
1802        self: Box<Self>,
1803        result: Result<http::Response, UpstreamRequestError>,
1804    ) -> Pin<Box<dyn Future<Output = ()> + Send + Sync>> {
1805        Box::pin(async {
1806            match result {
1807                Ok(mut response) => {
1808                    response.consume().await.ok();
1809                }
1810                Err(error) => {
1811                    relay_log::error!(error = &error as &dyn Error, "Failed to send metrics batch");
1812
1813                    // If the request did not arrive at the upstream, we are responsible for outcomes.
1814                    // Otherwise, the upstream is responsible to log outcomes.
1815                    if error.is_received() {
1816                        return;
1817                    }
1818
1819                    self.create_error_outcomes()
1820                }
1821            }
1822        })
1823    }
1824}
1825
1826/// Container for global and project level [`Quota`].
1827#[derive(Copy, Clone, Debug)]
1828#[cfg(feature = "processing")]
1829struct CombinedQuotas<'a> {
1830    global_quotas: &'a [Quota],
1831    project_quotas: &'a [Quota],
1832}
1833
1834#[cfg(feature = "processing")]
1835impl<'a> CombinedQuotas<'a> {
1836    /// Returns a new [`CombinedQuotas`].
1837    pub fn new(global_config: &'a GlobalConfig, project_quotas: &'a [Quota]) -> Self {
1838        Self {
1839            global_quotas: &global_config.quotas,
1840            project_quotas,
1841        }
1842    }
1843}
1844
1845#[cfg(feature = "processing")]
1846impl<'a> IntoIterator for CombinedQuotas<'a> {
1847    type Item = &'a Quota;
1848    type IntoIter = std::iter::Chain<std::slice::Iter<'a, Quota>, std::slice::Iter<'a, Quota>>;
1849
1850    fn into_iter(self) -> Self::IntoIter {
1851        self.global_quotas.iter().chain(self.project_quotas.iter())
1852    }
1853}
1854
1855#[cfg(test)]
1856mod tests {
1857    use insta::assert_debug_snapshot;
1858    use relay_common::glob2::LazyGlob;
1859    use relay_dynamic_config::ProjectConfig;
1860    use relay_event_normalization::{
1861        NormalizationConfig, RedactionRule, TransactionNameConfig, TransactionNameRule,
1862    };
1863    use relay_event_schema::protocol::{Event, EventId, TransactionSource};
1864    use relay_pii::DataScrubbingConfig;
1865    use relay_protocol::Annotated;
1866    #[cfg(feature = "processing")]
1867    use relay_quotas::DataCategory;
1868    use similar_asserts::assert_eq;
1869
1870    use crate::testutils::{create_test_processor, create_test_processor_with_addrs};
1871
1872    #[cfg(feature = "processing")]
1873    use {
1874        relay_metrics::BucketValue,
1875        relay_quotas::{QuotaScope, ReasonCode},
1876        relay_test::mock_service,
1877    };
1878
1879    use super::*;
1880
1881    async fn process_to_single_envelope<'a>(
1882        processor: &EnvelopeProcessorService,
1883        envelope: ManagedEnvelope,
1884        ctx: processing::Context<'a>,
1885    ) -> Box<Envelope> {
1886        let mut outputs = processor.process(envelope, ctx).await;
1887        assert_eq!(outputs.len(), 1);
1888
1889        let Output { main, metrics } = outputs.pop().unwrap();
1890
1891        if let Some(metrics) = metrics {
1892            metrics.accept(drop);
1893        }
1894
1895        main.unwrap()
1896            .serialize_envelope(ctx.to_forward())
1897            .unwrap()
1898            .accept(|envelope| envelope)
1899    }
1900
1901    #[cfg(feature = "processing")]
1902    fn mock_quota(id: &str) -> Quota {
1903        Quota {
1904            id: Some(id.into()),
1905            categories: [DataCategory::MetricBucket].into(),
1906            scope: QuotaScope::Organization,
1907            scope_id: None,
1908            limit: Some(0),
1909            window: None,
1910            reason_code: None,
1911            namespace: None,
1912        }
1913    }
1914
1915    #[cfg(feature = "processing")]
1916    #[test]
1917    fn test_dynamic_quotas() {
1918        let global_config = relay_dynamic_config::GlobalConfig {
1919            quotas: vec![mock_quota("foo"), mock_quota("bar")],
1920            ..Default::default()
1921        };
1922
1923        let project_quotas = vec![mock_quota("baz"), mock_quota("qux")];
1924
1925        let dynamic_quotas = CombinedQuotas::new(&global_config, &project_quotas);
1926
1927        let quota_ids = dynamic_quotas.into_iter().filter_map(|q| q.id.as_deref());
1928        assert!(quota_ids.eq(["foo", "bar", "baz", "qux"]));
1929    }
1930
1931    /// Ensures that if we ratelimit one batch of buckets in [`FlushBuckets`] message, it won't
1932    /// also ratelimit the next batches in the same message automatically.
1933    #[cfg(feature = "processing")]
1934    #[tokio::test]
1935    async fn test_ratelimit_per_batch() {
1936        use relay_base_schema::organization::OrganizationId;
1937        use relay_protocol::FiniteF64;
1938
1939        let rate_limited_org = Scoping {
1940            organization_id: OrganizationId::new(1),
1941            project_id: ProjectId::new(21),
1942            project_key: ProjectKey::parse("00000000000000000000000000000000").unwrap(),
1943            key_id: Some(17),
1944        };
1945
1946        let not_rate_limited_org = Scoping {
1947            organization_id: OrganizationId::new(2),
1948            project_id: ProjectId::new(21),
1949            project_key: ProjectKey::parse("11111111111111111111111111111111").unwrap(),
1950            key_id: Some(17),
1951        };
1952
1953        let message = {
1954            let project_info = {
1955                let quota = Quota {
1956                    id: Some("testing".into()),
1957                    categories: [DataCategory::MetricBucket].into(),
1958                    scope: relay_quotas::QuotaScope::Organization,
1959                    scope_id: Some(rate_limited_org.organization_id.to_string().into()),
1960                    limit: Some(0),
1961                    window: None,
1962                    reason_code: Some(ReasonCode::new("test")),
1963                    namespace: None,
1964                };
1965
1966                let mut config = ProjectConfig::default();
1967                config.quotas.push(quota);
1968
1969                Arc::new(ProjectInfo {
1970                    config,
1971                    ..Default::default()
1972                })
1973            };
1974
1975            let project_metrics = |scoping| ProjectBuckets {
1976                buckets: vec![Bucket {
1977                    name: "d:spans/bar".into(),
1978                    value: BucketValue::Counter(FiniteF64::new(1.0).unwrap()),
1979                    timestamp: UnixTimestamp::now(),
1980                    tags: Default::default(),
1981                    width: 10,
1982                    metadata: BucketMetadata::default(),
1983                }],
1984                rate_limits: Default::default(),
1985                project_info: project_info.clone(),
1986                scoping,
1987            };
1988
1989            let buckets = hashbrown::HashMap::from([
1990                (
1991                    rate_limited_org.project_key,
1992                    project_metrics(rate_limited_org),
1993                ),
1994                (
1995                    not_rate_limited_org.project_key,
1996                    project_metrics(not_rate_limited_org),
1997                ),
1998            ]);
1999
2000            FlushBuckets {
2001                partition_key: 0,
2002                buckets,
2003            }
2004        };
2005
2006        // ensure the order of the map while iterating is as expected.
2007        assert_eq!(message.buckets.keys().count(), 2);
2008
2009        let config = {
2010            let config_json = serde_json::json!({
2011                "processing": {
2012                    "enabled": true,
2013                    "kafka_config": [],
2014                    "redis": {
2015                        "server": std::env::var("RELAY_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_owned()),
2016                    }
2017                }
2018            });
2019            Config::from_json_value(config_json).unwrap()
2020        };
2021
2022        let (store, handle) = {
2023            let f = |org_ids: &mut Vec<OrganizationId>, msg: Store| {
2024                let org_id = match msg {
2025                    Store::Metrics(x) => x.scoping.organization_id,
2026                    _ => panic!("received envelope when expecting only metrics"),
2027                };
2028                org_ids.push(org_id);
2029            };
2030
2031            mock_service("store_forwarder", vec![], f)
2032        };
2033
2034        let processor = create_test_processor(config).await;
2035        assert!(processor.redis_rate_limiter_enabled());
2036
2037        processor.encode_metrics_processing(message, &store).await;
2038
2039        drop(store);
2040        let orgs_not_ratelimited = handle.await.unwrap();
2041
2042        assert_eq!(
2043            orgs_not_ratelimited,
2044            vec![not_rate_limited_org.organization_id]
2045        );
2046    }
2047
2048    #[tokio::test]
2049    async fn test_browser_version_extraction_with_pii_like_data() {
2050        let processor = create_test_processor(Default::default()).await;
2051        let outcome_aggregator = Addr::dummy();
2052        let event_id = EventId::new();
2053
2054        let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
2055            .parse()
2056            .unwrap();
2057
2058        let request_meta = RequestMeta::new(dsn);
2059        let mut envelope = Envelope::from_request(Some(event_id), request_meta);
2060
2061        envelope.add_item({
2062                let mut item = Item::new(ItemType::Event);
2063                item.set_payload(
2064                    ContentType::Json,
2065                    r#"
2066                    {
2067                        "request": {
2068                            "headers": [
2069                                ["User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"]
2070                            ]
2071                        }
2072                    }
2073                "#,
2074                );
2075                item
2076            });
2077
2078        let mut datascrubbing_settings = DataScrubbingConfig::default();
2079        // enable all the default scrubbing
2080        datascrubbing_settings.scrub_data = true;
2081        datascrubbing_settings.scrub_defaults = true;
2082        datascrubbing_settings.scrub_ip_addresses = true;
2083
2084        // Make sure to mask any IP-like looking data
2085        let pii_config = serde_json::from_str(r#"{"applications": {"**": ["@ip:mask"]}}"#).unwrap();
2086
2087        let config = ProjectConfig {
2088            datascrubbing_settings,
2089            pii_config: Some(pii_config),
2090            ..Default::default()
2091        };
2092
2093        let project_info = ProjectInfo {
2094            config,
2095            ..Default::default()
2096        };
2097
2098        let envelope = ManagedEnvelope::new(envelope, outcome_aggregator);
2099
2100        let ctx = processing::Context {
2101            project_info: &project_info,
2102            ..processing::Context::for_test()
2103        };
2104
2105        let new_envelope = process_to_single_envelope(&processor, envelope, ctx).await;
2106
2107        let event_item = new_envelope.items().last().unwrap();
2108        let annotated_event: Annotated<Event> =
2109            Annotated::from_json_bytes(&event_item.payload()).unwrap();
2110        let event = annotated_event.into_value().unwrap();
2111        let headers = event
2112            .request
2113            .into_value()
2114            .unwrap()
2115            .headers
2116            .into_value()
2117            .unwrap();
2118
2119        // IP-like data must be masked
2120        assert_eq!(
2121            Some(
2122                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/********* Safari/537.36"
2123            ),
2124            headers.get_header("User-Agent")
2125        );
2126        // But we still get correct browser and version number
2127        let contexts = event.contexts.into_value().unwrap();
2128        let browser = contexts.0.get("browser").unwrap();
2129        assert_eq!(
2130            r#"{"browser":"Chrome 103.0.0","name":"Chrome","version":"103.0.0","type":"browser"}"#,
2131            browser.to_json().unwrap()
2132        );
2133    }
2134
2135    #[tokio::test]
2136    #[cfg(feature = "processing")]
2137    async fn test_materialize_dsc() {
2138        use crate::services::projects::project::PublicKeyConfig;
2139
2140        let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
2141            .parse()
2142            .unwrap();
2143        let request_meta = RequestMeta::new(dsn);
2144        let mut envelope = Envelope::from_request(None, request_meta);
2145
2146        let dsc = r#"{
2147            "trace_id": "00000000-0000-0000-0000-000000000001",
2148            "public_key": "e12d836b15bb49d7bbf99e64295d995b",
2149            "sample_rate": "0.2"
2150        }"#;
2151        envelope.set_dsc(serde_json::from_str(dsc).unwrap());
2152
2153        let mut item = Item::new(ItemType::Event);
2154        item.set_payload(ContentType::Json, r#"{}"#);
2155        envelope.add_item(item);
2156
2157        let outcome_aggregator = Addr::dummy();
2158        let managed_envelope = ManagedEnvelope::new(envelope, outcome_aggregator);
2159
2160        let mut project_info = ProjectInfo::default();
2161        project_info.public_keys.push(PublicKeyConfig {
2162            public_key: ProjectKey::parse("e12d836b15bb49d7bbf99e64295d995b").unwrap(),
2163            numeric_id: Some(1),
2164        });
2165
2166        let config = serde_json::json!({
2167            "processing": {
2168                "enabled": true,
2169                "kafka_config": [],
2170            }
2171        });
2172
2173        let processor =
2174            create_test_processor(Config::from_json_value(config.clone()).unwrap()).await;
2175        let config = Config::from_json_value(config).unwrap();
2176        let ctx = processing::Context {
2177            config: &config,
2178            project_info: &project_info,
2179            sampling_project_info: Some(&project_info),
2180            ..processing::Context::for_test()
2181        };
2182
2183        let envelope = process_to_single_envelope(&processor, managed_envelope, ctx).await;
2184        let event = envelope
2185            .get_item_by(|item| item.ty() == &ItemType::Event)
2186            .unwrap();
2187
2188        let event = Annotated::<Event>::from_json_bytes(&event.payload()).unwrap();
2189        insta::assert_debug_snapshot!(event.value().unwrap()._dsc, @r###"
2190        Object(
2191            {
2192                "environment": ~,
2193                "public_key": String(
2194                    "e12d836b15bb49d7bbf99e64295d995b",
2195                ),
2196                "release": ~,
2197                "replay_id": ~,
2198                "sample_rate": String(
2199                    "0.2",
2200                ),
2201                "trace_id": String(
2202                    "00000000000000000000000000000001",
2203                ),
2204                "transaction": ~,
2205            },
2206        )
2207        "###);
2208    }
2209
2210    fn capture_test_event(transaction_name: &str, source: TransactionSource) -> Vec<String> {
2211        let mut event = Annotated::<Event>::from_json(
2212            r#"
2213            {
2214                "type": "transaction",
2215                "transaction": "/foo/",
2216                "timestamp": 946684810.0,
2217                "start_timestamp": 946684800.0,
2218                "contexts": {
2219                    "trace": {
2220                        "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
2221                        "span_id": "fa90fdead5f74053",
2222                        "op": "http.server",
2223                        "type": "trace"
2224                    }
2225                },
2226                "transaction_info": {
2227                    "source": "url"
2228                }
2229            }
2230            "#,
2231        )
2232        .unwrap();
2233        let e = event.value_mut().as_mut().unwrap();
2234        e.transaction.set_value(Some(transaction_name.into()));
2235
2236        e.transaction_info
2237            .value_mut()
2238            .as_mut()
2239            .unwrap()
2240            .source
2241            .set_value(Some(source));
2242
2243        relay_statsd::with_capturing_test_client(|| {
2244            utils::log_transaction_name_metrics(&mut event, |event| {
2245                let config = NormalizationConfig {
2246                    transaction_name_config: TransactionNameConfig {
2247                        rules: &[TransactionNameRule {
2248                            pattern: LazyGlob::new("/foo/*/**".to_owned()),
2249                            expiry: DateTime::<Utc>::MAX_UTC,
2250                            redaction: RedactionRule::Replace {
2251                                substitution: "*".to_owned(),
2252                            },
2253                        }],
2254                    },
2255                    ..Default::default()
2256                };
2257                relay_event_normalization::normalize_event(event, &config)
2258            });
2259        })
2260    }
2261
2262    #[test]
2263    fn test_log_transaction_metrics_none() {
2264        let captures = capture_test_event("/nothing", TransactionSource::Url);
2265        insta::assert_debug_snapshot!(captures, @r###"
2266        [
2267            "event.transaction_name_changes:1|c|#source_in:url,changes:none,source_out:sanitized,is_404:false",
2268        ]
2269        "###);
2270    }
2271
2272    #[test]
2273    fn test_log_transaction_metrics_rule() {
2274        let captures = capture_test_event("/foo/john/denver", TransactionSource::Url);
2275        insta::assert_debug_snapshot!(captures, @r###"
2276        [
2277            "event.transaction_name_changes:1|c|#source_in:url,changes:rule,source_out:sanitized,is_404:false",
2278        ]
2279        "###);
2280    }
2281
2282    #[test]
2283    fn test_log_transaction_metrics_pattern() {
2284        let captures = capture_test_event("/something/12345", TransactionSource::Url);
2285        insta::assert_debug_snapshot!(captures, @r###"
2286        [
2287            "event.transaction_name_changes:1|c|#source_in:url,changes:pattern,source_out:sanitized,is_404:false",
2288        ]
2289        "###);
2290    }
2291
2292    #[test]
2293    fn test_log_transaction_metrics_both() {
2294        let captures = capture_test_event("/foo/john/12345", TransactionSource::Url);
2295        insta::assert_debug_snapshot!(captures, @r###"
2296        [
2297            "event.transaction_name_changes:1|c|#source_in:url,changes:both,source_out:sanitized,is_404:false",
2298        ]
2299        "###);
2300    }
2301
2302    #[test]
2303    fn test_log_transaction_metrics_no_match() {
2304        let captures = capture_test_event("/foo/john/12345", TransactionSource::Route);
2305        insta::assert_debug_snapshot!(captures, @r###"
2306        [
2307            "event.transaction_name_changes:1|c|#source_in:route,changes:none,source_out:route,is_404:false",
2308        ]
2309        "###);
2310    }
2311
2312    #[tokio::test]
2313    async fn test_process_metrics_bucket_metadata() {
2314        let mut token = Cogs::noop().timed(ResourceId::Relay, AppFeature::Unattributed);
2315        let project_key = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap();
2316        let received_at = Utc::now();
2317        let config = Config::default();
2318
2319        let (aggregator, mut aggregator_rx) = Addr::custom();
2320        let processor = create_test_processor_with_addrs(
2321            config,
2322            Addrs {
2323                aggregator,
2324                ..Default::default()
2325            },
2326        )
2327        .await;
2328
2329        let mut item = Item::new(ItemType::Statsd);
2330        item.set_payload(ContentType::Text, "spans/foo:3182887624:4267882815|s");
2331        for (source, expected_received_at) in [
2332            (
2333                BucketSource::External,
2334                Some(UnixTimestamp::from_datetime(received_at).unwrap()),
2335            ),
2336            (BucketSource::Internal, None),
2337        ] {
2338            let message = ProcessMetrics {
2339                data: MetricData::Raw(vec![item.clone()]),
2340                project_key,
2341                source,
2342                received_at,
2343                sent_at: Some(Utc::now()),
2344            };
2345            processor.handle_process_metrics(&mut token, message);
2346
2347            let Aggregator::MergeBuckets(merge_buckets) = aggregator_rx.recv().await.unwrap();
2348            let buckets = merge_buckets.buckets;
2349            assert_eq!(buckets.len(), 1);
2350            assert_eq!(buckets[0].metadata.received_at, expected_received_at);
2351        }
2352    }
2353
2354    #[tokio::test]
2355    async fn test_process_batched_metrics() {
2356        let mut token = Cogs::noop().timed(ResourceId::Relay, AppFeature::Unattributed);
2357        let received_at = Utc::now();
2358        let config = Config::default();
2359
2360        let (aggregator, mut aggregator_rx) = Addr::custom();
2361        let processor = create_test_processor_with_addrs(
2362            config,
2363            Addrs {
2364                aggregator,
2365                ..Default::default()
2366            },
2367        )
2368        .await;
2369
2370        let payload = r#"{
2371    "buckets": {
2372        "11111111111111111111111111111111": [
2373            {
2374                "timestamp": 1615889440,
2375                "width": 0,
2376                "name": "d:transactions/endpoint.response_time@millisecond",
2377                "type": "d",
2378                "value": [
2379                  68.0
2380                ],
2381                "tags": {
2382                  "route": "user_index"
2383                }
2384            }
2385        ],
2386        "22222222222222222222222222222222": [
2387            {
2388                "timestamp": 1615889440,
2389                "width": 0,
2390                "name": "d:transactions/endpoint.cache_rate@none",
2391                "type": "d",
2392                "value": [
2393                  36.0
2394                ]
2395            }
2396        ]
2397    }
2398}
2399"#;
2400        let message = ProcessBatchedMetrics {
2401            payload: Bytes::from(payload),
2402            source: BucketSource::Internal,
2403            received_at,
2404            sent_at: Some(Utc::now()),
2405        };
2406        processor.handle_process_batched_metrics(&mut token, message);
2407
2408        let Aggregator::MergeBuckets(mb1) = aggregator_rx.recv().await.unwrap();
2409        let Aggregator::MergeBuckets(mb2) = aggregator_rx.recv().await.unwrap();
2410
2411        let mut messages = vec![mb1, mb2];
2412        messages.sort_by_key(|mb| mb.project_key);
2413
2414        let actual = messages
2415            .into_iter()
2416            .map(|mb| (mb.project_key, mb.buckets))
2417            .collect::<Vec<_>>();
2418
2419        assert_debug_snapshot!(actual, @r###"
2420        [
2421            (
2422                ProjectKey("11111111111111111111111111111111"),
2423                [
2424                    Bucket {
2425                        timestamp: UnixTimestamp(1615889440),
2426                        width: 0,
2427                        name: MetricName(
2428                            "d:transactions/endpoint.response_time@millisecond",
2429                        ),
2430                        value: Distribution(
2431                            [
2432                                68.0,
2433                            ],
2434                        ),
2435                        tags: {
2436                            "route": "user_index",
2437                        },
2438                        metadata: BucketMetadata {
2439                            merges: 1,
2440                            received_at: None,
2441                            extracted_from_indexed: false,
2442                        },
2443                    },
2444                ],
2445            ),
2446            (
2447                ProjectKey("22222222222222222222222222222222"),
2448                [
2449                    Bucket {
2450                        timestamp: UnixTimestamp(1615889440),
2451                        width: 0,
2452                        name: MetricName(
2453                            "d:transactions/endpoint.cache_rate@none",
2454                        ),
2455                        value: Distribution(
2456                            [
2457                                36.0,
2458                            ],
2459                        ),
2460                        tags: {},
2461                        metadata: BucketMetadata {
2462                            merges: 1,
2463                            received_at: None,
2464                            extracted_from_indexed: false,
2465                        },
2466                    },
2467                ],
2468            ),
2469        ]
2470        "###);
2471    }
2472}