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