Skip to content

Correct semantics to use isolation scope and not current scope #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ extreme lenghts to keep its own SDK setup separate from the SDK setup of the
tested code.

`pytest-sentry` exposes the `sentry_test_scope` fixture whose return value is
the `Scope` being used to send events to Sentry. Use `with use_scope(entry_test_scope):`
the isolation `Scope` being used to send events to Sentry. Use `with use_isolation_scope(sentry_test_scope)`:
to temporarily switch context. You can use this to set custom tags like so::

```python
def test_foo(sentry_test_scope):
with use_scope(sentry_test_scope):
with use_isolation_scope(sentry_test_scope):
sentry_sdk.set_tag("pull_request", os.environ['EXAMPLE_CI_PULL_REQUEST'])
```

Expand Down
2 changes: 1 addition & 1 deletion pytest_sentry/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@pytest.fixture
def sentry_test_scope(request):
"""
Gives back the current scope.
Gives back the isolation Scope.
"""

item = request.node
Expand Down
24 changes: 14 additions & 10 deletions pytest_sentry/helpers.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import sentry_sdk
from sentry_sdk.scope import ScopeType
from sentry_sdk.opentelemetry.scope import setup_scope_context_management

from pytest_sentry.client import Client


DEFAULT_SCOPE = sentry_sdk.Scope(client=Client())
setup_scope_context_management()
DEFAULT_ISOLATION_SCOPE = sentry_sdk.Scope(ty=ScopeType.ISOLATION)
DEFAULT_ISOLATION_SCOPE.set_client(Client())

_scope_cache = {}
_isolation_scope_cache = {}


def _resolve_scope_marker_value(marker_value):
if id(marker_value) not in _scope_cache:
_scope_cache[id(marker_value)] = rv = _resolve_scope_marker_value_uncached(
if id(marker_value) not in _isolation_scope_cache:
_isolation_scope_cache[id(marker_value)] = rv = _resolve_scope_marker_value_uncached(
marker_value
)
return rv

return _scope_cache[id(marker_value)]
return _isolation_scope_cache[id(marker_value)]


def _resolve_scope_marker_value_uncached(marker_value):
if marker_value is None:
# If no special configuration is provided
# (like pytestmark or @pytest.mark.sentry_client() decorator)
# use the default scope
marker_value = DEFAULT_SCOPE
marker_value = DEFAULT_ISOLATION_SCOPE
else:
marker_value = marker_value.args[0]

Expand All @@ -37,25 +41,25 @@ def _resolve_scope_marker_value_uncached(marker_value):

if isinstance(marker_value, str):
# If a DSN string is provided, create a new client and use that
scope = sentry_sdk.get_current_scope()
scope = sentry_sdk.get_isolation_scope()
scope.set_client(Client(marker_value))
return scope

if isinstance(marker_value, dict):
# If a dict is provided, create a new client using the dict as Client options
scope = sentry_sdk.get_current_scope()
scope = sentry_sdk.get_isolation_scope()
scope.set_client(Client(**marker_value))
return scope

if isinstance(marker_value, Client):
# If a Client instance is provided, use that
scope = sentry_sdk.get_current_scope()
scope = sentry_sdk.get_isolation_scope()
scope.set_client(marker_value)
return scope

if isinstance(marker_value, sentry_sdk.Scope):
# If a Scope instance is provided, use the client from it
scope = sentry_sdk.get_current_scope()
scope = sentry_sdk.get_isolation_scope()
scope.set_client(marker_value.client)
return marker_value

Expand Down
32 changes: 10 additions & 22 deletions pytest_sentry/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,19 @@ def hookwrapper(itemgetter, **kwargs):
"""

@wrapt.decorator
def _with_scope(wrapped, instance, args, kwargs):
def _with_isolation_scope(wrapped, instance, args, kwargs):
item = itemgetter(*args, **kwargs)
scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client"))
isolation_scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client"))

if scope.client.get_integration(PytestIntegration) is None:
if isolation_scope.client.get_integration(PytestIntegration) is None:
yield
else:
with sentry_sdk.use_scope(scope):
with sentry_sdk.use_isolation_scope(isolation_scope):
gen = wrapped(*args, **kwargs)

while True:
try:
with sentry_sdk.use_scope(scope):
chunk = next(gen)

y = yield chunk

with sentry_sdk.use_scope(scope):
gen.send(y)

except StopIteration:
break
yield from gen

def inner(f):
return pytest.hookimpl(hookwrapper=True, **kwargs)(_with_scope(f))
return pytest.hookimpl(hookwrapper=True, **kwargs)(_with_isolation_scope(f))

return inner

Expand Down Expand Up @@ -89,7 +77,7 @@ def pytest_runtest_call(item):

# We use the full name including parameters because then we can identify
# how often a single test has run as part of the same GITHUB_RUN_ID.
with sentry_sdk.continue_trace(dict(sentry_sdk.get_current_scope().iter_trace_propagation_headers())):
with sentry_sdk.continue_trace(dict(sentry_sdk.get_isolation_scope().iter_trace_propagation_headers())):
with sentry_sdk.start_span(op=op, name=name) as span:
span.set_attribute("pytest-sentry.rerun", is_rerun)
if is_rerun:
Expand All @@ -107,7 +95,7 @@ def pytest_fixture_setup(fixturedef, request):
op = "pytest.fixture.setup"
name = "{} {}".format(op, fixturedef.argname)

with sentry_sdk.continue_trace(dict(sentry_sdk.get_current_scope().iter_trace_propagation_headers())):
with sentry_sdk.continue_trace(dict(sentry_sdk.get_isolation_scope().iter_trace_propagation_headers())):
with sentry_sdk.start_span(op=op, name=name) as root_span:
root_span.set_tag("pytest.fixture.scope", fixturedef.scope)
yield
Expand Down Expand Up @@ -135,8 +123,8 @@ def pytest_runtest_makereport(item, call):
call.excinfo
]

scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client"))
integration = scope.client.get_integration(PytestIntegration)
isolation_scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client"))
integration = isolation_scope.client.get_integration(PytestIntegration)

if (cur_exc_chain and call.excinfo is None) or (integration is not None and integration.always_report):
for exc_info in cur_exc_chain:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_envvars.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def capture_envelope(self, envelope):


def test_basic(sentry_test_scope):
with sentry_sdk.use_scope(sentry_test_scope):
with sentry_sdk.use_isolation_scope(sentry_test_scope):
sentry_test_scope.capture_message("hi")

(event,) = events
Expand Down
12 changes: 6 additions & 6 deletions tests/test_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,25 @@

pytestmark = pytest.mark.sentry_client(GLOBAL_CLIENT)

_DEFAULT_GLOBAL_SCOPE = sentry_sdk.Scope.get_global_scope()
_DEFAULT_ISOLATION_SCOPE = sentry_sdk.Scope.get_isolation_scope()
_DEFAULT_GLOBAL_SCOPE = sentry_sdk.get_global_scope()
_DEFAULT_ISOLATION_SCOPE = sentry_sdk.get_isolation_scope()


def _assert_right_scopes():
global_scope = sentry_sdk.Scope.get_global_scope()
global_scope = sentry_sdk.get_global_scope()
assert global_scope is _DEFAULT_GLOBAL_SCOPE

isolation_scope = sentry_sdk.Scope.get_isolation_scope()
isolation_scope = sentry_sdk.get_isolation_scope()
assert isolation_scope is _DEFAULT_ISOLATION_SCOPE


def test_basic():
_assert_right_scopes()


def test_sentry_test_scope(sentry_test_scope):
def test_correct_span():
# Ensure that we are within a root span (started by the pytest_runtest_call hook)
assert sentry_test_scope.span is not None
assert sentry_sdk.get_current_scope().span is not None


class TestSimpleClass(object):
Expand Down