diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 8cde8dc3ab..0f1cdfc741 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -67,11 +67,13 @@ def scopemethod(f): def capture_event( event, # type: Event hint=None, # type: Optional[Hint] + scope=None, # type: Optional[Any] + **scope_args # type: Dict[str, Any] ): # type: (...) -> Optional[str] hub = Hub.current if hub is not None: - return hub.capture_event(event, hint) + return hub.capture_event(event, hint, scope=scope, **scope_args) return None @@ -79,22 +81,26 @@ def capture_event( def capture_message( message, # type: str level=None, # type: Optional[str] + scope=None, # type: Optional[Any] + **scope_args # type: Dict[str, Any] ): # type: (...) -> Optional[str] hub = Hub.current if hub is not None: - return hub.capture_message(message, level) + return hub.capture_message(message, level, scope=scope, **scope_args) return None @hubmethod def capture_exception( error=None, # type: Optional[BaseException] + scope=None, # type: Optional[Any] + **scope_args # type: Dict[str, Any] ): # type: (...) -> Optional[str] hub = Hub.current if hub is not None: - return hub.capture_exception(error) + return hub.capture_exception(error, scope=scope, **scope_args) return None diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 0849d468dc..9dadc2c8e2 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -23,6 +23,7 @@ from typing import Any from typing import Optional from typing import Tuple + from typing import Dict from typing import List from typing import Callable from typing import Generator @@ -47,6 +48,24 @@ def overload(x): _local = ContextVar("sentry_current_hub") +def _update_scope(base, scope_change, scope_kwargs): + # type: (Scope, Optional[Any], Dict[str, Any]) -> Scope + if scope_change and scope_kwargs: + raise TypeError("cannot provide scope and kwargs") + if scope_change is not None: + final_scope = copy.copy(base) + if callable(scope_change): + scope_change(final_scope) + else: + final_scope.update_from_scope(scope_change) + elif scope_kwargs: + final_scope = copy.copy(base) + final_scope.update_from_kwargs(scope_kwargs) + else: + final_scope = base + return final_scope + + def _should_send_default_pii(): # type: () -> bool client = Hub.current.client @@ -285,11 +304,14 @@ def capture_event( self, event, # type: Event hint=None, # type: Optional[Hint] + scope=None, # type: Optional[Any] + **scope_args # type: Dict[str, Any] ): # type: (...) -> Optional[str] """Captures an event. Alias of :py:meth:`sentry_sdk.Client.capture_event`. """ - client, scope = self._stack[-1] + client, top_scope = self._stack[-1] + scope = _update_scope(top_scope, scope, scope_args) if client is not None: rv = client.capture_event(event, hint, scope) if rv is not None: @@ -301,6 +323,8 @@ def capture_message( self, message, # type: str level=None, # type: Optional[str] + scope=None, # type: Optional[Any] + **scope_args # type: Dict[str, Any] ): # type: (...) -> Optional[str] """Captures a message. The message is just a string. If no level @@ -312,10 +336,15 @@ def capture_message( return None if level is None: level = "info" - return self.capture_event({"message": message, "level": level}) + return self.capture_event( + {"message": message, "level": level}, scope=scope, **scope_args + ) def capture_exception( - self, error=None # type: Optional[Union[BaseException, ExcInfo]] + self, + error=None, # type: Optional[Union[BaseException, ExcInfo]] + scope=None, # type: Optional[Any] + **scope_args # type: Dict[str, Any] ): # type: (...) -> Optional[str] """Captures an exception. @@ -334,7 +363,7 @@ def capture_exception( event, hint = event_from_exception(exc_info, client_options=client.options) try: - return self.capture_event(event, hint=hint) + return self.capture_event(event, hint=hint, scope=scope, **scope_args) except Exception: self._capture_internal_exception(sys.exc_info()) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 1ea2f11b17..8b970351cd 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -323,6 +323,50 @@ def _drop(event, cause, ty): return event + def update_from_scope(self, scope): + # type: (Scope) -> None + if scope._level is not None: + self._level = scope._level + if scope._fingerprint is not None: + self._fingerprint = scope._fingerprint + if scope._transaction is not None: + self._transaction = scope._transaction + if scope._user is not None: + self._user = scope._user + if scope._tags: + self._tags.update(scope._tags) + if scope._contexts: + self._contexts.update(scope._contexts) + if scope._extras: + self._extras.update(scope._extras) + if scope._breadcrumbs: + self._breadcrumbs.extend(scope._breadcrumbs) + if scope._span: + self._span = scope._span + + def update_from_kwargs( + self, + user=None, # type: Optional[Any] + level=None, # type: Optional[str] + extras=None, # type: Optional[Dict[str, Any]] + contexts=None, # type: Optional[Dict[str, Any]] + tags=None, # type: Optional[Dict[str, str]] + fingerprint=None, # type: Optional[List[str]] + ): + # type: (...) -> None + if level is not None: + self._level = level + if user is not None: + self._user = user + if extras is not None: + self._extras.update(extras) + if contexts is not None: + self._contexts.update(contexts) + if tags is not None: + self._tags.update(tags) + if fingerprint is not None: + self._fingerprint = fingerprint + def __copy__(self): # type: () -> Scope rv = object.__new__(self.__class__) # type: Scope diff --git a/tests/test_scope.py b/tests/test_scope.py index b9c3335116..0e73584985 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -1,4 +1,5 @@ import copy +from sentry_sdk import capture_exception from sentry_sdk.scope import Scope @@ -15,3 +16,49 @@ def test_copying(): assert "bam" not in s2._tags assert s1._fingerprint is s2._fingerprint + + +def test_merging(sentry_init, capture_events): + sentry_init() + + s = Scope() + s.set_user({"id": 42}) + + events = capture_events() + + capture_exception(NameError(), scope=s) + + (event,) = events + assert event["user"] == {"id": 42} + + +def test_common_args(): + s = Scope() + s.update_from_kwargs( + user={"id": 23}, + level="warning", + extras={"k": "v"}, + contexts={"os": {"name": "Blafasel"}}, + tags={"x": "y"}, + fingerprint=["foo"], + ) + + s2 = Scope() + s2.set_extra("foo", "bar") + s2.set_tag("a", "b") + s2.set_context("device", {"a": "b"}) + s2.update_from_scope(s) + + assert s._user == {"id": 23} + assert s._level == "warning" + assert s._extras == {"k": "v"} + assert s._contexts == {"os": {"name": "Blafasel"}} + assert s._tags == {"x": "y"} + assert s._fingerprint == ["foo"] + + assert s._user == s2._user + assert s._level == s2._level + assert s._fingerprint == s2._fingerprint + assert s2._extras == {"k": "v", "foo": "bar"} + assert s2._tags == {"a": "b", "x": "y"} + assert s2._contexts == {"os": {"name": "Blafasel"}, "device": {"a": "b"}}