From 286f469c1cf6386c3b4612f3797134e3ee9ac8c4 Mon Sep 17 00:00:00 2001 From: "Daniel Szoke (via Pi Coding Agent)" Date: Thu, 16 Apr 2026 20:09:35 +0000 Subject: [PATCH] feat(metrics): Add before_send_metric callback (#1064) Run metrics through `ClientOptions::before_send_metric` after scope/default attribute enrichment and before enqueue so applications can filter or mutate metrics before batching. Co-authored-by: Joris Bayer Closes #1025 Closes [RUST-170](https://linear.app/getsentry/issue/RUST-170/add-before-send-metric-callback-processing-in-sentry-core) --- sentry-core/src/client.rs | 7 +++++- sentry-core/src/clientoptions.rs | 17 +++++++++++++- sentry-core/tests/metrics.rs | 40 ++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index ee89cee6..92dbca1e 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -570,7 +570,8 @@ impl Client { } } - /// Prepares a metric to be sent, setting trace association data and default attributes. + /// Prepares a metric to be sent, setting the `trace_id` and other default attributes, and + /// processing it through `before_send_metric`. #[cfg(feature = "metrics")] fn prepare_metric(&self, mut metric: Metric, scope: &Scope) -> Option { scope.apply_to_metric(&mut metric, self.options.send_default_pii); @@ -579,6 +580,10 @@ impl Client { metric.attributes.entry(key.clone()).or_insert(val.clone()); } + if let Some(ref func) = self.options.before_send_metric { + metric = func(metric)?; + } + Some(metric) } } diff --git a/sentry-core/src/clientoptions.rs b/sentry-core/src/clientoptions.rs index 103296ac..86c3de80 100644 --- a/sentry-core/src/clientoptions.rs +++ b/sentry-core/src/clientoptions.rs @@ -7,6 +7,8 @@ use crate::constants::USER_AGENT; use crate::performance::TracesSampler; #[cfg(feature = "logs")] use crate::protocol::Log; +#[cfg(feature = "metrics")] +use crate::protocol::Metric; use crate::protocol::{Breadcrumb, Event}; use crate::types::Dsn; use crate::{Integration, IntoDsn, TransportFactory}; @@ -175,6 +177,9 @@ pub struct ClientOptions { /// Determines whether captured metrics should be sent to Sentry (defaults to false). #[cfg(feature = "metrics")] pub enable_metrics: bool, + /// Callback that is executed for each Metric before sending. + #[cfg(feature = "metrics")] + pub before_send_metric: Option>, // Other options not documented in Unified API /// Disable SSL verification. /// @@ -235,6 +240,12 @@ impl fmt::Debug for ClientOptions { struct BeforeSendLog; self.before_send_log.as_ref().map(|_| BeforeSendLog) }; + #[cfg(feature = "metrics")] + let before_send_metric = { + #[derive(Debug)] + struct BeforeSendMetric; + self.before_send_metric.as_ref().map(|_| BeforeSendMetric) + }; #[derive(Debug)] struct TransportFactory; @@ -282,7 +293,9 @@ impl fmt::Debug for ClientOptions { .field("before_send_log", &before_send_log); #[cfg(feature = "metrics")] - debug_struct.field("enable_metrics", &self.enable_metrics); + debug_struct + .field("enable_metrics", &self.enable_metrics) + .field("before_send_metric", &before_send_metric); debug_struct.field("user_agent", &self.user_agent).finish() } @@ -325,6 +338,8 @@ impl Default for ClientOptions { before_send_log: None, #[cfg(feature = "metrics")] enable_metrics: false, + #[cfg(feature = "metrics")] + before_send_metric: None, } } } diff --git a/sentry-core/tests/metrics.rs b/sentry-core/tests/metrics.rs index fcaea9a0..7b811045 100644 --- a/sentry-core/tests/metrics.rs +++ b/sentry-core/tests/metrics.rs @@ -1,6 +1,7 @@ #![cfg(all(feature = "test", feature = "metrics"))] use std::collections::HashSet; +use std::sync::Arc; use std::time::SystemTime; use anyhow::{Context, Result}; @@ -494,6 +495,45 @@ fn metric_user_attributes_do_not_overwrite_explicit() { assert_eq!(metric.attributes, expected_attributes); } +/// Test that `before_send_metric` can filter out metrics. +#[test] +fn before_send_metric_can_drop() { + let options = ClientOptions { + enable_metrics: true, + before_send_metric: Some(Arc::new(|_| None)), + ..Default::default() + }; + + let envelopes = test::with_captured_envelopes_options(|| capture_test_metric("test"), options); + assert!( + envelopes.is_empty(), + "metric should be dropped by before_send_metric" + ); +} + +/// Test that `before_send_metric` can modify metrics. +#[test] +fn before_send_metric_can_modify() { + let options = ClientOptions { + enable_metrics: true, + before_send_metric: Some(Arc::new(|mut metric| { + metric + .attributes + .insert("added_by_callback".into(), LogAttribute(Value::from("yes"))); + Some(metric) + })), + ..Default::default() + }; + + let envelopes = test::with_captured_envelopes_options(|| capture_test_metric("test"), options); + let metric = extract_single_metric(envelopes).expect("expected a single-metric envelope"); + + assert_eq!( + metric.attributes.get("added_by_callback"), + Some(&LogAttribute(Value::from("yes"))), + ); +} + /// Returns a [`Metric`] with [type `Counter`](MetricType), /// the provided name, and a value of `1.0`. fn test_metric(name: S) -> Metric