use std::cell::RefCell;
use std::sync::{LazyLock, Mutex};
use crate::scope::Scope;
static BASE_SCOPE: LazyLock<Mutex<Scope>> = LazyLock::new(|| Mutex::new(Scope::new()));
thread_local! {
static SCOPE_STACK: RefCell<Vec<Scope>> = const { RefCell::new(Vec::new()) };
}
pub fn current_scope_snapshot() -> Scope {
SCOPE_STACK.with(|stack| {
if let Some(top) = stack.borrow().last() {
return top.clone();
}
match BASE_SCOPE.lock() {
Ok(g) => g.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
})
}
pub fn configure_scope<F: FnOnce(&mut Scope)>(f: F) {
SCOPE_STACK.with(|stack| {
let has_frame = !stack.borrow().is_empty();
if has_frame {
let mut frames = stack.borrow_mut();
f(frames.last_mut().expect("checked non-empty"));
} else {
match BASE_SCOPE.lock() {
Ok(mut g) => f(&mut g),
Err(poisoned) => f(&mut poisoned.into_inner()),
}
}
});
}
struct ScopeGuard;
impl Drop for ScopeGuard {
fn drop(&mut self) {
SCOPE_STACK.with(|stack| {
stack.borrow_mut().pop();
});
}
}
pub fn with_scope<R>(body: impl FnOnce() -> R) -> R {
let forked = current_scope_snapshot();
SCOPE_STACK.with(|stack| stack.borrow_mut().push(forked));
let _guard = ScopeGuard;
body()
}
#[cfg(test)]
pub fn reset_for_test() {
SCOPE_STACK.with(|stack| stack.borrow_mut().clear());
if let Ok(mut g) = BASE_SCOPE.lock() {
*g = Scope::new();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::User;
#[test]
fn base_scope_is_visible_without_frame() {
reset_for_test();
configure_scope(|s| {
s.set_tag("region", "us");
});
let snap = current_scope_snapshot();
assert_eq!(snap.tags()["region"], "us");
}
#[test]
fn with_scope_isolates_mutations() {
reset_for_test();
configure_scope(|s| {
s.set_tag("base", "1");
});
with_scope(|| {
configure_scope(|s| {
s.set_tag("inner", "2");
s.set_user(User::new().id("u"));
});
let snap = current_scope_snapshot();
assert_eq!(snap.tags()["base"], "1", "inherits base");
assert_eq!(snap.tags()["inner"], "2");
assert!(snap.user().is_some());
});
let snap = current_scope_snapshot();
assert_eq!(snap.tags()["base"], "1");
assert!(snap.tags().get("inner").is_none());
assert!(snap.user().is_none());
}
#[test]
fn scope_frame_pops_on_panic() {
reset_for_test();
let result = std::panic::catch_unwind(|| {
with_scope(|| {
configure_scope(|s| {
s.set_tag("leak", "x");
});
panic!("boom");
});
});
assert!(result.is_err());
let snap = current_scope_snapshot();
assert!(
snap.tags().get("leak").is_none(),
"frame leaked after panic"
);
}
}