Skip to content

Commit 2939f24

Browse files
authored
ref: Improve errors when scope stack is broken (#194)
* ref: Improve errors when scope stack is broken * ref: Named scopes for debugging * fix: Tests
1 parent 809b25c commit 2939f24

File tree

5 files changed

+76
-7
lines changed

5 files changed

+76
-7
lines changed

sentry_sdk/hub.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,41 @@ def __exit__(self, exc_type, exc_value, tb):
8080

8181

8282
class _ScopeManager(object):
83-
def __init__(self, hub, layer):
83+
def __init__(self, hub):
8484
self._hub = hub
85-
self._layer = layer
85+
self._original_len = len(hub._stack)
86+
self._layer = hub._stack[-1]
8687

8788
def __enter__(self):
8889
scope = self._layer[1]
8990
assert scope is not None
9091
return scope
9192

9293
def __exit__(self, exc_type, exc_value, tb):
93-
layer = self._hub.pop_scope_unsafe()
94-
assert layer[1] == self._layer[1], "popped wrong scope"
95-
if layer[0] != self._layer[0]:
94+
current_len = len(self._hub._stack)
95+
if current_len < self._original_len:
96+
logger.error(
97+
"Scope popped too soon. Popped %s scopes too many.",
98+
self._original_len - current_len,
99+
)
100+
return
101+
elif current_len > self._original_len:
102+
logger.warning(
103+
"Leaked %s scopes: %s",
104+
current_len - self._original_len,
105+
self._hub._stack[self._original_len :],
106+
)
107+
108+
layer = self._hub._stack[self._original_len - 1]
109+
del self._hub._stack[self._original_len - 1 :]
110+
111+
if layer[1] != self._layer[1]:
112+
logger.error(
113+
"Wrong scope found. Meant to pop %s, but popped %s.",
114+
layer[1],
115+
self._layer[1],
116+
)
117+
elif layer[0] != self._layer[0]:
96118
warning = (
97119
"init() called inside of pushed scope. This might be entirely "
98120
"legitimate but usually occurs when initializing the SDK inside "
@@ -288,7 +310,7 @@ def push_scope(self, callback=None):
288310
new_layer = (client, copy.copy(scope))
289311
self._stack.append(new_layer)
290312

291-
return _ScopeManager(self, new_layer)
313+
return _ScopeManager(self)
292314

293315
scope = push_scope
294316

sentry_sdk/integrations/flask.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def _push_appctx(*args, **kwargs):
6262
# always want to push scope regardless of whether WSGI app might already
6363
# have (not the case for CLI for example)
6464
hub.push_scope()
65+
with hub.configure_scope() as scope:
66+
scope._name = "flask"
6567

6668

6769
def _pop_appctx(*args, **kwargs):

sentry_sdk/integrations/wsgi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def __call__(self, environ, start_response):
6161
hub.push_scope()
6262
with capture_internal_exceptions():
6363
with hub.configure_scope() as scope:
64+
scope._name = "wsgi"
6465
scope.add_event_processor(_make_wsgi_event_processor(environ))
6566

6667
try:

sentry_sdk/scope.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Scope(object):
2323

2424
__slots__ = (
2525
"_level",
26+
"_name",
2627
"_fingerprint",
2728
"_transaction",
2829
"_user",
@@ -38,6 +39,7 @@ def __init__(self):
3839
self._event_processors = []
3940
self._error_processors = []
4041

42+
self._name = None
4143
self.clear()
4244

4345
@_attr_setter
@@ -174,6 +176,7 @@ def __copy__(self):
174176
rv = object.__new__(self.__class__)
175177

176178
rv._level = self._level
179+
rv._name = self._name
177180
rv._fingerprint = self._fingerprint
178181
rv._transaction = self._transaction
179182
rv._user = self._user
@@ -187,3 +190,10 @@ def __copy__(self):
187190
rv._error_processors = list(self._error_processors)
188191

189192
return rv
193+
194+
def __repr__(self):
195+
return "<%s id=%s name=%s>" % (
196+
self.__class__.__name__,
197+
hex(id(self)),
198+
self._name,
199+
)

tests/test_basics.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def test_integration_scoping():
226226
assert len(events) == 1
227227

228228

229-
def test_client_initialized_within_scope(sentry_init, capture_events, caplog):
229+
def test_client_initialized_within_scope(sentry_init, caplog):
230230
caplog.set_level(logging.WARNING)
231231

232232
sentry_init(debug=True)
@@ -237,3 +237,37 @@ def test_client_initialized_within_scope(sentry_init, capture_events, caplog):
237237
record, = (x for x in caplog.records if x.levelname == "WARNING")
238238

239239
assert record.msg.startswith("init() called inside of pushed scope.")
240+
241+
242+
def test_scope_leaks_cleaned_up(sentry_init, caplog):
243+
caplog.set_level(logging.WARNING)
244+
245+
sentry_init(debug=True)
246+
247+
old_stack = list(Hub.current._stack)
248+
249+
with push_scope():
250+
push_scope()
251+
252+
assert Hub.current._stack == old_stack
253+
254+
record, = (x for x in caplog.records if x.levelname == "WARNING")
255+
256+
assert record.message.startswith("Leaked 1 scopes:")
257+
258+
259+
def test_scope_popped_too_soon(sentry_init, caplog):
260+
caplog.set_level(logging.ERROR)
261+
262+
sentry_init(debug=True)
263+
264+
old_stack = list(Hub.current._stack)
265+
266+
with push_scope():
267+
Hub.current.pop_scope_unsafe()
268+
269+
assert Hub.current._stack == old_stack
270+
271+
record, = (x for x in caplog.records if x.levelname == "ERROR")
272+
273+
assert record.message == ("Scope popped too soon. Popped 1 scopes too many.")

0 commit comments

Comments
 (0)