From 948703877c63cb677fdb72fbca6c967ed19a022d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 11 Dec 2023 12:54:34 +0100 Subject: [PATCH 01/82] (1) Move `add_breadcrumb` and session function from Hub to Scope (#2578) Moved some functionality from Hub to Scope or Client: - moved `add_breadcrumb` from Hub to Scope - moved session functions from Hub to Scope - moved `get_integration1` from Hub to Client. This is preparation work for refactoring how we deal with Hubs and Scopes in the future. Each commit is moving one function, so it should be easy to review commit by commit. --- sentry_sdk/client.py | 19 +++++++++ sentry_sdk/hub.py | 62 +++++---------------------- sentry_sdk/scope.py | 99 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 124 insertions(+), 56 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 8aad751470..846fc0a7b6 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -43,7 +43,10 @@ from typing import Dict from typing import Optional from typing import Sequence + from typing import Type + from typing import Union + from sentry_sdk.integrations import Integration from sentry_sdk.scope import Scope from sentry_sdk._types import Event, Hint from sentry_sdk.session import Session @@ -653,6 +656,22 @@ def capture_session( else: self.session_flusher.add_session(session) + def get_integration( + self, name_or_class # type: Union[str, Type[Integration]] + ): + # type: (...) -> Any + """Returns the integration for this client by name or class. + If the client does not have that integration then `None` is returned. + """ + if isinstance(name_or_class, str): + integration_name = name_or_class + elif name_or_class.identifier is not None: + integration_name = name_or_class.identifier + else: + raise ValueError("Integration has no name") + + return self.integrations.get(integration_name) + def close( self, timeout=None, # type: Optional[float] diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 2525dc56f1..032ccd09e7 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -3,7 +3,7 @@ from contextlib import contextmanager -from sentry_sdk._compat import datetime_utcnow, with_metaclass +from sentry_sdk._compat import with_metaclass from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.scope import Scope from sentry_sdk.client import Client @@ -15,7 +15,6 @@ BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, ) -from sentry_sdk.session import Session from sentry_sdk.tracing_utils import ( has_tracing_enabled, normalize_incoming_data, @@ -294,18 +293,9 @@ def get_integration( If the return value is not `None` the hub is guaranteed to have a client attached. """ - if isinstance(name_or_class, str): - integration_name = name_or_class - elif name_or_class.identifier is not None: - integration_name = name_or_class.identifier - else: - raise ValueError("Integration has no name") - client = self.client if client is not None: - rv = client.integrations.get(integration_name) - if rv is not None: - return rv + return client.get_integration(name_or_class) @property def client(self): @@ -430,31 +420,9 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): logger.info("Dropped breadcrumb because no client bound") return - crumb = dict(crumb or ()) # type: Breadcrumb - crumb.update(kwargs) - if not crumb: - return - - hint = dict(hint or ()) # type: Hint - - if crumb.get("timestamp") is None: - crumb["timestamp"] = datetime_utcnow() - if crumb.get("type") is None: - crumb["type"] = "default" - - if client.options["before_breadcrumb"] is not None: - new_crumb = client.options["before_breadcrumb"](crumb, hint) - else: - new_crumb = crumb - - if new_crumb is not None: - scope._breadcrumbs.append(new_crumb) - else: - logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) + kwargs["client"] = client - max_breadcrumbs = client.options["max_breadcrumbs"] # type: int - while len(scope._breadcrumbs) > max_breadcrumbs: - scope._breadcrumbs.popleft() + scope.add_breadcrumb(crumb, hint, **kwargs) def start_span(self, span=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs): # type: (Optional[Span], str, Any) -> Span @@ -712,12 +680,9 @@ def start_session( ): # type: (...) -> None """Starts a new session.""" - self.end_session() client, scope = self._stack[-1] - scope._session = Session( - release=client.options["release"] if client else None, - environment=client.options["environment"] if client else None, - user=scope._user, + scope.start_session( + client=client, session_mode=session_mode, ) @@ -725,13 +690,7 @@ def end_session(self): # type: (...) -> None """Ends the current session if there is one.""" client, scope = self._stack[-1] - session = scope._session - self.scope._session = None - - if session is not None: - session.close() - if client is not None: - client.capture_session(session) + scope.end_session(client=client) def stop_auto_session_tracking(self): # type: (...) -> None @@ -740,9 +699,8 @@ def stop_auto_session_tracking(self): This temporarily session tracking for the current scope when called. To resume session tracking call `resume_auto_session_tracking`. """ - self.end_session() client, scope = self._stack[-1] - scope._force_auto_session_tracking = False + scope.stop_auto_session_tracking(client=client) def resume_auto_session_tracking(self): # type: (...) -> None @@ -750,8 +708,8 @@ def resume_auto_session_tracking(self): disabled earlier. This requires that generally automatic session tracking is enabled. """ - client, scope = self._stack[-1] - scope._force_auto_session_tracking = None + scope = self._stack[-1][1] + scope.resume_auto_session_tracking() def flush( self, diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 5096eccce0..8e9724b4c5 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -5,7 +5,10 @@ import uuid from sentry_sdk.attachments import Attachment +from sentry_sdk._compat import datetime_utcnow +from sentry_sdk.consts import FALSE_VALUES from sentry_sdk._functools import wraps +from sentry_sdk.session import Session from sentry_sdk.tracing_utils import ( Baggage, extract_sentrytrace_data, @@ -20,9 +23,6 @@ from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import logger, capture_internal_exceptions -from sentry_sdk.consts import FALSE_VALUES - - if TYPE_CHECKING: from typing import Any from typing import Dict @@ -36,6 +36,7 @@ from sentry_sdk._types import ( Breadcrumb, + BreadcrumbHint, Event, EventProcessor, ErrorProcessor, @@ -46,7 +47,6 @@ from sentry_sdk.profiler import Profile from sentry_sdk.tracing import Span - from sentry_sdk.session import Session F = TypeVar("F", bound=Callable[..., Any]) T = TypeVar("T") @@ -517,6 +517,97 @@ def add_attachment( ) ) + def add_breadcrumb(self, crumb=None, hint=None, **kwargs): + # type: (Optional[Breadcrumb], Optional[BreadcrumbHint], Any) -> None + """ + Adds a breadcrumb. + + :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. + + :param hint: An optional value that can be used by `before_breadcrumb` + to customize the breadcrumbs that are emitted. + """ + client = kwargs.pop("client", None) + if client is None: + return + + before_breadcrumb = client.options.get("before_breadcrumb") + max_breadcrumbs = client.options.get("max_breadcrumbs") + + crumb = dict(crumb or ()) # type: Breadcrumb + crumb.update(kwargs) + if not crumb: + return + + hint = dict(hint or ()) # type: Hint + + if crumb.get("timestamp") is None: + crumb["timestamp"] = datetime_utcnow() + if crumb.get("type") is None: + crumb["type"] = "default" + + if before_breadcrumb is not None: + new_crumb = before_breadcrumb(crumb, hint) + else: + new_crumb = crumb + + if new_crumb is not None: + self._breadcrumbs.append(new_crumb) + else: + logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) + + while len(self._breadcrumbs) > max_breadcrumbs: + self._breadcrumbs.popleft() + + def start_session(self, *args, **kwargs): + # type: (*Any, **Any) -> None + """Starts a new session.""" + client = kwargs.pop("client", None) + session_mode = kwargs.pop("session_mode", "application") + + self.end_session(client=client) + + self._session = Session( + release=client.options["release"] if client else None, + environment=client.options["environment"] if client else None, + user=self._user, + session_mode=session_mode, + ) + + def end_session(self, *args, **kwargs): + # type: (*Any, **Any) -> None + """Ends the current session if there is one.""" + client = kwargs.pop("client", None) + + session = self._session + self._session = None + + if session is not None: + session.close() + if client is not None: + client.capture_session(session) + + def stop_auto_session_tracking(self, *args, **kwargs): + # type: (*Any, **Any) -> None + """Stops automatic session tracking. + + This temporarily session tracking for the current scope when called. + To resume session tracking call `resume_auto_session_tracking`. + """ + client = kwargs.pop("client", None) + + self.end_session(client=client) + + self._force_auto_session_tracking = False + + def resume_auto_session_tracking(self): + # type: (...) -> None + """Resumes automatic session tracking for the current scope if + disabled earlier. This requires that generally automatic session + tracking is enabled. + """ + self._force_auto_session_tracking = None + def add_event_processor( self, func # type: EventProcessor ): From eff8f7894b31c3a986344e05eb89b31c183b07c9 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 09:30:09 +0100 Subject: [PATCH 02/82] (5) Add `reset()` to thread local ContextVar and no-op `copy_context()` (#2570) Improves the capabilities of our threadlocal context variables, we use when no "real" context variables are available (for example in old Python versions) - Adds a no-op [copy_context](https://docs.python.org/3/library/contextvars.html#contextvars.copy_context) function to use in environments without context vars - Adds [reset](https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar.reset) functionality to our threadlocal based context vars. This is preparation work for refactoring how we deal with Hubs and Scopes in the future. --- sentry_sdk/utils.py | 50 +++++++++++++++++++++++++-------- tests/utils/test_contextvars.py | 2 +- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index d547e363b6..25399cd908 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -4,6 +4,7 @@ import logging import math import os +import random import re import subprocess import sys @@ -1248,24 +1249,49 @@ def _make_threadlocal_contextvars(local): class ContextVar(object): # Super-limited impl of ContextVar - def __init__(self, name): - # type: (str) -> None + def __init__(self, name, default=None): + # type: (str, Any) -> None self._name = name + self._default = default self._local = local() + self._original_local = local() - def get(self, default): + def get(self, default=None): # type: (Any) -> Any - return getattr(self._local, "value", default) + return getattr(self._local, "value", default or self._default) def set(self, value): - # type: (Any) -> None + # type: (Any) -> Any + token = str(random.getrandbits(64)) + original_value = self.get() + setattr(self._original_local, token, original_value) self._local.value = value + return token + + def reset(self, token): + # type: (Any) -> None + self._local.value = getattr(self._original_local, token) + del self._original_local[token] return ContextVar +def _make_noop_copy_context(): + # type: () -> Callable[[], Any] + class NoOpContext: + def run(self, func, *args, **kwargs): + # type: (Callable[..., Any], *Any, **Any) -> Any + return func(*args, **kwargs) + + def copy_context(): + # type: () -> NoOpContext + return NoOpContext() + + return copy_context + + def _get_contextvars(): - # type: () -> Tuple[bool, type] + # type: () -> Tuple[bool, type, Callable[[], Any]] """ Figure out the "right" contextvars installation to use. Returns a `contextvars.ContextVar`-like class with a limited API. @@ -1281,17 +1307,17 @@ def _get_contextvars(): # `aiocontextvars` is absolutely required for functional # contextvars on Python 3.6. try: - from aiocontextvars import ContextVar + from aiocontextvars import ContextVar, copy_context - return True, ContextVar + return True, ContextVar, copy_context except ImportError: pass else: # On Python 3.7 contextvars are functional. try: - from contextvars import ContextVar + from contextvars import ContextVar, copy_context - return True, ContextVar + return True, ContextVar, copy_context except ImportError: pass @@ -1299,10 +1325,10 @@ def _get_contextvars(): from threading import local - return False, _make_threadlocal_contextvars(local) + return False, _make_threadlocal_contextvars(local), _make_noop_copy_context() -HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars() +HAS_REAL_CONTEXTVARS, ContextVar, copy_context = _get_contextvars() CONTEXTVARS_ERROR_MESSAGE = """ diff --git a/tests/utils/test_contextvars.py b/tests/utils/test_contextvars.py index a6d296bb1f..faf33e8580 100644 --- a/tests/utils/test_contextvars.py +++ b/tests/utils/test_contextvars.py @@ -12,7 +12,7 @@ def test_leaks(maybe_monkeypatched_threading): from sentry_sdk import utils - _, ContextVar = utils._get_contextvars() # noqa: N806 + _, ContextVar, _ = utils._get_contextvars() # noqa: N806 ts = [] From 79e15f5a86404c28b8b7f63f4359cd6a559de024 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 12:20:33 +0100 Subject: [PATCH 03/82] (2) Move `capture_*` from Hub to Client (#2555) Moved some functionality from Hub to Client: - Capture Event: - moved `capture_event` from Hub to Client - created new `capture_event` in Scope that calls `capture_event` in Client - made `capture_event` in Hub call the new `capture_event` in Scope - Capture Exception: - created new `capture_exception` in Scope - made `capture_exception` in Hub call the new one in Scope - Capture Message: - created new `capture_message` in Scope - made `capture_message` in Hub call the new one in Scope - renamed `**scope_args` to `**scope_kwargs` because it contains keyword arguments. - moved `_update_scope` from Hub to Scope and renamed it to `_merge_scopes` This is preparation work for refactoring how we deal with Hubs and Scopes in the future. Its properly easier to reason about this change when checking out the branch than looking at the diff. --- docs/apidocs.rst | 3 + sentry_sdk/api.py | 12 ++-- sentry_sdk/client.py | 6 +- sentry_sdk/hub.py | 120 ++++++++++++++++++----------------- sentry_sdk/scope.py | 146 ++++++++++++++++++++++++++++++++++++++++++- tests/test_client.py | 33 +++++----- 6 files changed, 238 insertions(+), 82 deletions(-) diff --git a/docs/apidocs.rst b/docs/apidocs.rst index dc4117e559..855778484d 100644 --- a/docs/apidocs.rst +++ b/docs/apidocs.rst @@ -11,6 +11,9 @@ API Docs .. autoclass:: sentry_sdk.Client :members: +.. autoclass:: sentry_sdk.client._Client + :members: + .. autoclass:: sentry_sdk.Transport :members: diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index f0c6a87432..ffa525ca66 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -82,10 +82,10 @@ def capture_event( event, # type: Event hint=None, # type: Optional[Hint] scope=None, # type: Optional[Any] - **scope_args # type: Any + **scope_kwargs # type: Any ): # type: (...) -> Optional[str] - return Hub.current.capture_event(event, hint, scope=scope, **scope_args) + return Hub.current.capture_event(event, hint, scope=scope, **scope_kwargs) @hubmethod @@ -93,20 +93,20 @@ def capture_message( message, # type: str level=None, # type: Optional[str] scope=None, # type: Optional[Any] - **scope_args # type: Any + **scope_kwargs # type: Any ): # type: (...) -> Optional[str] - return Hub.current.capture_message(message, level, scope=scope, **scope_args) + return Hub.current.capture_message(message, level, scope=scope, **scope_kwargs) @hubmethod def capture_exception( error=None, # type: Optional[Union[BaseException, ExcInfo]] scope=None, # type: Optional[Any] - **scope_args # type: Any + **scope_kwargs # type: Any ): # type: (...) -> Optional[str] - return Hub.current.capture_exception(error, scope=scope, **scope_args) + return Hub.current.capture_exception(error, scope=scope, **scope_kwargs) @hubmethod diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 4097c4f0ed..70ffdbe2aa 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -156,6 +156,8 @@ class _Client(object): forwarding them to sentry through the configured transport. It takes the client options as keyword arguments and optionally the DSN as first argument. + + Alias of :py:class:`Client`. (Was created for better intelisense support) """ def __init__(self, *args, **kwargs): @@ -560,8 +562,8 @@ def capture_event( :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - :param scope: An optional scope to use for determining whether this event - should be captured. + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. """ diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 032ccd09e7..cf748bb8ea 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -21,8 +21,6 @@ ) from sentry_sdk.utils import ( - exc_info_from_error, - event_from_exception, logger, ContextVar, ) @@ -65,24 +63,6 @@ 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 @@ -322,76 +302,100 @@ def bind_client( top = self._stack[-1] self._stack[-1] = (new, top[1]) - def capture_event(self, event, hint=None, scope=None, **scope_args): + def capture_event(self, event, hint=None, scope=None, **scope_kwargs): # type: (Event, Optional[Hint], Optional[Scope], Any) -> Optional[str] """ Captures an event. - Alias of :py:meth:`sentry_sdk.Client.capture_event`. + Alias of :py:meth:`sentry_sdk.Scope.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. - :param scope_args: For supported `**scope_args` see - :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. """ client, top_scope = self._stack[-1] - scope = _update_scope(top_scope, scope, scope_args) - if client is not None: - is_transaction = event.get("type") == "transaction" - rv = client.capture_event(event, hint, scope) - if rv is not None and not is_transaction: - self._last_event_id = rv - return rv - return None + if client is None: + return None + + last_event_id = top_scope.capture_event( + event, hint, client=client, scope=scope, **scope_kwargs + ) - def capture_message(self, message, level=None, scope=None, **scope_args): + is_transaction = event.get("type") == "transaction" + if last_event_id is not None and not is_transaction: + self._last_event_id = last_event_id + + return last_event_id + + def capture_message(self, message, level=None, scope=None, **scope_kwargs): # type: (str, Optional[str], Optional[Scope], Any) -> Optional[str] """ Captures a message. - :param message: The string to send as the message. + Alias of :py:meth:`sentry_sdk.Scope.capture_message`. + + :param message: The string to send as the message to Sentry. :param level: If no level is provided, the default level is `info`. - :param scope: An optional :py:class:`sentry_sdk.Scope` to use. + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. - :param scope_args: For supported `**scope_args` see - :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`). """ - if self.client is None: + client, top_scope = self._stack[-1] + if client is None: return None - if level is None: - level = "info" - return self.capture_event( - {"message": message, "level": level}, scope=scope, **scope_args + + last_event_id = top_scope.capture_message( + message, level=level, client=client, scope=scope, **scope_kwargs ) - def capture_exception(self, error=None, scope=None, **scope_args): + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id + + def capture_exception(self, error=None, scope=None, **scope_kwargs): # type: (Optional[Union[BaseException, ExcInfo]], Optional[Scope], Any) -> Optional[str] """Captures an exception. - :param error: An exception to catch. If `None`, `sys.exc_info()` will be used. + Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. - :param scope_args: For supported `**scope_args` see - :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`). """ - client = self.client + client, top_scope = self._stack[-1] if client is None: return None - if error is not None: - exc_info = exc_info_from_error(error) - else: - exc_info = sys.exc_info() - event, hint = event_from_exception(exc_info, client_options=client.options) - try: - return self.capture_event(event, hint=hint, scope=scope, **scope_args) - except Exception: - self._capture_internal_exception(sys.exc_info()) + last_event_id = top_scope.capture_exception( + error, client=client, scope=scope, **scope_kwargs + ) - return None + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id def _capture_internal_exception( self, exc_info # type: Any @@ -401,6 +405,8 @@ def _capture_internal_exception( Capture an exception that is likely caused by a bug in the SDK itself. + Duplicated in :py:meth:`sentry_sdk.Client._capture_internal_exception`. + These exceptions do not end up in Sentry and are just logged instead. """ logger.error("Internal error in sentry_sdk", exc_info=exc_info) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 8e9724b4c5..c715847d38 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -2,6 +2,7 @@ from collections import deque from itertools import chain import os +import sys import uuid from sentry_sdk.attachments import Attachment @@ -21,7 +22,12 @@ Transaction, ) from sentry_sdk._types import TYPE_CHECKING -from sentry_sdk.utils import logger, capture_internal_exceptions +from sentry_sdk.utils import ( + event_from_exception, + exc_info_from_error, + logger, + capture_internal_exceptions, +) if TYPE_CHECKING: from typing import Any @@ -37,14 +43,16 @@ from sentry_sdk._types import ( Breadcrumb, BreadcrumbHint, + ErrorProcessor, Event, EventProcessor, - ErrorProcessor, ExcInfo, Hint, Type, + Union, ) + import sentry_sdk from sentry_sdk.profiler import Profile from sentry_sdk.tracing import Span @@ -81,6 +89,28 @@ def wrapper(self, *args, **kwargs): return wrapper # type: ignore +def _merge_scopes(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(base) + if callable(scope_change): + scope_change(final_scope) + else: + final_scope.update_from_scope(scope_change) + + elif scope_kwargs: + final_scope = copy(base) + final_scope.update_from_kwargs(**scope_kwargs) + + else: + final_scope = base + + return final_scope + + class Scope(object): """The scope holds extra information that should be sent with all events that belong to it. @@ -559,6 +589,118 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): while len(self._breadcrumbs) > max_breadcrumbs: self._breadcrumbs.popleft() + def capture_event(self, event, hint=None, client=None, scope=None, **scope_kwargs): + # type: (Event, Optional[Hint], Optional[sentry_sdk.Client], Optional[Scope], Any) -> Optional[str] + """ + Captures an event. + + Merges given scope data and calls :py:meth:`sentry_sdk.Client.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param client: The client to use for sending the event to Sentry. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`). + """ + if client is None: + return None + + scope = _merge_scopes(self, scope, scope_kwargs) + + return client.capture_event(event=event, hint=hint, scope=scope) + + def capture_message( + self, message, level=None, client=None, scope=None, **scope_kwargs + ): + # type: (str, Optional[str], Optional[sentry_sdk.Client], Optional[Scope], Any) -> Optional[str] + """ + Captures a message. + + :param message: The string to send as the message. + + :param level: If no level is provided, the default level is `info`. + + :param client: The client to use for sending the event to Sentry. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`). + """ + if client is None: + return None + + if level is None: + level = "info" + + event = { + "message": message, + "level": level, + } + + return self.capture_event(event, client=client, scope=scope, **scope_kwargs) + + def capture_exception(self, error=None, client=None, scope=None, **scope_kwargs): + # type: (Optional[Union[BaseException, ExcInfo]], Optional[sentry_sdk.Client], Optional[Scope], Any) -> Optional[str] + """Captures an exception. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param client: The client to use for sending the event to Sentry. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`). + """ + if client is None: + return None + + if error is not None: + exc_info = exc_info_from_error(error) + else: + exc_info = sys.exc_info() + + event, hint = event_from_exception(exc_info, client_options=client.options) + + try: + return self.capture_event( + event, hint=hint, client=client, scope=scope, **scope_kwargs + ) + except Exception: + self._capture_internal_exception(sys.exc_info()) + + return None + + def _capture_internal_exception( + self, exc_info # type: Any + ): + # type: (...) -> Any + """ + Capture an exception that is likely caused by a bug in the SDK + itself. + + These exceptions do not end up in Sentry and are just logged instead. + """ + logger.error("Internal error in sentry_sdk", exc_info=exc_info) + def start_session(self, *args, **kwargs): # type: (*Any, **Any) -> None """Starts a new session.""" diff --git a/tests/test_client.py b/tests/test_client.py index 5a7a5cff16..fa55c1111a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -20,7 +20,7 @@ ) from sentry_sdk.integrations.executing import ExecutingIntegration from sentry_sdk.transport import Transport -from sentry_sdk._compat import reraise, text_type, PY2 +from sentry_sdk._compat import text_type, PY2 from sentry_sdk.utils import HAS_CHAINED_EXCEPTIONS from sentry_sdk.utils import logger from sentry_sdk.serializer import MAX_DATABAG_BREADTH @@ -358,24 +358,27 @@ def test_simple_transport(sentry_init): def test_ignore_errors(sentry_init, capture_events): - class MyDivisionError(ZeroDivisionError): - pass + with mock.patch( + "sentry_sdk.scope.Scope._capture_internal_exception" + ) as mock_capture_internal_exception: - def raise_it(exc_info): - reraise(*exc_info) + class MyDivisionError(ZeroDivisionError): + pass - sentry_init(ignore_errors=[ZeroDivisionError], transport=_TestTransport()) - Hub.current._capture_internal_exception = raise_it + sentry_init(ignore_errors=[ZeroDivisionError], transport=_TestTransport()) - def e(exc): - try: - raise exc - except Exception: - capture_exception() + def e(exc): + try: + raise exc + except Exception: + capture_exception() + + e(ZeroDivisionError()) + e(MyDivisionError()) + e(ValueError()) - e(ZeroDivisionError()) - e(MyDivisionError()) - pytest.raises(EventCapturedError, lambda: e(ValueError())) + assert mock_capture_internal_exception.call_count == 1 + assert mock_capture_internal_exception.call_args[0][0][0] == EventCapturedError def test_with_locals_deprecation_enabled(sentry_init): From 5f332e3777b16ac2a00b961c86e471497b0b5c63 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 12:23:03 +0100 Subject: [PATCH 04/82] (3) Move tracing related functions from Hub to Scope (#2558) Moved some functionality from Hub to Client: - sorted some typing imports - moved `get_traceparent` from Hub to Scope - moved `get_baggage` from Hub to Scope - moved `iter_trace_propagation_headers` from Hub to Scope - moved `trace_propagation_meta` from Hub to Scope This is preparation work for refactoring how we deal with Hubs and Scopes in the future. --- sentry_sdk/hub.py | 169 +++++------------------------- sentry_sdk/scope.py | 250 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 267 insertions(+), 152 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index cf748bb8ea..45afb56cc9 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -7,17 +7,10 @@ from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.scope import Scope from sentry_sdk.client import Client -from sentry_sdk.profiler import Profile from sentry_sdk.tracing import ( NoOpSpan, Span, Transaction, - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import ( - has_tracing_enabled, - normalize_incoming_data, ) from sentry_sdk.utils import ( @@ -28,18 +21,18 @@ from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: - from typing import Union 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 ContextManager + from typing import Dict from typing import Generator + from typing import List + from typing import Optional + from typing import overload + from typing import Tuple from typing import Type from typing import TypeVar - from typing import overload - from typing import ContextManager + from typing import Union from sentry_sdk.integrations import Integration from sentry_sdk._types import ( @@ -447,54 +440,12 @@ def start_span(self, span=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs): For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. """ - configuration_instrumenter = self.client and self.client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - # THIS BLOCK IS DEPRECATED - # TODO: consider removing this in a future release. - # This is for backwards compatibility with releases before - # start_transaction existed, to allow for a smoother transition. - if isinstance(span, Transaction) or "transaction" in kwargs: - deprecation_msg = ( - "Deprecated: use start_transaction to start transactions and " - "Transaction.start_child to start spans." - ) - - if isinstance(span, Transaction): - logger.warning(deprecation_msg) - return self.start_transaction(span) - - if "transaction" in kwargs: - logger.warning(deprecation_msg) - name = kwargs.pop("transaction") - return self.start_transaction(name=name, **kwargs) - - # THIS BLOCK IS DEPRECATED - # We do not pass a span into start_span in our code base, so I deprecate this. - if span is not None: - deprecation_msg = "Deprecated: passing a span into `start_span` is deprecated and will be removed in the future." - logger.warning(deprecation_msg) - return span - - kwargs.setdefault("hub", self) - - active_span = self.scope.span - if active_span is not None: - new_child_span = active_span.start_child(**kwargs) - return new_child_span + client, scope = self._stack[-1] - # If there is already a trace_id in the propagation context, use it. - # This does not need to be done for `start_child` above because it takes - # the trace_id from the parent span. - if "trace_id" not in kwargs: - traceparent = self.get_traceparent() - trace_id = traceparent.split("-")[0] if traceparent else None - if trace_id is not None: - kwargs["trace_id"] = trace_id + kwargs["hub"] = self + kwargs["client"] = client - return Span(**kwargs) + return scope.start_span(span=span, instrumenter=instrumenter, **kwargs) def start_transaction( self, transaction=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs @@ -524,55 +475,25 @@ def start_transaction( For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. """ - configuration_instrumenter = self.client and self.client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - custom_sampling_context = kwargs.pop("custom_sampling_context", {}) - - # if we haven't been given a transaction, make one - if transaction is None: - kwargs.setdefault("hub", self) - transaction = Transaction(**kwargs) - - # use traces_sample_rate, traces_sampler, and/or inheritance to make a - # sampling decision - sampling_context = { - "transaction_context": transaction.to_json(), - "parent_sampled": transaction.parent_sampled, - } - sampling_context.update(custom_sampling_context) - transaction._set_initial_sampling_decision(sampling_context=sampling_context) - - profile = Profile(transaction, hub=self) - profile._set_initial_sampling_decision(sampling_context=sampling_context) + client, scope = self._stack[-1] - # we don't bother to keep spans if we already know we're not going to - # send the transaction - if transaction.sampled: - max_spans = ( - self.client and self.client.options["_experiments"].get("max_spans") - ) or 1000 - transaction.init_span_recorder(maxlen=max_spans) + kwargs["hub"] = self + kwargs["client"] = client - return transaction + return scope.start_transaction( + transaction=transaction, instrumenter=instrumenter, **kwargs + ) def continue_trace(self, environ_or_headers, op=None, name=None, source=None): # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str]) -> Transaction """ Sets the propagation context from environment or headers and returns a transaction. """ - with self.configure_scope() as scope: - scope.generate_propagation_context(environ_or_headers) + scope = self._stack[-1][1] - transaction = Transaction.continue_from_headers( - normalize_incoming_data(environ_or_headers), - op=op, - name=name, - source=source, + return scope.continue_trace( + environ_or_headers=environ_or_headers, op=op, name=name, source=source ) - return transaction @overload def push_scope( @@ -735,25 +656,16 @@ def get_traceparent(self): """ Returns the traceparent either from the active span or from the scope. """ - if self.client is not None: - if has_tracing_enabled(self.client.options) and self.scope.span is not None: - return self.scope.span.to_traceparent() - - return self.scope.get_traceparent() + client, scope = self._stack[-1] + return scope.get_traceparent(client=client) def get_baggage(self): # type: () -> Optional[str] """ Returns Baggage either from the active span or from the scope. """ - if ( - self.client is not None - and has_tracing_enabled(self.client.options) - and self.scope.span is not None - ): - baggage = self.scope.span.to_baggage() - else: - baggage = self.scope.get_baggage() + client, scope = self._stack[-1] + baggage = scope.get_baggage(client=client) if baggage is not None: return baggage.serialize() @@ -767,19 +679,9 @@ def iter_trace_propagation_headers(self, span=None): from the span representing the request, if available, or the current span on the scope if not. """ - client = self._stack[-1][0] - propagate_traces = client and client.options["propagate_traces"] - if not propagate_traces: - return - - span = span or self.scope.span + client, scope = self._stack[-1] - if client and has_tracing_enabled(client.options) and span is not None: - for header in span.iter_headers(): - yield header - else: - for header in self.scope.iter_headers(): - yield header + return scope.iter_trace_propagation_headers(span=span, client=client) def trace_propagation_meta(self, span=None): # type: (Optional[Span]) -> str @@ -792,23 +694,8 @@ def trace_propagation_meta(self, span=None): "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." ) - meta = "" - - sentry_trace = self.get_traceparent() - if sentry_trace is not None: - meta += '' % ( - SENTRY_TRACE_HEADER_NAME, - sentry_trace, - ) - - baggage = self.get_baggage() - if baggage is not None: - meta += '' % ( - BAGGAGE_HEADER_NAME, - baggage, - ) - - return meta + client, scope = self._stack[-1] + return scope.trace_propagation_meta(span=span, client=client) GLOBAL_HUB = Hub() diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c715847d38..9507306812 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -7,8 +7,9 @@ from sentry_sdk.attachments import Attachment from sentry_sdk._compat import datetime_utcnow -from sentry_sdk.consts import FALSE_VALUES +from sentry_sdk.consts import FALSE_VALUES, INSTRUMENTER from sentry_sdk._functools import wraps +from sentry_sdk.profiler import Profile from sentry_sdk.session import Session from sentry_sdk.tracing_utils import ( Baggage, @@ -19,6 +20,8 @@ from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, + NoOpSpan, + Span, Transaction, ) from sentry_sdk._types import TYPE_CHECKING @@ -31,14 +34,16 @@ if TYPE_CHECKING: from typing import Any + from typing import Callable + from typing import Deque from typing import Dict + from typing import Generator from typing import Iterator - from typing import Optional - from typing import Deque from typing import List - from typing import Callable + from typing import Optional from typing import Tuple from typing import TypeVar + from typing import Union from sentry_sdk._types import ( Breadcrumb, @@ -53,8 +58,6 @@ ) import sentry_sdk - from sentry_sdk.profiler import Profile - from sentry_sdk.tracing import Span F = TypeVar("F", bound=Callable[..., Any]) T = TypeVar("T") @@ -274,11 +277,22 @@ def get_dynamic_sampling_context(self): return self._propagation_context["dynamic_sampling_context"] - def get_traceparent(self): - # type: () -> Optional[str] + def get_traceparent(self, *args, **kwargs): + # type: (Any, Any) -> Optional[str] """ - Returns the Sentry "sentry-trace" header (aka the traceparent) from the Propagation Context. + Returns the Sentry "sentry-trace" header (aka the traceparent) from the + currently active span or the scopes Propagation Context. """ + client = kwargs.pop("client", None) + + # If we have an active span, return traceparent from there + if ( + client is not None + and has_tracing_enabled(client.options) + and self.span is not None + ): + return self.span.to_traceparent() + if self._propagation_context is None: return None @@ -288,8 +302,18 @@ def get_traceparent(self): ) return traceparent - def get_baggage(self): - # type: () -> Optional[Baggage] + def get_baggage(self, *args, **kwargs): + # type: (Any, Any) -> Optional[Baggage] + client = kwargs.pop("client", None) + + # If we have an active span, return baggage from there + if ( + client is not None + and has_tracing_enabled(client.options) + and self.span is not None + ): + return self.span.to_baggage() + if self._propagation_context is None: return None @@ -318,6 +342,38 @@ def get_trace_context(self): return trace_context + def trace_propagation_meta(self, *args, **kwargs): + # type: (*Any, **Any) -> str + """ + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + span = kwargs.pop("span", None) + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + client = kwargs.pop("client", None) + + meta = "" + + sentry_trace = self.get_traceparent(client=client) + if sentry_trace is not None: + meta += '' % ( + SENTRY_TRACE_HEADER_NAME, + sentry_trace, + ) + + baggage = self.get_baggage(client=client) + if baggage is not None: + meta += '' % ( + BAGGAGE_HEADER_NAME, + baggage.serialize(), + ) + + return meta + def iter_headers(self): # type: () -> Iterator[Tuple[str, str]] """ @@ -333,6 +389,29 @@ def iter_headers(self): baggage = Baggage(dsc).serialize() yield BAGGAGE_HEADER_NAME, baggage + def iter_trace_propagation_headers(self, *args, **kwargs): + # type: (Any, Any) -> Generator[Tuple[str, str], None, None] + """ + Return HTTP headers which allow propagation of trace data. Data taken + from the span representing the request, if available, or the current + span on the scope if not. + """ + span = kwargs.pop("span", None) + client = kwargs.pop("client", None) + + propagate_traces = client and client.options["propagate_traces"] + if not propagate_traces: + return + + span = span or self.span + + if client and has_tracing_enabled(client.options) and span is not None: + for header in span.iter_headers(): + yield header + else: + for header in self.iter_headers(): + yield header + def clear(self): # type: () -> None """Clears the entire scope.""" @@ -589,6 +668,155 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): while len(self._breadcrumbs) > max_breadcrumbs: self._breadcrumbs.popleft() + def start_transaction( + self, transaction=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs + ): + # type: (Optional[Transaction], str, Any) -> Union[Transaction, NoOpSpan] + """ + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. + """ + hub = kwargs.pop("hub", None) + client = kwargs.pop("client", None) + + configuration_instrumenter = client and client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + custom_sampling_context = kwargs.pop("custom_sampling_context", {}) + + # if we haven't been given a transaction, make one + if transaction is None: + kwargs.setdefault("hub", hub) + transaction = Transaction(**kwargs) + + # use traces_sample_rate, traces_sampler, and/or inheritance to make a + # sampling decision + sampling_context = { + "transaction_context": transaction.to_json(), + "parent_sampled": transaction.parent_sampled, + } + sampling_context.update(custom_sampling_context) + transaction._set_initial_sampling_decision(sampling_context=sampling_context) + + profile = Profile(transaction, hub=hub) + profile._set_initial_sampling_decision(sampling_context=sampling_context) + + # we don't bother to keep spans if we already know we're not going to + # send the transaction + if transaction.sampled: + max_spans = ( + client and client.options["_experiments"].get("max_spans") + ) or 1000 + transaction.init_span_recorder(maxlen=max_spans) + + return transaction + + def start_span(self, span=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs): + # type: (Optional[Span], str, Any) -> Span + """ + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + """ + client = kwargs.get("client", None) + + configuration_instrumenter = client and client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + # THIS BLOCK IS DEPRECATED + # TODO: consider removing this in a future release. + # This is for backwards compatibility with releases before + # start_transaction existed, to allow for a smoother transition. + if isinstance(span, Transaction) or "transaction" in kwargs: + deprecation_msg = ( + "Deprecated: use start_transaction to start transactions and " + "Transaction.start_child to start spans." + ) + + if isinstance(span, Transaction): + logger.warning(deprecation_msg) + return self.start_transaction(span, **kwargs) + + if "transaction" in kwargs: + logger.warning(deprecation_msg) + name = kwargs.pop("transaction") + return self.start_transaction(name=name, **kwargs) + + # THIS BLOCK IS DEPRECATED + # We do not pass a span into start_span in our code base, so I deprecate this. + if span is not None: + deprecation_msg = "Deprecated: passing a span into `start_span` is deprecated and will be removed in the future." + logger.warning(deprecation_msg) + return span + + kwargs.pop("client") + + active_span = self.span + if active_span is not None: + new_child_span = active_span.start_child(**kwargs) + return new_child_span + + # If there is already a trace_id in the propagation context, use it. + # This does not need to be done for `start_child` above because it takes + # the trace_id from the parent span. + if "trace_id" not in kwargs: + traceparent = self.get_traceparent() + trace_id = traceparent.split("-")[0] if traceparent else None + if trace_id is not None: + kwargs["trace_id"] = trace_id + + return Span(**kwargs) + + def continue_trace(self, environ_or_headers, op=None, name=None, source=None): + # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str]) -> Transaction + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + self.generate_propagation_context(environ_or_headers) + + transaction = Transaction.continue_from_headers( + normalize_incoming_data(environ_or_headers), + op=op, + name=name, + source=source, + ) + + return transaction + def capture_event(self, event, hint=None, client=None, scope=None, **scope_kwargs): # type: (Event, Optional[Hint], Optional[sentry_sdk.Client], Optional[Scope], Any) -> Optional[str] """ From eee394d2ef2eabfcc54cd8bc9e039f11c45aa7c3 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 13:06:40 +0100 Subject: [PATCH 05/82] Added new API --- docs/api.rst | 13 +++ docs/apidocs.rst | 3 + sentry_sdk/api.py | 57 +++++++++++ sentry_sdk/client.py | 115 +++++++++++++++++++++- sentry_sdk/scope.py | 221 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 403 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index f504bbb642..ed03c8a337 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -41,6 +41,19 @@ Distributed Tracing .. autofunction:: sentry_sdk.api.get_traceparent +New Scopes/Client APIs +====================== + +.. autofunction:: sentry_sdk.api.get_client +.. autofunction:: sentry_sdk.api.sentry_is_initialized +.. autofunction:: sentry_sdk.api.get_current_scope +.. autofunction:: sentry_sdk.api.get_isolation_scope +.. autofunction:: sentry_sdk.api.get_global_scope + +.. autofunction:: sentry_sdk.api.set_current_scope +.. autofunction:: sentry_sdk.api.set_isolation_scope + + Managing Scope (advanced) ========================= diff --git a/docs/apidocs.rst b/docs/apidocs.rst index 855778484d..52f87544d0 100644 --- a/docs/apidocs.rst +++ b/docs/apidocs.rst @@ -14,6 +14,9 @@ API Docs .. autoclass:: sentry_sdk.client._Client :members: +.. autoclass:: sentry_sdk.client.NoopClient + :members: + .. autoclass:: sentry_sdk.Transport :members: diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index ffa525ca66..181fae2cf7 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,5 +1,6 @@ import inspect +from sentry_sdk import scope from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.hub import Hub from sentry_sdk.scope import Scope @@ -15,6 +16,7 @@ from typing import ContextManager from typing import Union + from sentry_sdk.client import Client, NoopClient from sentry_sdk._types import ( Event, Hint, @@ -77,6 +79,61 @@ def scopemethod(f): return f +def sentry_is_initialized(): + # type: () -> bool + """ + Returns whether Sentry has been initialized or not. + If an client is available Sentry is initialized. + .. versionadded:: 1.XX.0 + """ + return Scope.get_client().is_active() + + +@scopemethod +def get_client(): + # type: () -> Union[Client, NoopClient] + return Scope.get_client() + + +@scopemethod +def get_current_scope(): + # type: () -> Scope + return Scope.get_current_scope() + + +@scopemethod +def get_isolation_scope(): + # type: () -> Scope + return Scope.get_isolation_scope() + + +@scopemethod +def get_global_scope(): + # type: () -> Scope + return Scope.get_global_scope() + + +def set_current_scope(new_current_scope): + # type: (Scope) -> None + """ + Sets the given scope as the new current scope overwritting the existing current scope. + :param new_current_scope: The scope to set as the new current scope. + .. versionadded:: 1.XX.0 + """ + + scope.sentry_current_scope.set(new_current_scope) + + +def set_isolation_scope(new_isolation_scope): + # type: (Scope) -> None + """ + Sets the given scope as the new isolation scope overwritting the existing isolation scope. + :param new_isolation_scope: The scope to set as the new isolation scope. + .. versionadded:: 1.XX.0 + """ + scope.sentry_isolation_scope.set(new_isolation_scope) + + @hubmethod def capture_event( event, # type: Event diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 70ffdbe2aa..761a1b5025 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -151,13 +151,116 @@ def _get_options(*args, **kwargs): module_not_found_error = ImportError # type: ignore -class _Client(object): +class NoopClient: + """ + A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. + .. versionadded:: 1.XX.0 + """ + + options = _get_options() # type: Dict[str, Any] + metrics_aggregator = None # type: Optional[Any] + monitor = None # type: Optional[Any] + transport = None # type: Optional[Any] + + def __repr__(self): + # type: () -> str + return "<{} id={}>".format(self.__class__.__name__, id(self)) + + # new! + def should_send_default_pii(self): + # type: () -> bool + return False + + # new! + def is_active(self): + # type: () -> bool + """ + Returns weither the client is active (able to send data to Sentry) + .. versionadded:: 1.XX.0 + """ + + return False + + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + return None + + def __getstate__(self, *args, **kwargs): + # type: (*Any, **Any) -> Any + return {"options": {}} + + def __setstate__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + pass + + def _setup_instrumentation(self, *args, **kwargs): + # type: (*Any, **Any) -> None + return None + + def _init_impl(self, *args, **kwargs): + # type: (*Any, **Any) -> None + return None + + @property + def dsn(self): + # type: () -> Optional[str] + return None + + def _prepare_event(self, *args, **kwargs): + # type: (*Any, **Any) -> Optional[Any] + return None + + def _is_ignored_error(self, *args, **kwargs): + # type: (*Any, **Any) -> bool + return True + + def _should_capture(self, *args, **kwargs): + # type: (*Any, **Any) -> bool + return False + + def _should_sample_error(self, *args, **kwargs): + # type: (*Any, **Any) -> bool + return False + + def _update_session_from_event(self, *args, **kwargs): + # type: (*Any, **Any) -> None + return None + + def capture_event(self, *args, **kwargs): + # type: (*Any, **Any) -> Optional[str] + return None + + def capture_session(self, *args, **kwargs): + # type: (*Any, **Any) -> None + return None + + def get_integration(self, *args, **kwargs): + # type: (*Any, **Any) -> Any + return None + + def close(self, *args, **kwargs): + # type: (*Any, **Any) -> None + return None + + def flush(self, *args, **kwargs): + # type: (*Any, **Any) -> None + return None + + def __enter__(self): + # type: () -> NoopClient + return self + + def __exit__(self, exc_type, exc_value, tb): + # type: (Any, Any, Any) -> None + return None + +class _Client(NoopClient): """The client is internally responsible for capturing the events and forwarding them to sentry through the configured transport. It takes the client options as keyword arguments and optionally the DSN as first argument. - Alias of :py:class:`Client`. (Was created for better intelisense support) + Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) """ def __init__(self, *args, **kwargs): @@ -297,6 +400,14 @@ def _capture_envelope(envelope): self._setup_instrumentation(self.options.get("functions_to_trace", [])) + def is_active(self): + # type: () -> bool + """ + Returns weither the client is active (able to send data to Sentry) + .. versionadded:: 1.XX.0 + """ + return True + @property def dsn(self): # type: () -> Optional[str] diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 9507306812..c3792a8f70 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1,5 +1,6 @@ -from copy import copy +from copy import copy, deepcopy from collections import deque +from contextlib import contextmanager from itertools import chain import os import sys @@ -26,10 +27,12 @@ ) from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import ( + capture_internal_exceptions, + copy_context, + ContextVar, event_from_exception, exc_info_from_error, logger, - capture_internal_exceptions, ) if TYPE_CHECKING: @@ -63,6 +66,10 @@ T = TypeVar("T") +SENTRY_GLOBAL_SCOPE = None # type: Optional[Scope] +sentry_isolation_scope = ContextVar("sentry_isolation_scope", default=None) +sentry_current_scope = ContextVar("sentry_current_scope", default=None) + global_event_processors = [] # type: List[EventProcessor] @@ -114,6 +121,38 @@ def _merge_scopes(base, scope_change, scope_kwargs): return final_scope +def _copy_on_write(property_name): + # type: (str) -> Callable[[Any], Any] + """ + Decorator that implements copy-on-write on a property of the Scope. + .. versionadded:: 1.XX.0 + """ + + def decorator(func): + # type: (Callable[[Any], Any]) -> Callable[[Any], Any] + @wraps(func) + def wrapper(*args, **kwargs): + # type: (*Any, **Any) -> Any + self = args[0] + + same_property_different_scope = self.is_forked and id( + getattr(self, property_name) + ) == id(getattr(self.original_scope, property_name)) + + if same_property_different_scope: + setattr( + self, + property_name, + deepcopy(getattr(self.original_scope, property_name)), + ) + + return func(*args, **kwargs) + + return wrapper + + return decorator + + class Scope(object): """The scope holds extra information that should be sent with all events that belong to it. @@ -147,21 +186,132 @@ class Scope(object): "_force_auto_session_tracking", "_profile", "_propagation_context", + "client", + "original_scope", + "_ty", ) - def __init__(self): - # type: () -> None + def __init__(self, ty=None, client=None): + # type: (Optional[str], Optional[sentry_sdk.Client]) -> None + self._ty = ty + self.original_scope = None # type: Optional[Scope] + self._event_processors = [] # type: List[EventProcessor] self._error_processors = [] # type: List[ErrorProcessor] self._name = None # type: Optional[str] self._propagation_context = None # type: Optional[Dict[str, Any]] + self.set_client(client) + self.clear() incoming_trace_information = self._load_trace_data_from_env() self.generate_propagation_context(incoming_data=incoming_trace_information) + @classmethod + def get_current_scope(cls): + # type: () -> Scope + """ + Returns the current scope. + .. versionadded:: 1.XX.0 + """ + scope = sentry_current_scope.get() + if scope is None: + scope = Scope(ty="current") + sentry_current_scope.set(scope) + + return scope + + @classmethod + def get_isolation_scope(cls): + # type: () -> Scope + """ + Returns the isolation scope. + .. versionadded:: 1.XX.0 + """ + scope = sentry_isolation_scope.get() + if scope is None: + scope = Scope(ty="isolation") + sentry_isolation_scope.set(scope) + + return scope + + @classmethod + def get_global_scope(cls): + # type: () -> Scope + """ + Returns the global scope. + .. versionadded:: 1.XX.0 + """ + global SENTRY_GLOBAL_SCOPE + if SENTRY_GLOBAL_SCOPE is None: + SENTRY_GLOBAL_SCOPE = Scope(ty="global") + + return SENTRY_GLOBAL_SCOPE + + @classmethod + def get_client(cls): + # type: () -> Union[sentry_sdk.Client, sentry_sdk.client.NoopClient] + """ + Returns the currently used :py:class:`sentry_sdk.Client`. + This checks the current scope, the isolation scope and the global scope for a client. + If no client is available a :py:class:`sentry_sdk.client.NoopClient` is returned. + .. versionadded:: 1.XX.0 + """ + client = Scope.get_current_scope().client + if client is not None: + return client + + client = Scope.get_isolation_scope().client + if client is not None: + return client + + client = Scope.get_global_scope().client + if client is not None: + return client + + return NoopClient() + + def set_client(self, client=None): + # type: (Optional[sentry_sdk.Client]) -> None + """ + Sets the client for this scope. + :param client: The client to use in this scope. + If `None` the client of the scope will be deleted. + .. versionadded:: 1.XX.0 + """ + self.client = client + + @property + def is_forked(self): + # type: () -> bool + """ + Weither this scope is a fork of another scope. + .. versionadded:: 1.XX.0 + """ + return self.original_scope is not None + + def fork(self): + # type: () -> Scope + """ + Returns a fork of this scope. + .. versionadded:: 1.XX.0 + """ + self.original_scope = self + return copy(self) + + def isolate(self): + # type: () -> None + """ + Creates a new isolation scope for this scope. + The new isolation scope will be a fork of the current isolation scope. + .. versionadded:: 1.XX.0 + """ + isolation_scope = Scope.get_isolation_scope() + forked_isolation_scope = isolation_scope.fork() + sentry_isolation_scope.set(forked_isolation_scope) + def _load_trace_data_from_env(self): # type: () -> Optional[Dict[str, str]] """ @@ -1251,3 +1401,66 @@ def __repr__(self): hex(id(self)), self._name, ) + + +def _with_new_scope(): + # type: () -> Generator[Scope, None, None] + + current_scope = Scope.get_current_scope() + forked_scope = current_scope.fork() + token = sentry_current_scope.set(forked_scope) + + try: + yield forked_scope + + finally: + # restore original scope + sentry_current_scope.reset(token) + + +@contextmanager +def new_scope(): + # type: () -> Generator[Scope, None, None] + """ + Context manager that forks the current scope and runs the wrapped code in it. + .. versionadded:: 1.XX.0 + """ + ctx = copy_context() # This does not exist in Python 2.7 + return ctx.run(_with_new_scope) + + +def _with_isolated_scope(): + # type: () -> Generator[Scope, None, None] + + # fork current scope + current_scope = Scope.get_current_scope() + forked_current_scope = current_scope.fork() + current_token = sentry_current_scope.set(forked_current_scope) + + # fork isolation scope + isolation_scope = Scope.get_isolation_scope() + forked_isolation_scope = isolation_scope.fork() + isolation_token = sentry_isolation_scope.set(forked_isolation_scope) + + try: + yield forked_isolation_scope + + finally: + # restore original scopes + sentry_current_scope.reset(current_token) + sentry_isolation_scope.reset(isolation_token) + + +@contextmanager +def isolated_scope(): + # type: () -> Generator[Scope, None, None] + """ + Context manager that forks the current isolation scope (and the related current scope) and runs the wrapped code in it. + .. versionadded:: 1.XX.0 + """ + ctx = copy_context() + return ctx.run(_with_isolated_scope) + + +# Circular imports +from sentry_sdk.client import NoopClient \ No newline at end of file From 5816b54a4c9b3ce905b9e73a62b071371181713d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 13:14:05 +0100 Subject: [PATCH 06/82] formatting --- sentry_sdk/client.py | 1 + sentry_sdk/scope.py | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 761a1b5025..048453dc08 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -254,6 +254,7 @@ def __exit__(self, exc_type, exc_value, tb): # type: (Any, Any, Any) -> None return None + class _Client(NoopClient): """The client is internally responsible for capturing the events and forwarding them to sentry through the configured transport. It takes diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c3792a8f70..0ad1fda514 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -29,7 +29,7 @@ from sentry_sdk.utils import ( capture_internal_exceptions, copy_context, - ContextVar, + ContextVar, event_from_exception, exc_info_from_error, logger, @@ -188,7 +188,7 @@ class Scope(object): "_propagation_context", "client", "original_scope", - "_ty", + "_ty", ) def __init__(self, ty=None, client=None): @@ -203,7 +203,7 @@ def __init__(self, ty=None, client=None): self._propagation_context = None # type: Optional[Dict[str, Any]] self.set_client(client) - + self.clear() incoming_trace_information = self._load_trace_data_from_env() @@ -279,6 +279,7 @@ def set_client(self, client=None): Sets the client for this scope. :param client: The client to use in this scope. If `None` the client of the scope will be deleted. + .. versionadded:: 1.XX.0 """ self.client = client @@ -306,6 +307,7 @@ def isolate(self): """ Creates a new isolation scope for this scope. The new isolation scope will be a fork of the current isolation scope. + .. versionadded:: 1.XX.0 """ isolation_scope = Scope.get_isolation_scope() @@ -1463,4 +1465,4 @@ def isolated_scope(): # Circular imports -from sentry_sdk.client import NoopClient \ No newline at end of file +from sentry_sdk.client import NoopClient From 7a440fe2f8fd710a347642cf62a715cd98198793 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 13:16:16 +0100 Subject: [PATCH 07/82] formatting --- sentry_sdk/scope.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 0ad1fda514..a5b79727e3 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -125,6 +125,7 @@ def _copy_on_write(property_name): # type: (str) -> Callable[[Any], Any] """ Decorator that implements copy-on-write on a property of the Scope. + .. versionadded:: 1.XX.0 """ @@ -214,6 +215,7 @@ def get_current_scope(cls): # type: () -> Scope """ Returns the current scope. + .. versionadded:: 1.XX.0 """ scope = sentry_current_scope.get() @@ -228,6 +230,7 @@ def get_isolation_scope(cls): # type: () -> Scope """ Returns the isolation scope. + .. versionadded:: 1.XX.0 """ scope = sentry_isolation_scope.get() @@ -242,6 +245,7 @@ def get_global_scope(cls): # type: () -> Scope """ Returns the global scope. + .. versionadded:: 1.XX.0 """ global SENTRY_GLOBAL_SCOPE @@ -257,6 +261,7 @@ def get_client(cls): Returns the currently used :py:class:`sentry_sdk.Client`. This checks the current scope, the isolation scope and the global scope for a client. If no client is available a :py:class:`sentry_sdk.client.NoopClient` is returned. + .. versionadded:: 1.XX.0 """ client = Scope.get_current_scope().client @@ -289,6 +294,7 @@ def is_forked(self): # type: () -> bool """ Weither this scope is a fork of another scope. + .. versionadded:: 1.XX.0 """ return self.original_scope is not None @@ -297,6 +303,7 @@ def fork(self): # type: () -> Scope """ Returns a fork of this scope. + .. versionadded:: 1.XX.0 """ self.original_scope = self @@ -1425,6 +1432,7 @@ def new_scope(): # type: () -> Generator[Scope, None, None] """ Context manager that forks the current scope and runs the wrapped code in it. + .. versionadded:: 1.XX.0 """ ctx = copy_context() # This does not exist in Python 2.7 @@ -1457,7 +1465,9 @@ def _with_isolated_scope(): def isolated_scope(): # type: () -> Generator[Scope, None, None] """ - Context manager that forks the current isolation scope (and the related current scope) and runs the wrapped code in it. + Context manager that forks the current isolation scope + (and the related current scope) and runs the wrapped code in it. + .. versionadded:: 1.XX.0 """ ctx = copy_context() From 0a40f62aa63f3dd06fc45db674228151d592dbb1 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 13:16:48 +0100 Subject: [PATCH 08/82] formatting --- sentry_sdk/scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index a5b79727e3..daff2a1229 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1465,7 +1465,7 @@ def _with_isolated_scope(): def isolated_scope(): # type: () -> Generator[Scope, None, None] """ - Context manager that forks the current isolation scope + Context manager that forks the current isolation scope (and the related current scope) and runs the wrapped code in it. .. versionadded:: 1.XX.0 From ea8f13966a408e74d909face92f959550119880c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 13:29:08 +0100 Subject: [PATCH 09/82] Removed duplicated import --- sentry_sdk/scope.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index daff2a1229..329095f782 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -57,7 +57,6 @@ ExcInfo, Hint, Type, - Union, ) import sentry_sdk From d9c88e7a2429ff810ae3ee048afb4191cf8c3d04 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 13:40:27 +0100 Subject: [PATCH 10/82] formatting --- sentry_sdk/api.py | 4 +++- sentry_sdk/client.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 181fae2cf7..e5c80d0858 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -84,6 +84,7 @@ def sentry_is_initialized(): """ Returns whether Sentry has been initialized or not. If an client is available Sentry is initialized. + .. versionadded:: 1.XX.0 """ return Scope.get_client().is_active() @@ -118,9 +119,9 @@ def set_current_scope(new_current_scope): """ Sets the given scope as the new current scope overwritting the existing current scope. :param new_current_scope: The scope to set as the new current scope. + .. versionadded:: 1.XX.0 """ - scope.sentry_current_scope.set(new_current_scope) @@ -129,6 +130,7 @@ def set_isolation_scope(new_isolation_scope): """ Sets the given scope as the new isolation scope overwritting the existing isolation scope. :param new_isolation_scope: The scope to set as the new isolation scope. + .. versionadded:: 1.XX.0 """ scope.sentry_isolation_scope.set(new_isolation_scope) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 048453dc08..ef52a8a34c 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -154,6 +154,7 @@ def _get_options(*args, **kwargs): class NoopClient: """ A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. + .. versionadded:: 1.XX.0 """ @@ -176,6 +177,7 @@ def is_active(self): # type: () -> bool """ Returns weither the client is active (able to send data to Sentry) + .. versionadded:: 1.XX.0 """ @@ -405,6 +407,7 @@ def is_active(self): # type: () -> bool """ Returns weither the client is active (able to send data to Sentry) + .. versionadded:: 1.XX.0 """ return True From 67eea1ef5c980fb98fd8b3d7e7abc01fa8499caa Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 13:41:57 +0100 Subject: [PATCH 11/82] formatting --- sentry_sdk/client.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index ef52a8a34c..d65199ed88 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -157,7 +157,6 @@ class NoopClient: .. versionadded:: 1.XX.0 """ - options = _get_options() # type: Dict[str, Any] metrics_aggregator = None # type: Optional[Any] monitor = None # type: Optional[Any] @@ -167,12 +166,10 @@ def __repr__(self): # type: () -> str return "<{} id={}>".format(self.__class__.__name__, id(self)) - # new! def should_send_default_pii(self): # type: () -> bool return False - # new! def is_active(self): # type: () -> bool """ @@ -180,7 +177,6 @@ def is_active(self): .. versionadded:: 1.XX.0 """ - return False def __init__(self, *args, **kwargs): @@ -407,7 +403,7 @@ def is_active(self): # type: () -> bool """ Returns weither the client is active (able to send data to Sentry) - + .. versionadded:: 1.XX.0 """ return True From 76d352e1ca2a14df26ec492580a947ad3c28c2d8 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 13:42:54 +0100 Subject: [PATCH 12/82] formatting --- sentry_sdk/client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index d65199ed88..f5afe67fd0 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -170,15 +170,6 @@ def should_send_default_pii(self): # type: () -> bool return False - def is_active(self): - # type: () -> bool - """ - Returns weither the client is active (able to send data to Sentry) - - .. versionadded:: 1.XX.0 - """ - return False - def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None return None @@ -199,6 +190,15 @@ def _init_impl(self, *args, **kwargs): # type: (*Any, **Any) -> None return None + def is_active(self): + # type: () -> bool + """ + Returns weither the client is active (able to send data to Sentry) + + .. versionadded:: 1.XX.0 + """ + return False + @property def dsn(self): # type: () -> Optional[str] From 0feab1a68ac2e25c32b69bfe336bd32b4dad6b70 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 19 Dec 2023 13:46:22 +0100 Subject: [PATCH 13/82] formatting --- sentry_sdk/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index f5afe67fd0..3904554eb1 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -157,6 +157,7 @@ class NoopClient: .. versionadded:: 1.XX.0 """ + options = _get_options() # type: Dict[str, Any] metrics_aggregator = None # type: Optional[Any] monitor = None # type: Optional[Any] From cce06c7e55b9236ec2b9fa7a91695b7f2c28bec4 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 9 Jan 2024 16:34:54 +0100 Subject: [PATCH 14/82] trigger ci From 1f06fbb3e9199261c3b0bf8ae3371c935e2a6912 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 9 Jan 2024 16:35:13 +0100 Subject: [PATCH 15/82] trigger ci From 9d2a7b1ef769635b89c25194baffd68cb84ca608 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 10 Jan 2024 09:04:30 +0100 Subject: [PATCH 16/82] Fixed import --- sentry_sdk/scope.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 9507306812..7678def407 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -54,7 +54,6 @@ ExcInfo, Hint, Type, - Union, ) import sentry_sdk From 1b17770d4beac598570a94e908358cd94f9b0e41 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 10 Jan 2024 09:06:51 +0100 Subject: [PATCH 17/82] Renamed api --- docs/api.rst | 2 +- sentry_sdk/api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ed03c8a337..85248341c2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -45,7 +45,7 @@ New Scopes/Client APIs ====================== .. autofunction:: sentry_sdk.api.get_client -.. autofunction:: sentry_sdk.api.sentry_is_initialized +.. autofunction:: sentry_sdk.api.is_initialized .. autofunction:: sentry_sdk.api.get_current_scope .. autofunction:: sentry_sdk.api.get_isolation_scope .. autofunction:: sentry_sdk.api.get_global_scope diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index e5c80d0858..b6a2db72db 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -79,7 +79,7 @@ def scopemethod(f): return f -def sentry_is_initialized(): +def is_initialized(): # type: () -> bool """ Returns whether Sentry has been initialized or not. From e034aa9b47b87adb00bfa4280b8ecb060d0b8921 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 10 Jan 2024 09:10:00 +0100 Subject: [PATCH 18/82] Renamed scope vars --- sentry_sdk/api.py | 4 ++-- sentry_sdk/scope.py | 36 ++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index b6a2db72db..19f12d5c0f 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -122,7 +122,7 @@ def set_current_scope(new_current_scope): .. versionadded:: 1.XX.0 """ - scope.sentry_current_scope.set(new_current_scope) + scope.current_scope.set(new_current_scope) def set_isolation_scope(new_isolation_scope): @@ -133,7 +133,7 @@ def set_isolation_scope(new_isolation_scope): .. versionadded:: 1.XX.0 """ - scope.sentry_isolation_scope.set(new_isolation_scope) + scope.isolation_scope.set(new_isolation_scope) @hubmethod diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index daff2a1229..35a86e4396 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -66,9 +66,9 @@ T = TypeVar("T") -SENTRY_GLOBAL_SCOPE = None # type: Optional[Scope] -sentry_isolation_scope = ContextVar("sentry_isolation_scope", default=None) -sentry_current_scope = ContextVar("sentry_current_scope", default=None) +GLOBAL_SCOPE = None # type: Optional[Scope] +isolation_scope = ContextVar("isolation_scope", default=None) +current_scope = ContextVar("current_scope", default=None) global_event_processors = [] # type: List[EventProcessor] @@ -218,10 +218,10 @@ def get_current_scope(cls): .. versionadded:: 1.XX.0 """ - scope = sentry_current_scope.get() + scope = current_scope.get() if scope is None: scope = Scope(ty="current") - sentry_current_scope.set(scope) + current_scope.set(scope) return scope @@ -233,10 +233,10 @@ def get_isolation_scope(cls): .. versionadded:: 1.XX.0 """ - scope = sentry_isolation_scope.get() + scope = isolation_scope.get() if scope is None: scope = Scope(ty="isolation") - sentry_isolation_scope.set(scope) + isolation_scope.set(scope) return scope @@ -248,11 +248,11 @@ def get_global_scope(cls): .. versionadded:: 1.XX.0 """ - global SENTRY_GLOBAL_SCOPE - if SENTRY_GLOBAL_SCOPE is None: - SENTRY_GLOBAL_SCOPE = Scope(ty="global") + global GLOBAL_SCOPE + if GLOBAL_SCOPE is None: + GLOBAL_SCOPE = Scope(ty="global") - return SENTRY_GLOBAL_SCOPE + return GLOBAL_SCOPE @classmethod def get_client(cls): @@ -319,7 +319,7 @@ def isolate(self): """ isolation_scope = Scope.get_isolation_scope() forked_isolation_scope = isolation_scope.fork() - sentry_isolation_scope.set(forked_isolation_scope) + isolation_scope.set(forked_isolation_scope) def _load_trace_data_from_env(self): # type: () -> Optional[Dict[str, str]] @@ -1417,14 +1417,14 @@ def _with_new_scope(): current_scope = Scope.get_current_scope() forked_scope = current_scope.fork() - token = sentry_current_scope.set(forked_scope) + token = current_scope.set(forked_scope) try: yield forked_scope finally: # restore original scope - sentry_current_scope.reset(token) + current_scope.reset(token) @contextmanager @@ -1445,20 +1445,20 @@ def _with_isolated_scope(): # fork current scope current_scope = Scope.get_current_scope() forked_current_scope = current_scope.fork() - current_token = sentry_current_scope.set(forked_current_scope) + current_token = current_scope.set(forked_current_scope) # fork isolation scope isolation_scope = Scope.get_isolation_scope() forked_isolation_scope = isolation_scope.fork() - isolation_token = sentry_isolation_scope.set(forked_isolation_scope) + isolation_token = isolation_scope.set(forked_isolation_scope) try: yield forked_isolation_scope finally: # restore original scopes - sentry_current_scope.reset(current_token) - sentry_isolation_scope.reset(isolation_token) + current_scope.reset(current_token) + isolation_scope.reset(isolation_token) @contextmanager From ba74c1ec201b43c7aface090fb154e438cbddce0 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 10 Jan 2024 09:39:26 +0100 Subject: [PATCH 19/82] Made scope.client never None --- sentry_sdk/scope.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 35a86e4396..c6c3b11f33 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -203,6 +203,10 @@ def __init__(self, ty=None, client=None): self._name = None # type: Optional[str] self._propagation_context = None # type: Optional[Dict[str, Any]] + self.client = ( + NoopClient() + ) # type: Union[sentry_sdk.Client, sentry_sdk.client.NoopClient] + self.set_client(client) self.clear() @@ -283,11 +287,11 @@ def set_client(self, client=None): """ Sets the client for this scope. :param client: The client to use in this scope. - If `None` the client of the scope will be deleted. + If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NoopClient`. .. versionadded:: 1.XX.0 """ - self.client = client + self.client = client or NoopClient() @property def is_forked(self): From 6a6bcf7b295878cc167eca14a313f1a09a845e32 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 10 Jan 2024 09:42:44 +0100 Subject: [PATCH 20/82] now for real --- sentry_sdk/scope.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c6c3b11f33..b5273e2a86 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -207,7 +207,8 @@ def __init__(self, ty=None, client=None): NoopClient() ) # type: Union[sentry_sdk.Client, sentry_sdk.client.NoopClient] - self.set_client(client) + if client is not None: + self.set_client(client) self.clear() From 94ef082d4f3b1bc2dfc0bec03f8abd0ce44e339f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 10 Jan 2024 10:08:32 +0100 Subject: [PATCH 21/82] Better naming for scope context and global vars --- sentry_sdk/api.py | 4 ++-- sentry_sdk/scope.py | 48 ++++++++++++++++++++++----------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 19f12d5c0f..c8f6d39157 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -122,7 +122,7 @@ def set_current_scope(new_current_scope): .. versionadded:: 1.XX.0 """ - scope.current_scope.set(new_current_scope) + scope._current_scope.set(new_current_scope) def set_isolation_scope(new_isolation_scope): @@ -133,7 +133,7 @@ def set_isolation_scope(new_isolation_scope): .. versionadded:: 1.XX.0 """ - scope.isolation_scope.set(new_isolation_scope) + scope._isolation_scope.set(new_isolation_scope) @hubmethod diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index b5273e2a86..73e78cf23c 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -66,9 +66,9 @@ T = TypeVar("T") -GLOBAL_SCOPE = None # type: Optional[Scope] -isolation_scope = ContextVar("isolation_scope", default=None) -current_scope = ContextVar("current_scope", default=None) +_global_scope = None # type: Optional[Scope] +_isolation_scope = ContextVar("isolation_scope", default=None) +_current_scope = ContextVar("current_scope", default=None) global_event_processors = [] # type: List[EventProcessor] @@ -223,12 +223,12 @@ def get_current_scope(cls): .. versionadded:: 1.XX.0 """ - scope = current_scope.get() - if scope is None: - scope = Scope(ty="current") - current_scope.set(scope) + current_scope = _current_scope.get() + if current_scope is None: + current_scope = Scope(ty="current") + _current_scope.set(current_scope) - return scope + return current_scope @classmethod def get_isolation_scope(cls): @@ -238,12 +238,12 @@ def get_isolation_scope(cls): .. versionadded:: 1.XX.0 """ - scope = isolation_scope.get() - if scope is None: - scope = Scope(ty="isolation") - isolation_scope.set(scope) + isolation_scope = _isolation_scope.get() + if isolation_scope is None: + isolation_scope = Scope(ty="isolation") + _isolation_scope.set(isolation_scope) - return scope + return isolation_scope @classmethod def get_global_scope(cls): @@ -253,11 +253,11 @@ def get_global_scope(cls): .. versionadded:: 1.XX.0 """ - global GLOBAL_SCOPE - if GLOBAL_SCOPE is None: - GLOBAL_SCOPE = Scope(ty="global") + global _global_scope + if _global_scope is None: + _global_scope = Scope(ty="global") - return GLOBAL_SCOPE + return _global_scope @classmethod def get_client(cls): @@ -324,7 +324,7 @@ def isolate(self): """ isolation_scope = Scope.get_isolation_scope() forked_isolation_scope = isolation_scope.fork() - isolation_scope.set(forked_isolation_scope) + _isolation_scope.set(forked_isolation_scope) def _load_trace_data_from_env(self): # type: () -> Optional[Dict[str, str]] @@ -1422,14 +1422,14 @@ def _with_new_scope(): current_scope = Scope.get_current_scope() forked_scope = current_scope.fork() - token = current_scope.set(forked_scope) + token = _current_scope.set(forked_scope) try: yield forked_scope finally: # restore original scope - current_scope.reset(token) + _current_scope.reset(token) @contextmanager @@ -1450,20 +1450,20 @@ def _with_isolated_scope(): # fork current scope current_scope = Scope.get_current_scope() forked_current_scope = current_scope.fork() - current_token = current_scope.set(forked_current_scope) + current_token = _current_scope.set(forked_current_scope) # fork isolation scope isolation_scope = Scope.get_isolation_scope() forked_isolation_scope = isolation_scope.fork() - isolation_token = isolation_scope.set(forked_isolation_scope) + isolation_token = _isolation_scope.set(forked_isolation_scope) try: yield forked_isolation_scope finally: # restore original scopes - current_scope.reset(current_token) - isolation_scope.reset(isolation_token) + _current_scope.reset(current_token) + _isolation_scope.reset(isolation_token) @contextmanager From d2fb4b5e5af3ea42787f041662f71bfe30afb574 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 22 Jan 2024 15:48:08 +0100 Subject: [PATCH 22/82] Removed private methods from NoopClient because they should never be called from outside so they are not needed --- sentry_sdk/client.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index f513615526..0b792e76e5 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -183,14 +183,6 @@ def __setstate__(self, *args, **kwargs): # type: (*Any, **Any) -> None pass - def _setup_instrumentation(self, *args, **kwargs): - # type: (*Any, **Any) -> None - return None - - def _init_impl(self, *args, **kwargs): - # type: (*Any, **Any) -> None - return None - def is_active(self): # type: () -> bool """ @@ -205,26 +197,6 @@ def dsn(self): # type: () -> Optional[str] return None - def _prepare_event(self, *args, **kwargs): - # type: (*Any, **Any) -> Optional[Any] - return None - - def _is_ignored_error(self, *args, **kwargs): - # type: (*Any, **Any) -> bool - return True - - def _should_capture(self, *args, **kwargs): - # type: (*Any, **Any) -> bool - return False - - def _should_sample_error(self, *args, **kwargs): - # type: (*Any, **Any) -> bool - return False - - def _update_session_from_event(self, *args, **kwargs): - # type: (*Any, **Any) -> None - return None - def capture_event(self, *args, **kwargs): # type: (*Any, **Any) -> Optional[str] return None From 62889d55f04989f93a79985ade322edfa66264dd Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 08:33:54 +0100 Subject: [PATCH 23/82] Cleanup and mechanism to prevent recursion --- sentry_sdk/scope.py | 100 +++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 42 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 73e78cf23c..f65d12e8fe 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -99,28 +99,6 @@ def wrapper(self, *args, **kwargs): return wrapper # type: ignore -def _merge_scopes(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(base) - if callable(scope_change): - scope_change(final_scope) - else: - final_scope.update_from_scope(scope_change) - - elif scope_kwargs: - final_scope = copy(base) - final_scope.update_from_kwargs(**scope_kwargs) - - else: - final_scope = base - - return final_scope - - def _copy_on_write(property_name): # type: (str) -> Callable[[Any], Any] """ @@ -189,12 +167,12 @@ class Scope(object): "_propagation_context", "client", "original_scope", - "_ty", + "_type", ) def __init__(self, ty=None, client=None): # type: (Optional[str], Optional[sentry_sdk.Client]) -> None - self._ty = ty + self._type = ty self.original_scope = None # type: Optional[Scope] self._event_processors = [] # type: List[EventProcessor] @@ -216,49 +194,87 @@ def __init__(self, ty=None, client=None): self.generate_propagation_context(incoming_data=incoming_trace_information) @classmethod - def get_current_scope(cls): - # type: () -> Scope + def get_current_scope(cls, should_create_scope=True): + # type: (bool) -> Scope """ Returns the current scope. + :parm should_create_scope: If `True` a new scope will be created if no scope is available. + .. versionadded:: 1.XX.0 """ current_scope = _current_scope.get() - if current_scope is None: + if current_scope is None and should_create_scope: current_scope = Scope(ty="current") _current_scope.set(current_scope) return current_scope @classmethod - def get_isolation_scope(cls): - # type: () -> Scope + def get_isolation_scope(cls, should_create_scope=True): + # type: (bool) -> Scope """ Returns the isolation scope. + :parm should_create_scope: If `True` a new scope will be created if no scope is available. + .. versionadded:: 1.XX.0 """ isolation_scope = _isolation_scope.get() - if isolation_scope is None: + if isolation_scope is None and should_create_scope: isolation_scope = Scope(ty="isolation") _isolation_scope.set(isolation_scope) return isolation_scope @classmethod - def get_global_scope(cls): - # type: () -> Scope + def get_global_scope(cls, should_create_scope=True): + # type: (bool) -> Scope """ Returns the global scope. + :parm should_create_scope: If `True` a new scope will be created if no scope is available. + .. versionadded:: 1.XX.0 """ global _global_scope - if _global_scope is None: + if _global_scope is None and should_create_scope: _global_scope = Scope(ty="global") return _global_scope + @classmethod + def _merge_scopes(cls, additional_scope=None, additional_scope_kwargs=None): + # type: (Optional[Scope], Optional[Dict[str, Any]]) -> Scope + """ + Merges global, isolation and current scope into a new scope and + adds the given additional scope or additional scope kwargs to it. + """ + if additional_scope and additional_scope_kwargs: + raise TypeError("cannot provide scope and kwargs") + + global_scope = Scope.get_global_scope(should_create_scope=False) + final_scope = copy(global_scope) or Scope() + + isolation_scope = Scope.get_isolation_scope(should_create_scope=False) + if isolation_scope is not None: + final_scope.update_from_scope(isolation_scope) + + current_scope = Scope.get_current_scope(should_create_scope=False) + if current_scope is not None: + final_scope.update_from_scope(current_scope) + + if additional_scope is not None: + if callable(additional_scope): + additional_scope(final_scope) + else: + final_scope.update_from_scope(additional_scope) + + elif additional_scope_kwargs: + final_scope.update_from_kwargs(**additional_scope_kwargs) + + return final_scope + @classmethod def get_client(cls): # type: () -> Union[sentry_sdk.Client, sentry_sdk.client.NoopClient] @@ -269,17 +285,17 @@ def get_client(cls): .. versionadded:: 1.XX.0 """ - client = Scope.get_current_scope().client - if client is not None: - return client + scope = Scope.get_current_scope(should_create_scope=False) + if scope and scope.client.is_active(): + return scope.client - client = Scope.get_isolation_scope().client - if client is not None: - return client + scope = Scope.get_isolation_scope(should_create_scope=False) + if scope and scope.client.is_active(): + return scope.client - client = Scope.get_global_scope().client - if client is not None: - return client + scope = Scope.get_global_scope(should_create_scope=False) + if scope: + return scope.client return NoopClient() From f51c691fdb2418ac3dfe08522231e47e35679804 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 09:00:30 +0100 Subject: [PATCH 24/82] Cleanup --- sentry_sdk/client.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 0b792e76e5..42164559bc 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -46,11 +46,12 @@ from typing import Type from typing import Union + from sentry_sdk._types import Event, Hint from sentry_sdk.integrations import Integration + from sentry_sdk.metrics import MetricsAggregator from sentry_sdk.scope import Scope - from sentry_sdk._types import Event, Hint from sentry_sdk.session import Session - + from sentry_sdk.transport import Transport _client_init_debug = ContextVar("client_init_debug") @@ -159,22 +160,18 @@ class NoopClient: """ options = _get_options() # type: Dict[str, Any] - metrics_aggregator = None # type: Optional[Any] - monitor = None # type: Optional[Any] - transport = None # type: Optional[Any] - - def __repr__(self): - # type: () -> str - return "<{} id={}>".format(self.__class__.__name__, id(self)) - - def should_send_default_pii(self): - # type: () -> bool - return False + metrics_aggregator = None # type: Optional[MetricsAggregator] + monitor = None # type: Optional[Monitor] + transport = None # type: Optional[Transport] def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None return None + def __repr__(self): + # type: () -> str + return "<{} id={}>".format(self.__class__.__name__, id(self)) + def __getstate__(self, *args, **kwargs): # type: (*Any, **Any) -> Any return {"options": {}} @@ -183,6 +180,15 @@ def __setstate__(self, *args, **kwargs): # type: (*Any, **Any) -> None pass + @property + def dsn(self): + # type: () -> Optional[str] + return None + + def should_send_default_pii(self): + # type: () -> bool + return False + def is_active(self): # type: () -> bool """ @@ -192,11 +198,6 @@ def is_active(self): """ return False - @property - def dsn(self): - # type: () -> Optional[str] - return None - def capture_event(self, *args, **kwargs): # type: (*Any, **Any) -> Optional[str] return None @@ -206,7 +207,7 @@ def capture_session(self, *args, **kwargs): return None def get_integration(self, *args, **kwargs): - # type: (*Any, **Any) -> Any + # type: (*Any, **Any) -> Optional[Integration] return None def close(self, *args, **kwargs): From 3eb22434b116a830d6e3bffe2533feb21b475b5e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 09:28:30 +0100 Subject: [PATCH 25/82] Improved scope api --- sentry_sdk/scope.py | 61 +++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index f65d12e8fe..4d04be7ac5 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -57,7 +57,6 @@ ExcInfo, Hint, Type, - Union, ) import sentry_sdk @@ -194,51 +193,45 @@ def __init__(self, ty=None, client=None): self.generate_propagation_context(incoming_data=incoming_trace_information) @classmethod - def get_current_scope(cls, should_create_scope=True): - # type: (bool) -> Scope + def get_current_scope(cls): + # type: () -> Scope """ Returns the current scope. - :parm should_create_scope: If `True` a new scope will be created if no scope is available. - .. versionadded:: 1.XX.0 """ current_scope = _current_scope.get() - if current_scope is None and should_create_scope: + if current_scope is None: current_scope = Scope(ty="current") _current_scope.set(current_scope) return current_scope @classmethod - def get_isolation_scope(cls, should_create_scope=True): - # type: (bool) -> Scope + def get_isolation_scope(cls): + # type: () -> Scope """ Returns the isolation scope. - :parm should_create_scope: If `True` a new scope will be created if no scope is available. - .. versionadded:: 1.XX.0 """ isolation_scope = _isolation_scope.get() - if isolation_scope is None and should_create_scope: + if isolation_scope is None: isolation_scope = Scope(ty="isolation") _isolation_scope.set(isolation_scope) return isolation_scope @classmethod - def get_global_scope(cls, should_create_scope=True): - # type: (bool) -> Scope + def get_global_scope(cls): + # type: () -> Scope """ Returns the global scope. - :parm should_create_scope: If `True` a new scope will be created if no scope is available. - .. versionadded:: 1.XX.0 """ global _global_scope - if _global_scope is None and should_create_scope: + if _global_scope is None: _global_scope = Scope(ty="global") return _global_scope @@ -253,14 +246,16 @@ def _merge_scopes(cls, additional_scope=None, additional_scope_kwargs=None): if additional_scope and additional_scope_kwargs: raise TypeError("cannot provide scope and kwargs") - global_scope = Scope.get_global_scope(should_create_scope=False) - final_scope = copy(global_scope) or Scope() + global _global_scope + + final_scope = copy(_global_scope) if _global_scope is not None else Scope() + final_scope._type = "merged" - isolation_scope = Scope.get_isolation_scope(should_create_scope=False) + isolation_scope = _isolation_scope.get() if isolation_scope is not None: final_scope.update_from_scope(isolation_scope) - current_scope = Scope.get_current_scope(should_create_scope=False) + current_scope = _current_scope.get() if current_scope is not None: final_scope.update_from_scope(current_scope) @@ -285,17 +280,17 @@ def get_client(cls): .. versionadded:: 1.XX.0 """ - scope = Scope.get_current_scope(should_create_scope=False) - if scope and scope.client.is_active(): - return scope.client + current_scope = _current_scope.get() + if current_scope is not None and current_scope.client.is_active(): + return current_scope.client - scope = Scope.get_isolation_scope(should_create_scope=False) - if scope and scope.client.is_active(): - return scope.client + isolation_scope = _isolation_scope.get() + if isolation_scope and isolation_scope.client.is_active(): + return isolation_scope.client - scope = Scope.get_global_scope(should_create_scope=False) - if scope: - return scope.client + global _global_scope + if _global_scope is not None: + return _global_scope.client return NoopClient() @@ -327,8 +322,10 @@ def fork(self): .. versionadded:: 1.XX.0 """ - self.original_scope = self - return copy(self) + forked_scope = copy(self) + forked_scope.original_scope = self + + return forked_scope def isolate(self): # type: () -> None @@ -1022,7 +1019,7 @@ def capture_event(self, event, hint=None, client=None, scope=None, **scope_kwarg if client is None: return None - scope = _merge_scopes(self, scope, scope_kwargs) + scope = Scope._merge_scopes(scope, scope_kwargs) return client.capture_event(event=event, hint=hint, scope=scope) From 553cbbc768ba8f1ad747dc4ee2ba36ca7e971ff4 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 09:41:25 +0100 Subject: [PATCH 26/82] Bring back old _merge_scopes --- sentry_sdk/scope.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 4d04be7ac5..4f572fc935 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -98,6 +98,28 @@ def wrapper(self, *args, **kwargs): return wrapper # type: ignore +def _merge_scopes(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(base) + if callable(scope_change): + scope_change(final_scope) + else: + final_scope.update_from_scope(scope_change) + + elif scope_kwargs: + final_scope = copy(base) + final_scope.update_from_kwargs(**scope_kwargs) + + else: + final_scope = base + + return final_scope + + def _copy_on_write(property_name): # type: (str) -> Callable[[Any], Any] """ @@ -1019,7 +1041,7 @@ def capture_event(self, event, hint=None, client=None, scope=None, **scope_kwarg if client is None: return None - scope = Scope._merge_scopes(scope, scope_kwargs) + scope = _merge_scopes(self, scope, scope_kwargs) return client.capture_event(event=event, hint=hint, scope=scope) From b63483cd6b1caede9c591c0acd3531950de14fbe Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 09:41:59 +0100 Subject: [PATCH 27/82] docs --- sentry_sdk/scope.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 4f572fc935..93e972fcea 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -264,6 +264,8 @@ def _merge_scopes(cls, additional_scope=None, additional_scope_kwargs=None): """ Merges global, isolation and current scope into a new scope and adds the given additional scope or additional scope kwargs to it. + + .. versionadded:: 1.XX.0 """ if additional_scope and additional_scope_kwargs: raise TypeError("cannot provide scope and kwargs") From af542087731583d28b3281f8c93ebba0d0fa5fb0 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 12:28:18 +0100 Subject: [PATCH 28/82] Added some tests --- sentry_sdk/__init__.py | 7 ++++ sentry_sdk/api.py | 7 ++++ tests/test_api.py | 82 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 562da90739..0753c1c74f 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -40,6 +40,13 @@ "get_baggage", "continue_trace", "trace", + "get_client", + "get_current_scope", + "get_global_scope", + "get_isolation_scope", + "is_initialized", + "set_current_scope", + "set_isolation_scope", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index c8f6d39157..e7726283f7 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -58,6 +58,13 @@ def overload(x): "get_traceparent", "get_baggage", "continue_trace", + "get_client", + "get_current_scope", + "get_global_scope", + "get_isolation_scope", + "is_initialized", + "set_current_scope", + "set_isolation_scope", ] diff --git a/tests/test_api.py b/tests/test_api.py index 1adb9095f0..10f488f001 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,12 +1,25 @@ +import pytest + from sentry_sdk import ( configure_scope, continue_trace, get_baggage, + get_client, + get_current_scope, get_current_span, + get_global_scope, + get_isolation_scope, get_traceparent, + is_initialized, + set_current_scope, + set_isolation_scope, start_transaction, ) + +from sentry_sdk.client import Client, NoopClient from sentry_sdk.hub import Hub +from sentry_sdk.scope import Scope + try: from unittest import mock # python 3.3 and above @@ -113,3 +126,72 @@ def test_continue_trace(sentry_init): assert propagation_context["dynamic_sampling_context"] == { "trace_id": "566e3688a61d4bc888951642d6f14a19" } + + +@pytest.mark.forked +def test_is_initialized(): + assert not is_initialized() + + scope = Scope.get_global_scope() + scope.set_client(Client()) + assert is_initialized() + + +@pytest.mark.forked +def test_get_client(): + client = get_client() + assert client is not None + assert client.__class__ == NoopClient + assert not client.is_active() + + +@pytest.mark.forked +def test_get_current_scope(): + scope = get_current_scope() + assert scope is not None + assert scope.__class__ == Scope + assert scope._type == "current" + + +@pytest.mark.forked +def test_get_isolation_scope(): + scope = get_isolation_scope() + assert scope is not None + assert scope.__class__ == Scope + assert scope._type == "isolation" + + +@pytest.mark.forked +def test_get_global_scope(): + scope = get_global_scope() + assert scope is not None + assert scope.__class__ == Scope + assert scope._type == "global" + + +@pytest.mark.forked +def test_set_current_scope(): + scope = Scope(ty="test_something") + set_current_scope(scope) + + current_scope = Scope.get_current_scope() + assert current_scope == scope + assert current_scope._type == "test_something" + + isolation_scope = Scope.get_isolation_scope() + assert isolation_scope != scope + assert isolation_scope._type == "isolation" + + +@pytest.mark.forked +def test_set_isolation_scope(): + scope = Scope(ty="test_more") + set_isolation_scope(scope) + + current_scope = Scope.get_current_scope() + assert current_scope != scope + assert current_scope._type == "current" + + isolation_scope = Scope.get_isolation_scope() + assert isolation_scope == scope + assert isolation_scope._type == "test_more" From 7366818bebb96eb80e068999d1fa9b372f5fc928 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 12:53:29 +0100 Subject: [PATCH 29/82] Added tests --- tests/test_scope.py | 112 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/test_scope.py b/tests/test_scope.py index 8bdd46e02f..493aef8598 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -2,6 +2,7 @@ import os import pytest from sentry_sdk import capture_exception +from sentry_sdk.client import Client, NoopClient from sentry_sdk.scope import Scope try: @@ -157,3 +158,114 @@ def test_load_trace_data_from_env(env, excepted_value): s = Scope() incoming_trace_data = s._load_trace_data_from_env() assert incoming_trace_data == excepted_value + + +@pytest.mark.forked +def test_scope_client(): + scope = Scope(ty="test_something") + assert scope._type == "test_something" + assert scope.client is not None + assert scope.client.__class__ == NoopClient + + custom_client = Client() + scope = Scope(ty="test_more", client=custom_client) + assert scope._type == "test_more" + assert scope.client is not None + assert scope.client.__class__ == Client + assert scope.client == custom_client + + +@pytest.mark.forked +def test_get_current_scope(): + scope = Scope.get_current_scope() + assert scope is not None + assert scope.__class__ == Scope + assert scope._type == "current" + + +@pytest.mark.forked +def test_get_isolation_scope(): + scope = Scope.get_isolation_scope() + assert scope is not None + assert scope.__class__ == Scope + assert scope._type == "isolation" + + +@pytest.mark.forked +def test_get_global_scope(): + scope = Scope.get_global_scope() + assert scope is not None + assert scope.__class__ == Scope + assert scope._type == "global" + + +@pytest.mark.forked +def test_get_client(): + client = Scope.get_client() + assert client is not None + assert client.__class__ == NoopClient + assert not client.is_active() + + +@pytest.mark.forked +def test_set_client(): + client1 = Client() + client2 = Client() + client3 = Client() + + current_scope = Scope.get_current_scope() + isolation_scope = Scope.get_isolation_scope() + global_scope = Scope.get_global_scope() + + current_scope.set_client(client1) + isolation_scope.set_client(client2) + global_scope.set_client(client3) + + client = Scope.get_client() + assert client == client1 + + current_scope.set_client(None) + isolation_scope.set_client(client2) + global_scope.set_client(client3) + + client = Scope.get_client() + assert client == client2 + + current_scope.set_client(None) + isolation_scope.set_client(None) + global_scope.set_client(client3) + + client = Scope.get_client() + assert client == client3 + + +@pytest.mark.forked +def test_is_forked(): + scope = Scope() + assert not scope.is_forked + + +@pytest.mark.forked +def test_fork(): + scope = Scope() + forked_scope = scope.fork() + + assert forked_scope.is_forked + assert not scope.is_forked + assert scope != forked_scope + assert forked_scope.original_scope == scope + + +@pytest.mark.forked +def test_isolate(): + isolation_scope_before = Scope.get_isolation_scope() + + scope = Scope() + scope.isolate() + + isolation_scope_after = Scope.get_isolation_scope() + + assert isolation_scope_after != isolation_scope_before + assert isolation_scope_after.is_forked + assert isolation_scope_after.original_scope == isolation_scope_before + assert not scope.is_forked From 9ad022b8882d7ff0a6b6653dd4bfe30060220581 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 14:09:35 +0100 Subject: [PATCH 30/82] Apply suggestions from code review Co-authored-by: Ivana Kellyerova --- sentry_sdk/api.py | 6 +++--- sentry_sdk/client.py | 4 ++-- sentry_sdk/scope.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index e7726283f7..09de8900bb 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -90,7 +90,7 @@ def is_initialized(): # type: () -> bool """ Returns whether Sentry has been initialized or not. - If an client is available Sentry is initialized. + If a client is available Sentry is initialized. .. versionadded:: 1.XX.0 """ @@ -124,7 +124,7 @@ def get_global_scope(): def set_current_scope(new_current_scope): # type: (Scope) -> None """ - Sets the given scope as the new current scope overwritting the existing current scope. + Sets the given scope as the new current scope overwriting the existing current scope. :param new_current_scope: The scope to set as the new current scope. .. versionadded:: 1.XX.0 @@ -135,7 +135,7 @@ def set_current_scope(new_current_scope): def set_isolation_scope(new_isolation_scope): # type: (Scope) -> None """ - Sets the given scope as the new isolation scope overwritting the existing isolation scope. + Sets the given scope as the new isolation scope overwriting the existing isolation scope. :param new_isolation_scope: The scope to set as the new isolation scope. .. versionadded:: 1.XX.0 diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 42164559bc..7e6a552e73 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -192,7 +192,7 @@ def should_send_default_pii(self): def is_active(self): # type: () -> bool """ - Returns weither the client is active (able to send data to Sentry) + Returns whether the client is active (able to send data to Sentry) .. versionadded:: 1.XX.0 """ @@ -382,7 +382,7 @@ def _capture_envelope(envelope): def is_active(self): # type: () -> bool """ - Returns weither the client is active (able to send data to Sentry) + Returns whether the client is active (able to send data to Sentry) .. versionadded:: 1.XX.0 """ diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 93e972fcea..ee25d24c3c 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -333,7 +333,7 @@ def set_client(self, client=None): def is_forked(self): # type: () -> bool """ - Weither this scope is a fork of another scope. + Whether this scope is a fork of another scope. .. versionadded:: 1.XX.0 """ From 699ef786a92cefc84b8261dbd058be3d0e716ba7 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 14:15:42 +0100 Subject: [PATCH 31/82] Fixing stuff --- sentry_sdk/scope.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index ee25d24c3c..afe3308fae 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -270,8 +270,6 @@ def _merge_scopes(cls, additional_scope=None, additional_scope_kwargs=None): if additional_scope and additional_scope_kwargs: raise TypeError("cannot provide scope and kwargs") - global _global_scope - final_scope = copy(_global_scope) if _global_scope is not None else Scope() final_scope._type = "merged" @@ -309,10 +307,9 @@ def get_client(cls): return current_scope.client isolation_scope = _isolation_scope.get() - if isolation_scope and isolation_scope.client.is_active(): + if isolation_scope is not None and isolation_scope.client.is_active(): return isolation_scope.client - global _global_scope if _global_scope is not None: return _global_scope.client From 55a9f4fdff845c280bfdd46c6736167785256fae Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 14:29:33 +0100 Subject: [PATCH 32/82] Improved Client inheritance --- docs/apidocs.rst | 5 ++++- sentry_sdk/api.py | 4 ++-- sentry_sdk/client.py | 26 ++++++++++++++++++-------- sentry_sdk/scope.py | 8 +++----- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/docs/apidocs.rst b/docs/apidocs.rst index 52f87544d0..eb9f35ee75 100644 --- a/docs/apidocs.rst +++ b/docs/apidocs.rst @@ -11,12 +11,15 @@ API Docs .. autoclass:: sentry_sdk.Client :members: -.. autoclass:: sentry_sdk.client._Client +.. autoclass:: sentry_sdk.client.BaseClient :members: .. autoclass:: sentry_sdk.client.NoopClient :members: +.. autoclass:: sentry_sdk.client._Client + :members: + .. autoclass:: sentry_sdk.Transport :members: diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 09de8900bb..dd7a7505be 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -16,7 +16,7 @@ from typing import ContextManager from typing import Union - from sentry_sdk.client import Client, NoopClient + from sentry_sdk.client import BaseClient from sentry_sdk._types import ( Event, Hint, @@ -99,7 +99,7 @@ def is_initialized(): @scopemethod def get_client(): - # type: () -> Union[Client, NoopClient] + # type: () -> BaseClient return Scope.get_client() diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 7e6a552e73..661f869f20 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -152,9 +152,9 @@ def _get_options(*args, **kwargs): module_not_found_error = ImportError # type: ignore -class NoopClient: +class BaseClient: """ - A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. + The basic definition of a client that is used for sending data to Sentry. .. versionadded:: 1.XX.0 """ @@ -192,7 +192,7 @@ def should_send_default_pii(self): def is_active(self): # type: () -> bool """ - Returns whether the client is active (able to send data to Sentry) + Returns weither the client is active (able to send data to Sentry) .. versionadded:: 1.XX.0 """ @@ -219,7 +219,7 @@ def flush(self, *args, **kwargs): return None def __enter__(self): - # type: () -> NoopClient + # type: () -> BaseClient return self def __exit__(self, exc_type, exc_value, tb): @@ -227,8 +227,19 @@ def __exit__(self, exc_type, exc_value, tb): return None -class _Client(NoopClient): - """The client is internally responsible for capturing the events and +class NoopClient(BaseClient): + """ + A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. + + .. versionadded:: 1.XX.0 + """ + + pass + + +class _Client(BaseClient): + """ + The client is internally responsible for capturing the events and forwarding them to sentry through the configured transport. It takes the client options as keyword arguments and optionally the DSN as first argument. @@ -382,7 +393,7 @@ def _capture_envelope(envelope): def is_active(self): # type: () -> bool """ - Returns whether the client is active (able to send data to Sentry) + Returns weither the client is active (able to send data to Sentry) .. versionadded:: 1.XX.0 """ @@ -654,7 +665,6 @@ def capture_event( :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. """ diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index afe3308fae..41755162e5 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -202,9 +202,7 @@ def __init__(self, ty=None, client=None): self._name = None # type: Optional[str] self._propagation_context = None # type: Optional[Dict[str, Any]] - self.client = ( - NoopClient() - ) # type: Union[sentry_sdk.Client, sentry_sdk.client.NoopClient] + self.client = NoopClient() # type: sentry_sdk.client.BaseClient if client is not None: self.set_client(client) @@ -294,7 +292,7 @@ def _merge_scopes(cls, additional_scope=None, additional_scope_kwargs=None): @classmethod def get_client(cls): - # type: () -> Union[sentry_sdk.Client, sentry_sdk.client.NoopClient] + # type: () -> sentry_sdk.client.BaseClient """ Returns the currently used :py:class:`sentry_sdk.Client`. This checks the current scope, the isolation scope and the global scope for a client. @@ -316,7 +314,7 @@ def get_client(cls): return NoopClient() def set_client(self, client=None): - # type: (Optional[sentry_sdk.Client]) -> None + # type: (Optional[sentry_sdk.client.BaseClient]) -> None """ Sets the client for this scope. :param client: The client to use in this scope. From 09737e128e587be497ee910e89e4a701d1a84d7b Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 23 Jan 2024 14:31:01 +0100 Subject: [PATCH 33/82] Removed useless code --- sentry_sdk/client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 661f869f20..ce80ddde8a 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -168,10 +168,6 @@ def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None return None - def __repr__(self): - # type: () -> str - return "<{} id={}>".format(self.__class__.__name__, id(self)) - def __getstate__(self, *args, **kwargs): # type: (*Any, **Any) -> Any return {"options": {}} From 0c47518ab24664c624deaacf296f6d291e94908a Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 24 Jan 2024 15:51:55 +0100 Subject: [PATCH 34/82] Removed useless context copying --- sentry_sdk/scope.py | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 41755162e5..bd63c140a6 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1449,9 +1449,14 @@ def __repr__(self): ) -def _with_new_scope(): +@contextmanager +def new_scope(): # type: () -> Generator[Scope, None, None] + """ + Context manager that forks the current scope and runs the wrapped code in it. + .. versionadded:: 1.XX.0 + """ current_scope = Scope.get_current_scope() forked_scope = current_scope.fork() token = _current_scope.set(forked_scope) @@ -1465,20 +1470,14 @@ def _with_new_scope(): @contextmanager -def new_scope(): +def isolated_scope(): # type: () -> Generator[Scope, None, None] """ - Context manager that forks the current scope and runs the wrapped code in it. + Context manager that forks the current isolation scope + (and the related current scope) and runs the wrapped code in it. .. versionadded:: 1.XX.0 """ - ctx = copy_context() # This does not exist in Python 2.7 - return ctx.run(_with_new_scope) - - -def _with_isolated_scope(): - # type: () -> Generator[Scope, None, None] - # fork current scope current_scope = Scope.get_current_scope() forked_current_scope = current_scope.fork() @@ -1498,18 +1497,5 @@ def _with_isolated_scope(): _isolation_scope.reset(isolation_token) -@contextmanager -def isolated_scope(): - # type: () -> Generator[Scope, None, None] - """ - Context manager that forks the current isolation scope - (and the related current scope) and runs the wrapped code in it. - - .. versionadded:: 1.XX.0 - """ - ctx = copy_context() - return ctx.run(_with_isolated_scope) - - # Circular imports from sentry_sdk.client import NoopClient From 01b663a76a8494ab751fc821a687ee68495618d0 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 24 Jan 2024 15:54:01 +0100 Subject: [PATCH 35/82] Fixed import --- sentry_sdk/scope.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index bd63c140a6..c7a0f56148 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -28,7 +28,6 @@ from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import ( capture_internal_exceptions, - copy_context, ContextVar, event_from_exception, exc_info_from_error, From 50afa9da15d50f8ba86b0c4b9d45aea97158313a Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 24 Jan 2024 16:39:26 +0100 Subject: [PATCH 36/82] Added more tests --- sentry_sdk/__init__.py | 2 + sentry_sdk/api.py | 4 +- tests/test_scope.py | 128 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 0753c1c74f..4e16db0286 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -47,6 +47,8 @@ "is_initialized", "set_current_scope", "set_isolation_scope", + "new_scope", + "isolated_scope", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index dd7a7505be..3760da64db 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -3,7 +3,7 @@ from sentry_sdk import scope from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.hub import Hub -from sentry_sdk.scope import Scope +from sentry_sdk.scope import Scope, new_scope, isolated_scope from sentry_sdk.tracing import NoOpSpan, Transaction if TYPE_CHECKING: @@ -65,6 +65,8 @@ def overload(x): "is_initialized", "set_current_scope", "set_isolation_scope", + "new_scope", + "isolated_scope", ] diff --git a/tests/test_scope.py b/tests/test_scope.py index 493aef8598..982e0ff7e5 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -1,7 +1,7 @@ import copy import os import pytest -from sentry_sdk import capture_exception +from sentry_sdk import capture_exception, new_scope, isolated_scope from sentry_sdk.client import Client, NoopClient from sentry_sdk.scope import Scope @@ -269,3 +269,129 @@ def test_isolate(): assert isolation_scope_after.is_forked assert isolation_scope_after.original_scope == isolation_scope_before assert not scope.is_forked + + +@pytest.mark.forked +def test_get_global_scope_tags(): + global_scope1 = Scope.get_global_scope() + global_scope2 = Scope.get_global_scope() + assert global_scope1 == global_scope2 + assert global_scope1.client.__class__ == NoopClient + assert not global_scope1.client.is_active() + assert global_scope2.client.__class__ == NoopClient + assert not global_scope2.client.is_active() + + global_scope1.set_tag("tag1", "value") + tags_scope1 = global_scope1._tags + tags_scope2 = global_scope2._tags + assert tags_scope1 == tags_scope2 + assert global_scope1.client.__class__ == NoopClient + assert not global_scope1.client.is_active() + assert global_scope2.client.__class__ == NoopClient + assert not global_scope2.client.is_active() + + +@pytest.mark.forked +def test_get_global_with_new_scope(): + original_global_scope = Scope.get_global_scope() + + with new_scope() as scope: + in_with_global_scope = Scope.get_global_scope() + + assert scope is not in_with_global_scope + assert in_with_global_scope is original_global_scope + + after_with_global_scope = Scope.get_global_scope() + assert after_with_global_scope is original_global_scope + + +@pytest.mark.forked +def test_get_global_with_isolated_scope(): + original_global_scope = Scope.get_global_scope() + + with isolated_scope() as scope: + in_with_global_scope = Scope.get_global_scope() + + assert scope is not in_with_global_scope + assert in_with_global_scope is original_global_scope + + after_with_global_scope = Scope.get_global_scope() + assert after_with_global_scope is original_global_scope + + +@pytest.mark.forked +def test_get_isolation_scope_tags(): + isolation_scope1 = Scope.get_isolation_scope() + isolation_scope2 = Scope.get_isolation_scope() + assert isolation_scope1 == isolation_scope2 + assert isolation_scope1.client.__class__ == NoopClient + assert not isolation_scope1.client.is_active() + assert isolation_scope2.client.__class__ == NoopClient + assert not isolation_scope2.client.is_active() + + isolation_scope1.set_tag("tag1", "value") + tags_scope1 = isolation_scope1._tags + tags_scope2 = isolation_scope2._tags + assert tags_scope1 == tags_scope2 + assert isolation_scope1.client.__class__ == NoopClient + assert not isolation_scope1.client.is_active() + assert isolation_scope2.client.__class__ == NoopClient + assert not isolation_scope2.client.is_active() + + +@pytest.mark.forked +def test_with_isolated_scope(): + original_current_scope = Scope.get_current_scope() + original_isolation_scope = Scope.get_isolation_scope() + + with isolated_scope() as scope: + in_with_current_scope = Scope.get_current_scope() + in_with_isolation_scope = Scope.get_isolation_scope() + + assert scope is in_with_isolation_scope + assert in_with_current_scope is not original_current_scope + assert in_with_isolation_scope is not original_isolation_scope + + after_with_current_scope = Scope.get_current_scope() + after_with_isolation_scope = Scope.get_isolation_scope() + assert after_with_current_scope is original_current_scope + assert after_with_isolation_scope is original_isolation_scope + + +@pytest.mark.forked +def test_get_current_scope_tags(): + scope1 = Scope.get_current_scope() + scope2 = Scope.get_current_scope() + assert id(scope1) == id(scope2) + assert scope1.client.__class__ == NoopClient + assert not scope1.client.is_active() + assert scope2.client.__class__ == NoopClient + assert not scope2.client.is_active() + + scope1.set_tag("tag1", "value") + tags_scope1 = scope1._tags + tags_scope2 = scope2._tags + assert tags_scope1 == tags_scope2 + assert scope1.client.__class__ == NoopClient + assert not scope1.client.is_active() + assert scope2.client.__class__ == NoopClient + assert not scope2.client.is_active() + + +@pytest.mark.forked +def test_with_new_scope(): + original_current_scope = Scope.get_current_scope() + original_isolation_scope = Scope.get_isolation_scope() + + with new_scope() as scope: + in_with_current_scope = Scope.get_current_scope() + in_with_isolation_scope = Scope.get_isolation_scope() + + assert scope is in_with_current_scope + assert in_with_current_scope is not original_current_scope + assert in_with_isolation_scope is original_isolation_scope + + after_with_current_scope = Scope.get_current_scope() + after_with_isolation_scope = Scope.get_isolation_scope() + assert after_with_current_scope is original_current_scope + assert after_with_isolation_scope is original_isolation_scope From c16be2b5792d1d8288b23b89420ca67e6bcbcc71 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 24 Jan 2024 16:44:20 +0100 Subject: [PATCH 37/82] Sorted __all__ --- sentry_sdk/__init__.py | 36 ++++++++++++++++++------------------ sentry_sdk/api.py | 34 +++++++++++++++++----------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 4e16db0286..2ee942af13 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -19,36 +19,36 @@ "init", "integrations", # From sentry_sdk.api + "add_breadcrumb", "capture_event", - "capture_message", "capture_exception", - "add_breadcrumb", + "capture_message", "configure_scope", - "push_scope", + "continue_trace", "flush", - "last_event_id", - "start_span", - "start_transaction", - "set_tag", - "set_context", - "set_extra", - "set_user", - "set_level", - "set_measurement", - "get_current_span", - "get_traceparent", "get_baggage", - "continue_trace", - "trace", "get_client", "get_current_scope", + "get_current_span", "get_global_scope", "get_isolation_scope", + "get_traceparent", "is_initialized", + "isolated_scope", + "last_event_id", + "new_scope", + "push_scope", + "set_context", "set_current_scope", + "set_extra", "set_isolation_scope", - "new_scope", - "isolated_scope", + "set_level", + "set_measurement", + "set_tag", + "set_user", + "start_span", + "start_transaction", + "trace", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 3760da64db..d7561cc34d 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -38,35 +38,35 @@ def overload(x): # When changing this, update __all__ in __init__.py too __all__ = [ + "add_breadcrumb", "capture_event", - "capture_message", "capture_exception", - "add_breadcrumb", + "capture_message", "configure_scope", - "push_scope", + "continue_trace", "flush", - "last_event_id", - "start_span", - "start_transaction", - "set_tag", - "set_context", - "set_extra", - "set_user", - "set_level", - "set_measurement", - "get_current_span", - "get_traceparent", "get_baggage", - "continue_trace", "get_client", "get_current_scope", + "get_current_span", "get_global_scope", "get_isolation_scope", + "get_traceparent", "is_initialized", + "isolated_scope", + "last_event_id", + "new_scope", + "push_scope", + "set_context", "set_current_scope", + "set_extra", "set_isolation_scope", - "new_scope", - "isolated_scope", + "set_level", + "set_measurement", + "set_tag", + "set_user", + "start_span", + "start_transaction", ] From 91157431c90277ebabff2e9a2813e1c22ad391a6 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 08:45:17 +0100 Subject: [PATCH 38/82] Deletion of thread local vars is not possible in older Python --- sentry_sdk/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 25399cd908..ac93dfebbf 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1271,7 +1271,7 @@ def set(self, value): def reset(self, token): # type: (Any) -> None self._local.value = getattr(self._original_local, token) - del self._original_local[token] + self._original_local[token] = None return ContextVar From 70b3d99de323a9f95591898a42c2eaf868722e19 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 09:12:30 +0100 Subject: [PATCH 39/82] Fixed deletion of thread local var in Python 3.6+ --- sentry_sdk/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index ac93dfebbf..98904ccd95 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1271,7 +1271,8 @@ def set(self, value): def reset(self, token): # type: (Any) -> None self._local.value = getattr(self._original_local, token) - self._original_local[token] = None + # delete the original value (this way it works in Python 3.6+) + del self._original_local.__dict__[token] return ContextVar From 059f05101bee0f6646c080fc9fd4d6ff4ccce3ab Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 11:01:35 +0100 Subject: [PATCH 40/82] Fixed docstring --- sentry_sdk/client.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 333a033e56..6e80ae90ed 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -237,16 +237,12 @@ class NoopClient(BaseClient): class _Client(BaseClient): """ - The client is internally responsible for capturing the events and - forwarding them to sentry through the configured transport. It takes - the client options as keyword arguments and optionally the DSN as first - argument. - - <<<<<<< HEAD - Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) - ======= - Alias of :py:class:`Client`. (Was created for better intelisense support) - >>>>>>> master + The client is internally responsible for capturing the events and + forwarding them to sentry through the configured transport. It takes + the client options as keyword arguments and optionally the DSN as first + argument. + + Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) """ def __init__(self, *args, **kwargs): From b027411f7d4c5bd10db7635a615c94a4bcd4d9cd Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 11:02:13 +0100 Subject: [PATCH 41/82] Fixed docstring --- sentry_sdk/client.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 6e80ae90ed..240ec1dec2 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -658,17 +658,13 @@ def capture_event( # type: (...) -> Optional[str] """Captures an event. - :param event: A ready-made event that can be directly sent to Sentry. + :param event: A ready-made event that can be directly sent to Sentry. - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - <<<<<<< HEAD - ======= - The `scope` and `scope_kwargs` parameters are mutually exclusive. - >>>>>>> master + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. + :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. """ if disable_capture_event.get(False): return None From 8c4fddce642df1c4c8c81d15338bcbb32d6200d0 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 11:04:45 +0100 Subject: [PATCH 42/82] Fixed imports --- sentry_sdk/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 240ec1dec2..c316c8e6e4 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -46,8 +46,6 @@ from typing import Type from typing import Union - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope from sentry_sdk._types import Event, Hint from sentry_sdk.integrations import Integration from sentry_sdk.metrics import MetricsAggregator @@ -55,6 +53,7 @@ from sentry_sdk.session import Session from sentry_sdk.transport import Transport + _client_init_debug = ContextVar("client_init_debug") From 4d884e90bb7adb573cae5e3f60b4fa97b4a2f55e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 11:27:47 +0100 Subject: [PATCH 43/82] Back to normal delete of context var. --- sentry_sdk/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 98904ccd95..282665cf79 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1272,7 +1272,7 @@ def reset(self, token): # type: (Any) -> None self._local.value = getattr(self._original_local, token) # delete the original value (this way it works in Python 3.6+) - del self._original_local.__dict__[token] + del self._original_local[token] return ContextVar From 1c1b9116cabe27e345d1688142c83154c0c75b8c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 11:51:11 +0100 Subject: [PATCH 44/82] Delete from the underlying __dict__. This is ok according to the docs. --- sentry_sdk/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 282665cf79..98904ccd95 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1272,7 +1272,7 @@ def reset(self, token): # type: (Any) -> None self._local.value = getattr(self._original_local, token) # delete the original value (this way it works in Python 3.6+) - del self._original_local[token] + del self._original_local.__dict__[token] return ContextVar From abd7c378fdfc327684594091f128cba64adce0b5 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 13:00:47 +0100 Subject: [PATCH 45/82] deactivated tests to try something --- scripts/runtox.sh | 6 +- tests/test_scope.py | 248 ++++++++++++++++++++++---------------------- 2 files changed, 127 insertions(+), 127 deletions(-) diff --git a/scripts/runtox.sh b/scripts/runtox.sh index dbbb4f2e10..0818c4bf18 100755 --- a/scripts/runtox.sh +++ b/scripts/runtox.sh @@ -29,10 +29,10 @@ export TOX_PARALLEL_NO_SPINNER=1 if $excludelatest; then echo "Excluding latest" - ENV="$($TOXPATH -l | grep -- "$searchstring" | grep -v -- '-latest' | tr $'\n' ',')" + ENV="$(python -m $TOXPATH -l | grep -- "$searchstring" | grep -v -- '-latest' | tr $'\n' ',')" else echo "Including latest" - ENV="$($TOXPATH -l | grep -- "$searchstring" | tr $'\n' ',')" + ENV="$(python -m $TOXPATH -l | grep -- "$searchstring" | tr $'\n' ',')" fi if [ -z "${ENV}" ]; then @@ -40,4 +40,4 @@ if [ -z "${ENV}" ]; then exit 0 fi -exec $TOXPATH -vv -e "$ENV" -- "${@:2}" +exec python -m $TOXPATH -vv -e "$ENV" -- "${@:2}" diff --git a/tests/test_scope.py b/tests/test_scope.py index 982e0ff7e5..327e82a160 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -271,127 +271,127 @@ def test_isolate(): assert not scope.is_forked -@pytest.mark.forked -def test_get_global_scope_tags(): - global_scope1 = Scope.get_global_scope() - global_scope2 = Scope.get_global_scope() - assert global_scope1 == global_scope2 - assert global_scope1.client.__class__ == NoopClient - assert not global_scope1.client.is_active() - assert global_scope2.client.__class__ == NoopClient - assert not global_scope2.client.is_active() - - global_scope1.set_tag("tag1", "value") - tags_scope1 = global_scope1._tags - tags_scope2 = global_scope2._tags - assert tags_scope1 == tags_scope2 - assert global_scope1.client.__class__ == NoopClient - assert not global_scope1.client.is_active() - assert global_scope2.client.__class__ == NoopClient - assert not global_scope2.client.is_active() - - -@pytest.mark.forked -def test_get_global_with_new_scope(): - original_global_scope = Scope.get_global_scope() - - with new_scope() as scope: - in_with_global_scope = Scope.get_global_scope() - - assert scope is not in_with_global_scope - assert in_with_global_scope is original_global_scope - - after_with_global_scope = Scope.get_global_scope() - assert after_with_global_scope is original_global_scope - - -@pytest.mark.forked -def test_get_global_with_isolated_scope(): - original_global_scope = Scope.get_global_scope() - - with isolated_scope() as scope: - in_with_global_scope = Scope.get_global_scope() - - assert scope is not in_with_global_scope - assert in_with_global_scope is original_global_scope - - after_with_global_scope = Scope.get_global_scope() - assert after_with_global_scope is original_global_scope - - -@pytest.mark.forked -def test_get_isolation_scope_tags(): - isolation_scope1 = Scope.get_isolation_scope() - isolation_scope2 = Scope.get_isolation_scope() - assert isolation_scope1 == isolation_scope2 - assert isolation_scope1.client.__class__ == NoopClient - assert not isolation_scope1.client.is_active() - assert isolation_scope2.client.__class__ == NoopClient - assert not isolation_scope2.client.is_active() - - isolation_scope1.set_tag("tag1", "value") - tags_scope1 = isolation_scope1._tags - tags_scope2 = isolation_scope2._tags - assert tags_scope1 == tags_scope2 - assert isolation_scope1.client.__class__ == NoopClient - assert not isolation_scope1.client.is_active() - assert isolation_scope2.client.__class__ == NoopClient - assert not isolation_scope2.client.is_active() - - -@pytest.mark.forked -def test_with_isolated_scope(): - original_current_scope = Scope.get_current_scope() - original_isolation_scope = Scope.get_isolation_scope() - - with isolated_scope() as scope: - in_with_current_scope = Scope.get_current_scope() - in_with_isolation_scope = Scope.get_isolation_scope() - - assert scope is in_with_isolation_scope - assert in_with_current_scope is not original_current_scope - assert in_with_isolation_scope is not original_isolation_scope - - after_with_current_scope = Scope.get_current_scope() - after_with_isolation_scope = Scope.get_isolation_scope() - assert after_with_current_scope is original_current_scope - assert after_with_isolation_scope is original_isolation_scope - - -@pytest.mark.forked -def test_get_current_scope_tags(): - scope1 = Scope.get_current_scope() - scope2 = Scope.get_current_scope() - assert id(scope1) == id(scope2) - assert scope1.client.__class__ == NoopClient - assert not scope1.client.is_active() - assert scope2.client.__class__ == NoopClient - assert not scope2.client.is_active() - - scope1.set_tag("tag1", "value") - tags_scope1 = scope1._tags - tags_scope2 = scope2._tags - assert tags_scope1 == tags_scope2 - assert scope1.client.__class__ == NoopClient - assert not scope1.client.is_active() - assert scope2.client.__class__ == NoopClient - assert not scope2.client.is_active() - - -@pytest.mark.forked -def test_with_new_scope(): - original_current_scope = Scope.get_current_scope() - original_isolation_scope = Scope.get_isolation_scope() - - with new_scope() as scope: - in_with_current_scope = Scope.get_current_scope() - in_with_isolation_scope = Scope.get_isolation_scope() - - assert scope is in_with_current_scope - assert in_with_current_scope is not original_current_scope - assert in_with_isolation_scope is original_isolation_scope - - after_with_current_scope = Scope.get_current_scope() - after_with_isolation_scope = Scope.get_isolation_scope() - assert after_with_current_scope is original_current_scope - assert after_with_isolation_scope is original_isolation_scope +# @pytest.mark.forked +# def test_get_global_scope_tags(): +# global_scope1 = Scope.get_global_scope() +# global_scope2 = Scope.get_global_scope() +# assert global_scope1 == global_scope2 +# assert global_scope1.client.__class__ == NoopClient +# assert not global_scope1.client.is_active() +# assert global_scope2.client.__class__ == NoopClient +# assert not global_scope2.client.is_active() + +# global_scope1.set_tag("tag1", "value") +# tags_scope1 = global_scope1._tags +# tags_scope2 = global_scope2._tags +# assert tags_scope1 == tags_scope2 +# assert global_scope1.client.__class__ == NoopClient +# assert not global_scope1.client.is_active() +# assert global_scope2.client.__class__ == NoopClient +# assert not global_scope2.client.is_active() + + +# @pytest.mark.forked +# def test_get_global_with_new_scope(): +# original_global_scope = Scope.get_global_scope() + +# with new_scope() as scope: +# in_with_global_scope = Scope.get_global_scope() + +# assert scope is not in_with_global_scope +# assert in_with_global_scope is original_global_scope + +# after_with_global_scope = Scope.get_global_scope() +# assert after_with_global_scope is original_global_scope + + +# @pytest.mark.forked +# def test_get_global_with_isolated_scope(): +# original_global_scope = Scope.get_global_scope() + +# with isolated_scope() as scope: +# in_with_global_scope = Scope.get_global_scope() + +# assert scope is not in_with_global_scope +# assert in_with_global_scope is original_global_scope + +# after_with_global_scope = Scope.get_global_scope() +# assert after_with_global_scope is original_global_scope + + +# @pytest.mark.forked +# def test_get_isolation_scope_tags(): +# isolation_scope1 = Scope.get_isolation_scope() +# isolation_scope2 = Scope.get_isolation_scope() +# assert isolation_scope1 == isolation_scope2 +# assert isolation_scope1.client.__class__ == NoopClient +# assert not isolation_scope1.client.is_active() +# assert isolation_scope2.client.__class__ == NoopClient +# assert not isolation_scope2.client.is_active() + +# isolation_scope1.set_tag("tag1", "value") +# tags_scope1 = isolation_scope1._tags +# tags_scope2 = isolation_scope2._tags +# assert tags_scope1 == tags_scope2 +# assert isolation_scope1.client.__class__ == NoopClient +# assert not isolation_scope1.client.is_active() +# assert isolation_scope2.client.__class__ == NoopClient +# assert not isolation_scope2.client.is_active() + + +# @pytest.mark.forked +# def test_with_isolated_scope(): +# original_current_scope = Scope.get_current_scope() +# original_isolation_scope = Scope.get_isolation_scope() + +# with isolated_scope() as scope: +# in_with_current_scope = Scope.get_current_scope() +# in_with_isolation_scope = Scope.get_isolation_scope() + +# assert scope is in_with_isolation_scope +# assert in_with_current_scope is not original_current_scope +# assert in_with_isolation_scope is not original_isolation_scope + +# after_with_current_scope = Scope.get_current_scope() +# after_with_isolation_scope = Scope.get_isolation_scope() +# assert after_with_current_scope is original_current_scope +# assert after_with_isolation_scope is original_isolation_scope + + +# @pytest.mark.forked +# def test_get_current_scope_tags(): +# scope1 = Scope.get_current_scope() +# scope2 = Scope.get_current_scope() +# assert id(scope1) == id(scope2) +# assert scope1.client.__class__ == NoopClient +# assert not scope1.client.is_active() +# assert scope2.client.__class__ == NoopClient +# assert not scope2.client.is_active() + +# scope1.set_tag("tag1", "value") +# tags_scope1 = scope1._tags +# tags_scope2 = scope2._tags +# assert tags_scope1 == tags_scope2 +# assert scope1.client.__class__ == NoopClient +# assert not scope1.client.is_active() +# assert scope2.client.__class__ == NoopClient +# assert not scope2.client.is_active() + + +# @pytest.mark.forked +# def test_with_new_scope(): +# original_current_scope = Scope.get_current_scope() +# original_isolation_scope = Scope.get_isolation_scope() + +# with new_scope() as scope: +# in_with_current_scope = Scope.get_current_scope() +# in_with_isolation_scope = Scope.get_isolation_scope() + +# assert scope is in_with_current_scope +# assert in_with_current_scope is not original_current_scope +# assert in_with_isolation_scope is original_isolation_scope + +# after_with_current_scope = Scope.get_current_scope() +# after_with_isolation_scope = Scope.get_isolation_scope() +# assert after_with_current_scope is original_current_scope +# assert after_with_isolation_scope is original_isolation_scope From 3420b246fe80f895b250e15df09163807d30d5ac Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 13:13:40 +0100 Subject: [PATCH 46/82] enable one test --- tests/test_scope.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 327e82a160..685a5f50f3 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -378,20 +378,20 @@ def test_isolate(): # assert not scope2.client.is_active() -# @pytest.mark.forked -# def test_with_new_scope(): -# original_current_scope = Scope.get_current_scope() -# original_isolation_scope = Scope.get_isolation_scope() - -# with new_scope() as scope: -# in_with_current_scope = Scope.get_current_scope() -# in_with_isolation_scope = Scope.get_isolation_scope() - -# assert scope is in_with_current_scope -# assert in_with_current_scope is not original_current_scope -# assert in_with_isolation_scope is original_isolation_scope - -# after_with_current_scope = Scope.get_current_scope() -# after_with_isolation_scope = Scope.get_isolation_scope() -# assert after_with_current_scope is original_current_scope -# assert after_with_isolation_scope is original_isolation_scope +@pytest.mark.forked +def test_with_new_scope(): + original_current_scope = Scope.get_current_scope() + original_isolation_scope = Scope.get_isolation_scope() + + with new_scope() as scope: + in_with_current_scope = Scope.get_current_scope() + in_with_isolation_scope = Scope.get_isolation_scope() + + assert scope is in_with_current_scope + assert in_with_current_scope is not original_current_scope + assert in_with_isolation_scope is original_isolation_scope + + after_with_current_scope = Scope.get_current_scope() + after_with_isolation_scope = Scope.get_isolation_scope() + assert after_with_current_scope is original_current_scope + assert after_with_isolation_scope is original_isolation_scope From 668a7a412d850bc644e67e2e1e425e3f8c10fee4 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 13:19:45 +0100 Subject: [PATCH 47/82] Enabled more tests --- tests/test_scope.py | 100 ++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 685a5f50f3..9147244410 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -319,63 +319,63 @@ def test_isolate(): # assert after_with_global_scope is original_global_scope -# @pytest.mark.forked -# def test_get_isolation_scope_tags(): -# isolation_scope1 = Scope.get_isolation_scope() -# isolation_scope2 = Scope.get_isolation_scope() -# assert isolation_scope1 == isolation_scope2 -# assert isolation_scope1.client.__class__ == NoopClient -# assert not isolation_scope1.client.is_active() -# assert isolation_scope2.client.__class__ == NoopClient -# assert not isolation_scope2.client.is_active() - -# isolation_scope1.set_tag("tag1", "value") -# tags_scope1 = isolation_scope1._tags -# tags_scope2 = isolation_scope2._tags -# assert tags_scope1 == tags_scope2 -# assert isolation_scope1.client.__class__ == NoopClient -# assert not isolation_scope1.client.is_active() -# assert isolation_scope2.client.__class__ == NoopClient -# assert not isolation_scope2.client.is_active() +@pytest.mark.forked +def test_get_isolation_scope_tags(): + isolation_scope1 = Scope.get_isolation_scope() + isolation_scope2 = Scope.get_isolation_scope() + assert isolation_scope1 == isolation_scope2 + assert isolation_scope1.client.__class__ == NoopClient + assert not isolation_scope1.client.is_active() + assert isolation_scope2.client.__class__ == NoopClient + assert not isolation_scope2.client.is_active() + + isolation_scope1.set_tag("tag1", "value") + tags_scope1 = isolation_scope1._tags + tags_scope2 = isolation_scope2._tags + assert tags_scope1 == tags_scope2 + assert isolation_scope1.client.__class__ == NoopClient + assert not isolation_scope1.client.is_active() + assert isolation_scope2.client.__class__ == NoopClient + assert not isolation_scope2.client.is_active() -# @pytest.mark.forked -# def test_with_isolated_scope(): -# original_current_scope = Scope.get_current_scope() -# original_isolation_scope = Scope.get_isolation_scope() +@pytest.mark.forked +def test_with_isolated_scope(): + original_current_scope = Scope.get_current_scope() + original_isolation_scope = Scope.get_isolation_scope() -# with isolated_scope() as scope: -# in_with_current_scope = Scope.get_current_scope() -# in_with_isolation_scope = Scope.get_isolation_scope() + with isolated_scope() as scope: + in_with_current_scope = Scope.get_current_scope() + in_with_isolation_scope = Scope.get_isolation_scope() -# assert scope is in_with_isolation_scope -# assert in_with_current_scope is not original_current_scope -# assert in_with_isolation_scope is not original_isolation_scope + assert scope is in_with_isolation_scope + assert in_with_current_scope is not original_current_scope + assert in_with_isolation_scope is not original_isolation_scope -# after_with_current_scope = Scope.get_current_scope() -# after_with_isolation_scope = Scope.get_isolation_scope() -# assert after_with_current_scope is original_current_scope -# assert after_with_isolation_scope is original_isolation_scope + after_with_current_scope = Scope.get_current_scope() + after_with_isolation_scope = Scope.get_isolation_scope() + assert after_with_current_scope is original_current_scope + assert after_with_isolation_scope is original_isolation_scope -# @pytest.mark.forked -# def test_get_current_scope_tags(): -# scope1 = Scope.get_current_scope() -# scope2 = Scope.get_current_scope() -# assert id(scope1) == id(scope2) -# assert scope1.client.__class__ == NoopClient -# assert not scope1.client.is_active() -# assert scope2.client.__class__ == NoopClient -# assert not scope2.client.is_active() - -# scope1.set_tag("tag1", "value") -# tags_scope1 = scope1._tags -# tags_scope2 = scope2._tags -# assert tags_scope1 == tags_scope2 -# assert scope1.client.__class__ == NoopClient -# assert not scope1.client.is_active() -# assert scope2.client.__class__ == NoopClient -# assert not scope2.client.is_active() +@pytest.mark.forked +def test_get_current_scope_tags(): + scope1 = Scope.get_current_scope() + scope2 = Scope.get_current_scope() + assert id(scope1) == id(scope2) + assert scope1.client.__class__ == NoopClient + assert not scope1.client.is_active() + assert scope2.client.__class__ == NoopClient + assert not scope2.client.is_active() + + scope1.set_tag("tag1", "value") + tags_scope1 = scope1._tags + tags_scope2 = scope2._tags + assert tags_scope1 == tags_scope2 + assert scope1.client.__class__ == NoopClient + assert not scope1.client.is_active() + assert scope2.client.__class__ == NoopClient + assert not scope2.client.is_active() @pytest.mark.forked From 95302bd1dc22d6fb4f0dcdebcfb97e0880380eb7 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 13:24:15 +0100 Subject: [PATCH 48/82] Another two tests --- tests/test_scope.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 9147244410..df747fdf56 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -291,32 +291,32 @@ def test_isolate(): # assert not global_scope2.client.is_active() -# @pytest.mark.forked -# def test_get_global_with_new_scope(): -# original_global_scope = Scope.get_global_scope() +@pytest.mark.forked +def test_get_global_with_new_scope(): + original_global_scope = Scope.get_global_scope() -# with new_scope() as scope: -# in_with_global_scope = Scope.get_global_scope() + with new_scope() as scope: + in_with_global_scope = Scope.get_global_scope() -# assert scope is not in_with_global_scope -# assert in_with_global_scope is original_global_scope + assert scope is not in_with_global_scope + assert in_with_global_scope is original_global_scope -# after_with_global_scope = Scope.get_global_scope() -# assert after_with_global_scope is original_global_scope + after_with_global_scope = Scope.get_global_scope() + assert after_with_global_scope is original_global_scope -# @pytest.mark.forked -# def test_get_global_with_isolated_scope(): -# original_global_scope = Scope.get_global_scope() +@pytest.mark.forked +def test_get_global_with_isolated_scope(): + original_global_scope = Scope.get_global_scope() -# with isolated_scope() as scope: -# in_with_global_scope = Scope.get_global_scope() + with isolated_scope() as scope: + in_with_global_scope = Scope.get_global_scope() -# assert scope is not in_with_global_scope -# assert in_with_global_scope is original_global_scope + assert scope is not in_with_global_scope + assert in_with_global_scope is original_global_scope -# after_with_global_scope = Scope.get_global_scope() -# assert after_with_global_scope is original_global_scope + after_with_global_scope = Scope.get_global_scope() + assert after_with_global_scope is original_global_scope @pytest.mark.forked From c54ca926e0460b61e5dbc2547c329e20954b80fc Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 13:36:28 +0100 Subject: [PATCH 49/82] one gone again --- tests/test_scope.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index df747fdf56..a3483d1310 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -305,18 +305,18 @@ def test_get_global_with_new_scope(): assert after_with_global_scope is original_global_scope -@pytest.mark.forked -def test_get_global_with_isolated_scope(): - original_global_scope = Scope.get_global_scope() +# @pytest.mark.forked +# def test_get_global_with_isolated_scope(): +# original_global_scope = Scope.get_global_scope() - with isolated_scope() as scope: - in_with_global_scope = Scope.get_global_scope() +# with isolated_scope() as scope: +# in_with_global_scope = Scope.get_global_scope() - assert scope is not in_with_global_scope - assert in_with_global_scope is original_global_scope +# assert scope is not in_with_global_scope +# assert in_with_global_scope is original_global_scope - after_with_global_scope = Scope.get_global_scope() - assert after_with_global_scope is original_global_scope +# after_with_global_scope = Scope.get_global_scope() +# assert after_with_global_scope is original_global_scope @pytest.mark.forked From efffd01242176e6aca0d78eb6b35275784f950c2 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 13:40:45 +0100 Subject: [PATCH 50/82] and testing the other one --- tests/test_scope.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index a3483d1310..3b188dda75 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -291,25 +291,11 @@ def test_isolate(): # assert not global_scope2.client.is_active() -@pytest.mark.forked -def test_get_global_with_new_scope(): - original_global_scope = Scope.get_global_scope() - - with new_scope() as scope: - in_with_global_scope = Scope.get_global_scope() - - assert scope is not in_with_global_scope - assert in_with_global_scope is original_global_scope - - after_with_global_scope = Scope.get_global_scope() - assert after_with_global_scope is original_global_scope - - # @pytest.mark.forked -# def test_get_global_with_isolated_scope(): +# def test_get_global_with_new_scope(): # original_global_scope = Scope.get_global_scope() -# with isolated_scope() as scope: +# with new_scope() as scope: # in_with_global_scope = Scope.get_global_scope() # assert scope is not in_with_global_scope @@ -319,6 +305,20 @@ def test_get_global_with_new_scope(): # assert after_with_global_scope is original_global_scope +@pytest.mark.forked +def test_get_global_with_isolated_scope(): + original_global_scope = Scope.get_global_scope() + + with isolated_scope() as scope: + in_with_global_scope = Scope.get_global_scope() + + assert scope is not in_with_global_scope + assert in_with_global_scope is original_global_scope + + after_with_global_scope = Scope.get_global_scope() + assert after_with_global_scope is original_global_scope + + @pytest.mark.forked def test_get_isolation_scope_tags(): isolation_scope1 = Scope.get_isolation_scope() From 81eea9aac37ce5a73d87e82430e29b266a3919e4 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 13:45:18 +0100 Subject: [PATCH 51/82] Check first test --- tests/test_scope.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 3b188dda75..3cc0dbf0c8 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -271,24 +271,24 @@ def test_isolate(): assert not scope.is_forked -# @pytest.mark.forked -# def test_get_global_scope_tags(): -# global_scope1 = Scope.get_global_scope() -# global_scope2 = Scope.get_global_scope() -# assert global_scope1 == global_scope2 -# assert global_scope1.client.__class__ == NoopClient -# assert not global_scope1.client.is_active() -# assert global_scope2.client.__class__ == NoopClient -# assert not global_scope2.client.is_active() - -# global_scope1.set_tag("tag1", "value") -# tags_scope1 = global_scope1._tags -# tags_scope2 = global_scope2._tags -# assert tags_scope1 == tags_scope2 -# assert global_scope1.client.__class__ == NoopClient -# assert not global_scope1.client.is_active() -# assert global_scope2.client.__class__ == NoopClient -# assert not global_scope2.client.is_active() +@pytest.mark.forked +def test_get_global_scope_tags(): + global_scope1 = Scope.get_global_scope() + global_scope2 = Scope.get_global_scope() + assert global_scope1 == global_scope2 + assert global_scope1.client.__class__ == NoopClient + assert not global_scope1.client.is_active() + assert global_scope2.client.__class__ == NoopClient + assert not global_scope2.client.is_active() + + global_scope1.set_tag("tag1", "value") + tags_scope1 = global_scope1._tags + tags_scope2 = global_scope2._tags + assert tags_scope1 == tags_scope2 + assert global_scope1.client.__class__ == NoopClient + assert not global_scope1.client.is_active() + assert global_scope2.client.__class__ == NoopClient + assert not global_scope2.client.is_active() # @pytest.mark.forked From e957fdbdb756970f209cec9449fea11a046ed99e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 14:04:43 +0100 Subject: [PATCH 52/82] If this is green now, i am back to square one. --- tests/test_scope.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 3cc0dbf0c8..982e0ff7e5 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -291,18 +291,18 @@ def test_get_global_scope_tags(): assert not global_scope2.client.is_active() -# @pytest.mark.forked -# def test_get_global_with_new_scope(): -# original_global_scope = Scope.get_global_scope() +@pytest.mark.forked +def test_get_global_with_new_scope(): + original_global_scope = Scope.get_global_scope() -# with new_scope() as scope: -# in_with_global_scope = Scope.get_global_scope() + with new_scope() as scope: + in_with_global_scope = Scope.get_global_scope() -# assert scope is not in_with_global_scope -# assert in_with_global_scope is original_global_scope + assert scope is not in_with_global_scope + assert in_with_global_scope is original_global_scope -# after_with_global_scope = Scope.get_global_scope() -# assert after_with_global_scope is original_global_scope + after_with_global_scope = Scope.get_global_scope() + assert after_with_global_scope is original_global_scope @pytest.mark.forked From f2ee7c4a579fd8330a5bcfb89e47413d55ed9541 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 14:10:24 +0100 Subject: [PATCH 53/82] this is weird --- tests/test_scope.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 982e0ff7e5..86874ebd43 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -199,12 +199,12 @@ def test_get_global_scope(): assert scope._type == "global" -@pytest.mark.forked -def test_get_client(): - client = Scope.get_client() - assert client is not None - assert client.__class__ == NoopClient - assert not client.is_active() +# @pytest.mark.forked +# def test_get_client(): +# client = Scope.get_client() +# assert client is not None +# assert client.__class__ == NoopClient +# assert not client.is_active() @pytest.mark.forked @@ -291,6 +291,7 @@ def test_get_global_scope_tags(): assert not global_scope2.client.is_active() +# xxx @pytest.mark.forked def test_get_global_with_new_scope(): original_global_scope = Scope.get_global_scope() From 8ed4e3440c646bfcb7b6640034c00135be0dd365 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 14:20:58 +0100 Subject: [PATCH 54/82] one more test --- tests/test_scope.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 86874ebd43..3f760644a6 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -11,19 +11,19 @@ import mock # python < 3.3 -def test_copying(): - s1 = Scope() - s1.fingerprint = {} - s1.set_tag("foo", "bar") +# def test_copying(): +# s1 = Scope() +# s1.fingerprint = {} +# s1.set_tag("foo", "bar") - s2 = copy.copy(s1) - assert "foo" in s2._tags +# s2 = copy.copy(s1) +# assert "foo" in s2._tags - s1.set_tag("bam", "baz") - assert "bam" in s1._tags - assert "bam" not in s2._tags +# s1.set_tag("bam", "baz") +# assert "bam" in s1._tags +# assert "bam" not in s2._tags - assert s1._fingerprint is s2._fingerprint +# assert s1._fingerprint is s2._fingerprint def test_merging(sentry_init, capture_events): @@ -199,12 +199,16 @@ def test_get_global_scope(): assert scope._type == "global" -# @pytest.mark.forked -# def test_get_client(): -# client = Scope.get_client() -# assert client is not None -# assert client.__class__ == NoopClient -# assert not client.is_active() +# so it seem that when I comment out ONE test than everything is green, +# and it does not matter which test I comment out + + +@pytest.mark.forked +def test_get_client(): + client = Scope.get_client() + assert client is not None + assert client.__class__ == NoopClient + assert not client.is_active() @pytest.mark.forked From e73d85c7ac58154565c0f68dd7b15df221114fe9 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 15:17:34 +0100 Subject: [PATCH 55/82] forking more test --- tests/test_scope.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 3f760644a6..b906b81341 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -11,21 +11,23 @@ import mock # python < 3.3 -# def test_copying(): -# s1 = Scope() -# s1.fingerprint = {} -# s1.set_tag("foo", "bar") +@pytest.mark.forked +def test_copying(): + s1 = Scope() + s1.fingerprint = {} + s1.set_tag("foo", "bar") -# s2 = copy.copy(s1) -# assert "foo" in s2._tags + s2 = copy.copy(s1) + assert "foo" in s2._tags -# s1.set_tag("bam", "baz") -# assert "bam" in s1._tags -# assert "bam" not in s2._tags + s1.set_tag("bam", "baz") + assert "bam" in s1._tags + assert "bam" not in s2._tags -# assert s1._fingerprint is s2._fingerprint + assert s1._fingerprint is s2._fingerprint +@pytest.mark.forked def test_merging(sentry_init, capture_events): sentry_init() @@ -40,6 +42,7 @@ def test_merging(sentry_init, capture_events): assert event["user"] == {"id": "42"} +@pytest.mark.forked def test_common_args(): s = Scope() s.update_from_kwargs( @@ -81,6 +84,7 @@ def test_common_args(): SENTRY_TRACE_VALUE = "771a43a4192642f0b136d5159a501700-1234567890abcdef-1" +@pytest.mark.forked @pytest.mark.parametrize( "env,excepted_value", [ From 1c96e1f95e7d78c2566a8ddab8089ce8dd3c632c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 15:33:52 +0100 Subject: [PATCH 56/82] more forking --- tests/test_profiler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_profiler.py b/tests/test_profiler.py index 866349792a..868b7b0ed5 100644 --- a/tests/test_profiler.py +++ b/tests/test_profiler.py @@ -282,6 +282,7 @@ def test_minimum_unique_samples_required( assert reports == [("insufficient_data", "profile")] +@pytest.mark.forked @requires_python_version(3, 3) def test_profile_captured( sentry_init, From 753620a16012c155a0b7f2b56b398759f51ea497 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 15:50:09 +0100 Subject: [PATCH 57/82] Cleanup --- scripts/runtox.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/runtox.sh b/scripts/runtox.sh index 0818c4bf18..dbbb4f2e10 100755 --- a/scripts/runtox.sh +++ b/scripts/runtox.sh @@ -29,10 +29,10 @@ export TOX_PARALLEL_NO_SPINNER=1 if $excludelatest; then echo "Excluding latest" - ENV="$(python -m $TOXPATH -l | grep -- "$searchstring" | grep -v -- '-latest' | tr $'\n' ',')" + ENV="$($TOXPATH -l | grep -- "$searchstring" | grep -v -- '-latest' | tr $'\n' ',')" else echo "Including latest" - ENV="$(python -m $TOXPATH -l | grep -- "$searchstring" | tr $'\n' ',')" + ENV="$($TOXPATH -l | grep -- "$searchstring" | tr $'\n' ',')" fi if [ -z "${ENV}" ]; then @@ -40,4 +40,4 @@ if [ -z "${ENV}" ]; then exit 0 fi -exec python -m $TOXPATH -vv -e "$ENV" -- "${@:2}" +exec $TOXPATH -vv -e "$ENV" -- "${@:2}" From 017209876f1e490e93d4245284d5c85609a7f424 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 16:08:34 +0100 Subject: [PATCH 58/82] Made ScopeType an enum --- sentry_sdk/scope.py | 16 ++++++++++++---- tests/test_api.py | 20 ++++++++++---------- tests/test_scope.py | 8 ++++---- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c7a0f56148..c03914c6fc 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1,6 +1,7 @@ from copy import copy, deepcopy from collections import deque from contextlib import contextmanager +from enum import Enum from itertools import chain import os import sys @@ -71,6 +72,13 @@ global_event_processors = [] # type: List[EventProcessor] +class ScopeType(Enum): + CURRENT = "current" + ISOLATION = "isolation" + GLOBAL = "global" + MERGED = "merged" + + def add_global_event_processor(processor): # type: (EventProcessor) -> None global_event_processors.append(processor) @@ -221,7 +229,7 @@ def get_current_scope(cls): """ current_scope = _current_scope.get() if current_scope is None: - current_scope = Scope(ty="current") + current_scope = Scope(ty=ScopeType.CURRENT) _current_scope.set(current_scope) return current_scope @@ -236,7 +244,7 @@ def get_isolation_scope(cls): """ isolation_scope = _isolation_scope.get() if isolation_scope is None: - isolation_scope = Scope(ty="isolation") + isolation_scope = Scope(ty=ScopeType.ISOLATION) _isolation_scope.set(isolation_scope) return isolation_scope @@ -251,7 +259,7 @@ def get_global_scope(cls): """ global _global_scope if _global_scope is None: - _global_scope = Scope(ty="global") + _global_scope = Scope(ty=ScopeType.GLOBAL) return _global_scope @@ -268,7 +276,7 @@ def _merge_scopes(cls, additional_scope=None, additional_scope_kwargs=None): raise TypeError("cannot provide scope and kwargs") final_scope = copy(_global_scope) if _global_scope is not None else Scope() - final_scope._type = "merged" + final_scope._type = ScopeType.MERGED isolation_scope = _isolation_scope.get() if isolation_scope is not None: diff --git a/tests/test_api.py b/tests/test_api.py index 10f488f001..f754b928c6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -18,7 +18,7 @@ from sentry_sdk.client import Client, NoopClient from sentry_sdk.hub import Hub -from sentry_sdk.scope import Scope +from sentry_sdk.scope import Scope, ScopeType try: @@ -150,7 +150,7 @@ def test_get_current_scope(): scope = get_current_scope() assert scope is not None assert scope.__class__ == Scope - assert scope._type == "current" + assert scope._type == ScopeType.CURRENT @pytest.mark.forked @@ -158,7 +158,7 @@ def test_get_isolation_scope(): scope = get_isolation_scope() assert scope is not None assert scope.__class__ == Scope - assert scope._type == "isolation" + assert scope._type == ScopeType.ISOLATION @pytest.mark.forked @@ -166,32 +166,32 @@ def test_get_global_scope(): scope = get_global_scope() assert scope is not None assert scope.__class__ == Scope - assert scope._type == "global" + assert scope._type == ScopeType.GLOBAL @pytest.mark.forked def test_set_current_scope(): - scope = Scope(ty="test_something") + scope = Scope(ty=ScopeType.ISOLATION) set_current_scope(scope) current_scope = Scope.get_current_scope() assert current_scope == scope - assert current_scope._type == "test_something" + assert current_scope._type == ScopeType.ISOLATION isolation_scope = Scope.get_isolation_scope() assert isolation_scope != scope - assert isolation_scope._type == "isolation" + assert isolation_scope._type == ScopeType.ISOLATION @pytest.mark.forked def test_set_isolation_scope(): - scope = Scope(ty="test_more") + scope = Scope(ty=ScopeType.GLOBAL) set_isolation_scope(scope) current_scope = Scope.get_current_scope() assert current_scope != scope - assert current_scope._type == "current" + assert current_scope._type == ScopeType.CURRENT isolation_scope = Scope.get_isolation_scope() assert isolation_scope == scope - assert isolation_scope._type == "test_more" + assert isolation_scope._type == ScopeType.GLOBAL diff --git a/tests/test_scope.py b/tests/test_scope.py index b906b81341..1052c6cfb4 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -3,7 +3,7 @@ import pytest from sentry_sdk import capture_exception, new_scope, isolated_scope from sentry_sdk.client import Client, NoopClient -from sentry_sdk.scope import Scope +from sentry_sdk.scope import Scope, ScopeType try: from unittest import mock # python 3.3 and above @@ -184,7 +184,7 @@ def test_get_current_scope(): scope = Scope.get_current_scope() assert scope is not None assert scope.__class__ == Scope - assert scope._type == "current" + assert scope._type == ScopeType.CURRENT @pytest.mark.forked @@ -192,7 +192,7 @@ def test_get_isolation_scope(): scope = Scope.get_isolation_scope() assert scope is not None assert scope.__class__ == Scope - assert scope._type == "isolation" + assert scope._type == ScopeType.ISOLATION @pytest.mark.forked @@ -200,7 +200,7 @@ def test_get_global_scope(): scope = Scope.get_global_scope() assert scope is not None assert scope.__class__ == Scope - assert scope._type == "global" + assert scope._type == ScopeType.GLOBAL # so it seem that when I comment out ONE test than everything is green, From f82bb36140c7ff927ce2abccbdc9459d125d135c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 16:11:27 +0100 Subject: [PATCH 59/82] Improved test asserts --- tests/test_scope.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 1052c6cfb4..7ef2d101ad 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -203,10 +203,6 @@ def test_get_global_scope(): assert scope._type == ScopeType.GLOBAL -# so it seem that when I comment out ONE test than everything is green, -# and it does not matter which test I comment out - - @pytest.mark.forked def test_get_client(): client = Scope.get_client() @@ -292,7 +288,7 @@ def test_get_global_scope_tags(): global_scope1.set_tag("tag1", "value") tags_scope1 = global_scope1._tags tags_scope2 = global_scope2._tags - assert tags_scope1 == tags_scope2 + assert tags_scope1 == tags_scope2 == {"tag1": "value"} assert global_scope1.client.__class__ == NoopClient assert not global_scope1.client.is_active() assert global_scope2.client.__class__ == NoopClient @@ -341,7 +337,7 @@ def test_get_isolation_scope_tags(): isolation_scope1.set_tag("tag1", "value") tags_scope1 = isolation_scope1._tags tags_scope2 = isolation_scope2._tags - assert tags_scope1 == tags_scope2 + assert tags_scope1 == tags_scope2 == {"tag1": "value"} assert isolation_scope1.client.__class__ == NoopClient assert not isolation_scope1.client.is_active() assert isolation_scope2.client.__class__ == NoopClient @@ -380,7 +376,7 @@ def test_get_current_scope_tags(): scope1.set_tag("tag1", "value") tags_scope1 = scope1._tags tags_scope2 = scope2._tags - assert tags_scope1 == tags_scope2 + assert tags_scope1 == tags_scope2 == {"tag1": "value"} assert scope1.client.__class__ == NoopClient assert not scope1.client.is_active() assert scope2.client.__class__ == NoopClient From 387f433e70afa80e504c5ef175d2e2550549970c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 25 Jan 2024 16:17:06 +0100 Subject: [PATCH 60/82] Fixed typing --- sentry_sdk/scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c03914c6fc..4272ee13a4 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -199,7 +199,7 @@ class Scope(object): ) def __init__(self, ty=None, client=None): - # type: (Optional[str], Optional[sentry_sdk.Client]) -> None + # type: (Optional[ScopeType], Optional[sentry_sdk.Client]) -> None self._type = ty self.original_scope = None # type: Optional[Scope] From 4d747d6e62d0ed697557b3c8d26d8e4794dd3729 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 26 Jan 2024 08:54:56 +0100 Subject: [PATCH 61/82] Moved _copy_on_write to other PR. Makes more sense there because we can have tests for it --- sentry_sdk/scope.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 4272ee13a4..56fdf536e9 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -127,39 +127,6 @@ def _merge_scopes(base, scope_change, scope_kwargs): return final_scope -def _copy_on_write(property_name): - # type: (str) -> Callable[[Any], Any] - """ - Decorator that implements copy-on-write on a property of the Scope. - - .. versionadded:: 1.XX.0 - """ - - def decorator(func): - # type: (Callable[[Any], Any]) -> Callable[[Any], Any] - @wraps(func) - def wrapper(*args, **kwargs): - # type: (*Any, **Any) -> Any - self = args[0] - - same_property_different_scope = self.is_forked and id( - getattr(self, property_name) - ) == id(getattr(self.original_scope, property_name)) - - if same_property_different_scope: - setattr( - self, - property_name, - deepcopy(getattr(self.original_scope, property_name)), - ) - - return func(*args, **kwargs) - - return wrapper - - return decorator - - class Scope(object): """The scope holds extra information that should be sent with all events that belong to it. From cb27c4c691eab31553b01b18400884ad364224c8 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 26 Jan 2024 14:17:31 +0100 Subject: [PATCH 62/82] linting --- sentry_sdk/scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 56fdf536e9..bcba558011 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1,4 +1,4 @@ -from copy import copy, deepcopy +from copy import copy from collections import deque from contextlib import contextmanager from enum import Enum From 9ae58ee978af4e2da92354fc216e00f0013fa52b Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 26 Jan 2024 14:23:16 +0100 Subject: [PATCH 63/82] reformat --- sentry_sdk/integrations/arq.py | 12 +++--- sentry_sdk/integrations/huey.py | 16 +++++--- .../integrations/opentelemetry/integration.py | 1 + sentry_sdk/scope.py | 6 +-- sentry_sdk/serializer.py | 12 +++--- sentry_sdk/tracing.py | 6 +-- tests/integrations/asyncpg/test_asyncpg.py | 1 + tests/integrations/aws_lambda/client.py | 14 ++++--- .../test_clickhouse_driver.py | 1 + .../integrations/django/myapp/custom_urls.py | 1 + tests/integrations/django/myapp/settings.py | 1 - tests/integrations/django/myapp/urls.py | 1 + tests/integrations/gcp/test_gcp.py | 1 + .../integrations/starlette/test_starlette.py | 8 ++-- tests/test_profiler.py | 40 ++++++++++++------- 15 files changed, 72 insertions(+), 49 deletions(-) diff --git a/sentry_sdk/integrations/arq.py b/sentry_sdk/integrations/arq.py index f46d1204c5..ed045b854a 100644 --- a/sentry_sdk/integrations/arq.py +++ b/sentry_sdk/integrations/arq.py @@ -149,12 +149,12 @@ def event_processor(event, hint): extra = event.setdefault("extra", {}) extra["arq-job"] = { "task": ctx["job_name"], - "args": args - if _should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE, - "kwargs": kwargs - if _should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE, + "args": ( + args if _should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + kwargs if _should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), "retry": ctx["job_try"], } diff --git a/sentry_sdk/integrations/huey.py b/sentry_sdk/integrations/huey.py index 52b0e549a2..9641160099 100644 --- a/sentry_sdk/integrations/huey.py +++ b/sentry_sdk/integrations/huey.py @@ -73,12 +73,16 @@ def event_processor(event, hint): extra = event.setdefault("extra", {}) extra["huey-job"] = { "task": task.name, - "args": task.args - if _should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE, - "kwargs": task.kwargs - if _should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE, + "args": ( + task.args + if _should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + task.kwargs + if _should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), "retry": (task.default_retries or 0) - task.retries, } diff --git a/sentry_sdk/integrations/opentelemetry/integration.py b/sentry_sdk/integrations/opentelemetry/integration.py index e1a4318f67..9e62d1feca 100644 --- a/sentry_sdk/integrations/opentelemetry/integration.py +++ b/sentry_sdk/integrations/opentelemetry/integration.py @@ -3,6 +3,7 @@ are experimental and not suitable for production use. They may be changed or removed at any time without prior notice. """ + import sys from importlib import import_module diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index bcba558011..f736a74d0b 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -441,9 +441,9 @@ def get_dynamic_sampling_context(self): baggage = self.get_baggage() if baggage is not None: - self._propagation_context[ - "dynamic_sampling_context" - ] = baggage.dynamic_sampling_context() + self._propagation_context["dynamic_sampling_context"] = ( + baggage.dynamic_sampling_context() + ) return self._propagation_context["dynamic_sampling_context"] diff --git a/sentry_sdk/serializer.py b/sentry_sdk/serializer.py index 7925cf5ec8..51496f57ce 100644 --- a/sentry_sdk/serializer.py +++ b/sentry_sdk/serializer.py @@ -348,9 +348,9 @@ def _serialize_node_impl( should_repr_strings=should_repr_strings, is_databag=is_databag, is_request_body=is_request_body, - remaining_depth=remaining_depth - 1 - if remaining_depth is not None - else None, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), remaining_breadth=remaining_breadth, ) rv_dict[str_k] = v @@ -375,9 +375,9 @@ def _serialize_node_impl( should_repr_strings=should_repr_strings, is_databag=is_databag, is_request_body=is_request_body, - remaining_depth=remaining_depth - 1 - if remaining_depth is not None - else None, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), remaining_breadth=remaining_breadth, ) ) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 82ec994e14..80e9ace939 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -544,9 +544,9 @@ def get_trace_context(self): rv["status"] = self.status if self.containing_transaction: - rv[ - "dynamic_sampling_context" - ] = self.containing_transaction.get_baggage().dynamic_sampling_context() + rv["dynamic_sampling_context"] = ( + self.containing_transaction.get_baggage().dynamic_sampling_context() + ) return rv diff --git a/tests/integrations/asyncpg/test_asyncpg.py b/tests/integrations/asyncpg/test_asyncpg.py index 9177d68bdf..2a31c59dee 100644 --- a/tests/integrations/asyncpg/test_asyncpg.py +++ b/tests/integrations/asyncpg/test_asyncpg.py @@ -8,6 +8,7 @@ The tests use the following credentials to establish a database connection. """ + import os diff --git a/tests/integrations/aws_lambda/client.py b/tests/integrations/aws_lambda/client.py index 3c4816a477..298ebd920d 100644 --- a/tests/integrations/aws_lambda/client.py +++ b/tests/integrations/aws_lambda/client.py @@ -386,12 +386,14 @@ def repl(runtime, verbose): _REPL_CODE.format(line=line), b"", cleanup.append, - subprocess_kwargs={ - "stdout": subprocess.DEVNULL, - "stderr": subprocess.DEVNULL, - } - if not verbose - else {}, + subprocess_kwargs=( + { + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL, + } + if not verbose + else {} + ), ) for line in base64.b64decode(response["LogResult"]).splitlines(): diff --git a/tests/integrations/clickhouse_driver/test_clickhouse_driver.py b/tests/integrations/clickhouse_driver/test_clickhouse_driver.py index 6b0fa566d4..74a04fac44 100644 --- a/tests/integrations/clickhouse_driver/test_clickhouse_driver.py +++ b/tests/integrations/clickhouse_driver/test_clickhouse_driver.py @@ -4,6 +4,7 @@ docker run -d -p 18123:8123 -p9000:9000 --name clickhouse-test --ulimit nofile=262144:262144 --rm clickhouse/clickhouse-server ``` """ + import clickhouse_driver from clickhouse_driver import Client, connect diff --git a/tests/integrations/django/myapp/custom_urls.py b/tests/integrations/django/myapp/custom_urls.py index 6dfa2ed2f1..bc703e0afe 100644 --- a/tests/integrations/django/myapp/custom_urls.py +++ b/tests/integrations/django/myapp/custom_urls.py @@ -13,6 +13,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from __future__ import absolute_import try: diff --git a/tests/integrations/django/myapp/settings.py b/tests/integrations/django/myapp/settings.py index b8b083eb81..ac06d9204e 100644 --- a/tests/integrations/django/myapp/settings.py +++ b/tests/integrations/django/myapp/settings.py @@ -10,7 +10,6 @@ https://docs.djangoproject.com/en/2.0/ref/settings/ """ - # We shouldn't access settings while setting up integrations. Initialize SDK # here to provoke any errors that might occur. import sentry_sdk diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py index 0a62e4a076..706be13c3a 100644 --- a/tests/integrations/django/myapp/urls.py +++ b/tests/integrations/django/myapp/urls.py @@ -13,6 +13,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from __future__ import absolute_import try: diff --git a/tests/integrations/gcp/test_gcp.py b/tests/integrations/gcp/test_gcp.py index 678219dc8b..9c4e11e8d5 100644 --- a/tests/integrations/gcp/test_gcp.py +++ b/tests/integrations/gcp/test_gcp.py @@ -2,6 +2,7 @@ # GCP Cloud Functions unit tests """ + import json from textwrap import dedent import tempfile diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 329048e23c..202f8b53de 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -779,9 +779,11 @@ def test_middleware_partial_receive_send(sentry_init, capture_events): }, { "op": "middleware.starlette.receive", - "description": "_ASGIAdapter.send..receive" - if STARLETTE_VERSION < (0, 21) - else "_TestClientTransport.handle_request..receive", + "description": ( + "_ASGIAdapter.send..receive" + if STARLETTE_VERSION < (0, 21) + else "_TestClientTransport.handle_request..receive" + ), "tags": {"starlette.middleware_name": "ServerErrorMiddleware"}, }, { diff --git a/tests/test_profiler.py b/tests/test_profiler.py index 868b7b0ed5..94659ff02f 100644 --- a/tests/test_profiler.py +++ b/tests/test_profiler.py @@ -394,9 +394,11 @@ def static_method(): ), pytest.param( GetFrame().instance_method_wrapped()(), - "wrapped" - if sys.version_info < (3, 11) - else "GetFrame.instance_method_wrapped..wrapped", + ( + "wrapped" + if sys.version_info < (3, 11) + else "GetFrame.instance_method_wrapped..wrapped" + ), id="instance_method_wrapped", ), pytest.param( @@ -406,9 +408,11 @@ def static_method(): ), pytest.param( GetFrame().class_method_wrapped()(), - "wrapped" - if sys.version_info < (3, 11) - else "GetFrame.class_method_wrapped..wrapped", + ( + "wrapped" + if sys.version_info < (3, 11) + else "GetFrame.class_method_wrapped..wrapped" + ), id="class_method_wrapped", ), pytest.param( @@ -423,9 +427,11 @@ def static_method(): ), pytest.param( GetFrame().inherited_instance_method_wrapped()(), - "wrapped" - if sys.version_info < (3, 11) - else "GetFrameBase.inherited_instance_method_wrapped..wrapped", + ( + "wrapped" + if sys.version_info < (3, 11) + else "GetFrameBase.inherited_instance_method_wrapped..wrapped" + ), id="instance_method_wrapped", ), pytest.param( @@ -435,16 +441,20 @@ def static_method(): ), pytest.param( GetFrame().inherited_class_method_wrapped()(), - "wrapped" - if sys.version_info < (3, 11) - else "GetFrameBase.inherited_class_method_wrapped..wrapped", + ( + "wrapped" + if sys.version_info < (3, 11) + else "GetFrameBase.inherited_class_method_wrapped..wrapped" + ), id="inherited_class_method_wrapped", ), pytest.param( GetFrame().inherited_static_method(), - "inherited_static_method" - if sys.version_info < (3, 11) - else "GetFrameBase.inherited_static_method", + ( + "inherited_static_method" + if sys.version_info < (3, 11) + else "GetFrameBase.inherited_static_method" + ), id="inherited_static_method", ), ], From fca14a3121be3c325ecfd38c1e7265083a6d359f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 29 Jan 2024 10:05:48 +0100 Subject: [PATCH 64/82] Update test matrix --- .../workflows/test-integrations-databases.yml | 24 +++++++++++++++++++ .../test-integrations-web-frameworks-2.yml | 24 ------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test-integrations-databases.yml b/.github/workflows/test-integrations-databases.yml index c5b4de2be4..8239849de8 100644 --- a/.github/workflows/test-integrations-databases.yml +++ b/.github/workflows/test-integrations-databases.yml @@ -76,6 +76,14 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-pymongo-latest" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch + - name: Test redis latest + run: | + set -x # print commands that are executed + ./scripts/runtox.sh "py${{ matrix.python-version }}-redis-latest" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch + - name: Test rediscluster latest + run: | + set -x # print commands that are executed + ./scripts/runtox.sh "py${{ matrix.python-version }}-rediscluster-latest" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - name: Test sqlalchemy latest run: | set -x # print commands that are executed @@ -146,6 +154,14 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-pymongo" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch + - name: Test redis pinned + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-redis" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch + - name: Test rediscluster pinned + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-rediscluster" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - name: Test sqlalchemy pinned run: | set -x # print commands that are executed @@ -205,6 +221,14 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py2.7-pymongo" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch + - name: Test redis py27 + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py2.7-redis" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch + - name: Test rediscluster py27 + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py2.7-rediscluster" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - name: Test sqlalchemy py27 run: | set -x # print commands that are executed diff --git a/.github/workflows/test-integrations-web-frameworks-2.yml b/.github/workflows/test-integrations-web-frameworks-2.yml index 6971bf95db..a1c2db9aa3 100644 --- a/.github/workflows/test-integrations-web-frameworks-2.yml +++ b/.github/workflows/test-integrations-web-frameworks-2.yml @@ -66,14 +66,6 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-quart-latest" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - - name: Test redis latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-redis-latest" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - - name: Test rediscluster latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-rediscluster-latest" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - name: Test sanic latest run: | set -x # print commands that are executed @@ -142,14 +134,6 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-quart" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - - name: Test redis pinned - run: | - set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-redis" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - - name: Test rediscluster pinned - run: | - set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-rediscluster" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - name: Test sanic pinned run: | set -x # print commands that are executed @@ -207,14 +191,6 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py2.7-quart" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - - name: Test redis py27 - run: | - set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py2.7-redis" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - - name: Test rediscluster py27 - run: | - set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py2.7-rediscluster" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch - name: Test sanic py27 run: | set -x # print commands that are executed From 041e004f4d8261d3d746d8e064f4dd38eec35289 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 29 Jan 2024 10:50:18 +0100 Subject: [PATCH 65/82] updated apidocs --- sentry_sdk/api.py | 6 +++--- sentry_sdk/client.py | 8 ++++---- sentry_sdk/scope.py | 28 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index d7561cc34d..2d7072383f 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -94,7 +94,7 @@ def is_initialized(): Returns whether Sentry has been initialized or not. If a client is available Sentry is initialized. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ return Scope.get_client().is_active() @@ -129,7 +129,7 @@ def set_current_scope(new_current_scope): Sets the given scope as the new current scope overwriting the existing current scope. :param new_current_scope: The scope to set as the new current scope. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ scope._current_scope.set(new_current_scope) @@ -140,7 +140,7 @@ def set_isolation_scope(new_isolation_scope): Sets the given scope as the new isolation scope overwriting the existing isolation scope. :param new_isolation_scope: The scope to set as the new isolation scope. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ scope._isolation_scope.set(new_isolation_scope) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index c316c8e6e4..2b515dd200 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -157,7 +157,7 @@ class BaseClient: """ The basic definition of a client that is used for sending data to Sentry. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ options = _get_options() # type: Dict[str, Any] @@ -191,7 +191,7 @@ def is_active(self): """ Returns weither the client is active (able to send data to Sentry) - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ return False @@ -228,7 +228,7 @@ class NoopClient(BaseClient): """ A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ pass @@ -392,7 +392,7 @@ def is_active(self): """ Returns weither the client is active (able to send data to Sentry) - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ return True diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index f736a74d0b..d02ded1165 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -192,7 +192,7 @@ def get_current_scope(cls): """ Returns the current scope. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ current_scope = _current_scope.get() if current_scope is None: @@ -207,7 +207,7 @@ def get_isolation_scope(cls): """ Returns the isolation scope. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ isolation_scope = _isolation_scope.get() if isolation_scope is None: @@ -222,7 +222,7 @@ def get_global_scope(cls): """ Returns the global scope. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ global _global_scope if _global_scope is None: @@ -237,7 +237,7 @@ def _merge_scopes(cls, additional_scope=None, additional_scope_kwargs=None): Merges global, isolation and current scope into a new scope and adds the given additional scope or additional scope kwargs to it. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ if additional_scope and additional_scope_kwargs: raise TypeError("cannot provide scope and kwargs") @@ -272,7 +272,7 @@ def get_client(cls): This checks the current scope, the isolation scope and the global scope for a client. If no client is available a :py:class:`sentry_sdk.client.NoopClient` is returned. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ current_scope = _current_scope.get() if current_scope is not None and current_scope.client.is_active(): @@ -294,7 +294,7 @@ def set_client(self, client=None): :param client: The client to use in this scope. If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NoopClient`. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ self.client = client or NoopClient() @@ -304,7 +304,7 @@ def is_forked(self): """ Whether this scope is a fork of another scope. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ return self.original_scope is not None @@ -313,7 +313,7 @@ def fork(self): """ Returns a fork of this scope. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ forked_scope = copy(self) forked_scope.original_scope = self @@ -326,7 +326,7 @@ def isolate(self): Creates a new isolation scope for this scope. The new isolation scope will be a fork of the current isolation scope. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ isolation_scope = Scope.get_isolation_scope() forked_isolation_scope = isolation_scope.fork() @@ -441,9 +441,9 @@ def get_dynamic_sampling_context(self): baggage = self.get_baggage() if baggage is not None: - self._propagation_context["dynamic_sampling_context"] = ( - baggage.dynamic_sampling_context() - ) + self._propagation_context[ + "dynamic_sampling_context" + ] = baggage.dynamic_sampling_context() return self._propagation_context["dynamic_sampling_context"] @@ -1429,7 +1429,7 @@ def new_scope(): """ Context manager that forks the current scope and runs the wrapped code in it. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ current_scope = Scope.get_current_scope() forked_scope = current_scope.fork() @@ -1450,7 +1450,7 @@ def isolated_scope(): Context manager that forks the current isolation scope (and the related current scope) and runs the wrapped code in it. - .. versionadded:: 1.XX.0 + .. versionadded:: X.X.X """ # fork current scope current_scope = Scope.get_current_scope() From 689ad15466c6f16c6c4474c1a1a006283c06a257 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 5 Feb 2024 15:47:01 +0100 Subject: [PATCH 66/82] Cleanup --- sentry_sdk/client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 2b515dd200..3438ffd903 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -165,10 +165,6 @@ class BaseClient: monitor = None # type: Optional[Monitor] transport = None # type: Optional[Transport] - def __init__(self, *args, **kwargs): - # type: (*Any, **Any) -> None - return None - def __getstate__(self, *args, **kwargs): # type: (*Any, **Any) -> Any return {"options": {}} From d3917e0ce784a0b2997dd6a4939e659ef7be37a0 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 6 Feb 2024 15:47:48 +0100 Subject: [PATCH 67/82] formatting --- sentry_sdk/scope.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index d02ded1165..a33690b8a4 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -441,9 +441,9 @@ def get_dynamic_sampling_context(self): baggage = self.get_baggage() if baggage is not None: - self._propagation_context[ - "dynamic_sampling_context" - ] = baggage.dynamic_sampling_context() + self._propagation_context["dynamic_sampling_context"] = ( + baggage.dynamic_sampling_context() + ) return self._propagation_context["dynamic_sampling_context"] From f6cb4efb2cb6b2be702e6163f88252708d3398d8 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 09:03:49 +0100 Subject: [PATCH 68/82] Update sentry_sdk/scope.py Co-authored-by: Daniel Szoke --- sentry_sdk/scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index a33690b8a4..55047b7fcd 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -296,7 +296,7 @@ def set_client(self, client=None): .. versionadded:: X.X.X """ - self.client = client or NoopClient() + self.client = client if client is not None else NoopClient() @property def is_forked(self): From 72365d4bdc26ed6bf08e8101cb6b8f3088663704 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 09:34:12 +0100 Subject: [PATCH 69/82] Renamed NoopClient to NonRecordingClient to be more like otel --- docs/apidocs.rst | 2 +- sentry_sdk/client.py | 2 +- sentry_sdk/scope.py | 12 ++++++------ tests/test_api.py | 4 ++-- tests/test_scope.py | 30 +++++++++++++++--------------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/apidocs.rst b/docs/apidocs.rst index eb9f35ee75..27c8ef2f73 100644 --- a/docs/apidocs.rst +++ b/docs/apidocs.rst @@ -14,7 +14,7 @@ API Docs .. autoclass:: sentry_sdk.client.BaseClient :members: -.. autoclass:: sentry_sdk.client.NoopClient +.. autoclass:: sentry_sdk.client.NonRecordingClient :members: .. autoclass:: sentry_sdk.client._Client diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 3640784de9..9128210b27 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -220,7 +220,7 @@ def __exit__(self, exc_type, exc_value, tb): return None -class NoopClient(BaseClient): +class NonRecordingClient(BaseClient): """ A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 55047b7fcd..d853bfe4ad 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -176,7 +176,7 @@ def __init__(self, ty=None, client=None): self._name = None # type: Optional[str] self._propagation_context = None # type: Optional[Dict[str, Any]] - self.client = NoopClient() # type: sentry_sdk.client.BaseClient + self.client = NonRecordingClient() # type: sentry_sdk.client.BaseClient if client is not None: self.set_client(client) @@ -270,7 +270,7 @@ def get_client(cls): """ Returns the currently used :py:class:`sentry_sdk.Client`. This checks the current scope, the isolation scope and the global scope for a client. - If no client is available a :py:class:`sentry_sdk.client.NoopClient` is returned. + If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. .. versionadded:: X.X.X """ @@ -285,18 +285,18 @@ def get_client(cls): if _global_scope is not None: return _global_scope.client - return NoopClient() + return NonRecordingClient() def set_client(self, client=None): # type: (Optional[sentry_sdk.client.BaseClient]) -> None """ Sets the client for this scope. :param client: The client to use in this scope. - If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NoopClient`. + If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. .. versionadded:: X.X.X """ - self.client = client if client is not None else NoopClient() + self.client = client if client is not None else NonRecordingClient() @property def is_forked(self): @@ -1472,4 +1472,4 @@ def isolated_scope(): # Circular imports -from sentry_sdk.client import NoopClient +from sentry_sdk.client import NonRecordingClient diff --git a/tests/test_api.py b/tests/test_api.py index f754b928c6..7ed2019d1d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -16,7 +16,7 @@ start_transaction, ) -from sentry_sdk.client import Client, NoopClient +from sentry_sdk.client import Client, NonRecordingClient from sentry_sdk.hub import Hub from sentry_sdk.scope import Scope, ScopeType @@ -141,7 +141,7 @@ def test_is_initialized(): def test_get_client(): client = get_client() assert client is not None - assert client.__class__ == NoopClient + assert client.__class__ == NonRecordingClient assert not client.is_active() diff --git a/tests/test_scope.py b/tests/test_scope.py index 7ef2d101ad..9ca7db142c 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -2,7 +2,7 @@ import os import pytest from sentry_sdk import capture_exception, new_scope, isolated_scope -from sentry_sdk.client import Client, NoopClient +from sentry_sdk.client import Client, NonRecordingClient from sentry_sdk.scope import Scope, ScopeType try: @@ -169,7 +169,7 @@ def test_scope_client(): scope = Scope(ty="test_something") assert scope._type == "test_something" assert scope.client is not None - assert scope.client.__class__ == NoopClient + assert scope.client.__class__ == NonRecordingClient custom_client = Client() scope = Scope(ty="test_more", client=custom_client) @@ -207,7 +207,7 @@ def test_get_global_scope(): def test_get_client(): client = Scope.get_client() assert client is not None - assert client.__class__ == NoopClient + assert client.__class__ == NonRecordingClient assert not client.is_active() @@ -280,18 +280,18 @@ def test_get_global_scope_tags(): global_scope1 = Scope.get_global_scope() global_scope2 = Scope.get_global_scope() assert global_scope1 == global_scope2 - assert global_scope1.client.__class__ == NoopClient + assert global_scope1.client.__class__ == NonRecordingClient assert not global_scope1.client.is_active() - assert global_scope2.client.__class__ == NoopClient + assert global_scope2.client.__class__ == NonRecordingClient assert not global_scope2.client.is_active() global_scope1.set_tag("tag1", "value") tags_scope1 = global_scope1._tags tags_scope2 = global_scope2._tags assert tags_scope1 == tags_scope2 == {"tag1": "value"} - assert global_scope1.client.__class__ == NoopClient + assert global_scope1.client.__class__ == NonRecordingClient assert not global_scope1.client.is_active() - assert global_scope2.client.__class__ == NoopClient + assert global_scope2.client.__class__ == NonRecordingClient assert not global_scope2.client.is_active() @@ -329,18 +329,18 @@ def test_get_isolation_scope_tags(): isolation_scope1 = Scope.get_isolation_scope() isolation_scope2 = Scope.get_isolation_scope() assert isolation_scope1 == isolation_scope2 - assert isolation_scope1.client.__class__ == NoopClient + assert isolation_scope1.client.__class__ == NonRecordingClient assert not isolation_scope1.client.is_active() - assert isolation_scope2.client.__class__ == NoopClient + assert isolation_scope2.client.__class__ == NonRecordingClient assert not isolation_scope2.client.is_active() isolation_scope1.set_tag("tag1", "value") tags_scope1 = isolation_scope1._tags tags_scope2 = isolation_scope2._tags assert tags_scope1 == tags_scope2 == {"tag1": "value"} - assert isolation_scope1.client.__class__ == NoopClient + assert isolation_scope1.client.__class__ == NonRecordingClient assert not isolation_scope1.client.is_active() - assert isolation_scope2.client.__class__ == NoopClient + assert isolation_scope2.client.__class__ == NonRecordingClient assert not isolation_scope2.client.is_active() @@ -368,18 +368,18 @@ def test_get_current_scope_tags(): scope1 = Scope.get_current_scope() scope2 = Scope.get_current_scope() assert id(scope1) == id(scope2) - assert scope1.client.__class__ == NoopClient + assert scope1.client.__class__ == NonRecordingClient assert not scope1.client.is_active() - assert scope2.client.__class__ == NoopClient + assert scope2.client.__class__ == NonRecordingClient assert not scope2.client.is_active() scope1.set_tag("tag1", "value") tags_scope1 = scope1._tags tags_scope2 = scope2._tags assert tags_scope1 == tags_scope2 == {"tag1": "value"} - assert scope1.client.__class__ == NoopClient + assert scope1.client.__class__ == NonRecordingClient assert not scope1.client.is_active() - assert scope2.client.__class__ == NoopClient + assert scope2.client.__class__ == NonRecordingClient assert not scope2.client.is_active() From ae39a43a6d18fe5a61e57bfd7ebc87f235660d11 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 09:53:19 +0100 Subject: [PATCH 70/82] Added some comments --- sentry_sdk/scope.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index d853bfe4ad..c563686448 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -65,8 +65,20 @@ T = TypeVar("T") +# Holds data that will be added to **all** events sent by this process. +# In case this is a http server (think web framework) with multiple users +# the data will be added to events of all users. +# Typically this is used for process wide data such as the release. _global_scope = None # type: Optional[Scope] + +# Holds data for the active request. +# This is used to isolate data for different requests or users. +# The isolation scope is created by integrations and there should not +# be created manually _isolation_scope = ContextVar("isolation_scope", default=None) + +# Holds data for the active span. +# This can be used to manually add additional data to a span. _current_scope = ContextVar("current_scope", default=None) global_event_processors = [] # type: List[EventProcessor] From f908fe889cb5fe590c0c48bcd3a4f0c4dbafd441 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 10:04:13 +0100 Subject: [PATCH 71/82] Added the right version number --- sentry_sdk/api.py | 6 +++--- sentry_sdk/client.py | 8 ++++---- sentry_sdk/scope.py | 22 +++++++++++----------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 2d7072383f..23fa69ad6e 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -94,7 +94,7 @@ def is_initialized(): Returns whether Sentry has been initialized or not. If a client is available Sentry is initialized. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ return Scope.get_client().is_active() @@ -129,7 +129,7 @@ def set_current_scope(new_current_scope): Sets the given scope as the new current scope overwriting the existing current scope. :param new_current_scope: The scope to set as the new current scope. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ scope._current_scope.set(new_current_scope) @@ -140,7 +140,7 @@ def set_isolation_scope(new_isolation_scope): Sets the given scope as the new isolation scope overwriting the existing isolation scope. :param new_isolation_scope: The scope to set as the new isolation scope. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ scope._isolation_scope.set(new_isolation_scope) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 9128210b27..a47587ad49 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -157,7 +157,7 @@ class BaseClient: """ The basic definition of a client that is used for sending data to Sentry. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ options = _get_options() # type: Dict[str, Any] @@ -187,7 +187,7 @@ def is_active(self): """ Returns weither the client is active (able to send data to Sentry) - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ return False @@ -224,7 +224,7 @@ class NonRecordingClient(BaseClient): """ A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ pass @@ -388,7 +388,7 @@ def is_active(self): """ Returns weither the client is active (able to send data to Sentry) - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ return True diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c563686448..f0e10faa0b 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -204,7 +204,7 @@ def get_current_scope(cls): """ Returns the current scope. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ current_scope = _current_scope.get() if current_scope is None: @@ -219,7 +219,7 @@ def get_isolation_scope(cls): """ Returns the isolation scope. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ isolation_scope = _isolation_scope.get() if isolation_scope is None: @@ -234,7 +234,7 @@ def get_global_scope(cls): """ Returns the global scope. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ global _global_scope if _global_scope is None: @@ -249,7 +249,7 @@ def _merge_scopes(cls, additional_scope=None, additional_scope_kwargs=None): Merges global, isolation and current scope into a new scope and adds the given additional scope or additional scope kwargs to it. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ if additional_scope and additional_scope_kwargs: raise TypeError("cannot provide scope and kwargs") @@ -284,7 +284,7 @@ def get_client(cls): This checks the current scope, the isolation scope and the global scope for a client. If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ current_scope = _current_scope.get() if current_scope is not None and current_scope.client.is_active(): @@ -306,7 +306,7 @@ def set_client(self, client=None): :param client: The client to use in this scope. If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ self.client = client if client is not None else NonRecordingClient() @@ -316,7 +316,7 @@ def is_forked(self): """ Whether this scope is a fork of another scope. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ return self.original_scope is not None @@ -325,7 +325,7 @@ def fork(self): """ Returns a fork of this scope. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ forked_scope = copy(self) forked_scope.original_scope = self @@ -338,7 +338,7 @@ def isolate(self): Creates a new isolation scope for this scope. The new isolation scope will be a fork of the current isolation scope. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ isolation_scope = Scope.get_isolation_scope() forked_isolation_scope = isolation_scope.fork() @@ -1441,7 +1441,7 @@ def new_scope(): """ Context manager that forks the current scope and runs the wrapped code in it. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ current_scope = Scope.get_current_scope() forked_scope = current_scope.fork() @@ -1462,7 +1462,7 @@ def isolated_scope(): Context manager that forks the current isolation scope (and the related current scope) and runs the wrapped code in it. - .. versionadded:: X.X.X + .. versionadded:: 2.0.0 """ # fork current scope current_scope = Scope.get_current_scope() From 9247623fb07dcc962e77c192e2ba1356b5918ce5 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 10:55:54 +0100 Subject: [PATCH 72/82] Added universal lock and use it when changing the global scope --- sentry_sdk/scope.py | 7 +++++- sentry_sdk/utils.py | 54 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index f0e10faa0b..504d43bcdf 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -30,9 +30,11 @@ from sentry_sdk.utils import ( capture_internal_exceptions, ContextVar, + create_universal_lock, event_from_exception, exc_info_from_error, logger, + UniversalLock, ) if TYPE_CHECKING: @@ -83,6 +85,8 @@ global_event_processors = [] # type: List[EventProcessor] +_lock = create_universal_lock() + class ScopeType(Enum): CURRENT = "current" @@ -238,7 +242,8 @@ def get_global_scope(cls): """ global _global_scope if _global_scope is None: - _global_scope = Scope(ty=ScopeType.GLOBAL) + with UniversalLock(_lock): + _global_scope = Scope(ty=ScopeType.GLOBAL) return _global_scope diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 8f13ba4a05..40ddd3c990 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -15,6 +15,16 @@ from decimal import Decimal from numbers import Real +try: + from gevent.lock import Semaphore as GeventSemaphore # type: ignore +except ImportError: + GeventSemaphore = None + +try: + import asyncio +except ImportError: + asyncio = None # type: ignore + try: # Python 3 from urllib.parse import parse_qs @@ -1759,3 +1769,47 @@ def is_module_patched(*args, **kwargs): def is_gevent(): # type: () -> bool return is_module_patched("threading") or is_module_patched("_thread") + + +class UniversalLock: + """ + A universal lock that works when used in threading, gevent or asyncio concurrency model. + """ + + def __init__(self, lock): + # type: (Union[threading.Lock, GeventSemaphore, asyncio.Lock]) -> None + self._lock = lock + + def acquire(self): + # type: () -> None + self._lock.acquire() + + def release(self): + # type: () -> None + self._lock.release() + + def __enter__(self): + # type: () -> None + self.acquire() + + def __exit__(self, exc_type, exc_value, traceback): + # type: (Any, Any, Any) -> None + self.release() + + +def create_universal_lock(): + # type: () -> Union[threading.Lock, GeventSemaphore, asyncio.Lock] + """ + Create a lock that works in threading, gevent and asyncio + """ + if GeventSemaphore is not None and is_gevent(): + lock = GeventSemaphore() + return lock + elif threading is not None: + lock = threading.Lock() + return lock + elif asyncio is not None: + lock = asyncio.Lock() + return lock + else: + raise RuntimeError("No supported concurrency library found.") From 6be5e5f3a8eda4b627c7755c958c8e403674d5e9 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 12:36:41 +0100 Subject: [PATCH 73/82] Added tests for UniversalLock --- tests/test_utils.py | 78 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 147064b541..aec9aaa922 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,11 +1,13 @@ import pytest import re import sys +import threading from datetime import timedelta from sentry_sdk._compat import duration_in_milliseconds from sentry_sdk.utils import ( Components, + create_universal_lock, Dsn, get_default_release, get_error_message, @@ -20,6 +22,7 @@ serialize_frame, is_sentry_url, _get_installed_modules, + UniversalLock, ) import sentry_sdk @@ -36,6 +39,20 @@ # Python 2 FileNotFoundError = IOError +try: + import gevent +except ImportError: + gevent = None + +try: + import asyncio +except ImportError: + asyncio = None + + +requires_gevent = pytest.mark.skipif(gevent is None, reason="gevent not enabled") +requires_asyncio = pytest.mark.skipif(asyncio is None, reason="asyncio not enabled") + def _normalize_distribution_name(name): # type: (str) -> str @@ -607,3 +624,64 @@ def test_default_release_empty_string(): ) def test_duration_in_milliseconds(timedelta, expected_milliseconds): assert duration_in_milliseconds(timedelta) == expected_milliseconds + + +global_test_var = { + "val": 0, +} + +lock = create_universal_lock() + + +def _modify_global(): + global global_test_var + for _ in range(100000): + with UniversalLock(lock): + old_val = global_test_var["val"] + global_test_var["val"] = old_val + 1 + + +@pytest.mark.forked +def test_universal_lock_threading(): + threads = [] + for _ in range(10): + t = threading.Thread(target=_modify_global) + threads.append(t) + t.start() + + for t in threads: + t.join() + + assert global_test_var["val"] == 100000 * 10 + + +# TODO: this test does not fail without the lock. +@pytest.mark.forked +@requires_gevent +def test_universal_lock_gevent(): + greenlets = [] + for _ in range(10): + greenlets.append(gevent.spawn(_modify_global)) + + gevent.joinall(greenlets) + + assert global_test_var["val"] == 100000 * 10 + + +async def _modify_global_async(): + global global_test_var + for _ in range(100000): + with UniversalLock(lock): + old_val = global_test_var["val"] + global_test_var["val"] = old_val + 1 + + +# TODO: this test does not fail without the lock. +@pytest.mark.forked +@pytest.mark.asyncio +@requires_asyncio +async def test_universal_lock_asyncio(): + tasks = [_modify_global_async() for _ in range(10)] + await asyncio.gather(*tasks) + + assert global_test_var["val"] == 100000 * 10 From 3b3537fc0f3cd8ff026430e1a4a9f2b0e53a0b14 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 13:59:38 +0100 Subject: [PATCH 74/82] Made properties of BaseClient actual properties, not class vars --- sentry_sdk/client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index a47587ad49..989f006518 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -159,11 +159,12 @@ class BaseClient: .. versionadded:: 2.0.0 """ + def __init__(self, options=_get_options()): + self.options = options # type: Dict[str, Any] - options = _get_options() # type: Dict[str, Any] - metrics_aggregator = None # type: Optional[MetricsAggregator] - monitor = None # type: Optional[Monitor] - transport = None # type: Optional[Transport] + self.transport = None # type: Optional[Transport] + self.monitor = None # type: Optional[Monitor] + self.metrics_aggregator = None # type: Optional[MetricsAggregator] def __getstate__(self, *args, **kwargs): # type: (*Any, **Any) -> Any @@ -242,8 +243,7 @@ class _Client(BaseClient): def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None - self.options = get_options(*args, **kwargs) # type: Dict[str, Any] - + super().__init__(options=get_options(*args, **kwargs)) self._init_impl() def __getstate__(self): From 27522d0c665c413d31121f7831dfa611f3e5a1b5 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 14:14:11 +0100 Subject: [PATCH 75/82] Fixed default value --- sentry_sdk/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 989f006518..de140525e6 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -159,8 +159,11 @@ class BaseClient: .. versionadded:: 2.0.0 """ - def __init__(self, options=_get_options()): - self.options = options # type: Dict[str, Any] + + def __init__(self, options=None): + self.options = ( + options if options is not None else _get_options() + ) # type: Dict[str, Any] self.transport = None # type: Optional[Transport] self.monitor = None # type: Optional[Monitor] From a821b93e5240d2b1c93195dc69f7d4db4acc269d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 14:14:43 +0100 Subject: [PATCH 76/82] Do not call async tests in python 2 --- tests/test_utils.py | 25 ------------------------- tests/test_utils_py3.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 tests/test_utils_py3.py diff --git a/tests/test_utils.py b/tests/test_utils.py index aec9aaa922..0da68bf4d0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -44,14 +44,8 @@ except ImportError: gevent = None -try: - import asyncio -except ImportError: - asyncio = None - requires_gevent = pytest.mark.skipif(gevent is None, reason="gevent not enabled") -requires_asyncio = pytest.mark.skipif(asyncio is None, reason="asyncio not enabled") def _normalize_distribution_name(name): @@ -666,22 +660,3 @@ def test_universal_lock_gevent(): gevent.joinall(greenlets) assert global_test_var["val"] == 100000 * 10 - - -async def _modify_global_async(): - global global_test_var - for _ in range(100000): - with UniversalLock(lock): - old_val = global_test_var["val"] - global_test_var["val"] = old_val + 1 - - -# TODO: this test does not fail without the lock. -@pytest.mark.forked -@pytest.mark.asyncio -@requires_asyncio -async def test_universal_lock_asyncio(): - tasks = [_modify_global_async() for _ in range(10)] - await asyncio.gather(*tasks) - - assert global_test_var["val"] == 100000 * 10 diff --git a/tests/test_utils_py3.py b/tests/test_utils_py3.py new file mode 100644 index 0000000000..c7d93925b8 --- /dev/null +++ b/tests/test_utils_py3.py @@ -0,0 +1,36 @@ +import pytest + +from sentry_sdk.utils import UniversalLock, create_universal_lock + +try: + import asyncio +except ImportError: + asyncio = None + + +requires_asyncio = pytest.mark.skipif(asyncio is None, reason="asyncio not enabled") + +global_test_var = { + "val": 0, +} + +lock = create_universal_lock() + + +async def _modify_global_async(): + global global_test_var + for _ in range(100000): + with UniversalLock(lock): + old_val = global_test_var["val"] + global_test_var["val"] = old_val + 1 + + +# TODO: this test does not fail without the lock. +@pytest.mark.forked +@pytest.mark.asyncio +@requires_asyncio +async def test_universal_lock_asyncio(): + tasks = [_modify_global_async() for _ in range(10)] + await asyncio.gather(*tasks) + + assert global_test_var["val"] == 100000 * 10 From fcb3f6881bde6d0e8044bacece019fb13120d3b8 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 14:20:42 +0100 Subject: [PATCH 77/82] Make it run in old python --- sentry_sdk/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index de140525e6..56d2a22007 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -161,6 +161,7 @@ class BaseClient: """ def __init__(self, options=None): + # type: (Optional[Dict[str, Any]]) -> None self.options = ( options if options is not None else _get_options() ) # type: Dict[str, Any] @@ -246,7 +247,7 @@ class _Client(BaseClient): def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None - super().__init__(options=get_options(*args, **kwargs)) + super(_Client, self).__init__(options=get_options(*args, **kwargs)) self._init_impl() def __getstate__(self): From 161a1accd9089dbb377b7b929604cd1cda0d03e9 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 14:41:50 +0100 Subject: [PATCH 78/82] Make it work with old python --- sentry_sdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 56d2a22007..c180262afa 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -153,7 +153,7 @@ def _get_options(*args, **kwargs): module_not_found_error = ImportError # type: ignore -class BaseClient: +class BaseClient(object): """ The basic definition of a client that is used for sending data to Sentry. From b7c0db5ad44fcc7e392f6654f681c696006adf13 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 14:51:59 +0100 Subject: [PATCH 79/82] Make api consistent --- sentry_sdk/api.py | 19 ++++--------------- sentry_sdk/scope.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 23fa69ad6e..ee1990ab29 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,6 +1,5 @@ import inspect -from sentry_sdk import scope from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.hub import Hub from sentry_sdk.scope import Scope, new_scope, isolated_scope @@ -123,26 +122,16 @@ def get_global_scope(): return Scope.get_global_scope() +@scopemethod def set_current_scope(new_current_scope): # type: (Scope) -> None - """ - Sets the given scope as the new current scope overwriting the existing current scope. - :param new_current_scope: The scope to set as the new current scope. - - .. versionadded:: 2.0.0 - """ - scope._current_scope.set(new_current_scope) + return Scope.set_current_scope(new_current_scope) +@scopemethod def set_isolation_scope(new_isolation_scope): # type: (Scope) -> None - """ - Sets the given scope as the new isolation scope overwriting the existing isolation scope. - :param new_isolation_scope: The scope to set as the new isolation scope. - - .. versionadded:: 2.0.0 - """ - scope._isolation_scope.set(new_isolation_scope) + return Scope.set_isolation_scope(new_isolation_scope) @hubmethod diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 504d43bcdf..df63a1a378 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -217,6 +217,17 @@ def get_current_scope(cls): return current_scope + @classmethod + def set_current_scope(cls, new_current_scope): + # type: (Scope) -> None + """ + Sets the given scope as the new current scope overwriting the existing current scope. + :param new_current_scope: The scope to set as the new current scope. + + .. versionadded:: 2.0.0 + """ + _current_scope.set(new_current_scope) + @classmethod def get_isolation_scope(cls): # type: () -> Scope @@ -232,6 +243,17 @@ def get_isolation_scope(cls): return isolation_scope + @classmethod + def set_isolation_scope(cls, new_isolation_scope): + # type: (Scope) -> None + """ + Sets the given scope as the new isolation scope overwriting the existing isolation scope. + :param new_isolation_scope: The scope to set as the new isolation scope. + + .. versionadded:: 2.0.0 + """ + _isolation_scope.set(new_isolation_scope) + @classmethod def get_global_scope(cls): # type: () -> Scope From 56b266b57e5f82d3da6214c9bdb505366550a282 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 15:17:05 +0100 Subject: [PATCH 80/82] Removed useless universal lock, because locking is not the responsibility of the sdk here --- sentry_sdk/scope.py | 7 +------ sentry_sdk/utils.py | 44 ---------------------------------------- tests/test_utils.py | 45 ----------------------------------------- tests/test_utils_py3.py | 36 --------------------------------- 4 files changed, 1 insertion(+), 131 deletions(-) delete mode 100644 tests/test_utils_py3.py diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index df63a1a378..bf304d5647 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -30,11 +30,9 @@ from sentry_sdk.utils import ( capture_internal_exceptions, ContextVar, - create_universal_lock, event_from_exception, exc_info_from_error, logger, - UniversalLock, ) if TYPE_CHECKING: @@ -85,8 +83,6 @@ global_event_processors = [] # type: List[EventProcessor] -_lock = create_universal_lock() - class ScopeType(Enum): CURRENT = "current" @@ -264,8 +260,7 @@ def get_global_scope(cls): """ global _global_scope if _global_scope is None: - with UniversalLock(_lock): - _global_scope = Scope(ty=ScopeType.GLOBAL) + _global_scope = Scope(ty=ScopeType.GLOBAL) return _global_scope diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 40ddd3c990..9d4fbdbcb8 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1769,47 +1769,3 @@ def is_module_patched(*args, **kwargs): def is_gevent(): # type: () -> bool return is_module_patched("threading") or is_module_patched("_thread") - - -class UniversalLock: - """ - A universal lock that works when used in threading, gevent or asyncio concurrency model. - """ - - def __init__(self, lock): - # type: (Union[threading.Lock, GeventSemaphore, asyncio.Lock]) -> None - self._lock = lock - - def acquire(self): - # type: () -> None - self._lock.acquire() - - def release(self): - # type: () -> None - self._lock.release() - - def __enter__(self): - # type: () -> None - self.acquire() - - def __exit__(self, exc_type, exc_value, traceback): - # type: (Any, Any, Any) -> None - self.release() - - -def create_universal_lock(): - # type: () -> Union[threading.Lock, GeventSemaphore, asyncio.Lock] - """ - Create a lock that works in threading, gevent and asyncio - """ - if GeventSemaphore is not None and is_gevent(): - lock = GeventSemaphore() - return lock - elif threading is not None: - lock = threading.Lock() - return lock - elif asyncio is not None: - lock = asyncio.Lock() - return lock - else: - raise RuntimeError("No supported concurrency library found.") diff --git a/tests/test_utils.py b/tests/test_utils.py index 0da68bf4d0..dfdbf0a0f0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,13 +1,11 @@ import pytest import re import sys -import threading from datetime import timedelta from sentry_sdk._compat import duration_in_milliseconds from sentry_sdk.utils import ( Components, - create_universal_lock, Dsn, get_default_release, get_error_message, @@ -22,7 +20,6 @@ serialize_frame, is_sentry_url, _get_installed_modules, - UniversalLock, ) import sentry_sdk @@ -618,45 +615,3 @@ def test_default_release_empty_string(): ) def test_duration_in_milliseconds(timedelta, expected_milliseconds): assert duration_in_milliseconds(timedelta) == expected_milliseconds - - -global_test_var = { - "val": 0, -} - -lock = create_universal_lock() - - -def _modify_global(): - global global_test_var - for _ in range(100000): - with UniversalLock(lock): - old_val = global_test_var["val"] - global_test_var["val"] = old_val + 1 - - -@pytest.mark.forked -def test_universal_lock_threading(): - threads = [] - for _ in range(10): - t = threading.Thread(target=_modify_global) - threads.append(t) - t.start() - - for t in threads: - t.join() - - assert global_test_var["val"] == 100000 * 10 - - -# TODO: this test does not fail without the lock. -@pytest.mark.forked -@requires_gevent -def test_universal_lock_gevent(): - greenlets = [] - for _ in range(10): - greenlets.append(gevent.spawn(_modify_global)) - - gevent.joinall(greenlets) - - assert global_test_var["val"] == 100000 * 10 diff --git a/tests/test_utils_py3.py b/tests/test_utils_py3.py deleted file mode 100644 index c7d93925b8..0000000000 --- a/tests/test_utils_py3.py +++ /dev/null @@ -1,36 +0,0 @@ -import pytest - -from sentry_sdk.utils import UniversalLock, create_universal_lock - -try: - import asyncio -except ImportError: - asyncio = None - - -requires_asyncio = pytest.mark.skipif(asyncio is None, reason="asyncio not enabled") - -global_test_var = { - "val": 0, -} - -lock = create_universal_lock() - - -async def _modify_global_async(): - global global_test_var - for _ in range(100000): - with UniversalLock(lock): - old_val = global_test_var["val"] - global_test_var["val"] = old_val + 1 - - -# TODO: this test does not fail without the lock. -@pytest.mark.forked -@pytest.mark.asyncio -@requires_asyncio -async def test_universal_lock_asyncio(): - tasks = [_modify_global_async() for _ in range(10)] - await asyncio.gather(*tasks) - - assert global_test_var["val"] == 100000 * 10 From f7f8d422af3dc5fc592a0c0f4d1d7c593d8384c9 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 Feb 2024 15:19:03 +0100 Subject: [PATCH 81/82] Apply suggestions from code review Co-authored-by: Ivana Kellyerova Co-authored-by: Daniel Szoke --- sentry_sdk/client.py | 4 ++-- sentry_sdk/scope.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index c180262afa..0e18dffbeb 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -190,7 +190,7 @@ def should_send_default_pii(self): def is_active(self): # type: () -> bool """ - Returns weither the client is active (able to send data to Sentry) + Returns whether the client is active (able to send data to Sentry) .. versionadded:: 2.0.0 """ @@ -390,7 +390,7 @@ def _capture_envelope(envelope): def is_active(self): # type: () -> bool """ - Returns weither the client is active (able to send data to Sentry) + Returns whether the client is active (able to send data to Sentry) .. versionadded:: 2.0.0 """ diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index bf304d5647..5bbb58a622 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -73,7 +73,7 @@ # Holds data for the active request. # This is used to isolate data for different requests or users. -# The isolation scope is created by integrations and there should not +# The isolation scope is usually created by integrations, but may also # be created manually _isolation_scope = ContextVar("isolation_scope", default=None) From b57cde631722fd599b768be8c989986fac11af7d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 13 Feb 2024 08:33:31 +0100 Subject: [PATCH 82/82] Prevent infinite recursion by using default options. --- sentry_sdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index c180262afa..03742e5cbd 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -163,7 +163,7 @@ class BaseClient(object): def __init__(self, options=None): # type: (Optional[Dict[str, Any]]) -> None self.options = ( - options if options is not None else _get_options() + options if options is not None else DEFAULT_OPTIONS ) # type: Dict[str, Any] self.transport = None # type: Optional[Transport]