Skip to content

Commit c99d54d

Browse files
authored
feat: Added support for with-scope like behavior (#65)
1 parent 020eb6f commit c99d54d

File tree

4 files changed

+85
-6
lines changed

4 files changed

+85
-6
lines changed

sentry_sdk/api.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ def inner():
105105
return inner()
106106

107107

108+
@hubmethod
109+
def push_scope(callback=None):
110+
hub = Hub.current
111+
if hub is not None:
112+
return hub.push_scope(callback)
113+
elif callback is None:
114+
115+
@contextmanager
116+
def inner():
117+
yield Scope()
118+
119+
return inner()
120+
121+
108122
@hubmethod
109123
def last_event_id():
110124
hub = Hub.current

sentry_sdk/hub.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ def __init__(self, hub, layer):
6060
self._layer = layer
6161

6262
def __enter__(self):
63-
return self
63+
scope = self._layer[1]
64+
if scope is None:
65+
scope = Scope()
66+
return scope
6467

6568
def __exit__(self, exc_type, exc_value, tb):
6669
assert self._hub.pop_scope_unsafe() == self._layer, "popped wrong scope"
@@ -205,13 +208,20 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs):
205208
while len(scope._breadcrumbs) >= client.options["max_breadcrumbs"]:
206209
scope._breadcrumbs.popleft()
207210

208-
def push_scope(self):
211+
def push_scope(self, callback=None):
209212
"""Pushes a new layer on the scope stack. Returns a context manager
210-
that should be used to pop the scope again."""
213+
that should be used to pop the scope again. Alternatively a callback
214+
can be provided that is executed in the context of the scope.
215+
"""
211216
client, scope = self._stack[-1]
212217
new_layer = (client, copy.copy(scope))
213218
self._stack.append(new_layer)
214-
return _ScopeManager(self, new_layer)
219+
220+
if callback is not None:
221+
if client is not None:
222+
callback(scope)
223+
else:
224+
return _ScopeManager(self, new_layer)
215225

216226
def pop_scope_unsafe(self):
217227
"""Pops a scope layer from the stack. Try to use the context manager
@@ -224,18 +234,28 @@ def configure_scope(self, callback=None):
224234
"""Reconfigures the scope."""
225235
client, scope = self._stack[-1]
226236
if callback is not None:
227-
if client is not None and scope is not None:
237+
if client is not None:
228238
callback(scope)
229239
return
230240

231241
@contextmanager
232242
def inner():
233-
if client is not None and scope is not None:
243+
if client is not None:
234244
yield scope
235245
else:
236246
yield Scope()
237247

238248
return inner()
239249

250+
def scope(self, callback=None):
251+
"""Pushes a new scope and yields it for configuration.
252+
253+
The scope is dropped at the end of the with statement. Alternatively
254+
a callback can be provided similar to `configure_scope`.
255+
"""
256+
with self.push_scope():
257+
client, scope = self._stack[-1]
258+
return self.configure_scope(callback)
259+
240260

241261
GLOBAL_HUB = Hub()

sentry_sdk/scope.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Scope(object):
1111
"""
1212

1313
__slots__ = (
14+
"_level",
1415
"_fingerprint",
1516
"_transaction",
1617
"_user",
@@ -28,6 +29,11 @@ def __init__(self):
2829

2930
self.clear()
3031

32+
@_attr_setter
33+
def level(self, value):
34+
"""When set this overrides the level."""
35+
self._level = value
36+
3137
@_attr_setter
3238
def fingerprint(self, value):
3339
"""When set this overrides the default fingerprint."""
@@ -69,6 +75,7 @@ def remove_extra(self, key):
6975

7076
def clear(self):
7177
"""Clears the entire scope."""
78+
self._level = None
7279
self._fingerprint = None
7380
self._transaction = None
7481
self._user = None
@@ -112,6 +119,9 @@ def apply_to_event(self, event, hint=None):
112119
def _drop(event, cause, ty):
113120
logger.info("%s (%s) dropped event (%s)", ty, cause, event)
114121

122+
if self._level is not None:
123+
event["level"] = self._level
124+
115125
event.setdefault("breadcrumbs", []).extend(self._breadcrumbs)
116126
if event.get("user") is None and self._user is not None:
117127
event["user"] = self._user
@@ -151,6 +161,7 @@ def _drop(event, cause, ty):
151161
def __copy__(self):
152162
rv = object.__new__(self.__class__)
153163

164+
rv._level = self._level
154165
rv._fingerprint = self._fingerprint
155166
rv._transaction = self._transaction
156167
rv._user = self._user

tests/test_basics.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from sentry_sdk import (
2+
push_scope,
23
configure_scope,
34
capture_exception,
45
add_breadcrumb,
@@ -105,3 +106,36 @@ def before_breadcrumb(crumb, hint):
105106
assert_hint.clear()
106107
add_breadcrumb(foo=42)
107108
add_breadcrumb(crumb=dict(foo=42))
109+
110+
111+
def test_push_scope(sentry_init, capture_events):
112+
sentry_init()
113+
events = capture_events()
114+
115+
with push_scope() as scope:
116+
scope.level = "warning"
117+
try:
118+
1 / 0
119+
except Exception as e:
120+
capture_exception(e)
121+
122+
event, = events
123+
124+
assert event["level"] == "warning"
125+
assert "exception" in event
126+
127+
128+
def test_push_scope_null_client(sentry_init, capture_events):
129+
sentry_init()
130+
events = capture_events()
131+
132+
Hub.current.bind_client(None)
133+
134+
with push_scope() as scope:
135+
scope.level = "warning"
136+
try:
137+
1 / 0
138+
except Exception as e:
139+
capture_exception(e)
140+
141+
assert len(events) == 0

0 commit comments

Comments
 (0)