From afe327d3400c824434e62b54c99c7e3a3794b74f Mon Sep 17 00:00:00 2001 From: Bardi Harborow Date: Fri, 10 Jul 2026 17:56:39 +1000 Subject: [PATCH 1/2] feat(core): Add `read_scope` for read-only scope access `Hub::configure_scope` clones the entire scope and writes it back under the hub's write lock, which is wasteful on hot paths that only need to read scope data (e.g. integrations resolving the current span per event). `Hub::read_scope` provides read-only access without the clone and write-back, and the global `sentry::read_scope` exposes the same via the active hub, mirroring `sentry::configure_scope`. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 6 ++++++ sentry-core/src/api.rs | 22 ++++++++++++++++++++++ sentry-core/src/hub.rs | 18 ++++++++++++++++++ sentry/tests/test_basic.rs | 16 ++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ada5ba4f4..9aa4ddb61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### New Features + +- Added `sentry::read_scope` and `Hub::read_scope`, read-only counterparts to [`configure_scope`](https://docs.rs/sentry-core/0.48.4/sentry_core/fn.configure_scope.html) that avoid cloning the scope and writing it back. + ## 0.48.4 ### New Features diff --git a/sentry-core/src/api.rs b/sentry-core/src/api.rs index a4279fe35..cb403232c 100644 --- a/sentry-core/src/api.rs +++ b/sentry-core/src/api.rs @@ -157,6 +157,28 @@ where Hub::with_active(|hub| hub.configure_scope(f)) } +/// Invokes a function with read-only access to the current scope. +/// +/// The function is passed a shared reference to the [`Scope`]. Because there +/// might currently not be a scope or client active it's possible that the +/// callback might not be called at all. As a result of this the return value +/// of this closure must have a default that is returned in such cases. +/// +/// # Examples +/// +/// ``` +/// # sentry::test::with_captured_events(|| { +/// let span = sentry::read_scope(|scope| scope.get_span()); +/// # }); +/// ``` +pub fn read_scope(f: F) -> R +where + R: Default, + F: FnOnce(&Scope) -> R, +{ + Hub::with_active(|hub| hub.read_scope(f)) +} + /// Temporarily pushes a scope for a single call optionally reconfiguring it. /// /// This function takes two arguments: the first is a callback that is passed diff --git a/sentry-core/src/hub.rs b/sentry-core/src/hub.rs index b8411173a..cc9df5197 100644 --- a/sentry-core/src/hub.rs +++ b/sentry-core/src/hub.rs @@ -216,6 +216,24 @@ impl Hub { } } + /// Invokes a function with read-only access to the current scope. + /// + /// See the global [`read_scope`](crate::read_scope) + /// for more documentation. + pub fn read_scope(&self, f: F) -> R + where + R: Default, + F: FnOnce(&Scope) -> R, + { + use_without_client!(f); + with_client_impl! {{ + // Clone the `Arc` out so the callback runs without + // holding the stack lock, keeping reentrant hub calls safe. + let scope = self.inner.with(|stack| stack.top().scope.clone()); + f(&scope) + }} + } + /// Invokes a function that can modify the current scope. /// /// See the global [`configure_scope`](fn.configure_scope.html) diff --git a/sentry/tests/test_basic.rs b/sentry/tests/test_basic.rs index eb1222d5e..f6f63c021 100644 --- a/sentry/tests/test_basic.rs +++ b/sentry/tests/test_basic.rs @@ -160,6 +160,22 @@ fn test_reentrant_configure_scope() { assert_eq!(events[0].tags["which_scope"], "scope1"); } +#[test] +fn test_reentrant_read_scope() { + let events = sentry::test::with_captured_events(|| { + sentry::read_scope(|_scope| { + sentry::configure_scope(|scope| { + scope.set_tag("which_scope", "reentrant"); + }); + }); + + sentry::capture_message("look ma, no deadlock!", sentry::Level::Info); + }); + + assert_eq!(events.len(), 1); + assert_eq!(events[0].tags["which_scope"], "reentrant"); +} + #[test] fn test_attached_stacktrace() { let logger = sentry_log::SentryLogger::new(); From a4ceaf309e1096d98971f1d987cf5206fa8b5210 Mon Sep 17 00:00:00 2001 From: Bardi Harborow Date: Fri, 10 Jul 2026 18:53:02 +1000 Subject: [PATCH 2/2] ref: Use `read_scope` for read-only scope access Switch pure-read `configure_scope` call sites to the new `read_scope`: the `sentry-tracing` layer resolves the parent span on every new tracing span, so this avoids cloning the entire scope and writing it back on that hot path. Also update the performance-demo example to demonstrate the new API. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 2 +- sentry-tracing/src/layer/mod.rs | 2 +- sentry/examples/performance-demo.rs | 4 ++-- sentry/tests/test_basic.rs | 4 +--- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aa4ddb61..e8b18c9cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### New Features -- Added `sentry::read_scope` and `Hub::read_scope`, read-only counterparts to [`configure_scope`](https://docs.rs/sentry-core/0.48.4/sentry_core/fn.configure_scope.html) that avoid cloning the scope and writing it back. +- Added `sentry::read_scope` and `Hub::read_scope`, read-only counterparts to [`configure_scope`](https://docs.rs/sentry-core/0.48.4/sentry_core/fn.configure_scope.html) that avoid cloning the scope and writing it back. The `sentry-tracing` integration now uses this to resolve the parent span when creating a new span. ## 0.48.4 diff --git a/sentry-tracing/src/layer/mod.rs b/sentry-tracing/src/layer/mod.rs index fba1a4e0a..23d02ffd0 100644 --- a/sentry-tracing/src/layer/mod.rs +++ b/sentry-tracing/src/layer/mod.rs @@ -309,7 +309,7 @@ where sentry_op.unwrap_or_else(|| format!("{}::{}", span.metadata().target(), span.name())); let hub = sentry_core::Hub::current(); - let parent_sentry_span = hub.configure_scope(|scope| scope.get_span()); + let parent_sentry_span = hub.read_scope(|scope| scope.get_span()); let mut sentry_span: sentry_core::TransactionOrSpan = match &parent_sentry_span { Some(parent) => parent.start_child(&sentry_op, sentry_name).into(), diff --git a/sentry/examples/performance-demo.rs b/sentry/examples/performance-demo.rs index ac7e45b5f..5c33828e2 100644 --- a/sentry/examples/performance-demo.rs +++ b/sentry/examples/performance-demo.rs @@ -37,7 +37,7 @@ fn main_span1() { let transaction_ctx = sentry::TransactionContext::continue_from_span( "background transaction", "root span", - sentry::configure_scope(|scope| scope.get_span()), + sentry::read_scope(|scope| scope.get_span()), ); thread::spawn(move || { let transaction = sentry::start_transaction(transaction_ctx); @@ -76,7 +76,7 @@ fn wrap_in_span(op: &str, description: &str, f: F) -> R where F: FnOnce() -> R, { - let parent = sentry::configure_scope(|scope| scope.get_span()); + let parent = sentry::read_scope(|scope| scope.get_span()); let span1: sentry::TransactionOrSpan = match &parent { Some(parent) => parent.start_child(op, description).into(), None => { diff --git a/sentry/tests/test_basic.rs b/sentry/tests/test_basic.rs index f6f63c021..575779602 100644 --- a/sentry/tests/test_basic.rs +++ b/sentry/tests/test_basic.rs @@ -35,9 +35,7 @@ fn test_event_trace_context_from_propagation_context() { let mut last_event_id = None::; let mut span = None; let events = sentry::test::with_captured_events(|| { - sentry::configure_scope(|scope| { - span = scope.get_span(); - }); + span = sentry::read_scope(|scope| scope.get_span()); sentry::capture_message("Hello World!", sentry::Level::Warning); last_event_id = sentry::last_event_id(); });