Skip to content

Commit 3daedeb

Browse files
committed
feat: configure_scope callbacks
1 parent 7f25a39 commit 3daedeb

File tree

5 files changed

+93
-12
lines changed

5 files changed

+93
-12
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,29 @@ Scopes can be nested. If you call ``push_scope`` inside of the
5151
``with``-statement again, that scope will be pushed onto a stack. It will also
5252
inherit all data from the outer scope.
5353

54+
### Scopes in unconfigured environments
55+
56+
If you never call ``init``, no data will ever get sent to any server. In such
57+
situations, code like this is essentially deadweight:
58+
59+
with sentry_sdk.configure_scope() as scope:
60+
scope.user = _get_user_data()
61+
62+
Sentry-Python supports an alternative syntax for configuring a scope that
63+
solves this problem:
64+
65+
@sentry_sdk.configure_scope
66+
def _(scope):
67+
scope.user = _get_user_data()
68+
69+
Your function will just not be executed if there is no client configured.
70+
71+
In your testing and development environment you still might want to run that
72+
code without sending any events. In that case, simply call ``init`` without a
73+
DSN:
74+
75+
sentry_sdk.init()
76+
5477
### Breadcrumbs
5578

5679
Breadcrumbs also live on the stack. By default any (non-debug) log message

sentry_minimal.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,17 @@ def add_breadcrumb(*args, **kwargs):
3838

3939

4040
@public
41-
@contextmanager
42-
def configure_scope():
41+
def configure_scope(callback=None):
4342
hub = Hub.current
4443
if hub is not None:
45-
with hub.configure_scope() as scope:
46-
yield scope
47-
else:
48-
yield Scope()
44+
return hub.configure_scope(callback)
45+
elif callback is None:
46+
47+
@contextmanager
48+
def inner():
49+
yield Scope()
50+
51+
return inner()
4952

5053

5154
@public

sentry_sdk/api.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ def __exit__(self, exc_type, exc_value, tb):
1717

1818
def init(*args, **kwargs):
1919
client = Client(*args, **kwargs)
20-
if client.dsn is not None:
21-
Hub.main.bind_client(client)
20+
Hub.main.bind_client(client)
2221
return _InitGuard(client)
2322

2423

sentry_sdk/hub.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,22 @@ def pop_scope_unsafe(self):
184184
assert self._stack
185185
return rv
186186

187-
@contextmanager
188-
def configure_scope(self):
187+
def configure_scope(self, callback=None):
189188
"""Reconfigures the scope."""
190-
yield self._stack[-1][1]
189+
client, scope = self._stack[-1]
190+
if callback is not None:
191+
if client is not None and scope is not None:
192+
callback(scope)
193+
else:
194+
195+
@contextmanager
196+
def inner():
197+
if client is not None and scope is not None:
198+
yield scope
199+
else:
200+
yield Scope()
201+
202+
return inner()
191203

192204
def _flush_event_processors(self):
193205
rv = self._pending_processors

tests/test_client.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import sys
44
import subprocess
55
from textwrap import dedent
6-
from sentry_sdk import Client
6+
from sentry_sdk import init, Hub, Client, configure_scope
7+
from sentry_sdk.hub import HubMeta
78
from sentry_sdk.transport import Transport
89
from sentry_sdk.utils import Event, Dsn
910

@@ -77,3 +78,46 @@ def send_event(pool, event, auth):
7778
end = time.time()
7879
assert int(end - start) == num_messages / 10
7980
assert output.count(b"HI") == num_messages
81+
82+
83+
def test_configure_scope_available(request, monkeypatch):
84+
# Test that scope is configured if client is configured
85+
init()
86+
request.addfinalizer(lambda: Hub.current.bind_client(None))
87+
88+
with configure_scope() as scope:
89+
assert scope is Hub.current._stack[-1][1]
90+
scope.set_tag("foo", "bar")
91+
92+
calls = []
93+
94+
def callback(scope):
95+
calls.append(scope)
96+
scope.set_tag("foo", "bar")
97+
98+
assert configure_scope(callback) is None
99+
assert len(calls) == 1
100+
assert calls[0] is Hub.current._stack[-1][1]
101+
102+
103+
@pytest.mark.parametrize("no_sdk", (True, False))
104+
def test_configure_scope_unavailable(no_sdk, monkeypatch):
105+
if no_sdk:
106+
# Emulate sentry_minimal without SDK installation: callbacks are not called
107+
monkeypatch.setattr(HubMeta, "current", None)
108+
assert not Hub.current
109+
else:
110+
# Still, no client configured
111+
assert Hub.current
112+
113+
calls = []
114+
115+
def callback(scope):
116+
calls.append(scope)
117+
scope.set_tag("foo", "bar")
118+
119+
with configure_scope() as scope:
120+
scope.set_tag("foo", "bar")
121+
122+
assert configure_scope(callback) is None
123+
assert not calls

0 commit comments

Comments
 (0)