diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 28827ec73a..dad2bfb658 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -13,6 +13,8 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## Removed +- Dropped support for gevent versions below 20.9. +- Dropped support for greenlet versions below 0.4.17. - Removed the deprecated Hub class and all uses of hub throughout the SDK in arguments, options, etc. Use a scope instead. - Removed the `auto_session_tracing` decorator. Use `track_session` instead. diff --git a/sentry_sdk/_init_implementation.py b/sentry_sdk/_init_implementation.py index 923fcf6df8..3051ae010b 100644 --- a/sentry_sdk/_init_implementation.py +++ b/sentry_sdk/_init_implementation.py @@ -1,7 +1,9 @@ +import re import warnings from typing import TYPE_CHECKING import sentry_sdk +from sentry_sdk.utils import logger, parse_version if TYPE_CHECKING: from typing import Any, ContextManager, Optional @@ -41,11 +43,32 @@ def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: c.close() -def _check_python_deprecations() -> None: - # Since we're likely to deprecate Python versions in the future, I'm keeping - # this handy function around. Use this to detect the Python version used and - # to output logger.warning()s if it's deprecated. - pass +def _check_version_deprecations() -> None: + try: + import gevent + + gevent_version = tuple( + int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2] + ) + if gevent_version < (20, 9): + logger.warning( + "sentry-sdk 3.x supports gevent 20.9.0 or newer. " + "Please upgrade gevent or downgrade to sentry-sdk 2.x." + ) + except Exception: + pass + + try: + import greenlet + + greenlet_version = parse_version(greenlet.__version__) + if greenlet_version is not None and greenlet_version < (0, 4, 17): + logger.warning( + "sentry-sdk 3.x supports greenlet 0.4.17 or newer. " + "Please upgrade greenlet or downgrade to sentry-sdk 2.x." + ) + except Exception: + pass def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": @@ -55,7 +78,7 @@ def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": """ client = sentry_sdk.Client(*args, **kwargs) sentry_sdk.get_global_scope().set_client(client) - _check_python_deprecations() + _check_version_deprecations() rv = _InitGuard(client) return rv diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index 423bb50f9d..30c1a64d9a 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -1,5 +1,6 @@ import inspect import sys +from contextvars import ContextVar from functools import wraps from typing import TYPE_CHECKING @@ -10,14 +11,16 @@ from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise +from sentry_sdk.utils import capture_internal_exceptions, reraise if TYPE_CHECKING: from typing import Any, Awaitable, Callable, Optional, TypeVar, Union F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) -_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) +_ai_pipeline_name: "ContextVar[Optional[str]]" = ContextVar( + "ai_pipeline_name", default=None +) def set_ai_pipeline_name(name: "Optional[str]") -> None: diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e8414d3f91..2168eabb9a 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -7,6 +7,7 @@ import uuid import warnings from collections.abc import Iterable, Mapping +from contextvars import ContextVar from datetime import datetime, timezone from importlib import import_module from typing import TYPE_CHECKING, Dict, List, cast, overload @@ -50,7 +51,6 @@ ) from sentry_sdk.utils import ( AnnotatedValue, - ContextVar, capture_internal_exceptions, current_stacktrace, datetime_from_isoformat, @@ -93,7 +93,7 @@ I = TypeVar("I", bound=Integration) # noqa: E741 -_client_init_debug = ContextVar("client_init_debug") +_client_init_debug: "ContextVar[bool]" = ContextVar("client_init_debug") SDK_INFO: "SDKInfo" = { diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 858bf273f2..eac6cb901a 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -41,8 +41,6 @@ should_propagate_trace, ) from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, SENSITIVE_DATA_SUBSTITUTE, AnnotatedValue, _register_control_flow_exception, @@ -108,14 +106,6 @@ def setup_once() -> None: version = parse_version(AIOHTTP_VERSION) _check_minimum_version(AioHttpIntegration, version) - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The aiohttp integration for Sentry requires Python 3.7+ " - " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - # In the aiohttp integration, all of their HTTP responses are Exceptions. # Because they have to be raised and handled by the framework, we need to # register the exceptions as control flow exceptions so that we don't diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 9921e6c1ff..eb06c9c49d 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -6,6 +6,7 @@ import inspect import sys +from contextvars import ContextVar from copy import deepcopy from functools import partial from typing import TYPE_CHECKING @@ -41,9 +42,6 @@ ) from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - ContextVar, _get_installed_modules, capture_internal_exceptions, event_from_exception, @@ -61,7 +59,9 @@ from sentry_sdk.tracing import Span -_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") +_asgi_middleware_applied: "ContextVar[bool]" = ContextVar( + "sentry_asgi_middleware_applied" +) _DEFAULT_TRANSACTION_NAME = "generic ASGI request" @@ -113,7 +113,6 @@ class SentryAsgiMiddleware: def __init__( self, app: "Any", - unsafe_context_data: bool = False, transaction_style: str = "endpoint", mechanism_type: str = "asgi", span_origin: str = "manual", @@ -126,15 +125,7 @@ def __init__( data to sent events and basic handling for exceptions bubbling up through the middleware. - :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. """ - if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise RuntimeError( - "The ASGI middleware for Sentry requires Python 3.7+ " - "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) if transaction_style not in TRANSACTION_STYLE_VALUES: raise ValueError( "Invalid value for transaction_style: %s (must be in %s)" diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index a0e9014666..a0cc88081f 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -1,13 +1,14 @@ import weakref +from contextvars import ContextVar from typing import TYPE_CHECKING import sentry_sdk from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import ContextVar, logger +from sentry_sdk.utils import logger if TYPE_CHECKING: - from typing import Optional + from typing import Any, Optional from sentry_sdk._types import Event, Hint @@ -16,7 +17,7 @@ class DedupeIntegration(Integration): identifier = "dedupe" def __init__(self) -> None: - self._last_seen = ContextVar("last-seen") + self._last_seen: "ContextVar[Any]" = ContextVar("last-seen") @staticmethod def setup_once() -> None: diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 0f8dfcd24a..b5bd550be5 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -23,15 +23,12 @@ record_sql_queries, ) from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, SENSITIVE_DATA_SUBSTITUTE, AnnotatedValue, capture_internal_exceptions, ensure_integration_enabled, event_from_exception, has_data_collection_enabled, - logger, transaction_from_function, walk_exception_chain, ) @@ -346,19 +343,6 @@ def _patch_channels() -> None: except ImportError: return - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because channels may not be used at all in - # the current process. That is the case when running traditional WSGI - # workers in gunicorn+gevent and the websocket stuff in a separate - # process. - logger.warning( - "We detected that you are using Django channels 2.0." - + CONTEXTVARS_ERROR_MESSAGE - ) - from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl patch_channels_asgi_handler_impl(AsgiHandler) @@ -370,16 +354,6 @@ def _patch_django_asgi_handler() -> None: except ImportError: return - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because Django's ASGI stuff may not be used - # at all. - logger.warning( - "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE - ) - from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl patch_django_asgi_handler_impl(ASGIHandler) diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index df287e6436..c5d52fe72d 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -99,7 +99,6 @@ async def sentry_patched_asgi_handler( middleware = SentryAsgiMiddleware( old_app.__get__(self, cls), - unsafe_context_data=True, span_origin=DjangoIntegration.origin, http_methods_to_capture=integration.http_methods_to_capture, )._run_asgi3 @@ -154,7 +153,6 @@ async def sentry_patched_asgi_handler( middleware = SentryAsgiMiddleware( lambda _scope: old_app.__get__(self, cls), - unsafe_context_data=True, span_origin=DjangoIntegration.origin, http_methods_to_capture=integration.http_methods_to_capture, ) diff --git a/sentry_sdk/integrations/django/middleware.py b/sentry_sdk/integrations/django/middleware.py index 01ed8962e0..dcd114e795 100644 --- a/sentry_sdk/integrations/django/middleware.py +++ b/sentry_sdk/integrations/django/middleware.py @@ -2,6 +2,7 @@ Create spans from Django middleware invocations """ +from contextvars import ContextVar from functools import wraps from typing import TYPE_CHECKING @@ -11,7 +12,6 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - ContextVar, capture_internal_exceptions, transaction_from_function, ) @@ -24,7 +24,7 @@ F = TypeVar("F", bound=Callable[..., Any]) -_import_string_should_wrap_middleware = ContextVar( +_import_string_should_wrap_middleware: "ContextVar[bool]" = ContextVar( "import_string_should_wrap_middleware" ) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 8a3f09ffa0..dd80505132 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -89,7 +89,6 @@ def __init__( ) -> None: super().__init__( app=app, - unsafe_context_data=False, transaction_style="endpoint", mechanism_type="asgi", span_origin=span_origin, diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 9f1ece76c8..3e91361ce5 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -19,8 +19,6 @@ from sentry_sdk.tracing import TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, capture_internal_exceptions, ensure_integration_enabled, event_from_exception, @@ -80,14 +78,6 @@ def setup_once() -> None: SanicIntegration.version = parse_version(SANIC_VERSION) _check_minimum_version(SanicIntegration, SanicIntegration.version) - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The sanic integration for Sentry requires Python 3.7+ " - " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - if SANIC_VERSION.startswith("0.8."): # Sanic 0.8 and older creates a logger named "root" and puts a # stringified version of every exception in there (without exc_info), diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 8963fc9e53..6bc26924ce 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -75,7 +75,6 @@ def __init__( ) -> None: super().__init__( app=app, - unsafe_context_data=False, transaction_style="endpoint", mechanism_type="asgi", span_origin=span_origin, diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index e71bf94d12..96c2298139 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -19,8 +19,6 @@ from sentry_sdk.tracing import TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, AnnotatedValue, capture_internal_exceptions, ensure_integration_enabled, @@ -54,14 +52,6 @@ class TornadoIntegration(Integration): def setup_once() -> None: _check_minimum_version(TornadoIntegration, TORNADO_VERSION) - if not HAS_REAL_CONTEXTVARS: - # Tornado is async. We better have contextvars or we're going to leak - # state between requests. - raise DidNotEnable( - "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" - + CONTEXTVARS_ERROR_MESSAGE - ) - ignore_logger("tornado.access") old_execute = RequestHandler._execute diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index b0156a9829..3d0cc4b876 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -1,4 +1,5 @@ import sys +from contextvars import ContextVar from functools import partial from typing import TYPE_CHECKING @@ -17,7 +18,6 @@ from sentry_sdk.tracing import Span, TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - ContextVar, capture_internal_exceptions, event_from_exception, has_data_collection_enabled, @@ -56,7 +56,9 @@ def __call__( pass -_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") +_wsgi_middleware_applied: "ContextVar[bool]" = ContextVar( + "sentry_wsgi_middleware_applied" +) _DEFAULT_TRANSACTION_NAME = "generic WSGI request" diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index fd0da3b6c0..a80c274740 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -4,6 +4,7 @@ import warnings from collections import deque from contextlib import contextmanager +from contextvars import ContextVar from copy import copy, deepcopy from datetime import datetime, timezone from enum import Enum @@ -49,7 +50,6 @@ is_ignored_span, ) from sentry_sdk.utils import ( - ContextVar, capture_internal_exception, capture_internal_exceptions, datetime_from_isoformat, @@ -118,11 +118,15 @@ # This is used to isolate data for different requests or users. # The isolation scope is usually created by integrations, but may also # be created manually -_isolation_scope = ContextVar("isolation_scope", default=None) +_isolation_scope: "ContextVar[Optional[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) +_current_scope: "ContextVar[Optional[Scope]]" = ContextVar( + "current_scope", default=None +) global_event_processors: "List[EventProcessor]" = [] @@ -467,8 +471,9 @@ def get_client(cls) -> "sentry_sdk.client.BaseClient": If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. """ current_scope = _current_scope.get() + try: - client = current_scope.client + client = current_scope.client # type: ignore[union-attr] except AttributeError: client = None @@ -477,7 +482,7 @@ def get_client(cls) -> "sentry_sdk.client.BaseClient": isolation_scope = _isolation_scope.get() try: - client = isolation_scope.client + client = isolation_scope.client # type: ignore[union-attr] except AttributeError: client = None diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index a6ece4faf1..ec32ee17b3 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -5,7 +5,6 @@ import logging import math import os -import random import re import subprocess import sys @@ -13,6 +12,7 @@ import time from collections import namedtuple from contextlib import contextmanager +from contextvars import ContextVar from datetime import datetime, timezone from decimal import Decimal from functools import partial, partialmethod, wraps @@ -1317,133 +1317,6 @@ def parse_version(version: str) -> "Optional[Tuple[int, ...]]": return release_tuple -def _is_contextvars_broken() -> bool: - """ - Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars. - """ - try: - import gevent - from gevent.monkey import is_object_patched - - # Get the MAJOR and MINOR version numbers of Gevent - version_tuple = tuple( - [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]] - ) - if is_object_patched("threading", "local"): - # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching - # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine. - # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609 - # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support - # for contextvars, is able to patch both thread locals and contextvars, in - # that case, check if contextvars are effectively patched. - if ( - # Gevent 20.9.0+ - (sys.version_info >= (3, 7) and version_tuple >= (20, 9)) - # Gevent 20.5.0+ or Python < 3.7 - or (is_object_patched("contextvars", "ContextVar")) - ): - return False - - return True - except ImportError: - pass - - try: - import greenlet - from eventlet.patcher import is_monkey_patched # type: ignore - - greenlet_version = parse_version(greenlet.__version__) - - if greenlet_version is None: - logger.error( - "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__." - ) - return False - - if is_monkey_patched("thread") and greenlet_version < (0, 5): - return True - except ImportError: - pass - - return False - - -def _make_threadlocal_contextvars(local: type) -> type: - class ContextVar: - # Super-limited impl of ContextVar - - def __init__(self, name: str, default: "Any" = None) -> None: - self._name = name - self._default = default - self._local = local() - self._original_local = local() - - def get(self, default: "Any" = None) -> "Any": - return getattr(self._local, "value", default or self._default) - - def set(self, value: "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: "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] - - return ContextVar - - -def _get_contextvars() -> "Tuple[bool, type]": - """ - Figure out the "right" contextvars installation to use. Returns a - `contextvars.ContextVar`-like class with a limited API. - - See https://docs.sentry.io/platforms/python/contextvars/ for more information. - """ - if not _is_contextvars_broken(): - # aiocontextvars is a PyPI package that ensures that the contextvars - # backport (also a PyPI package) works with asyncio under Python 3.6 - # - # Import it if available. - if sys.version_info < (3, 7): - # `aiocontextvars` is absolutely required for functional - # contextvars on Python 3.6. - try: - from aiocontextvars import ContextVar - - return True, ContextVar - except ImportError: - pass - else: - # On Python 3.7 contextvars are functional. - try: - from contextvars import ContextVar - - return True, ContextVar - except ImportError: - pass - - # Fall back to basic thread-local usage. - - from threading import local - - return False, _make_threadlocal_contextvars(local) - - -HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars() - -CONTEXTVARS_ERROR_MESSAGE = """ - -With asyncio/ASGI applications, the Sentry SDK requires a functional -installation of `contextvars` to avoid leaking scope/context data across -requests. - -Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information. -""" - _is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False) # These exceptions won't set the span status to error if they occur. Use @@ -1501,7 +1374,7 @@ def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]": return qualname_from_function(func) -disable_capture_event = ContextVar("disable_capture_event") +disable_capture_event: "ContextVar[bool]" = ContextVar("disable_capture_event") class ServerlessTimeoutWarning(Exception): # noqa: N818 diff --git a/tests/utils/test_contextvars.py b/tests/utils/test_contextvars.py deleted file mode 100644 index 50881314c1..0000000000 --- a/tests/utils/test_contextvars.py +++ /dev/null @@ -1,52 +0,0 @@ -import random -import time -from unittest import mock - -import pytest - - -def _run_contextvar_threaded_test(): - import threading - - # Need to explicitly call _get_contextvars because the SDK has already - # decided upon gevent on import. - from sentry_sdk import utils - - _, ContextVar = utils._get_contextvars() # noqa: N806 - - ts = [] - - var = ContextVar("test_contextvar_leaks") - - success = [] - - def run(): - value = int(random.random() * 1000) - var.set(value) - - for _ in range(100): - time.sleep(0) - assert var.get(None) == value - - success.append(1) - - for _ in range(20): - t = threading.Thread(target=run) - t.start() - ts.append(t) - - for t in ts: - t.join() - - assert len(success) == 20 - - -@pytest.mark.forked -def test_leaks(maybe_monkeypatched_threading): - _run_contextvar_threaded_test() - - -@pytest.mark.forked -@mock.patch("sentry_sdk.utils._is_contextvars_broken", return_value=True) -def test_leaks_when_is_contextvars_broken_is_false(maybe_monkeypatched_threading): - _run_contextvar_threaded_test()