From fbb40b04ed94a8f12089b82d5e336a2bb4c99c76 Mon Sep 17 00:00:00 2001 From: Anthony Collins Date: Wed, 2 Oct 2019 15:07:54 -0400 Subject: [PATCH 01/10] master: (sentry_sdk/utils.py) add push_scope_decorator --- sentry_sdk/utils.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index fe1dfb3793..9174c0188f 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1,3 +1,4 @@ +import functools import os import sys import linecache @@ -797,3 +798,37 @@ def transaction_from_function(func): disable_capture_event = ContextVar("disable_capture_event") + + +def push_scope_decorator(user=None, level=None, tags=None, extras=None): + """ + Wrap function in a push_scope. + :param user: dictionary containing id, username, email, and/or ip_address attributes. + :param level: (str) scope level (e.g. 'error') + :param tags: dictionary of key/value pairs for scope tags + :param extras: dictionary of key/value pairs for scope extras + :return: A proxy method that wraps the decorated function in a usage of the push_scope + context manager. + """ + if tags is not None and type(tags) is not dict: + raise Exception("tags must be a dictionary") + if extras is not None and type(extras) is not dict: + raise Exception("extra must be a dictionary") + + def create_sentry_push_scope(f): + @functools.wraps(f) + def __inner(*args, **kwargs): + with sentry_sdk.push_scope() as current_scope: + if user: + current_scope.user = user + if level: + current_scope.level = level + if tags: + for key, value in tags.iteritems(): + current_scope.set_tag(key, value) + if extras: + for key, value in extras.iteritems(): + current_scope.set_extra(key, value) + f(*args, **kwargs) + return __inner + return create_sentry_push_scope From 3592c0f4626397ddd9aa19214dc364b5f89f964d Mon Sep 17 00:00:00 2001 From: a-d-collins Date: Wed, 2 Oct 2019 19:21:28 -0400 Subject: [PATCH 02/10] master: (sentry_sdk/utils.py) fix iteritems incompatibility --- sentry_sdk/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 9174c0188f..6c0b63a2a8 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -7,7 +7,7 @@ from datetime import datetime import sentry_sdk -from sentry_sdk._compat import urlparse, text_type, implements_str, PY2 +from sentry_sdk._compat import urlparse, text_type, implements_str, PY2, iteritems from sentry_sdk._types import MYPY @@ -824,10 +824,10 @@ def __inner(*args, **kwargs): if level: current_scope.level = level if tags: - for key, value in tags.iteritems(): + for key, value in iteritems(tags): current_scope.set_tag(key, value) if extras: - for key, value in extras.iteritems(): + for key, value in iteritems(extras): current_scope.set_extra(key, value) f(*args, **kwargs) return __inner From 34062399e651b895ee839d8a3036da87f4c901a5 Mon Sep 17 00:00:00 2001 From: a-d-collins Date: Wed, 2 Oct 2019 23:04:57 -0400 Subject: [PATCH 03/10] master: (sentry_sdk/utils.py) reformat push_scope_decorator() to fit linter specs --- sentry_sdk/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 6c0b63a2a8..4acf5fe01e 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -801,10 +801,11 @@ def transaction_from_function(func): def push_scope_decorator(user=None, level=None, tags=None, extras=None): + # type: (Optional[Dict[str, str]], Optional[str], Optional[Dict[str, str]], Optional[Dict[str, str]]) -> Callable[..., Any] # noqa """ Wrap function in a push_scope. :param user: dictionary containing id, username, email, and/or ip_address attributes. - :param level: (str) scope level (e.g. 'error') + :param level: scope level (e.g. 'error') :param tags: dictionary of key/value pairs for scope tags :param extras: dictionary of key/value pairs for scope extras :return: A proxy method that wraps the decorated function in a usage of the push_scope @@ -816,8 +817,10 @@ def push_scope_decorator(user=None, level=None, tags=None, extras=None): raise Exception("extra must be a dictionary") def create_sentry_push_scope(f): + # type: (Callable[..., Any]) -> Callable[..., None] @functools.wraps(f) def __inner(*args, **kwargs): + # type: (*Any, **Any) -> None with sentry_sdk.push_scope() as current_scope: if user: current_scope.user = user @@ -830,5 +833,7 @@ def __inner(*args, **kwargs): for key, value in iteritems(extras): current_scope.set_extra(key, value) f(*args, **kwargs) + return __inner + return create_sentry_push_scope From 4b6d51007f4aebb619cab5330ed3e7507e3da803 Mon Sep 17 00:00:00 2001 From: a-d-collins Date: Fri, 4 Oct 2019 16:28:25 -0400 Subject: [PATCH 04/10] push-scope-decorator: (utils.py) remove push_scope_decorator, (hub.py) update Hub.push_scope() to allow it to act as both a contentmanager and a decorator, (api.py, utils.py) add new scopemethod api methods {set_tag, set_extra, set_user, set_level} that allow for updating of the current scope --- sentry_sdk/api.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ sentry_sdk/hub.py | 43 +++++++++++++++++++++++++++++++++++++----- sentry_sdk/scope.py | 12 ++++++++++++ sentry_sdk/utils.py | 42 +---------------------------------------- 4 files changed, 97 insertions(+), 46 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 873ea96dce..3400ec26cf 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -8,6 +8,7 @@ if MYPY: from typing import Any + from typing import Dict from typing import Optional from typing import overload from typing import Callable @@ -36,6 +37,10 @@ def overload(x): "flush", "last_event_id", "start_span", + "set_tag", + "set_extra", + "set_user", + "set_level", ] @@ -48,6 +53,15 @@ def hubmethod(f): return f +def scopemethod(f): + # type: (F) -> F + f.__doc__ = "%s\n\n%s" % ( + "Alias for :py:meth:`sentry_sdk.Scope.%s`" % f.__name__, + inspect.getdoc(getattr(Scope, f.__name__)), + ) + return f + + @hubmethod def capture_event( event, # type: Event @@ -163,6 +177,38 @@ def inner(): return None +@scopemethod # noqa +def set_tag(key, value): + # type: (str, Any) -> None + hub = Hub.current + if hub is not None: + hub.current_scope.set_tag(key, value) + + +@scopemethod # noqa +def set_extra(key, value): + # type: (str, Any) -> None + hub = Hub.current + if hub is not None: + hub.current_scope.set_extra(key, value) + + +@scopemethod # noqa +def set_user(value): + # type: (Dict[str, Any]) -> None + hub = Hub.current + if hub is not None: + hub.current_scope.set_user(value) + + +@scopemethod # noqa +def set_level(value): + # type: (str) -> None + hub = Hub.current + if hub is not None: + hub.current_scope.set_level(value) + + @hubmethod def flush( timeout=None, # type: Optional[float] diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 9fc5d41d02..2805e16086 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -1,4 +1,5 @@ import copy +import functools import random import sys @@ -123,6 +124,36 @@ def main(cls): return GLOBAL_HUB +class _PushScopeContextDecorator(object): + def __init__(self, hub): + # type: (Hub) -> None + self._hub = hub + + def __enter__(self): + # type: () -> Scope + client, scope = self._hub._stack[-1] + new_layer = (client, copy.copy(scope)) + self._hub._stack.append(new_layer) + + self._scope_manager_instance = _ScopeManager(self._hub) + sm_scope = self._scope_manager_instance.__enter__() + return sm_scope + + def __exit__(self, exc_type, exc_val, exc_tb): + # type: (Any, Any, Any) -> None + self._scope_manager_instance.__exit__(exc_type, exc_val, exc_tb) + + def __call__(self, f): + # type: (Callable[..., Any]) -> Callable[..., None] + @functools.wraps(f) + def decorated(*args, **kwargs): + # type: (*Any, **Any) -> Any + with self: + return f(*args, **kwargs) + + return decorated + + class _ScopeManager(object): def __init__(self, hub): # type: (Hub) -> None @@ -263,6 +294,12 @@ def client(self): """Returns the current client on the hub.""" return self._stack[-1][0] + @property + def current_scope(self): + # type: () -> Scope + """Returns the current scope on the hub.""" + return self._stack[-1][1] + def last_event_id(self): # type: () -> Optional[str] """Returns the last event ID.""" @@ -477,11 +514,7 @@ def push_scope( # noqa callback(scope) return None - client, scope = self._stack[-1] - new_layer = (client, copy.copy(scope)) - self._stack.append(new_layer) - - return _ScopeManager(self) + return _PushScopeContextDecorator(self) scope = push_scope diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index b0aa25e0b4..5ced036e10 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -123,6 +123,12 @@ def level(self, value): """When set this overrides the level.""" self._level = value + @_attr_setter + def set_level(self, value): + # type: (Optional[str]) -> None + """When set this overrides the level.""" + self._level = value + @_attr_setter def fingerprint(self, value): # type: (Optional[List[str]]) -> None @@ -144,6 +150,12 @@ def user(self, value): """When set a specific user is bound to the scope.""" self._user = value + @_attr_setter + def set_user(self, value): + # type: (Dict[str, Any]) -> None + """When set a specific user is bound to the scope.""" + self._user = value + @property def span(self): # type: () -> Optional[Span] diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 4acf5fe01e..fe1dfb3793 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1,4 +1,3 @@ -import functools import os import sys import linecache @@ -7,7 +6,7 @@ from datetime import datetime import sentry_sdk -from sentry_sdk._compat import urlparse, text_type, implements_str, PY2, iteritems +from sentry_sdk._compat import urlparse, text_type, implements_str, PY2 from sentry_sdk._types import MYPY @@ -798,42 +797,3 @@ def transaction_from_function(func): disable_capture_event = ContextVar("disable_capture_event") - - -def push_scope_decorator(user=None, level=None, tags=None, extras=None): - # type: (Optional[Dict[str, str]], Optional[str], Optional[Dict[str, str]], Optional[Dict[str, str]]) -> Callable[..., Any] # noqa - """ - Wrap function in a push_scope. - :param user: dictionary containing id, username, email, and/or ip_address attributes. - :param level: scope level (e.g. 'error') - :param tags: dictionary of key/value pairs for scope tags - :param extras: dictionary of key/value pairs for scope extras - :return: A proxy method that wraps the decorated function in a usage of the push_scope - context manager. - """ - if tags is not None and type(tags) is not dict: - raise Exception("tags must be a dictionary") - if extras is not None and type(extras) is not dict: - raise Exception("extra must be a dictionary") - - def create_sentry_push_scope(f): - # type: (Callable[..., Any]) -> Callable[..., None] - @functools.wraps(f) - def __inner(*args, **kwargs): - # type: (*Any, **Any) -> None - with sentry_sdk.push_scope() as current_scope: - if user: - current_scope.user = user - if level: - current_scope.level = level - if tags: - for key, value in iteritems(tags): - current_scope.set_tag(key, value) - if extras: - for key, value in iteritems(extras): - current_scope.set_extra(key, value) - f(*args, **kwargs) - - return __inner - - return create_sentry_push_scope From aaa1c0b483ba178d2c2b74385e73af1f6e518551 Mon Sep 17 00:00:00 2001 From: Anthony Collins Date: Fri, 4 Oct 2019 17:23:17 -0400 Subject: [PATCH 05/10] push-scope-decorator: (sentry_sdk/hub.py) have _PushScopeContextDecorator inherit from contextlib.ContextDecorator --- sentry_sdk/hub.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 2805e16086..eaeabb5815 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -1,10 +1,9 @@ import copy -import functools import random import sys from datetime import datetime -from contextlib import contextmanager +from contextlib import ContextDecorator, contextmanager from sentry_sdk._compat import with_metaclass from sentry_sdk.scope import Scope @@ -124,7 +123,7 @@ def main(cls): return GLOBAL_HUB -class _PushScopeContextDecorator(object): +class _PushScopeContextDecorator(ContextDecorator): def __init__(self, hub): # type: (Hub) -> None self._hub = hub @@ -143,16 +142,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): # type: (Any, Any, Any) -> None self._scope_manager_instance.__exit__(exc_type, exc_val, exc_tb) - def __call__(self, f): - # type: (Callable[..., Any]) -> Callable[..., None] - @functools.wraps(f) - def decorated(*args, **kwargs): - # type: (*Any, **Any) -> Any - with self: - return f(*args, **kwargs) - - return decorated - class _ScopeManager(object): def __init__(self, hub): From 9e0d94bd2ee6d1e5ad63ce7ca728ac1e6acb5c05 Mon Sep 17 00:00:00 2001 From: Anthony Collins Date: Fri, 4 Oct 2019 17:31:10 -0400 Subject: [PATCH 06/10] push-scope-decorator: (sentry_sdk/hub.py) combine _PushScopeContextDecorator with _ScopeManager --- sentry_sdk/hub.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index eaeabb5815..fcdc89db49 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -123,7 +123,7 @@ def main(cls): return GLOBAL_HUB -class _PushScopeContextDecorator(ContextDecorator): +class _ScopeManager(ContextDecorator): def __init__(self, hub): # type: (Hub) -> None self._hub = hub @@ -134,24 +134,9 @@ def __enter__(self): new_layer = (client, copy.copy(scope)) self._hub._stack.append(new_layer) - self._scope_manager_instance = _ScopeManager(self._hub) - sm_scope = self._scope_manager_instance.__enter__() - return sm_scope + self._original_len = len(self._hub._stack) + self._layer = self._hub._stack[-1] - def __exit__(self, exc_type, exc_val, exc_tb): - # type: (Any, Any, Any) -> None - self._scope_manager_instance.__exit__(exc_type, exc_val, exc_tb) - - -class _ScopeManager(object): - def __init__(self, hub): - # type: (Hub) -> None - self._hub = hub - self._original_len = len(hub._stack) - self._layer = hub._stack[-1] - - def __enter__(self): - # type: () -> Scope scope = self._layer[1] assert scope is not None return scope @@ -487,14 +472,14 @@ def push_scope( def push_scope( # noqa self, callback=None # type: Optional[Callable[[Scope], None]] ): - # type: (...) -> Optional[ContextManager[Scope]] + # type: (...) -> Optional[ContextDecorator[Scope]] """ Pushes a new layer on the scope stack. :param callback: If provided, this method pushes a scope, calls `callback`, and pops the scope again. - :returns: If no `callback` is provided, a context manager that should + :returns: If no `callback` is provided, a ContextDecorator that should be used to pop the scope again. """ @@ -503,7 +488,7 @@ def push_scope( # noqa callback(scope) return None - return _PushScopeContextDecorator(self) + return _ScopeManager(self) scope = push_scope From 27c82c82f094a1246ca91aaf3ec76175ab3a93c6 Mon Sep 17 00:00:00 2001 From: a-d-collins Date: Sat, 5 Oct 2019 11:16:46 -0400 Subject: [PATCH 07/10] push-scope-decorator: (hub.py) fix push_scope() typing --- sentry_sdk/hub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index fcdc89db49..4866a8d579 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -459,7 +459,7 @@ def start_span( def push_scope( self, callback=None # type: Optional[None] ): - # type: (...) -> ContextManager[Scope] + # type: (...) -> _ScopeManager pass @overload # noqa @@ -472,7 +472,7 @@ def push_scope( def push_scope( # noqa self, callback=None # type: Optional[Callable[[Scope], None]] ): - # type: (...) -> Optional[ContextDecorator[Scope]] + # type: (...) -> Optional[_ScopeManager] """ Pushes a new layer on the scope stack. From 31d8dfb7b7bb883073c9c5a17f82cbe8d7894897 Mon Sep 17 00:00:00 2001 From: a-d-collins Date: Sat, 5 Oct 2019 12:06:45 -0400 Subject: [PATCH 08/10] master: (sentry_sdk/_compat.py, sentry_sdk/hub.py) resolve py2 ContextDecorator compat issue, (sentry_sdk/api.py) update typing to match that of Hub.push_scope() --- sentry_sdk/_compat.py | 29 +++++++++++++++++++++++++++++ sentry_sdk/api.py | 6 +++--- sentry_sdk/hub.py | 12 ++++++------ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/_compat.py b/sentry_sdk/_compat.py index e357c96416..cc594dcefa 100644 --- a/sentry_sdk/_compat.py +++ b/sentry_sdk/_compat.py @@ -15,6 +15,35 @@ PY2 = sys.version_info[0] == 2 +if not PY2 and sys.version_info >= (3, 2): + from contextlib import ContextDecorator +else: + from functools import wraps + + + class ContextDecorator(object): + "A base class or mixin that enables context managers to work as decorators." + + def _recreate_cm(self): + """Return a recreated instance of self. + + Allows an otherwise one-shot context manager like + _GeneratorContextManager to support use as + a decorator via implicit recreation. + + This is a private interface just for _GeneratorContextManager. + See issue #11647 for details. + """ + return self + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwds): + with self._recreate_cm(): + return func(*args, **kwds) + + return inner + if PY2: import urlparse # noqa diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 3400ec26cf..60d88bd6a8 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,7 +1,7 @@ import inspect from contextlib import contextmanager -from sentry_sdk.hub import Hub +from sentry_sdk.hub import Hub, ScopeManager from sentry_sdk.scope import Scope from sentry_sdk._types import MYPY @@ -145,7 +145,7 @@ def inner(): @overload # noqa def push_scope(): - # type: () -> ContextManager[Scope] + # type: () -> ScopeManager pass @@ -161,7 +161,7 @@ def push_scope( def push_scope( callback=None # type: Optional[Callable[[Scope], None]] ): - # type: (...) -> Optional[ContextManager[Scope]] + # type: (...) -> Optional[ScopeManager] hub = Hub.current if hub is not None: return hub.push_scope(callback) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 4866a8d579..4d85460214 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -3,9 +3,9 @@ import sys from datetime import datetime -from contextlib import ContextDecorator, contextmanager +from contextlib import contextmanager -from sentry_sdk._compat import with_metaclass +from sentry_sdk._compat import with_metaclass, ContextDecorator from sentry_sdk.scope import Scope from sentry_sdk.client import Client from sentry_sdk.tracing import Span @@ -123,7 +123,7 @@ def main(cls): return GLOBAL_HUB -class _ScopeManager(ContextDecorator): +class ScopeManager(ContextDecorator): def __init__(self, hub): # type: (Hub) -> None self._hub = hub @@ -459,7 +459,7 @@ def start_span( def push_scope( self, callback=None # type: Optional[None] ): - # type: (...) -> _ScopeManager + # type: (...) -> ScopeManager pass @overload # noqa @@ -472,7 +472,7 @@ def push_scope( def push_scope( # noqa self, callback=None # type: Optional[Callable[[Scope], None]] ): - # type: (...) -> Optional[_ScopeManager] + # type: (...) -> Optional[ScopeManager] """ Pushes a new layer on the scope stack. @@ -488,7 +488,7 @@ def push_scope( # noqa callback(scope) return None - return _ScopeManager(self) + return ScopeManager(self) scope = push_scope From 1e52628a2fec7818244e773ba6085218923a1bf1 Mon Sep 17 00:00:00 2001 From: a-d-collins Date: Sat, 5 Oct 2019 13:33:24 -0400 Subject: [PATCH 09/10] master: (sentry_sdk/scope.py) remove improper uses of @_attr_setter, (tests/test_basics.py) remove unneeded test_scope_leaks_cleaned_up() --- sentry_sdk/scope.py | 2 -- tests/test_basics.py | 17 ----------------- 2 files changed, 19 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 5ced036e10..081ab7e6c8 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -123,7 +123,6 @@ def level(self, value): """When set this overrides the level.""" self._level = value - @_attr_setter def set_level(self, value): # type: (Optional[str]) -> None """When set this overrides the level.""" @@ -150,7 +149,6 @@ def user(self, value): """When set a specific user is bound to the scope.""" self._user = value - @_attr_setter def set_user(self, value): # type: (Dict[str, Any]) -> None """When set a specific user is bound to the scope.""" diff --git a/tests/test_basics.py b/tests/test_basics.py index 1d5a69b292..42a01eb3ca 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -240,23 +240,6 @@ def test_client_initialized_within_scope(sentry_init, caplog): assert record.msg.startswith("init() called inside of pushed scope.") -def test_scope_leaks_cleaned_up(sentry_init, caplog): - caplog.set_level(logging.WARNING) - - sentry_init(debug=True) - - old_stack = list(Hub.current._stack) - - with push_scope(): - push_scope() - - assert Hub.current._stack == old_stack - - record, = (x for x in caplog.records if x.levelname == "WARNING") - - assert record.message.startswith("Leaked 1 scopes:") - - def test_scope_popped_too_soon(sentry_init, caplog): caplog.set_level(logging.ERROR) From 74dedb01dab5b6d16bd4528cb381da07369e3c83 Mon Sep 17 00:00:00 2001 From: a-d-collins Date: Sun, 6 Oct 2019 10:56:04 -0400 Subject: [PATCH 10/10] master: (tests/test_basics.py) add test for push_scope as decorator, (sentry_sdk/_compat.py) satisfy linters --- sentry_sdk/_compat.py | 2 +- tests/test_basics.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/_compat.py b/sentry_sdk/_compat.py index cc594dcefa..87f521211b 100644 --- a/sentry_sdk/_compat.py +++ b/sentry_sdk/_compat.py @@ -20,7 +20,6 @@ else: from functools import wraps - class ContextDecorator(object): "A base class or mixin that enables context managers to work as decorators." @@ -44,6 +43,7 @@ def inner(*args, **kwds): return inner + if PY2: import urlparse # noqa diff --git a/tests/test_basics.py b/tests/test_basics.py index 42a01eb3ca..2299e232fe 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -11,6 +11,7 @@ add_breadcrumb, last_event_id, Hub, + set_level, ) from sentry_sdk.integrations.logging import LoggingIntegration @@ -174,6 +175,30 @@ def _(scope): assert Hub.current._stack[-1][1] is outer_scope +def test_push_scope_as_decorator(sentry_init, capture_events): + sentry_init() + events = capture_events() + + Hub.current.bind_client(None) + + outer_scope = Hub.current._stack[-1][1] + + @push_scope() + def foo(): + set_level("warning") + try: + 1 / 0 + except Exception as e: + capture_exception(e) + + foo() + + event, = events + assert outer_scope._level != "warning" + assert event["level"] == "warning" + assert "exception" in event + + def test_breadcrumbs(sentry_init, capture_events): sentry_init(max_breadcrumbs=10) events = capture_events()