From c7702061ca02bdb4dd38b4acd9d1a5eceb5874d4 Mon Sep 17 00:00:00 2001 From: Joris Bayer Date: Fri, 17 Jul 2026 12:56:03 +0200 Subject: [PATCH 1/7] wip --- .../src/processing/transactions/extraction.rs | 45 +----- .../src/processing/transactions/mod.rs | 12 +- .../src/processing/transactions/process.rs | 145 ++++++++++++------ .../processing/transactions/types/expanded.rs | 4 +- 4 files changed, 120 insertions(+), 86 deletions(-) diff --git a/relay-server/src/processing/transactions/extraction.rs b/relay-server/src/processing/transactions/extraction.rs index 24246f7cb4a..6c9bbec4a46 100644 --- a/relay-server/src/processing/transactions/extraction.rs +++ b/relay-server/src/processing/transactions/extraction.rs @@ -1,5 +1,5 @@ use relay_base_schema::project::ProjectId; -use relay_dynamic_config::{CombinedMetricExtractionConfig, ErrorBoundary, MetricExtractionGroups}; +use relay_dynamic_config::CombinedMetricExtractionConfig; use relay_event_normalization::span::tag_extraction; use relay_event_schema::protocol::{Event, Span}; use relay_metrics::MetricNamespace; @@ -28,6 +28,7 @@ pub fn extract_segment_span( /// Input arguments for [`extract_metrics`]. pub struct ExtractMetricsContext<'a> { + pub config: CombinedMetricExtractionConfig<'a>, pub dsc: Option<&'a DynamicSamplingContext>, pub project_id: ProjectId, pub ctx: Context<'a>, @@ -40,54 +41,24 @@ pub fn extract_metrics( event: &mut Annotated, extracted_metrics: &mut ProcessingExtractedMetrics, ctx: ExtractMetricsContext, -) -> bool { +) { let ExtractMetricsContext { + config, dsc, project_id, ctx, sampling_decision, extract_span_metrics, } = ctx; - // TODO(follow-up): this function should always extract metrics. Dynamic sampling should validate - // the full metrics extraction config and skip sampling if it is incomplete. - let Some(event) = event.value_mut() else { - // Nothing to extract, but metrics extraction was called. - return true; - }; - - // NOTE: This function requires a `metric_extraction` in the project config. Legacy configs - // will upsert this configuration from transaction and conditional tagging fields, even if - // it is not present in the actual project config payload. - let combined_config = { - let config = match &ctx.project_info.config.metric_extraction { - ErrorBoundary::Ok(config) if config.is_supported() => config, - _ => return false, - }; - let global_config = match &ctx.global_config.metric_extraction { - ErrorBoundary::Ok(global_config) => global_config, - #[allow(unused_variables)] - ErrorBoundary::Err(e) => { - if cfg!(feature = "processing") && ctx.config.processing_enabled() { - // Config is invalid, but we will try to extract what we can with just the - // project config. - relay_log::error!("Failed to parse global extraction config {e}"); - MetricExtractionGroups::EMPTY - } else { - // If there's an error with global metrics extraction, it is safe to assume that this - // Relay instance is not up-to-date, and we should skip extraction. - relay_log::debug!("Failed to parse global extraction config: {e}"); - return false; - } - } - }; - CombinedMetricExtractionConfig::new(global_config, config) + // Nothing to extract. + return; }; let metrics = crate::metrics_extraction::event::extract_metrics( event, crate::metrics_extraction::event::ExtractMetricsConfig { - config: combined_config, + config, sampling_decision, target_project_id: project_id, max_tag_value_size: ctx @@ -99,6 +70,4 @@ pub fn extract_metrics( }, ); extracted_metrics.extend(metrics, Some(sampling_decision)); - - true } diff --git a/relay-server/src/processing/transactions/mod.rs b/relay-server/src/processing/transactions/mod.rs index 13a9cd76749..dd7696d54af 100644 --- a/relay-server/src/processing/transactions/mod.rs +++ b/relay-server/src/processing/transactions/mod.rs @@ -150,8 +150,9 @@ impl Processor for TransactionProcessor { process::process_profile(&mut tx, ctx); relay_log::trace!("Sample transaction"); - let (tx, server_sample_rate) = match process::run_dynamic_sampling(tx, ctx, filters_status) - { + let (sampling_output, metrics_config) = + process::run_dynamic_sampling(tx, ctx, filters_status); + let (tx, server_sample_rate) = match sampling_output { SamplingOutput::Keep { payload, sample_rate, @@ -196,7 +197,12 @@ impl Processor for TransactionProcessor { let spans = self.limiter.enforce_quotas(spans, ctx).await.ok(); let (transaction, spans, metrics) = - process::split_indexed_and_total_with_extracted_spans(tx, spans, ctx); + process::split_indexed_and_total_with_extracted_spans( + tx, + spans, + ctx, + metrics_config, + ); return Ok(Output { main: Some(TransactionOutput::Indexed { spans, transaction }), diff --git a/relay-server/src/processing/transactions/process.rs b/relay-server/src/processing/transactions/process.rs index c13a0cad259..ad0666b1b98 100644 --- a/relay-server/src/processing/transactions/process.rs +++ b/relay-server/src/processing/transactions/process.rs @@ -1,4 +1,5 @@ use relay_base_schema::events::EventType; +use relay_dynamic_config::{CombinedMetricExtractionConfig, ErrorBoundary, MetricExtractionGroups}; use relay_event_normalization::GeoIpLookup; use relay_event_schema::protocol::{Event, Metrics}; use relay_profiling::{ProfileError, ProfileType}; @@ -204,7 +205,7 @@ pub enum SamplingOutput { payload: Managed>, sample_rate: Option, }, - /// The decision was discard keep only extracted metrics and an optional profile. + /// The decision was discard, keep only extracted metrics and an optional profile. Drop { metrics: Managed, profile: Option>>, @@ -212,25 +213,44 @@ pub enum SamplingOutput { } /// Computes the sampling decision for a transaction and associated items. +/// +/// Returns the sampling output as well as the validated metrics config, if possible / needed. pub fn run_dynamic_sampling( payload: Managed>, ctx: Context<'_>, filters_status: FiltersStatus, -) -> SamplingOutput { +) -> (SamplingOutput, Option) { + let metrics_config = get_metrics_config(ctx).ok(); + + if metrics_config.is_err() && !ctx.is_processing() { + // Defer dynamic sampling until the next relay. + return ( + SamplingOutput::Keep { + payload, + sample_rate: None, + }, + None, + ); + } + let sampling_result = make_dynamic_sampling_decision(&payload, ctx, filters_status); let sampling_match = match sampling_result { SamplingResult::Match(m) if m.decision().is_drop() => m, keep => { - return SamplingOutput::Keep { - payload, - sample_rate: keep.sample_rate(), - }; + return ( + SamplingOutput::Keep { + payload, + sample_rate: keep.sample_rate(), + }, + metrics_config., + ); } }; // At this point the decision is to drop the payload. - let (payload, metrics) = split_indexed_and_total(payload, ctx, SamplingDecision::Drop); + let (payload, metrics) = + split_indexed_and_total(payload, ctx, SamplingDecision::Drop, metrics_config); let (payload, profile) = payload.split_once(|mut payload, _| { let profile = payload.profile.take().map(|profile| StandaloneProfile { @@ -246,10 +266,38 @@ pub fn run_dynamic_sampling( let outcome = Outcome::FilteredSampling(sampling_match.into_matched_rules().into()); let _ = payload.reject_err(outcome); - SamplingOutput::Drop { - metrics, - profile: profile.transpose().map(Managed::boxed), - } + ( + SamplingOutput::Drop { + metrics, + profile: profile.transpose().map(Managed::boxed), + }, + None, // metrics were already extracted + ) +} + +fn get_metrics_config<'a>(ctx: Context<'a>) -> Result, ()> { + let config = match &ctx.project_info.config.metric_extraction { + ErrorBoundary::Ok(config) if config.is_supported() => config, + _ => return Err(()), + }; + let global_config = match &ctx.global_config.metric_extraction { + ErrorBoundary::Ok(global_config) => global_config, + #[allow(unused_variables)] + ErrorBoundary::Err(e) => { + if cfg!(feature = "processing") && ctx.config.processing_enabled() { + // Config is invalid, but we will try to extract what we can with just the + // project config. + relay_log::error!("Failed to parse global extraction config {e}"); + MetricExtractionGroups::EMPTY + } else { + // If there's an error with global metrics extraction, it is safe to assume that this + // Relay instance is not up-to-date, and we should skip extraction. + relay_log::debug!("Failed to parse global extraction config: {e}"); + return Err(()); + } + } + }; + Ok(CombinedMetricExtractionConfig::new(global_config, config)) } /// Computes the dynamic sampling decision for the unit of work, but does not perform action on data. @@ -292,10 +340,11 @@ type IndexedTransactionAndSpanAndMetrics = ( /// Splits transaction into indexed payload and metrics representing the total counts. /// /// Like [`split_indexed_and_total`] but works with [`ExtractedSpans`]. -pub fn split_indexed_and_total_with_extracted_spans( +pub fn split_indexed_and_total_with_extracted_spans<'a>( transaction: Managed>>, spans: Option>, - ctx: Context<'_>, + ctx: Context<'a>, + metrics_config: Option>, ) -> IndexedTransactionAndSpanAndMetrics { let scoping = transaction.scoping(); @@ -304,24 +353,27 @@ pub fn split_indexed_and_total_with_extracted_spans( r.lenient(DataCategory::MetricBucket); let mut metrics = ProcessingExtractedMetrics::new(); - - metrics_extracted = extraction::extract_metrics( - &mut tx.event, - &mut metrics, - ExtractMetricsContext { - dsc: tx.headers.dsc(), - project_id: scoping.project_id, - ctx, - sampling_decision: SamplingDecision::Keep, - extract_span_metrics: spans.is_some(), - }, - ); - - if !metrics_extracted { - // Invalid config or invalid original transaction - r.lenient(DataCategory::Transaction); - r.lenient(DataCategory::Span); - } + match metrics_config { + Some(config) => { + extraction::extract_metrics( + &mut tx.event, + &mut metrics, + ExtractMetricsContext { + config, + dsc: tx.headers.dsc(), + project_id: scoping.project_id, + ctx, + sampling_decision: SamplingDecision::Keep, + extract_span_metrics: spans.is_some(), + }, + ); + metrics_extracted = true; + } + None => { + r.lenient(DataCategory::Transaction); + r.lenient(DataCategory::Span); + } + }; // This really is a bug, we ignore here. // @@ -389,24 +441,29 @@ pub fn split_indexed_and_total( mut work: Managed>, ctx: Context<'_>, sampling_decision: SamplingDecision, + metrics_config: Option>, ) -> IndexedAndMetrics { let scoping = work.scoping(); let mut metrics = ProcessingExtractedMetrics::new(); let mut metrics_extracted = false; - work.modify(|work, _| { - metrics_extracted = extraction::extract_metrics( - &mut work.event, - &mut metrics, - ExtractMetricsContext { - dsc: work.headers.dsc(), - project_id: scoping.project_id, - ctx, - sampling_decision, - extract_span_metrics: true, - }, - ); - }); + if let Some(config) = metrics_config { + work.modify(|work, _| { + extraction::extract_metrics( + &mut work.event, + &mut metrics, + ExtractMetricsContext { + config, + dsc: work.headers.dsc(), + project_id: scoping.project_id, + ctx, + sampling_decision, + extract_span_metrics: true, + }, + ); + }); + metrics_extracted = true; + } work.split_once(|work, r| { r.lenient(DataCategory::MetricBucket); diff --git a/relay-server/src/processing/transactions/types/expanded.rs b/relay-server/src/processing/transactions/types/expanded.rs index 4b3b9a30340..73ab57d48d6 100644 --- a/relay-server/src/processing/transactions/types/expanded.rs +++ b/relay-server/src/processing/transactions/types/expanded.rs @@ -191,7 +191,9 @@ impl RateLimited for Managed>> { .await; if !limits.is_empty() { let error = Error::from(limits); - let (indexed, metrics) = split_indexed_and_total(self, ctx, SamplingDecision::Keep); + let (ctx, metrics_config) = ctx; + let (indexed, metrics) = + split_indexed_and_total(self, ctx, SamplingDecision::Keep, metrics_config); let _ = indexed.reject_err(error); return Ok(metrics.map(|metrics, _| Either::Right(metrics))); From af8330ed06f150adf6f288d92a9b1802dc04c2dc Mon Sep 17 00:00:00 2001 From: Joris Bayer Date: Fri, 17 Jul 2026 12:59:57 +0200 Subject: [PATCH 2/7] fix(transactions): Defer dynamic sampling --- relay-server/src/processing/transactions/process.rs | 13 +++++++------ .../src/processing/transactions/types/expanded.rs | 5 ++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/relay-server/src/processing/transactions/process.rs b/relay-server/src/processing/transactions/process.rs index ad0666b1b98..f58bbfa0c9c 100644 --- a/relay-server/src/processing/transactions/process.rs +++ b/relay-server/src/processing/transactions/process.rs @@ -219,8 +219,8 @@ pub fn run_dynamic_sampling( payload: Managed>, ctx: Context<'_>, filters_status: FiltersStatus, -) -> (SamplingOutput, Option) { - let metrics_config = get_metrics_config(ctx).ok(); +) -> (SamplingOutput, Option>) { + let metrics_config = get_metrics_config(ctx); if metrics_config.is_err() && !ctx.is_processing() { // Defer dynamic sampling until the next relay. @@ -243,7 +243,7 @@ pub fn run_dynamic_sampling( payload, sample_rate: keep.sample_rate(), }, - metrics_config., + metrics_config.ok(), ); } }; @@ -275,7 +275,8 @@ pub fn run_dynamic_sampling( ) } -fn get_metrics_config<'a>(ctx: Context<'a>) -> Result, ()> { +/// Compiles a valid metrics config from a [`Context`]. +pub fn get_metrics_config<'a>(ctx: Context<'a>) -> Result, ()> { let config = match &ctx.project_info.config.metric_extraction { ErrorBoundary::Ok(config) if config.is_supported() => config, _ => return Err(()), @@ -441,13 +442,13 @@ pub fn split_indexed_and_total( mut work: Managed>, ctx: Context<'_>, sampling_decision: SamplingDecision, - metrics_config: Option>, + metrics_config: Result, ()>, ) -> IndexedAndMetrics { let scoping = work.scoping(); let mut metrics = ProcessingExtractedMetrics::new(); let mut metrics_extracted = false; - if let Some(config) = metrics_config { + if let Ok(config) = metrics_config { work.modify(|work, _| { extraction::extract_metrics( &mut work.event, diff --git a/relay-server/src/processing/transactions/types/expanded.rs b/relay-server/src/processing/transactions/types/expanded.rs index 73ab57d48d6..c0c715712df 100644 --- a/relay-server/src/processing/transactions/types/expanded.rs +++ b/relay-server/src/processing/transactions/types/expanded.rs @@ -12,7 +12,7 @@ use crate::envelope::{ContentType, EnvelopeHeaders, Item, ItemType, Items}; use crate::managed::{Counted, Managed, Quantities, Rejected}; use crate::metrics_extraction::ExtractedMetrics; use crate::processing::transactions::Error; -use crate::processing::transactions::process::split_indexed_and_total; +use crate::processing::transactions::process::{get_metrics_config, split_indexed_and_total}; use crate::processing::utils::types::{Indexed, TotalAndIndexed, TotalCategory}; use crate::processing::{Context, CountRateLimited, RateLimited, RateLimiter}; use crate::statsd::RelayTimers; @@ -191,9 +191,8 @@ impl RateLimited for Managed>> { .await; if !limits.is_empty() { let error = Error::from(limits); - let (ctx, metrics_config) = ctx; let (indexed, metrics) = - split_indexed_and_total(self, ctx, SamplingDecision::Keep, metrics_config); + split_indexed_and_total(self, ctx, SamplingDecision::Keep, get_metrics_config(ctx)); let _ = indexed.reject_err(error); return Ok(metrics.map(|metrics, _| Either::Right(metrics))); From a575bd109d45b5caf5aa60218ec9ac7e620503f2 Mon Sep 17 00:00:00 2001 From: Joris Bayer Date: Fri, 17 Jul 2026 13:24:37 +0200 Subject: [PATCH 3/7] test --- tests/integration/test_dynamic_sampling.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/integration/test_dynamic_sampling.py b/tests/integration/test_dynamic_sampling.py index e7cbd592422..40bbd9adf16 100644 --- a/tests/integration/test_dynamic_sampling.py +++ b/tests/integration/test_dynamic_sampling.py @@ -936,6 +936,28 @@ def test_invalid_global_generic_filters_skip_dynamic_sampling(mini_sentry, relay assert mini_sentry.get_captured_envelope() +def test_invalid_metric_extraction_config_skips_dynamic_sampling(mini_sentry, relay): + relay = relay(mini_sentry, _outcomes_enabled_config()) + + project_id = 42 + config = mini_sentry.add_basic_project_config(project_id) + public_key = config["publicKeys"][0]["publicKey"] + + # Unsupported metric extraction config, so no metrics can be extracted + config["config"]["metricExtraction"] = { + "version": 666, # this version is too new for this relay + "metrics": [], + } + + # Reject all transactions with dynamic sampling + add_sampling_config(config, sample_rate=0, rule_type="transaction") + + envelope, _, _ = _create_transaction_envelope(public_key) + + relay.send_envelope(project_id, envelope) + assert mini_sentry.get_captured_envelope() + + def get_transaction_envelope( trace_id: str, segment_id: str, From 26554e33bad5e28bc2e573f4be265df018b98c55 Mon Sep 17 00:00:00 2001 From: Joris Bayer Date: Fri, 17 Jul 2026 14:47:05 +0200 Subject: [PATCH 4/7] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e189794024..2b01535cc96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +**Bug Fixes**: + +- Defer dynamic sampling until metrics config is valid. ([#6246](https://github.com/getsentry/relay/pull/6246)) + **Features**: - No longer write the deprecated `sentry.transaction` and `db.system` attributes. ([#6237](https://github.com/getsentry/relay/pull/6237), [#6238](https://github.com/getsentry/relay/pull/6238)) @@ -32,6 +36,7 @@ - Set sentry.trace.status on segment spans. ([#6140](https://github.com/getsentry/relay/pull/6140)) - Don't modify segment information for V2 web vital spans. ([#6160](https://github.com/getsentry/relay/pull/6160)) + - Support compressed minidumps when the `relay-minidump-uploads` feature is enabled. ([#6151](https://github.com/getsentry/relay/pull/6151)) - Make `--log-level` and `--log-format` take effect again and accept them on all subcommands. ([#6198](https://github.com/getsentry/relay/pull/6198)) - Parse two-component versions in iOS and iPadOS `raw_description` into `version` instead of `kernel_version`. ([#6197](https://github.com/getsentry/relay/pull/6197)) From 622557e4d5548173c0352b37b7a9ed22c157f32e Mon Sep 17 00:00:00 2001 From: David Herberth Date: Fri, 24 Jul 2026 09:26:00 +0200 Subject: [PATCH 5/7] simplify --- relay-dynamic-config/src/metrics.rs | 12 +- .../src/processing/transactions/mod.rs | 7 +- .../src/processing/transactions/process.rs | 145 ++++++++---------- .../processing/transactions/types/expanded.rs | 14 +- 4 files changed, 84 insertions(+), 94 deletions(-) diff --git a/relay-dynamic-config/src/metrics.rs b/relay-dynamic-config/src/metrics.rs index 8355587b5f1..ad1ea122ff6 100644 --- a/relay-dynamic-config/src/metrics.rs +++ b/relay-dynamic-config/src/metrics.rs @@ -80,6 +80,12 @@ pub struct CombinedMetricExtractionConfig<'a> { } impl<'a> CombinedMetricExtractionConfig<'a> { + /// Empty config, used in tests and as a fallback. + pub const EMPTY: Self = Self { + global: MetricExtractionGroups::EMPTY, + project: &MetricExtractionConfig::empty(), + }; + /// Creates a new combined view from two references. pub fn new(global: &'a MetricExtractionGroups, project: &'a MetricExtractionConfig) -> Self { for key in project.global_groups.keys() { @@ -230,12 +236,12 @@ impl MetricExtractionConfig { /// Returns an empty `MetricExtractionConfig` with the latest version. /// /// As opposed to `default()`, this will be enabled once populated with specs. - pub fn empty() -> Self { + pub const fn empty() -> Self { Self { version: Self::MAX_SUPPORTED_VERSION, global_groups: BTreeMap::new(), - metrics: Default::default(), - tags: Default::default(), + metrics: Vec::new(), + tags: Vec::new(), _conditional_tags_extended: false, _span_metrics_extended: false, } diff --git a/relay-server/src/processing/transactions/mod.rs b/relay-server/src/processing/transactions/mod.rs index eed6183474d..8678316bc38 100644 --- a/relay-server/src/processing/transactions/mod.rs +++ b/relay-server/src/processing/transactions/mod.rs @@ -145,9 +145,8 @@ impl Processor for TransactionProcessor { process::process_profile(&mut tx, ctx); relay_log::trace!("Sample transaction"); - let (sampling_output, metrics_config) = - process::run_dynamic_sampling(tx, ctx, filters_status); - let (tx, server_sample_rate) = match sampling_output { + let (tx, server_sample_rate) = match process::run_dynamic_sampling(tx, ctx, filters_status) + { SamplingOutput::Keep { payload, sample_rate, @@ -192,7 +191,7 @@ impl Processor for TransactionProcessor { let spans = self.limiter.enforce_quotas(spans, ctx).await.ok(); let (transaction, spans, metrics) = - process::split_indexed_and_total_with_extracted_spans(tx, spans, ctx, metrics_config); + process::split_indexed_and_total_with_extracted_spans(tx, spans, ctx); Ok(Output { main: Some(TransactionOutput::Indexed { spans, transaction }), diff --git a/relay-server/src/processing/transactions/process.rs b/relay-server/src/processing/transactions/process.rs index 6a21fb3bd02..d0b545ba6de 100644 --- a/relay-server/src/processing/transactions/process.rs +++ b/relay-server/src/processing/transactions/process.rs @@ -218,38 +218,33 @@ pub fn run_dynamic_sampling( payload: Managed>, ctx: Context<'_>, filters_status: FiltersStatus, -) -> (SamplingOutput, Option>) { - let metrics_config = get_metrics_config(ctx); - - if metrics_config.is_err() && !ctx.is_processing() { - // Defer dynamic sampling until the next relay. - return ( - SamplingOutput::Keep { +) -> SamplingOutput { + let conf = match get_metrics_config(ctx) { + Ok(conf) => conf, + Err(_) if ctx.is_processing() => CombinedMetricExtractionConfig::EMPTY, + Err(_) => { + // Defer dynamic sampling until the next relay. + return SamplingOutput::Keep { payload, sample_rate: None, - }, - None, - ); - } + }; + } + }; let sampling_result = make_dynamic_sampling_decision(&payload, ctx, filters_status); let sampling_match = match sampling_result { SamplingResult::Match(m) if m.decision().is_drop() => m, keep => { - return ( - SamplingOutput::Keep { - payload, - sample_rate: keep.sample_rate(), - }, - metrics_config.ok(), - ); + return SamplingOutput::Keep { + payload, + sample_rate: keep.sample_rate(), + }; } }; // At this point the decision is to drop the payload. - let (payload, metrics) = - split_indexed_and_total(payload, ctx, SamplingDecision::Drop, metrics_config); + let (payload, metrics) = split_indexed_and_total(payload, ctx, SamplingDecision::Drop, conf); let (payload, profile) = payload.split_once(|mut payload, _| { let profile = payload.profile.take().map(|profile| StandaloneProfile { @@ -265,13 +260,10 @@ pub fn run_dynamic_sampling( let outcome = Outcome::FilteredSampling(sampling_match.into_matched_rules().into()); let _ = payload.reject_err(outcome); - ( - SamplingOutput::Drop { - metrics, - profile: profile.transpose().map(Managed::boxed), - }, - None, // metrics were already extracted - ) + SamplingOutput::Drop { + metrics, + profile: profile.transpose().map(Managed::boxed), + } } /// Compiles a valid metrics config from a [`Context`]. @@ -282,9 +274,8 @@ pub fn get_metrics_config<'a>(ctx: Context<'a>) -> Result global_config, - #[allow(unused_variables)] ErrorBoundary::Err(e) => { - if cfg!(feature = "processing") && ctx.config.processing_enabled() { + if ctx.is_processing() { // Config is invalid, but we will try to extract what we can with just the // project config. relay_log::error!("Failed to parse global extraction config {e}"); @@ -297,6 +288,7 @@ pub fn get_metrics_config<'a>(ctx: Context<'a>) -> Result( +pub fn split_indexed_and_total_with_extracted_spans( transaction: Managed>>, spans: Option>, - ctx: Context<'a>, - metrics_config: Option>, + ctx: Context<'_>, ) -> IndexedTransactionAndSpanAndMetrics { let scoping = transaction.scoping(); - let mut metrics_extracted = false; + debug_assert!(ctx.is_processing()); + let (transaction, metrics) = transaction.split_once(|mut tx, r| { r.lenient(DataCategory::MetricBucket); let mut metrics = ProcessingExtractedMetrics::new(); - match metrics_config { - Some(config) => { - extraction::extract_metrics( - &mut tx.event, - &mut metrics, - ExtractMetricsContext { - config, - dsc: tx.headers.dsc(), - project_id: scoping.project_id, - ctx, - sampling_decision: SamplingDecision::Keep, - extract_span_metrics: spans.is_some(), - }, - ); - metrics_extracted = true; - } - None => { - r.lenient(DataCategory::Transaction); - r.lenient(DataCategory::Span); - } - }; + extraction::extract_metrics( + &mut tx.event, + &mut metrics, + ExtractMetricsContext { + dsc: tx.headers.dsc(), + project_id: scoping.project_id, + // We can fall back to the default, we're in a processing Relay in which the config + // must always be valid, worst case we fall back to a default empty config, because + // we can't really do anything else. + config: get_metrics_config(ctx).unwrap_or(CombinedMetricExtractionConfig::EMPTY), + ctx, + sampling_decision: SamplingDecision::Keep, + extract_span_metrics: spans.is_some(), + }, + ); // This really is a bug, we ignore here. // @@ -418,7 +404,7 @@ pub fn split_indexed_and_total_with_extracted_spans<'a>( // "Insurance" that metrics extracted from the transaction spans match the extracted // spans. r.modify_by(*c, -(*q as isize)); - } else if metrics_extracted { + } else { // Metrics were extracted but do not contain a span quantity, // be lenient about the span counts instead of failing bookkeeping. r.lenient(DataCategory::Span); @@ -438,40 +424,33 @@ type IndexedAndMetrics = ( /// Splits transaction into indexed payload and metrics representing the total counts. pub fn split_indexed_and_total( - mut work: Managed>, + work: Managed>, ctx: Context<'_>, sampling_decision: SamplingDecision, - metrics_config: Result, ()>, + config: CombinedMetricExtractionConfig<'_>, ) -> IndexedAndMetrics { let scoping = work.scoping(); - let mut metrics = ProcessingExtractedMetrics::new(); - let mut metrics_extracted = false; - if let Ok(config) = metrics_config { - work.modify(|work, _| { - extraction::extract_metrics( - &mut work.event, - &mut metrics, - ExtractMetricsContext { - config, - dsc: work.headers.dsc(), - project_id: scoping.project_id, - ctx, - sampling_decision, - extract_span_metrics: true, - }, - ); - }); - metrics_extracted = true; - } - - work.split_once(|work, r| { + work.split_once(|mut work, r| { r.lenient(DataCategory::MetricBucket); - if !metrics_extracted { - // Invalid config or invalid original transaction - r.lenient(DataCategory::Transaction); - r.lenient(DataCategory::Span); - } + + let mut metrics = ProcessingExtractedMetrics::new(); + + extraction::extract_metrics( + &mut work.event, + &mut metrics, + ExtractMetricsContext { + config, + dsc: work.headers.dsc(), + project_id: scoping.project_id, + ctx, + sampling_decision, + extract_span_metrics: true, + }, + ); + + r.lenient(DataCategory::Transaction); + r.lenient(DataCategory::Span); (Box::new(work.into_indexed()), metrics.into_inner()) }) diff --git a/relay-server/src/processing/transactions/types/expanded.rs b/relay-server/src/processing/transactions/types/expanded.rs index c0c715712df..4b0ba5ed377 100644 --- a/relay-server/src/processing/transactions/types/expanded.rs +++ b/relay-server/src/processing/transactions/types/expanded.rs @@ -191,11 +191,17 @@ impl RateLimited for Managed>> { .await; if !limits.is_empty() { let error = Error::from(limits); - let (indexed, metrics) = - split_indexed_and_total(self, ctx, SamplingDecision::Keep, get_metrics_config(ctx)); - let _ = indexed.reject_err(error); - return Ok(metrics.map(|metrics, _| Either::Right(metrics))); + if let Ok(config) = get_metrics_config(ctx) { + let (indexed, metrics) = + split_indexed_and_total(self, ctx, SamplingDecision::Keep, config); + let _ = indexed.reject_err(error); + + return Ok(metrics.map(|metrics, _| Either::Right(metrics))); + } else { + // If there is no config, we can't really do more except drop everything. + return Err(self.reject_err(error)); + } } let attachment_quantities = self.attachments.quantities(); From 59a207c17151430a14b553b1cf24f8e187264280 Mon Sep 17 00:00:00 2001 From: David Herberth Date: Fri, 24 Jul 2026 09:30:28 +0200 Subject: [PATCH 6/7] changelog fix --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79017b9a820..c02ddb64173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # Changelog -## 26.7.1 +## Unreleased **Bug Fixes**: - Defer dynamic sampling until metrics config is valid. ([#6246](https://github.com/getsentry/relay/pull/6246)) +## 26.7.1 + **Features**: - Emit web-vitals as metrics. ([#6118](https://github.com/getsentry/relay/pull/6118)) From bfda6e42a3746a9e14fa2286f6e4c325c7fff466 Mon Sep 17 00:00:00 2001 From: David Herberth Date: Fri, 24 Jul 2026 09:32:36 +0200 Subject: [PATCH 7/7] more simplify --- .../processing/transactions/types/expanded.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/relay-server/src/processing/transactions/types/expanded.rs b/relay-server/src/processing/transactions/types/expanded.rs index 4b0ba5ed377..f46f659b57a 100644 --- a/relay-server/src/processing/transactions/types/expanded.rs +++ b/relay-server/src/processing/transactions/types/expanded.rs @@ -1,4 +1,5 @@ use either::Either; +use relay_dynamic_config::CombinedMetricExtractionConfig; use relay_event_schema::protocol::{Event, SpanV2}; use relay_profiling::{ProfileMetadata, ProfileType}; use relay_protocol::Annotated; @@ -192,16 +193,13 @@ impl RateLimited for Managed>> { if !limits.is_empty() { let error = Error::from(limits); - if let Ok(config) = get_metrics_config(ctx) { - let (indexed, metrics) = - split_indexed_and_total(self, ctx, SamplingDecision::Keep, config); - let _ = indexed.reject_err(error); + // Everything will be dropped, might as well fallback to an empty config. + let config = get_metrics_config(ctx).unwrap_or(CombinedMetricExtractionConfig::EMPTY); + let (indexed, metrics) = + split_indexed_and_total(self, ctx, SamplingDecision::Keep, config); + let _ = indexed.reject_err(error); - return Ok(metrics.map(|metrics, _| Either::Right(metrics))); - } else { - // If there is no config, we can't really do more except drop everything. - return Err(self.reject_err(error)); - } + return Ok(metrics.map(|metrics, _| Either::Right(metrics))); } let attachment_quantities = self.attachments.quantities();