From e83239cc8c4580494142c017e57cb060156a0205 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 24 Oct 2025 08:59:14 +0200 Subject: [PATCH 01/16] meta: Prepare initial migration guide --- MIGRATION_GUIDE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 53396a37ba..d38654dcc9 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,3 +1,17 @@ +# Sentry SDK 3.0 Migration Guide + + +Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of what's changed. Looking for a more digestable summary? See the [guide in the docs](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x) with the most common migration patterns. + +## New Features + +## Changed + +## Removed + +## Deprecated + + # Sentry SDK 2.0 Migration Guide Looking to upgrade from Sentry SDK 1.x to 2.x? Here's a comprehensive list of what's changed. Looking for a more digestable summary? See the [guide in the docs](https://docs.sentry.io/platforms/python/migration/1.x-to-2.x) with the most common migration patterns. From ac3c02b452ebec03039b383b18523f4f7fce94db Mon Sep 17 00:00:00 2001 From: Alex Alderman Webb Date: Thu, 30 Oct 2025 09:26:01 +0100 Subject: [PATCH 02/16] feat: Turn unraisable exception integration on by default (#5044) Add `UnraisablehookIntegration` to the default integrations list. --- sentry_sdk/integrations/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 677d34a81e..d71b1f5ff6 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -1,3 +1,4 @@ +import sys from abc import ABC, abstractmethod from threading import Lock from typing import TYPE_CHECKING @@ -64,6 +65,11 @@ def iter_default_integrations( "sentry_sdk.integrations.threading.ThreadingIntegration", ] +if sys.version_info >= (3, 8): + _DEFAULT_INTEGRATIONS.append( + "sentry_sdk.integrations.unraisablehook.UnraisablehookIntegration" + ) + _AUTO_ENABLING_INTEGRATIONS = [ "sentry_sdk.integrations.aiohttp.AioHttpIntegration", "sentry_sdk.integrations.anthropic.AnthropicIntegration", From 18dbeae37b579507ba085aa8a0c35a2b41054458 Mon Sep 17 00:00:00 2001 From: Alex Alderman Webb Date: Thu, 19 Mar 2026 14:32:53 +0100 Subject: [PATCH 03/16] fix: Do not raise from None (#5709) Stop raising exceptions `from None` in the ASGI and asyncio integrations. Closes https://github.com/getsentry/sentry-python/issues/5624 --- scripts/find_raise_from_none.py | 8 +------- sentry_sdk/integrations/asyncio.py | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/scripts/find_raise_from_none.py b/scripts/find_raise_from_none.py index 63b2b84333..089a14a1df 100644 --- a/scripts/find_raise_from_none.py +++ b/scripts/find_raise_from_none.py @@ -42,17 +42,11 @@ def main(): for module_path in walk_package_modules(): scan_file(module_path) - # TODO: Investigate why we suppress exception chains here. - ignored_raises = { - pathlib.Path("sentry_sdk/integrations/asgi.py"): 2, - pathlib.Path("sentry_sdk/integrations/asyncio.py"): 1, - } - raise_from_none_count = { file: len(occurences) for file, occurences in RaiseFromNoneVisitor.line_numbers.items() } - if raise_from_none_count != ignored_raises: + if raise_from_none_count: exc = Exception("Detected unexpected raise ... from None.") exc.add_note( "Raise ... from None suppresses chained exceptions, removing valuable context." diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index 4fc600d36f..c7e25a6a76 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -173,8 +173,8 @@ async def _task_with_sentry_span_creation() -> "Any": with span_ctx if span_ctx else nullcontext(): try: result = await coro - except StopAsyncIteration as e: - raise e from None + except StopAsyncIteration: + raise except Exception: reraise(*_capture_exception()) From 30fef6f6635092a91db69df319c10971dc11cb43 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 28 Jul 2026 13:12:19 +0200 Subject: [PATCH 04/16] Remove suppress_chained_exceptions --- sentry_sdk/consts.py | 1 - sentry_sdk/integrations/asgi.py | 18 ------------------ 2 files changed, 19 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index c6db94d780..3abaa4b6d9 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -86,7 +86,6 @@ class CompressionAlgo(Enum): "before_send_span": Optional[ Callable[[SpanJSON, Hint], Optional[SpanJSON]] ], - "suppress_asgi_chained_exceptions": Optional[bool], "data_collection": Optional[DataCollectionUserOptions], }, total=False, diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index e1157dda77..9921e6c1ff 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -205,15 +205,6 @@ async def _run_app( return await self.app(scope, receive, send) except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_lifespan_exception(exc) - raise exc from None - exc_info = sys.exc_info() with capture_internal_exceptions(): self._capture_lifespan_exception(exc) @@ -365,15 +356,6 @@ async def _sentry_wrapped_send( ) except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_request_exception(exc) - raise exc from None - exc_info = sys.exc_info() with capture_internal_exceptions(): self._capture_request_exception(exc) From 55852b9d3b47ea73f466f9f7257ce24e57498669 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 10:03:20 +0200 Subject: [PATCH 05/16] docs: Add changes to migration guide (#6914) --- MIGRATION_GUIDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index d38654dcc9..00d0f3ac70 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -5,8 +5,12 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## New Features + ## Changed +- The UnraisableHookIntegration is now enabled by default. +- We now don't suppress chained exceptions in the ASGI and asyncio integrations by default. The related `suppress_asgi_chained_exceptions` experimental option was removed. + ## Removed ## Deprecated From eb8a968b44546cc6f639c1fbc9291b86974fabe0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 14:41:51 +0200 Subject: [PATCH 06/16] ref: Drop hub related code in new major (#6919) Remove everything hub related, including all sorts of compatibility shims around hubs/scopes. Also remove deprecated session methods. `configure_scope` and `push_scope` removal coming in a future PR. #### Issues Closes https://github.com/getsentry/sentry-python/issues/5001 --- MIGRATION_GUIDE.md | 3 + docs/apidocs.rst | 3 - sentry_sdk/__init__.py | 4 - sentry_sdk/hub.py | 741 ------------------ sentry_sdk/integrations/sanic.py | 2 +- sentry_sdk/integrations/threading.py | 17 +- sentry_sdk/profiler/transaction_profiler.py | 30 - sentry_sdk/scope.py | 2 +- sentry_sdk/sessions.py | 90 +-- sentry_sdk/tracing.py | 62 -- sentry_sdk/transport.py | 25 - tests/integrations/conftest.py | 20 +- .../integrations/threading/test_threading.py | 71 -- tests/new_scopes_compat/__init__.py | 7 - tests/new_scopes_compat/conftest.py | 9 - .../test_new_scopes_compat.py | 275 ------- .../test_new_scopes_compat_event.py | 498 ------------ tests/profiler/test_transaction_profiler.py | 19 - tests/test_basics.py | 138 ---- tests/test_client.py | 26 - tests/test_sessions.py | 105 +-- tests/test_transport.py | 20 - tests/tracing/test_deprecated.py | 27 - 23 files changed, 12 insertions(+), 2182 deletions(-) delete mode 100644 sentry_sdk/hub.py delete mode 100644 tests/new_scopes_compat/__init__.py delete mode 100644 tests/new_scopes_compat/conftest.py delete mode 100644 tests/new_scopes_compat/test_new_scopes_compat.py delete mode 100644 tests/new_scopes_compat/test_new_scopes_compat_event.py diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 00d0f3ac70..28827ec73a 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -13,6 +13,9 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## Removed +- 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. + ## Deprecated diff --git a/docs/apidocs.rst b/docs/apidocs.rst index a3c8a6e150..ffe265b276 100644 --- a/docs/apidocs.rst +++ b/docs/apidocs.rst @@ -2,9 +2,6 @@ API Docs ======== -.. autoclass:: sentry_sdk.Hub - :members: - .. autoclass:: sentry_sdk.Scope :members: diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 9163a06641..e47021ee1f 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -8,7 +8,6 @@ from sentry_sdk.api import * # noqa # isort: skip __all__ = [ # noqa - "Hub", "Scope", "Client", "Transport", @@ -66,6 +65,3 @@ init_debug_support() del init_debug_support - -# circular imports -from sentry_sdk.hub import Hub diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py deleted file mode 100644 index b17444d06e..0000000000 --- a/sentry_sdk/hub.py +++ /dev/null @@ -1,741 +0,0 @@ -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import ( - get_client, - get_current_scope, - get_global_scope, - get_isolation_scope, -) -from sentry_sdk._compat import with_metaclass -from sentry_sdk.client import Client -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.scope import _ScopeManager -from sentry_sdk.tracing import ( - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.utils import ( - ContextVar, - logger, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.tracing import TransactionKwargs - - T = TypeVar("T") - -else: - - def overload(x: "T") -> "T": - return x - - -class SentryHubDeprecationWarning(DeprecationWarning): - """ - A custom deprecation warning to inform users that the Hub is deprecated. - """ - - _MESSAGE = ( - "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " - "Please consult our 1.x to 2.x migration guide for details on how to migrate " - "`Hub` usage to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" - ) - - def __init__(self, *_: object) -> None: - super().__init__(self._MESSAGE) - - -@contextmanager -def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": - """Utility function to suppress deprecation warnings for the Hub.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) - yield - - -_local = ContextVar("sentry_current_hub") - - -class HubMeta(type): - @property - def current(cls) -> "Hub": - """Returns the current instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - rv = _local.get(None) - if rv is None: - with _suppress_hub_deprecation_warning(): - # This will raise a deprecation warning; suppress it since we already warned above. - rv = Hub(GLOBAL_HUB) - _local.set(rv) - return rv - - @property - def main(cls) -> "Hub": - """Returns the main instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - return GLOBAL_HUB - - -class Hub(with_metaclass(HubMeta)): # type: ignore - """ - .. deprecated:: 2.0.0 - The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. - - The hub wraps the concurrency management of the SDK. Each thread has - its own hub but the hub might transfer with the flow of execution if - context vars are available. - - If the hub is used with a with statement it's temporarily activated. - """ - - _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] - _scope: "Optional[Scope]" = None - - # Mypy doesn't pick up on the metaclass. - - if TYPE_CHECKING: - current: "Hub" = None # type: ignore[assignment] - main: "Optional[Hub]" = None - - def __init__( - self, - client_or_hub: "Optional[Union[Hub, Client]]" = None, - scope: "Optional[Any]" = None, - ) -> None: - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - - current_scope = None - - if isinstance(client_or_hub, Hub): - client = get_client() - if scope is None: - # hub cloning is going on, we use a fork of the current/isolation scope for context manager - scope = get_isolation_scope().fork() - current_scope = get_current_scope().fork() - else: - client = client_or_hub - get_global_scope().set_client(client) - - if scope is None: # so there is no Hub cloning going on - # just the current isolation scope is used for context manager - scope = get_isolation_scope() - current_scope = get_current_scope() - - if current_scope is None: - # just the current current scope is used for context manager - current_scope = get_current_scope() - - self._stack = [(client, scope)] # type: ignore - self._last_event_id: "Optional[str]" = None - self._old_hubs: "List[Hub]" = [] - - self._old_current_scopes: "List[Scope]" = [] - self._old_isolation_scopes: "List[Scope]" = [] - self._current_scope: "Scope" = current_scope - self._scope: "Scope" = scope - - def __enter__(self) -> "Hub": - self._old_hubs.append(Hub.current) - _local.set(self) - - current_scope = get_current_scope() - self._old_current_scopes.append(current_scope) - scope._current_scope.set(self._current_scope) - - isolation_scope = get_isolation_scope() - self._old_isolation_scopes.append(isolation_scope) - scope._isolation_scope.set(self._scope) - - return self - - def __exit__( - self, - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[Any]", - ) -> None: - old = self._old_hubs.pop() - _local.set(old) - - old_current_scope = self._old_current_scopes.pop() - scope._current_scope.set(old_current_scope) - - old_isolation_scope = self._old_isolation_scopes.pop() - scope._isolation_scope.set(old_isolation_scope) - - def run( - self, - callback: "Callable[[], T]", - ) -> "T": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Runs a callback in the context of the hub. Alternatively the - with statement can be used on the hub directly. - """ - with self: - return callback() - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Any": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. - - Returns the integration for this hub by name or class. If there - is no client bound or the client does not have that integration - then `None` is returned. - - If the return value is not `None` the hub is guaranteed to have a - client attached. - """ - return get_client().get_integration(name_or_class) - - @property - def client(self) -> "Optional[BaseClient]": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Please use :py:func:`sentry_sdk.api.get_client` instead. - - Returns the current client on the hub. - """ - client = get_client() - - if not client.is_active(): - return None - - return client - - @property - def scope(self) -> "Scope": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Returns the current scope on the hub. - """ - return get_isolation_scope() - - def last_event_id(self) -> "Optional[str]": - """ - Returns the last event ID. - - .. deprecated:: 1.40.5 - This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. - """ - logger.warning( - "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." - ) - return self._last_event_id - - def bind_client( - self, - new: "Optional[BaseClient]", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.set_client` instead. - - Binds a new client to the hub. - """ - get_global_scope().set_client(new) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. - - Captures an event. - - Alias of :py:meth:`sentry_sdk.Scope.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 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. - """ - last_event_id = get_current_scope().capture_event( - event, hint, scope=scope, **scope_kwargs - ) - - 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: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. - - Captures a 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 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._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_message( - message, level=level, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. - - Captures an exception. - - 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_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._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_exception( - error, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. - - 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. - """ - get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_span` instead. - - 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`. - """ - scope = get_current_scope() - return scope.start_span(instrumenter=instrumenter, **kwargs) - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. - - 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`. - """ - scope = get_current_scope() - - # For backwards compatibility, we allow passing the scope as the hub. - # We need a major release to make this nice. (if someone searches the code: deprecated) - # Type checking disabled for this line because deprecated keys are not allowed in the type signature. - kwargs["hub"] = scope # type: ignore - - return scope.start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - ) -> "Transaction": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. - - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers=environ_or_headers, op=op, name=name, source=source - ) - - @overload - def push_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def push_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def push_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - if callback is not None: - with self.push_scope() as scope: - callback(scope) - return None - - return _ScopeManager(self) - - def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pops a scope layer from the stack. - - Try to use the context manager :py:meth:`push_scope` instead. - """ - rv = self._stack.pop() - assert self._stack, "stack must have at least one layer" - return rv - - @overload - def configure_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def configure_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def configure_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - scope = get_isolation_scope() - - if continue_trace: - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - def start_session( - self, - session_mode: str = "application", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_session` instead. - - Starts a new session. - """ - get_isolation_scope().start_session( - session_mode=session_mode, - ) - - def end_session(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.end_session` instead. - - Ends the current session if there is one. - """ - get_isolation_scope().end_session() - - def stop_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. - - Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - get_isolation_scope().stop_auto_session_tracking() - - def resume_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. - - Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - get_isolation_scope().resume_auto_session_tracking() - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.flush` instead. - - Alias for :py:meth:`sentry_sdk.client._Client.flush` - """ - return get_client().flush(timeout=timeout, callback=callback) - - def get_traceparent(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. - - Returns the traceparent either from the active span or from the scope. - """ - current_scope = get_current_scope() - traceparent = current_scope.get_traceparent() - - if traceparent is None: - isolation_scope = get_isolation_scope() - traceparent = isolation_scope.get_traceparent() - - return traceparent - - def get_baggage(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. - - Returns Baggage either from the active span or from the scope. - """ - current_scope = get_current_scope() - baggage = current_scope.get_baggage() - - if baggage is None: - isolation_scope = get_isolation_scope() - baggage = isolation_scope.get_baggage() - - if baggage is not None: - return baggage.serialize() - - return None - - def iter_trace_propagation_headers( - self, span: "Optional[Span]" = None - ) -> "Generator[Tuple[str, str], None, None]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. - - 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. - """ - return get_current_scope().iter_trace_propagation_headers( - span=span, - ) - - def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. - - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - return get_current_scope().trace_propagation_meta( - span=span, - ) - - -with _suppress_hub_deprecation_warning(): - # Suppress deprecation warning for the Hub here, since we still always - # import this module. - GLOBAL_HUB = Hub() -_local.set(GLOBAL_HUB) - - -# Circular imports -from sentry_sdk import scope diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 2d839d1c61..9f1ece76c8 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -241,7 +241,7 @@ async def _context_exit( response_status = None if response is None else response.status # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception - # happens while trying to end the transaction, we still attempt to exit the hub. + # happens while trying to end the transaction, we still attempt to exit the scope. with capture_internal_exceptions(): span = request.ctx._sentry_root_span if isinstance(span, StreamedSpan): diff --git a/sentry_sdk/integrations/threading.py b/sentry_sdk/integrations/threading.py index f3ba046332..606caa852e 100644 --- a/sentry_sdk/integrations/threading.py +++ b/sentry_sdk/integrations/threading.py @@ -11,7 +11,6 @@ from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - logger, reraise, ) @@ -27,23 +26,9 @@ class ThreadingIntegration(Integration): identifier = "threading" - def __init__( - self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True - ) -> None: - if propagate_hub is not None: - logger.warning( - "Deprecated: propagate_hub is deprecated. This will be removed in the future." - ) - - # Note: propagate_hub did not have any effect on propagation of scope data - # scope data was always propagated no matter what the value of propagate_hub was - # This is why the default for propagate_scope is True - + def __init__(self, propagate_scope: bool = True) -> None: self.propagate_scope = propagate_scope - if propagate_hub is not None: - self.propagate_scope = propagate_hub - @staticmethod def setup_once() -> None: old_start = Thread.start diff --git a/sentry_sdk/profiler/transaction_profiler.py b/sentry_sdk/profiler/transaction_profiler.py index fe65774c0b..e0b5e34426 100644 --- a/sentry_sdk/profiler/transaction_profiler.py +++ b/sentry_sdk/profiler/transaction_profiler.py @@ -33,7 +33,6 @@ import threading import time import uuid -import warnings from abc import ABC, abstractmethod from collections import deque from typing import TYPE_CHECKING @@ -200,7 +199,6 @@ def __init__( self, sampled: "Optional[bool]", start_ns: int, - hub: "Optional[sentry_sdk.Hub]" = None, scheduler: "Optional[Scheduler]" = None, ) -> None: self.scheduler = _scheduler if scheduler is None else scheduler @@ -230,16 +228,6 @@ def __init__( self.unique_samples = 0 - # Backwards compatibility with the old hub property - self._hub: "Optional[sentry_sdk.Hub]" = None - if hub is not None: - self._hub = hub - warnings.warn( - "The `hub` parameter is deprecated. Please do not use it.", - DeprecationWarning, - stacklevel=2, - ) - def update_active_thread_id(self) -> None: self.active_thread_id = get_current_thread_meta()[0] logger.debug( @@ -502,24 +490,6 @@ def valid(self) -> bool: return True - @property - def hub(self) -> "Optional[sentry_sdk.Hub]": - warnings.warn( - "The `hub` attribute is deprecated. Please do not access it.", - DeprecationWarning, - stacklevel=2, - ) - return self._hub - - @hub.setter - def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: - warnings.warn( - "The `hub` attribute is deprecated. Please do not set it.", - DeprecationWarning, - stacklevel=2, - ) - self._hub = value - class Scheduler(ABC): mode: "ProfilerMode" = "unknown" diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 09e8876d6d..fd0da3b6c0 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -139,7 +139,7 @@ class ScopeType(Enum): class _ScopeManager: - def __init__(self, hub: "Optional[Any]" = None) -> None: + def __init__(self) -> None: self._old_scopes: "List[Scope]" = [] def __enter__(self) -> "Scope": diff --git a/sentry_sdk/sessions.py b/sentry_sdk/sessions.py index aabf874fcd..ea4a9a8757 100644 --- a/sentry_sdk/sessions.py +++ b/sentry_sdk/sessions.py @@ -1,5 +1,4 @@ import os -import warnings from contextlib import contextmanager from threading import Event, Lock, Thread from typing import TYPE_CHECKING @@ -10,76 +9,7 @@ from sentry_sdk.utils import format_timestamp if TYPE_CHECKING: - from typing import Any, Callable, Dict, Generator, List, Optional, Union - - -def is_auto_session_tracking_enabled( - hub: "Optional[sentry_sdk.Hub]" = None, -) -> "Union[Any, bool, None]": - """DEPRECATED: Utility function to find out if session tracking is enabled.""" - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - - should_track = hub.scope._force_auto_session_tracking - - if should_track is None: - client_options = hub.client.options if hub.client else {} - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking( - hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: Use track_session instead - Starts and stops a session automatically around a block. - """ - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "Use track_session instead.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - should_track = is_auto_session_tracking_enabled(hub) - if should_track: - hub.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - hub.end_session() - - -def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: - """ - DEPRECATED: Utility function to find out if session tracking is enabled. - """ - - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - return _is_auto_session_tracking_enabled(scope) + from typing import Any, Callable, Dict, Generator, List, Optional def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: @@ -95,24 +25,6 @@ def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: return should_track -@contextmanager -def auto_session_tracking_scope( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: This function is a deprecated alias for track_session. - Starts and stops a session automatically around a block. - """ - - warnings.warn( - "This function is a deprecated alias for track_session and will be removed in the next major release.", - DeprecationWarning, - stacklevel=2, - ) - - with track_session(scope, session_mode=session_mode): - yield - - @contextmanager def track_session( scope: "sentry_sdk.Scope", session_mode: str = "application" diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 5c088b936b..747bc7facc 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -76,9 +76,6 @@ class SpanKwargs(TypedDict, total=False): description: str """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" - hub: "Optional[sentry_sdk.Hub]" - """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" - status: str """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" @@ -240,7 +237,6 @@ class Span: .. deprecated:: 2.15.0 Please use the `name` parameter, instead. :param name: A string describing what operation is being performed within the span. - :param hub: The hub to use for this span. .. deprecated:: 2.0.0 Please use the `scope` parameter, instead. @@ -268,7 +264,6 @@ class Span: "_tags", "_data", "_span_recorder", - "hub", "_context_manager_state", "_containing_transaction", "scope", @@ -287,7 +282,6 @@ def __init__( sampled: "Optional[bool]" = None, op: "Optional[str]" = None, description: "Optional[str]" = None, - hub: "Optional[sentry_sdk.Hub]" = None, # deprecated status: "Optional[str]" = None, containing_transaction: "Optional[Transaction]" = None, start_timestamp: "Optional[Union[datetime, float]]" = None, @@ -303,7 +297,6 @@ def __init__( self.op = op self.description = name or description self.status = status - self.hub = hub # backwards compatibility self.scope = scope self.origin = origin self._measurements: "Dict[str, MeasurementValue]" = {} @@ -313,15 +306,6 @@ def __init__( self._flags: "Dict[str, bool]" = {} self._flags_capacity = 10 - if hub is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use `scope` instead.", - DeprecationWarning, - stacklevel=2, - ) - - self.scope = self.scope or hub.scope - if start_timestamp is None: start_timestamp = datetime.now(timezone.utc) elif isinstance(start_timestamp, float): @@ -915,38 +899,6 @@ def containing_transaction(self) -> "Transaction": # reference. return self - def _get_scope_from_finish_args( - self, - scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - ) -> "Optional[sentry_sdk.Scope]": - """ - Logic to get the scope from the arguments passed to finish. This - function exists for backwards compatibility with the old finish. - - TODO: Remove this function in the next major version. - """ - scope_or_hub = scope_arg - if hub_arg is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", - DeprecationWarning, - stacklevel=3, - ) - - scope_or_hub = hub_arg - - if isinstance(scope_or_hub, sentry_sdk.Hub): - warnings.warn( - "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", - DeprecationWarning, - stacklevel=3, - ) - - return scope_or_hub.scope - - return scope_or_hub - def _get_log_representation(self) -> str: return "{op}transaction <{name}>".format( op=("<" + self.op + "> " if self.op else ""), name=self.name @@ -956,8 +908,6 @@ def finish( self, scope: "Optional[sentry_sdk.Scope]" = None, end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, ) -> "Optional[str]": """Finishes the transaction and sends it to Sentry. All finished spans in the transaction will also be sent to Sentry. @@ -966,9 +916,6 @@ def finish( If not provided, the current Scope will be used. :param end_timestamp: Optional timestamp that should be used as timestamp instead of the current time. - :param hub: The hub to use for this transaction. - This argument is DEPRECATED. Please use the `scope` - parameter, instead. :return: The event ID if the transaction was sent to Sentry, otherwise None. @@ -977,10 +924,6 @@ def finish( # This transaction is already finished, ignore. return None - # For backwards compatibility, we must handle the case where `scope` - # or `hub` could both either be a `Scope` or a `Hub`. - scope = self._get_scope_from_finish_args(scope, hub) - scope = scope or self.scope or sentry_sdk.get_current_scope() client = sentry_sdk.get_client() @@ -1345,12 +1288,7 @@ def finish( self, scope: "Optional[sentry_sdk.Scope]" = None, end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, ) -> "Optional[str]": - """ - The `hub` parameter is deprecated. Please use the `scope` parameter, instead. - """ pass def set_measurement( diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 2b71fee429..d98b8597fa 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -42,7 +42,6 @@ import certifi import urllib3 -import sentry_sdk from sentry_sdk.consts import EndpointType from sentry_sdk.envelope import Envelope, Item, PayloadRef from sentry_sdk.utils import ( @@ -234,9 +233,6 @@ def __init__(self: "Self", options: "Dict[str, Any]") -> None: self._pool = self._make_pool() - # Backwards compatibility for deprecated `self.hub_class` attribute - self._hub_cls = sentry_sdk.Hub - experiments = options.get("_experiments", {}) compression_level = experiments.get( "transport_compression_level", @@ -715,27 +711,6 @@ def flush( self._worker.submit(lambda: self._flush_client_reports(force=True)) self._worker.flush(timeout, callback) - @staticmethod - def _warn_hub_cls() -> None: - """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" - warnings.warn( - "The `hub_cls` attribute is deprecated and will be removed in a future release.", - DeprecationWarning, - stacklevel=3, - ) - - @property - def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - return self._hub_cls - - @hub_cls.setter - def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - self._hub_cls = value - class HttpTransport(BaseHttpTransport): if TYPE_CHECKING: diff --git a/tests/integrations/conftest.py b/tests/integrations/conftest.py index 70a4b498d7..d97ba3265f 100644 --- a/tests/integrations/conftest.py +++ b/tests/integrations/conftest.py @@ -7,28 +7,16 @@ def capture_exceptions(monkeypatch): def inner(): errors = set() - old_capture_event_hub = sentry_sdk.Hub.capture_event - old_capture_event_scope = sentry_sdk.Scope.capture_event + old_capture_event = sentry_sdk.Scope.capture_event - def capture_event_hub(self, event, hint=None, scope=None): - """ - Can be removed when we remove push_scope and the Hub from the SDK. - """ + def capture_event(self, event, hint=None, scope=None): if hint: if "exc_info" in hint: error = hint["exc_info"][1] errors.add(error) - return old_capture_event_hub(self, event, hint=hint, scope=scope) + return old_capture_event(self, event, hint=hint, scope=scope) - def capture_event_scope(self, event, hint=None, scope=None): - if hint: - if "exc_info" in hint: - error = hint["exc_info"][1] - errors.add(error) - return old_capture_event_scope(self, event, hint=hint, scope=scope) - - monkeypatch.setattr(sentry_sdk.Hub, "capture_event", capture_event_hub) - monkeypatch.setattr(sentry_sdk.Scope, "capture_event", capture_event_scope) + monkeypatch.setattr(sentry_sdk.Scope, "capture_event", capture_event) return errors diff --git a/tests/integrations/threading/test_threading.py b/tests/integrations/threading/test_threading.py index 9c06ce24a7..70a4d47e37 100644 --- a/tests/integrations/threading/test_threading.py +++ b/tests/integrations/threading/test_threading.py @@ -38,77 +38,6 @@ def crash(): assert not events -@pytest.mark.filterwarnings("ignore:.*:pytest.PytestUnhandledThreadExceptionWarning") -@pytest.mark.parametrize("propagate_hub", (True, False)) -def test_propagates_hub(sentry_init, capture_events, propagate_hub): - sentry_init( - default_integrations=False, - integrations=[ThreadingIntegration(propagate_hub=propagate_hub)], - ) - events = capture_events() - - def stage1(): - sentry_sdk.get_isolation_scope().set_tag("stage1", "true") - - t = Thread(target=stage2) - t.start() - t.join() - - def stage2(): - 1 / 0 - - t = Thread(target=stage1) - t.start() - t.join() - - (event,) = events - - (exception,) = event["exception"]["values"] - - assert exception["type"] == "ZeroDivisionError" - assert exception["mechanism"]["type"] == "threading" - assert not exception["mechanism"]["handled"] - - # Free-threaded builds set thread_inherit_context to True, otherwise thread_inherit_context is False - if propagate_hub or getattr(sys.flags, "thread_inherit_context", None): - assert event["tags"]["stage1"] == "true" - else: - assert "stage1" not in event.get("tags", {}) - - -@pytest.mark.parametrize("propagate_hub", (True, False)) -def test_propagates_threadpool_hub(sentry_init, capture_events, propagate_hub): - sentry_init( - traces_sample_rate=1.0, - integrations=[ThreadingIntegration(propagate_hub=propagate_hub)], - ) - events = capture_events() - - def double(number): - with sentry_sdk.start_span(op="task", name=str(number)): - return number * 2 - - with sentry_sdk.start_transaction(name="test_handles_threadpool"): - with futures.ThreadPoolExecutor(max_workers=1) as executor: - tasks = [executor.submit(double, number) for number in [1, 2, 3, 4]] - for future in futures.as_completed(tasks): - print("Getting future value!", future.result()) - - sentry_sdk.flush() - - # Free-threaded builds set thread_inherit_context to True, otherwise thread_inherit_context is False - if propagate_hub or getattr(sys.flags, "thread_inherit_context", None): - assert len(events) == 1 - (event,) = events - assert event["spans"][0]["trace_id"] == event["spans"][1]["trace_id"] - assert event["spans"][1]["trace_id"] == event["spans"][2]["trace_id"] - assert event["spans"][2]["trace_id"] == event["spans"][3]["trace_id"] - assert event["spans"][3]["trace_id"] == event["spans"][0]["trace_id"] - else: - (event,) = events - assert len(event["spans"]) == 0 - - @pytest.mark.skip(reason="Temporarily disable to release SDK 2.0a1.") def test_circular_references(sentry_init, request): sentry_init(default_integrations=False, integrations=[ThreadingIntegration()]) diff --git a/tests/new_scopes_compat/__init__.py b/tests/new_scopes_compat/__init__.py deleted file mode 100644 index 45391bd9ad..0000000000 --- a/tests/new_scopes_compat/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Separate module for tests that check backwards compatibility of the Hub API with 1.x. -These tests should be removed once we remove the Hub API, likely in the next major. - -All tests in this module are run with hub isolation, provided by `isolate_hub` autouse -fixture, defined in `conftest.py`. -""" diff --git a/tests/new_scopes_compat/conftest.py b/tests/new_scopes_compat/conftest.py deleted file mode 100644 index 7023b9a63f..0000000000 --- a/tests/new_scopes_compat/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest - -import sentry_sdk - - -@pytest.fixture(autouse=True) -def isolate_hub(suppress_deprecation_warnings): - with sentry_sdk.Hub(None): - yield diff --git a/tests/new_scopes_compat/test_new_scopes_compat.py b/tests/new_scopes_compat/test_new_scopes_compat.py deleted file mode 100644 index 21e2ac27d3..0000000000 --- a/tests/new_scopes_compat/test_new_scopes_compat.py +++ /dev/null @@ -1,275 +0,0 @@ -import sentry_sdk -from sentry_sdk.hub import Hub - -""" -Those tests are meant to check the compatibility of the new scopes in SDK 2.0 with the old Hub/Scope system in SDK 1.x. - -Those tests have been run with the latest SDK 1.x versiona and the data used in the `assert` statements represents -the behvaior of the SDK 1.x. - -This makes sure that we are backwards compatible. (on a best effort basis, there will probably be some edge cases that are not covered here) -""" - - -def test_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with configure_scope` block. - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with sentry_sdk.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B1", 1) - scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B2": 1, "Z": 1} - - -def test_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with sentry_sdk.push_scope() as scope: # push scope - sentry_sdk.set_tag("B1", 1) - scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -def test_with_hub_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - hub.scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B2": 1, "Z": 1} - - -def test_with_hub_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` containing a `with configure_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - with hub.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1, "B5": 1} - assert event_z["tags"] == { - "A": 1, - "B1": 1, - "B2": 1, - "B3": 1, - "B4": 1, - "B5": 1, - "Z": 1, - } - - -def test_with_hub_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` containing a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - with hub.push_scope() as scope: # push scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B5": 1, "Z": 1} - - -def test_with_cloned_hub_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - hub.scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -def test_with_cloned_hub_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` containing a `with configure_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - with hub.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -def test_with_cloned_hub_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` containing a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - with hub.push_scope() as scope: # push scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} diff --git a/tests/new_scopes_compat/test_new_scopes_compat_event.py b/tests/new_scopes_compat/test_new_scopes_compat_event.py deleted file mode 100644 index 0b8a4ecba9..0000000000 --- a/tests/new_scopes_compat/test_new_scopes_compat_event.py +++ /dev/null @@ -1,498 +0,0 @@ -from unittest import mock - -import pytest - -import sentry_sdk -from sentry_sdk.hub import Hub -from sentry_sdk.integrations import iter_default_integrations -from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber - -""" -Those tests are meant to check the compatibility of the new scopes in SDK 2.0 with the old Hub/Scope system in SDK 1.x. - -Those tests have been run with the latest SDK 1.x version and the data used in the `assert` statements represents -the behvaior of the SDK 1.x. - -This makes sure that we are backwards compatible. (on a best effort basis, there will probably be some edge cases that are not covered here) -""" - - -@pytest.fixture -def integrations(): - return [ - integration.identifier - for integration in iter_default_integrations( - with_auto_enabling_integrations=False - ) - ] - - -@pytest.fixture -def expected_error(integrations): - def create_expected_error_event(trx, span): - return { - "level": "warning-X", - "exception": { - "values": [ - { - "mechanism": {"type": "generic", "handled": True}, - "module": None, - "type": "ValueError", - "value": "This is a test exception", - "stacktrace": { - "frames": [ - { - "filename": "tests/new_scopes_compat/test_new_scopes_compat_event.py", - "abs_path": mock.ANY, - "function": "_faulty_function", - "module": "tests.new_scopes_compat.test_new_scopes_compat_event", - "lineno": mock.ANY, - "pre_context": [ - " return create_expected_transaction_event", - "", - "", - "def _faulty_function():", - " try:", - ], - "context_line": ' raise ValueError("This is a test exception")', - "post_context": [ - " except ValueError as ex:", - " sentry_sdk.capture_exception(ex)", - "", - "", - "def _test_before_send(event, hint):", - ], - "vars": { - "ex": mock.ANY, - }, - "in_app": True, - } - ] - }, - } - ] - }, - "event_id": mock.ANY, - "timestamp": mock.ANY, - "contexts": { - "character": { - "name": "Mighty Fighter changed by before_send", - "age": 19, - "attack_type": "melee", - }, - "trace": { - "trace_id": trx.trace_id, - "span_id": span.span_id, - "parent_span_id": span.parent_span_id, - "op": "test_span", - "origin": "manual", - "description": None, - "data": { - "thread.id": mock.ANY, - "thread.name": "MainThread", - }, - }, - "runtime": { - "name": "CPython", - "version": mock.ANY, - "build": mock.ANY, - }, - }, - "user": { - "id": "123", - "email": "jane.doe@example.com", - }, - "transaction": "test_transaction", - "transaction_info": {"source": "custom"}, - "tags": {"tag1": "tag1_value", "tag2": "tag2_value"}, - "extra": { - "extra1": "extra1_value", - "extra2": "extra2_value", - "should_be_removed_by_event_scrubber": "[Filtered]", - "sys.argv": "[Filtered]", - }, - "breadcrumbs": { - "values": [ - { - "category": "error-level", - "message": "Authenticated user %s", - "level": "error", - "data": {"breadcrumb2": "somedata"}, - "timestamp": mock.ANY, - "type": "default", - } - ] - }, - "modules": mock.ANY, - "release": "0.1.2rc3", - "environment": "checking-compatibility-with-sdk1", - "server_name": mock.ANY, - "sdk": { - "name": "sentry.python", - "version": mock.ANY, - "packages": [{"name": "pypi:sentry-sdk", "version": mock.ANY}], - "integrations": integrations, - }, - "platform": "python", - "_meta": { - "extra": { - "should_be_removed_by_event_scrubber": { - "": {"rem": [["!config", "s"]]} - }, - "sys.argv": {"": {"rem": [["!config", "s"]]}}, - }, - }, - } - - return create_expected_error_event - - -@pytest.fixture -def expected_transaction(integrations): - def create_expected_transaction_event(trx, span): - return { - "type": "transaction", - "transaction": "test_transaction changed by before_send_transaction", - "transaction_info": {"source": "custom"}, - "contexts": { - "trace": { - "trace_id": trx.trace_id, - "span_id": trx.span_id, - "parent_span_id": None, - "op": "test_transaction_op", - "origin": "manual", - "description": None, - "data": { - "thread.id": mock.ANY, - "thread.name": "MainThread", - }, - }, - "character": { - "name": "Mighty Fighter changed by before_send_transaction", - "age": 19, - "attack_type": "melee", - }, - "runtime": { - "name": "CPython", - "version": mock.ANY, - "build": mock.ANY, - }, - }, - "tags": {"tag1": "tag1_value", "tag2": "tag2_value"}, - "timestamp": mock.ANY, - "start_timestamp": mock.ANY, - "spans": [ - { - "data": { - "thread.id": mock.ANY, - "thread.name": "MainThread", - }, - "trace_id": trx.trace_id, - "span_id": span.span_id, - "parent_span_id": span.parent_span_id, - "same_process_as_parent": True, - "op": "test_span", - "origin": "manual", - "description": None, - "start_timestamp": mock.ANY, - "timestamp": mock.ANY, - } - ], - "measurements": {"memory_used": {"value": 456, "unit": "byte"}}, - "event_id": mock.ANY, - "level": "warning-X", - "user": { - "id": "123", - "email": "jane.doe@example.com", - }, - "extra": { - "extra1": "extra1_value", - "extra2": "extra2_value", - "should_be_removed_by_event_scrubber": "[Filtered]", - "sys.argv": "[Filtered]", - }, - "release": "0.1.2rc3", - "environment": "checking-compatibility-with-sdk1", - "server_name": mock.ANY, - "sdk": { - "name": "sentry.python", - "version": mock.ANY, - "packages": [{"name": "pypi:sentry-sdk", "version": mock.ANY}], - "integrations": integrations, - }, - "platform": "python", - "_meta": { - "extra": { - "should_be_removed_by_event_scrubber": { - "": {"rem": [["!config", "s"]]} - }, - "sys.argv": {"": {"rem": [["!config", "s"]]}}, - }, - }, - } - - return create_expected_transaction_event - - -def _faulty_function(): - try: - raise ValueError("This is a test exception") - except ValueError as ex: - sentry_sdk.capture_exception(ex) - - -def _test_before_send(event, hint): - event["contexts"]["character"]["name"] += " changed by before_send" - return event - - -def _test_before_send_transaction(event, hint): - event["transaction"] += " changed by before_send_transaction" - event["contexts"]["character"]["name"] += " changed by before_send_transaction" - return event - - -def _test_before_breadcrumb(breadcrumb, hint): - if breadcrumb["category"] == "info-level": - return None - return breadcrumb - - -def _generate_event_data(scope=None): - """ - Generates some data to be used in the events sent by the tests. - """ - sentry_sdk.set_level("warning-X") - - sentry_sdk.add_breadcrumb( - category="info-level", - message="Authenticated user %s", - level="info", - data={"breadcrumb1": "somedata"}, - ) - sentry_sdk.add_breadcrumb( - category="error-level", - message="Authenticated user %s", - level="error", - data={"breadcrumb2": "somedata"}, - ) - - sentry_sdk.set_context( - "character", - { - "name": "Mighty Fighter", - "age": 19, - "attack_type": "melee", - }, - ) - - sentry_sdk.set_extra("extra1", "extra1_value") - sentry_sdk.set_extra("extra2", "extra2_value") - sentry_sdk.set_extra("should_be_removed_by_event_scrubber", "XXX") - - sentry_sdk.set_tag("tag1", "tag1_value") - sentry_sdk.set_tag("tag2", "tag2_value") - - sentry_sdk.set_user( - {"id": "123", "email": "jane.doe@example.com", "ip_address": "211.161.1.124"} - ) - - sentry_sdk.set_measurement("memory_used", 456, "byte") - - if scope is not None: - scope.add_attachment(bytes=b"Hello World", filename="hello.txt") - - -def _init_sentry_sdk(sentry_init): - sentry_init( - environment="checking-compatibility-with-sdk1", - release="0.1.2rc3", - before_send=_test_before_send, - before_send_transaction=_test_before_send_transaction, - before_breadcrumb=_test_before_breadcrumb, - event_scrubber=EventScrubber( - denylist=DEFAULT_DENYLIST - + ["should_be_removed_by_event_scrubber", "sys.argv"] - ), - send_default_pii=False, - traces_sample_rate=1.0, - auto_enabling_integrations=False, - ) - - -# -# The actual Tests start here! -# - - -def test_event(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - with sentry_sdk.configure_scope() as scope: # configure scope - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" - - -def test_event2(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with Hub(Hub.current): - sentry_sdk.set_tag("A", 1) # will not be added - - with Hub.current: # with hub - with sentry_sdk.push_scope() as scope: - scope.set_tag("B", 1) # will not be added - - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - with sentry_sdk.configure_scope() as scope: # configure scope - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" - - -def test_event3(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with Hub(Hub.current): - sentry_sdk.set_tag("A", 1) # will not be added - - with Hub.current: # with hub - with sentry_sdk.push_scope() as scope: - scope.set_tag("B", 1) # will not be added - - with sentry_sdk.push_scope() as scope: # push scope - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" - - -def test_event4(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with Hub(Hub.current): - sentry_sdk.set_tag("A", 1) # will not be added - - with Hub(Hub.current): # with hub clone - with sentry_sdk.push_scope() as scope: - scope.set_tag("B", 1) # will not be added - - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - with sentry_sdk.configure_scope() as scope: # configure scope - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" - - -def test_event5(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with Hub(Hub.current): - sentry_sdk.set_tag("A", 1) # will not be added - - with Hub(Hub.current): # with hub clone - with sentry_sdk.push_scope() as scope: - scope.set_tag("B", 1) # will not be added - - with sentry_sdk.push_scope() as scope: # push scope - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" diff --git a/tests/profiler/test_transaction_profiler.py b/tests/profiler/test_transaction_profiler.py index 749e91add0..e83f9c5dc3 100644 --- a/tests/profiler/test_transaction_profiler.py +++ b/tests/profiler/test_transaction_profiler.py @@ -9,7 +9,6 @@ import pytest -import sentry_sdk from sentry_sdk import start_transaction from sentry_sdk._lru_cache import LRUCache from sentry_sdk.profiler.transaction_profiler import ( @@ -814,24 +813,6 @@ def test_profile_processing( assert processed["samples"] == expected["samples"] -def test_hub_backwards_compatibility(suppress_deprecation_warnings): - hub = sentry_sdk.Hub() - - with pytest.warns(DeprecationWarning): - profile = Profile(True, 0, hub=hub) - - with pytest.warns(DeprecationWarning): - assert profile.hub is hub - - new_hub = sentry_sdk.Hub() - - with pytest.warns(DeprecationWarning): - profile.hub = new_hub - - with pytest.warns(DeprecationWarning): - assert profile.hub is new_hub - - def test_no_warning_without_hub(): with warnings.catch_warnings(): warnings.simplefilter("error") diff --git a/tests/test_basics.py b/tests/test_basics.py index 01d518eee6..989b5d7941 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -11,7 +11,6 @@ import sentry_sdk import sentry_sdk.scope from sentry_sdk import ( - Hub, add_breadcrumb, capture_event, capture_exception, @@ -23,7 +22,6 @@ push_scope, start_transaction, ) -from sentry_sdk.client import Client from sentry_sdk.integrations import ( _AUTO_ENABLING_INTEGRATIONS, _DEFAULT_INTEGRATIONS, @@ -313,59 +311,6 @@ def test_push_scope(sentry_init, capture_events, suppress_deprecation_warnings): assert "exception" in event -def test_push_scope_null_client( - sentry_init, capture_events, suppress_deprecation_warnings -): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - sentry_init() - events = capture_events() - - Hub.current.bind_client(None) - - with push_scope() as scope: - scope.level = "warning" - try: - 1 / 0 - except Exception as e: - capture_exception(e) - - assert len(events) == 0 - - -@pytest.mark.skip( - reason="This test is not valid anymore, because push_scope just returns the isolation scope. This test should be removed once the Hub is removed" -) -@pytest.mark.parametrize("null_client", (True, False)) -def test_push_scope_callback(sentry_init, null_client, capture_events): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - sentry_init() - - if null_client: - Hub.current.bind_client(None) - - outer_scope = Hub.current.scope - - calls = [] - - @push_scope - def _(scope): - assert scope is Hub.current.scope - assert scope is not outer_scope - calls.append(1) - - # push_scope always needs to execute the callback regardless of - # client state, because that actually runs usercode in it, not - # just scope config code - assert calls == [1] - - # Assert scope gets popped correctly - assert Hub.current.scope is outer_scope - - def test_breadcrumbs(sentry_init, capture_events): sentry_init(max_breadcrumbs=10) events = capture_events() @@ -636,71 +581,6 @@ def test_integrations( } == expected_integrations -@pytest.mark.skip( - reason="This test is not valid anymore, because with the new Scopes calling bind_client on the Hub sets the client on the global scope. This test should be removed once the Hub is removed" -) -def test_client_initialized_within_scope(sentry_init, caplog): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - caplog.set_level(logging.WARNING) - - sentry_init() - - with push_scope(): - Hub.current.bind_client(Client()) - - (record,) = (x for x in caplog.records if x.levelname == "WARNING") - - assert record.msg.startswith("init() called inside of pushed scope.") - - -@pytest.mark.skip( - reason="This test is not valid anymore, because with the new Scopes the push_scope just returns the isolation scope. This test should be removed once the Hub is removed" -) -def test_scope_leaks_cleaned_up(sentry_init, caplog): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - caplog.set_level(logging.WARNING) - - sentry_init() - - old_stack = list(Hub.current._stack) - - with push_scope(): - push_scope() - - assert Hub.current._stack == old_stack - - (record,) = (x for x in caplog.records if x.levelname == "WARNING") - - assert record.message.startswith("Leaked 1 scopes:") - - -@pytest.mark.skip( - reason="This test is not valid anymore, because with the new Scopes there is not pushing and popping of scopes. This test should be removed once the Hub is removed" -) -def test_scope_popped_too_soon(sentry_init, caplog): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - caplog.set_level(logging.ERROR) - - sentry_init() - - old_stack = list(Hub.current._stack) - - with push_scope(): - Hub.current.pop_scope_unsafe() - - assert Hub.current._stack == old_stack - - (record,) = (x for x in caplog.records if x.levelname == "ERROR") - - assert record.message == ("Scope popped too soon. Popped 1 scopes too many.") - - def test_scope_event_processor_order(sentry_init, capture_events): def before_send(event, hint): event["message"] += "baz" @@ -1127,24 +1007,6 @@ def test_last_event_id_scope(sentry_init): assert scope.last_event_id() is None -def test_hub_constructor_deprecation_warning(): - with pytest.warns(sentry_sdk.hub.SentryHubDeprecationWarning): - Hub() - - -def test_hub_current_deprecation_warning(): - with pytest.warns(sentry_sdk.hub.SentryHubDeprecationWarning) as warning_records: - Hub.current - - # Make sure we only issue one deprecation warning - assert len(warning_records) == 1 - - -def test_hub_main_deprecation_warnings(): - with pytest.warns(sentry_sdk.hub.SentryHubDeprecationWarning): - Hub.main - - @pytest.mark.skipif(sys.version_info < (3, 11), reason="add_note() not supported") def test_notes(sentry_init, capture_events): sentry_init() diff --git a/tests/test_client.py b/tests/test_client.py index 78868e434a..c549e2328a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -15,7 +15,6 @@ import sentry_sdk from sentry_sdk import ( Client, - Hub, add_breadcrumb, capture_event, capture_exception, @@ -635,31 +634,6 @@ def capture_envelope(self, envelope): assert output.count(b"HI") == num_messages -def test_configure_scope_available( - sentry_init, request, monkeypatch, suppress_deprecation_warnings -): - """ - Test that scope is configured if client is configured - - This test can be removed once configure_scope and the Hub are removed. - """ - sentry_init() - - with configure_scope() as scope: - assert scope is Hub.current.scope - scope.set_tag("foo", "bar") - - calls = [] - - def callback(scope): - calls.append(scope) - scope.set_tag("foo", "bar") - - assert configure_scope(callback) is None - assert len(calls) == 1 - assert calls[0] is Hub.current.scope - - @pytest.mark.tests_internal_exceptions def test_client_debug_option_enabled(sentry_init, caplog): sentry_init(debug=True) diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 731b188727..7f4fdf6748 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -1,7 +1,7 @@ from unittest import mock import sentry_sdk -from sentry_sdk.sessions import auto_session_tracking, track_session +from sentry_sdk.sessions import track_session def sorted_aggregates(item): @@ -83,47 +83,6 @@ def test_aggregates(sentry_init, capture_envelopes): assert aggregates[0]["errored"] == 1 -def test_aggregates_deprecated( - sentry_init, capture_envelopes, suppress_deprecation_warnings -): - sentry_init( - release="fun-release", - environment="not-fun-env", - ) - envelopes = capture_envelopes() - - with auto_session_tracking(session_mode="request"): - with sentry_sdk.new_scope() as scope: - try: - scope.set_user({"id": "42"}) - raise Exception("all is wrong") - except Exception: - sentry_sdk.capture_exception() - - with auto_session_tracking(session_mode="request"): - pass - - sentry_sdk.get_isolation_scope().start_session(session_mode="request") - sentry_sdk.get_isolation_scope().end_session() - sentry_sdk.flush() - - assert len(envelopes) == 2 - assert envelopes[0].get_event() is not None - - sess = envelopes[1] - assert len(sess.items) == 1 - sess_event = sess.items[0].payload.json - assert sess_event["attrs"] == { - "release": "fun-release", - "environment": "not-fun-env", - } - - aggregates = sorted_aggregates(sess_event) - assert len(aggregates) == 1 - assert aggregates[0]["exited"] == 2 - assert aggregates[0]["errored"] == 1 - - def test_aggregates_explicitly_disabled_session_tracking_request_mode( sentry_init, capture_envelopes ): @@ -157,38 +116,6 @@ def test_aggregates_explicitly_disabled_session_tracking_request_mode( assert "errored" not in aggregates[0] -def test_aggregates_explicitly_disabled_session_tracking_request_mode_deprecated( - sentry_init, capture_envelopes, suppress_deprecation_warnings -): - sentry_init( - release="fun-release", environment="not-fun-env", auto_session_tracking=False - ) - envelopes = capture_envelopes() - - with auto_session_tracking(session_mode="request"): - with sentry_sdk.new_scope(): - try: - raise Exception("all is wrong") - except Exception: - sentry_sdk.capture_exception() - - with auto_session_tracking(session_mode="request"): - pass - - sentry_sdk.get_isolation_scope().start_session(session_mode="request") - sentry_sdk.get_isolation_scope().end_session() - sentry_sdk.flush() - - sess = envelopes[1] - assert len(sess.items) == 1 - sess_event = sess.items[0].payload.json - - aggregates = sorted_aggregates(sess_event) - assert len(aggregates) == 1 - assert aggregates[0]["exited"] == 1 - assert "errored" not in aggregates[0] - - def test_no_thread_on_shutdown_no_errors(sentry_init): sentry_init( release="fun-release", @@ -218,36 +145,6 @@ def test_no_thread_on_shutdown_no_errors(sentry_init): # If we reach this point without error, the test is successful. -def test_no_thread_on_shutdown_no_errors_deprecated( - sentry_init, suppress_deprecation_warnings -): - sentry_init( - release="fun-release", - environment="not-fun-env", - ) - - # make it seem like the interpreter is shutting down - with mock.patch( - "threading.Thread.start", - side_effect=RuntimeError("can't create new thread at interpreter shutdown"), - ): - with auto_session_tracking(session_mode="request"): - with sentry_sdk.new_scope(): - try: - raise Exception("all is wrong") - except Exception: - sentry_sdk.capture_exception() - - with auto_session_tracking(session_mode="request"): - pass - - sentry_sdk.get_isolation_scope().start_session(session_mode="request") - sentry_sdk.get_isolation_scope().end_session() - sentry_sdk.flush() - - # If we reach this point without error, the test is successful. - - def test_top_level_start_session_basic(sentry_init, capture_envelopes): """Test that top-level start_session starts a session on the isolation scope.""" sentry_init(release="test-release", environment="test-env") diff --git a/tests/test_transport.py b/tests/test_transport.py index 8f74b66eed..044594f457 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -29,7 +29,6 @@ import sentry_sdk from sentry_sdk import ( Client, - Hub, add_breadcrumb, capture_message, get_isolation_scope, @@ -42,7 +41,6 @@ from sentry_sdk.transport import ( KEEP_ALIVE_SOCKET_OPTIONS, AsyncHttpTransport, - HttpTransport, _parse_rate_limits, ) @@ -802,24 +800,6 @@ def test_log_item_limits(capturing_server, response_code, item, make_client): } in report["discarded_events"] -def test_hub_cls_backwards_compat(): - class TestCustomHubClass(Hub): - pass - - transport = HttpTransport( - defaultdict(lambda: None, {"dsn": "https://123abc@example.com/123"}) - ) - - with pytest.deprecated_call(): - assert transport.hub_cls is Hub - - with pytest.deprecated_call(): - transport.hub_cls = TestCustomHubClass - - with pytest.deprecated_call(): - assert transport.hub_cls is TestCustomHubClass - - @pytest.mark.parametrize("quantity", (1, 2, 10)) def test_record_lost_event_quantity(capturing_server, make_client, quantity): client = make_client() diff --git a/tests/tracing/test_deprecated.py b/tests/tracing/test_deprecated.py index f030a465ff..8b8150f5d1 100644 --- a/tests/tracing/test_deprecated.py +++ b/tests/tracing/test_deprecated.py @@ -1,36 +1,9 @@ import warnings -import pytest - import sentry_sdk import sentry_sdk.tracing -@pytest.mark.parametrize( - "parameter_value_getter", - # Use lambda to avoid Hub deprecation warning here (will suppress it in the test) - (lambda: sentry_sdk.Hub(), lambda: sentry_sdk.Scope()), -) -def test_passing_hub_parameter_to_transaction_finish( - suppress_deprecation_warnings, parameter_value_getter -): - parameter_value = parameter_value_getter() - transaction = sentry_sdk.tracing.Transaction() - with pytest.warns(DeprecationWarning): - transaction.finish(hub=parameter_value) - - -def test_passing_hub_object_to_scope_transaction_finish(suppress_deprecation_warnings): - transaction = sentry_sdk.tracing.Transaction() - - # Do not move the following line under the `with` statement. Otherwise, the Hub.__init__ deprecation - # warning will be confused with the transaction.finish deprecation warning that we are testing. - hub = sentry_sdk.Hub() - - with pytest.warns(DeprecationWarning): - transaction.finish(hub) - - def test_no_warnings_scope_to_transaction_finish(): transaction = sentry_sdk.tracing.Transaction() with warnings.catch_warnings(): From 804378300bec331487581a22cc49355061a81ee0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 15:13:35 +0200 Subject: [PATCH 07/16] ref(strawberry): Take out of auto-enabling integrations (#6928) The integration requires additional configuration which should be intentional on the user's part. #### Issues Closes https://github.com/getsentry/sentry-python/issues/4993 --- MIGRATION_GUIDE.md | 13 +++++++++++++ sentry_sdk/integrations/__init__.py | 1 - 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 28827ec73a..7fc22517e3 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -8,6 +8,19 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## Changed +- The Strawberry integration won't auto-enable anymore if we detect `strawberry-graphql` is installed. Set it up manually, setting the `async_execution` integration option to either `True` or `False` depending on if your app is async or sync. + + ```python + from sentry_sdk.integrations.strawberry import StrawberryIntegration + + sentry_sdk.init( + integrations=[ + StrawberryIntegration(async_execution=True), # or False + ], + ... + ) + ``` + - The UnraisableHookIntegration is now enabled by default. - We now don't suppress chained exceptions in the ASGI and asyncio integrations by default. The related `suppress_asgi_chained_exceptions` experimental option was removed. diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index d71b1f5ff6..7aa496cec2 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -110,7 +110,6 @@ def iter_default_integrations( "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", "sentry_sdk.integrations.starlette.StarletteIntegration", "sentry_sdk.integrations.starlite.StarliteIntegration", - "sentry_sdk.integrations.strawberry.StrawberryIntegration", "sentry_sdk.integrations.tornado.TornadoIntegration", ] From 123c214046ab23f736db83ad9786b10517764a20 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 15:15:03 +0200 Subject: [PATCH 08/16] ref: Remove transaction profiler in new major (#6921) Closes https://github.com/getsentry/sentry-python/issues/6920 --- MIGRATION_GUIDE.md | 1 + docs/apidocs.rst | 3 - sentry_sdk/_types.py | 4 +- sentry_sdk/client.py | 34 +- sentry_sdk/consts.py | 12 - sentry_sdk/envelope.py | 8 - sentry_sdk/integrations/django/asgi.py | 4 - sentry_sdk/integrations/django/views.py | 6 - sentry_sdk/integrations/fastapi.py | 4 - sentry_sdk/integrations/quart.py | 4 - sentry_sdk/integrations/starlette.py | 2 - sentry_sdk/profiler/__init__.py | 22 - sentry_sdk/profiler/transaction_profiler.py | 772 ----------------- sentry_sdk/scope.py | 23 - sentry_sdk/tracing.py | 13 - tests/conftest.py | 5 +- tests/integrations/django/asgi/test_asgi.py | 81 -- tests/integrations/fastapi/test_fastapi.py | 39 - tests/integrations/quart/test_quart.py | 78 -- .../integrations/starlette/test_starlette.py | 39 - tests/integrations/wsgi/test_wsgi.py | 27 - tests/profiler/test_transaction_profiler.py | 819 ------------------ tests/test_client.py | 2 +- tests/test_envelope.py | 1 - 24 files changed, 12 insertions(+), 1991 deletions(-) delete mode 100644 sentry_sdk/profiler/transaction_profiler.py delete mode 100644 tests/profiler/test_transaction_profiler.py diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 7fc22517e3..930c6c654a 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -26,6 +26,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## Removed +- Transaction profiling and related code was removed. - 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/docs/apidocs.rst b/docs/apidocs.rst index ffe265b276..4991b8ccd6 100644 --- a/docs/apidocs.rst +++ b/docs/apidocs.rst @@ -29,9 +29,6 @@ API Docs .. autoclass:: sentry_sdk.tracing.Span :members: -.. autoclass:: sentry_sdk.profiler.transaction_profiler.Profile - :members: - .. autoclass:: sentry_sdk.session.Session :members: diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index e91fed097c..182a364130 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -274,7 +274,6 @@ class DataCollection(TypedDict): "monitor_config": Mapping[str, object], "monitor_slug": Optional[str], "platform": Literal["python"], - "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports "release": Optional[str], "request": dict[str, object], "sdk": Mapping[str, object], @@ -418,7 +417,6 @@ class DataCollection(TypedDict): "attachment", "session", "internal", - "profile", "profile_chunk", "monitor", "span", @@ -429,7 +427,7 @@ class DataCollection(TypedDict): SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] - ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] + ProfilerMode = Union[ContinuousProfilerMode] MonitorConfigScheduleType = Literal["crontab", "interval"] MonitorConfigScheduleUnit = Literal[ diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e8414d3f91..cb3de4b382 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -32,11 +32,6 @@ from sentry_sdk.integrations.dedupe import DedupeIntegration from sentry_sdk.monitor import Monitor from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler -from sentry_sdk.profiler.transaction_profiler import ( - Profile, - has_profiling_enabled, - setup_profiler, -) from sentry_sdk.scrubber import EventScrubber from sentry_sdk.serializer import serialize from sentry_sdk.sessions import SessionFlusher @@ -632,7 +627,6 @@ def _record_lost_event( self.options["send_default_pii"] = True self.options["error_sampler"] = sample_all self.options["traces_sampler"] = sample_all - self.options["profiles_sampler"] = sample_all # data_collection was resolved in _get_options() before this # spotlight override flipped send_default_pii on. Re-derive it so # data_collection agrees with should_send_default_pii() in @@ -708,20 +702,14 @@ def _record_lost_event( SDK_INFO["name"] = sdk_name logger.debug("Setting SDK name to '%s'", sdk_name) - if has_profiling_enabled(self.options): - try: - setup_profiler(self.options) - except Exception as e: - logger.debug("Can not set up profiler. (%s)", e) - else: - try: - setup_continuous_profiler( - self.options, - sdk_info=SDK_INFO, - capture_func=_capture_envelope, - ) - except Exception as e: - logger.debug("Can not set up continuous profiler. (%s)", e) + try: + setup_continuous_profiler( + self.options, + sdk_info=SDK_INFO, + capture_func=_capture_envelope, + ) + except Exception as e: + logger.debug("Can not set up continuous profiler. (%s)", e) finally: _client_init_debug.set(old_debug) @@ -733,7 +721,6 @@ def _record_lost_event( or self.log_batcher or self.metrics_batcher or self.span_batcher - or has_profiling_enabled(self.options) or isinstance(self.transport, HttpTransportCore) ): # If we have anything on that could spawn a background thread, we @@ -1121,8 +1108,6 @@ def capture_event( if not self._should_capture(event, hint, scope): return None - profile = event.pop("profile", None) - event_id = event.get("event_id") if event_id is None: event["event_id"] = event_id = uuid.uuid4().hex @@ -1163,9 +1148,6 @@ def capture_event( envelope = Envelope(headers=headers) - if is_transaction and isinstance(profile, Profile): - envelope.add_profile(profile.to_json(event_opt, self.options)) - if is_transaction and not span_recorder_has_gen_ai_span: envelope.add_transaction(event_opt) elif is_transaction: diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 3abaa4b6d9..36f7e4058e 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1301,8 +1301,6 @@ def __init__( traces_sample_rate: "Optional[float]" = None, trace_lifecycle: "Optional[Literal['static', 'stream']]" = None, traces_sampler: "Optional[TracesSampler]" = None, - profiles_sample_rate: "Optional[float]" = None, - profiles_sampler: "Optional[TracesSampler]" = None, profiler_mode: "Optional[ProfilerMode]" = None, profile_lifecycle: 'Literal["manual", "trace"]' = "manual", profile_session_sample_rate: "Optional[float]" = None, @@ -1705,16 +1703,6 @@ def __init__( Return a string for that repr value to be used or `None` to continue serializing how Sentry would have done it anyway. - :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled - transaction will be profiled. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be - profiled. - - :param profiles_sampler: - :param profiler_mode: :param profile_lifecycle: diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index d2d4aae31a..c4e3a4d42d 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -59,12 +59,6 @@ def add_transaction( ) -> None: self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) - def add_profile( - self, - profile: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) - def add_profile_chunk( self, profile_chunk: "Any", @@ -259,8 +253,6 @@ def data_category(self) -> "EventDataCategory": return "trace_metric" elif ty == "client_report": return "internal" - elif ty == "profile": - return "profile" elif ty == "profile_chunk": return "profile_chunk" elif ty == "check_in": diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index df287e6436..f3b1fd0898 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -188,10 +188,6 @@ async def sentry_wrapped_callback( if current_scope.transaction is not None: current_scope.transaction.update_active_thread() - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - integration = client.get_integration(DjangoIntegration) if not integration or not integration.middleware_spans: return await callback(request, *args, **kwargs) diff --git a/sentry_sdk/integrations/django/views.py b/sentry_sdk/integrations/django/views.py index b24e4df45b..eacd3b74fe 100644 --- a/sentry_sdk/integrations/django/views.py +++ b/sentry_sdk/integrations/django/views.py @@ -99,12 +99,6 @@ def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "A if current_scope.transaction is not None: current_scope.transaction.update_active_thread() - sentry_scope = sentry_sdk.get_isolation_scope() - # set the active thread id to the handler thread for sync views - # this isn't necessary for async views since that runs on main - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - integration = client.get_integration(DjangoIntegration) if not integration or not integration.middleware_spans: return callback(request, *args, **kwargs) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 0b048e11ee..14ee66eecf 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -181,10 +181,6 @@ def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": elif current_scope.transaction is not None: current_scope.transaction.update_active_thread() - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - return old_call(*args, **kwargs) _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] diff --git a/sentry_sdk/integrations/quart.py b/sentry_sdk/integrations/quart.py index 8c281f5874..c3238abe55 100644 --- a/sentry_sdk/integrations/quart.py +++ b/sentry_sdk/integrations/quart.py @@ -133,10 +133,6 @@ def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": if current_scope.transaction is not None: current_scope.transaction.update_active_thread() - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - return old_func(*args, **kwargs) return old_decorator(_sentry_func) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 21cdfeecc5..ef01e48346 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -622,8 +622,6 @@ def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": current_scope.transaction.update_active_thread() sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() request = args[0] diff --git a/sentry_sdk/profiler/__init__.py b/sentry_sdk/profiler/__init__.py index d562405295..99d8cd2d61 100644 --- a/sentry_sdk/profiler/__init__.py +++ b/sentry_sdk/profiler/__init__.py @@ -4,17 +4,6 @@ stop_profile_session, stop_profiler, ) -from sentry_sdk.profiler.transaction_profiler import ( - MAX_PROFILE_DURATION_NS, - PROFILE_MINIMUM_SAMPLES, - GeventScheduler, - Profile, - Scheduler, - ThreadScheduler, - has_profiling_enabled, - setup_profiler, - teardown_profiler, -) from sentry_sdk.profiler.utils import ( DEFAULT_SAMPLING_FREQUENCY, MAX_STACK_DEPTH, @@ -29,17 +18,6 @@ "start_profiler", "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` "stop_profiler", - # DEPRECATED: The following was re-exported for backwards compatibility. It - # will be removed from sentry_sdk.profiler in a future release. - "MAX_PROFILE_DURATION_NS", - "PROFILE_MINIMUM_SAMPLES", - "Profile", - "Scheduler", - "ThreadScheduler", - "GeventScheduler", - "has_profiling_enabled", - "setup_profiler", - "teardown_profiler", "DEFAULT_SAMPLING_FREQUENCY", "MAX_STACK_DEPTH", "get_frame_name", diff --git a/sentry_sdk/profiler/transaction_profiler.py b/sentry_sdk/profiler/transaction_profiler.py deleted file mode 100644 index e0b5e34426..0000000000 --- a/sentry_sdk/profiler/transaction_profiler.py +++ /dev/null @@ -1,772 +0,0 @@ -""" -This file is originally based on code from https://github.com/nylas/nylas-perftools, -which is published under the following license: - -The MIT License (MIT) - -Copyright (c) 2014 Nylas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import atexit -import os -import platform -import random -import sys -import threading -import time -import uuid -from abc import ABC, abstractmethod -from collections import deque -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - capture_internal_exceptions, - get_current_thread_meta, - is_gevent, - is_valid_sample_rate, - logger, - nanosecond_time, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type - - from typing_extensions import TypedDict - - from sentry_sdk._types import Event, ProfilerMode, SamplingContext - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - ProcessedThreadMetadata, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "elapsed_since_start_ns": str, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - ProcessedProfile = TypedDict( - "ProcessedProfile", - { - "frames": List[ProcessedFrame], - "stacks": List[ProcessedStack], - "samples": List[ProcessedSample], - "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - - ThreadPool = None - - -_scheduler: "Optional[Scheduler]" = None - - -# The minimum number of unique samples that must exist in a profile to be -# considered valid. -PROFILE_MINIMUM_SAMPLES = 2 - - -def has_profiling_enabled(options: "Dict[str, Any]") -> bool: - profiles_sampler = options["profiles_sampler"] - if profiles_sampler is not None: - return True - - profiles_sample_rate = options["profiles_sample_rate"] - if profiles_sample_rate is not None and profiles_sample_rate > 0: - return True - - profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") - if profiles_sample_rate is not None: - logger.warning( - "_experiments['profiles_sample_rate'] is deprecated. " - "Please use the non-experimental profiles_sample_rate option " - "directly." - ) - if profiles_sample_rate > 0: - return True - - return False - - -def setup_profiler(options: "Dict[str, Any]") -> bool: - global _scheduler - - if _scheduler is not None: - logger.debug("[Profiling] Profiler is already setup") - return False - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventScheduler.mode - else: - default_profiler_mode = ThreadScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - profiler_mode = options.get("_experiments", {}).get("profiler_mode") - if profiler_mode is not None: - logger.warning( - "_experiments['profiler_mode'] is deprecated. Please use the " - "non-experimental profiler_mode option directly." - ) - profiler_mode = profiler_mode or default_profiler_mode - - if ( - profiler_mode == ThreadScheduler.mode - # for legacy reasons, we'll keep supporting sleep mode for this scheduler - or profiler_mode == "sleep" - ): - _scheduler = ThreadScheduler(frequency=frequency) - elif profiler_mode == GeventScheduler.mode: - _scheduler = GeventScheduler(frequency=frequency) - else: - raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) - ) - _scheduler.setup() - - atexit.register(teardown_profiler) - - return True - - -def teardown_profiler() -> None: - global _scheduler - - if _scheduler is not None: - _scheduler.teardown() - - _scheduler = None - - -MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds - - -class Profile: - def __init__( - self, - sampled: "Optional[bool]", - start_ns: int, - scheduler: "Optional[Scheduler]" = None, - ) -> None: - self.scheduler = _scheduler if scheduler is None else scheduler - - self.event_id: str = uuid.uuid4().hex - - self.sampled: "Optional[bool]" = sampled - - # Various framework integrations are capable of overwriting the active thread id. - # If it is set to `None` at the end of the profile, we fall back to the default. - self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 - self.active_thread_id: "Optional[int]" = None - - try: - self.start_ns: int = start_ns - except AttributeError: - self.start_ns = 0 - - self.stop_ns: int = 0 - self.active: bool = False - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - self.unique_samples = 0 - - def update_active_thread_id(self) -> None: - self.active_thread_id = get_current_thread_meta()[0] - logger.debug( - "[Profiling] updating active thread id to {tid}".format( - tid=self.active_thread_id - ) - ) - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the profile's sampling decision according to the following - precedence rules: - - 1. If the transaction to be profiled is not sampled, that decision - will be used, regardless of anything else. - - 2. Use `profiles_sample_rate` to decide. - """ - - # The corresponding transaction was not sampled, - # so don't generate a profile for it. - if not self.sampled: - logger.debug( - "[Profiling] Discarding profile because transaction is discarded." - ) - self.sampled = False - return - - # The profiler hasn't been properly initialized. - if self.scheduler is None: - logger.debug( - "[Profiling] Discarding profile because profiler was not started." - ) - self.sampled = False - return - - client = sentry_sdk.get_client() - if not client.is_active(): - self.sampled = False - return - - options = client.options - - if callable(options.get("profiles_sampler")): - sample_rate = options["profiles_sampler"](sampling_context) - elif options["profiles_sample_rate"] is not None: - sample_rate = options["profiles_sample_rate"] - else: - sample_rate = options["_experiments"].get("profiles_sample_rate") - - # The profiles_sample_rate option was not set, so profiling - # was never enabled. - if sample_rate is None: - logger.debug( - "[Profiling] Discarding profile because profiling was not enabled." - ) - self.sampled = False - return - - if not is_valid_sample_rate(sample_rate, source="Profiling"): - logger.warning( - "[Profiling] Discarding profile because of invalid sample rate." - ) - self.sampled = False - return - - # Now we roll the dice. random.random is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = random.random() < float(sample_rate) - - if self.sampled: - logger.debug("[Profiling] Initializing profile") - else: - logger.debug( - "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( - sample_rate=float(sample_rate) - ) - ) - - def start(self) -> None: - if not self.sampled or self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Starting profile") - self.active = True - if not self.start_ns: - self.start_ns = nanosecond_time() - self.scheduler.start_profiling(self) - - def stop(self) -> None: - if not self.sampled or not self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Stopping profile") - self.active = False - self.stop_ns = nanosecond_time() - - def __enter__(self) -> "Profile": - scope = sentry_sdk.get_isolation_scope() - old_profile = scope.profile - scope.profile = self - - self._context_manager_state = (scope, old_profile) - - self.start() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - with capture_internal_exceptions(): - self.stop() - - scope, old_profile = self._context_manager_state - del self._context_manager_state - - scope.profile = old_profile - - def write(self, ts: int, sample: "ExtractedSample") -> None: - if not self.active: - return - - if ts < self.start_ns: - return - - offset = ts - self.start_ns - if offset > MAX_PROFILE_DURATION_NS: - self.stop() - return - - self.unique_samples += 1 - - elapsed_since_start_ns = str(offset) - - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "elapsed_since_start_ns": elapsed_since_start_ns, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def process(self) -> "ProcessedProfile": - # This collects the thread metadata at the end of a profile. Doing it - # this way means that any threads that terminate before the profile ends - # will not have any metadata associated with it. - thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - } - - return { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": thread_metadata, - } - - def to_json( - self, event_opt: "Event", options: "Dict[str, Any]" - ) -> "Dict[str, Any]": - profile = self.process() - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - return { - "environment": event_opt.get("environment"), - "event_id": self.event_id, - "platform": "python", - "profile": profile, - "release": event_opt.get("release", ""), - "timestamp": event_opt["start_timestamp"], - "version": "1", - "device": { - "architecture": platform.machine(), - }, - "os": { - "name": platform.system(), - "version": platform.release(), - }, - "runtime": { - "name": platform.python_implementation(), - "version": platform.python_version(), - }, - "transactions": [ - { - "id": event_opt["event_id"], - "name": event_opt["transaction"], - # we start the transaction before the profile and this is - # the transaction start time relative to the profile, so we - # hardcode it to 0 until we can start the profile before - "relative_start_ns": "0", - # use the duration of the profile instead of the transaction - # because we end the transaction after the profile - "relative_end_ns": str(self.stop_ns - self.start_ns), - "trace_id": event_opt["contexts"]["trace"]["trace_id"], - "active_thread_id": str( - self._default_active_thread_id - if self.active_thread_id is None - else self.active_thread_id - ), - } - ], - } - - def valid(self) -> bool: - client = sentry_sdk.get_client() - if not client.is_active(): - return False - - if not has_profiling_enabled(client.options): - return False - - if self.sampled is None or not self.sampled: - if client.transport: - client.transport.record_lost_event( - "sample_rate", data_category="profile" - ) - return False - - if self.unique_samples < PROFILE_MINIMUM_SAMPLES: - if client.transport: - client.transport.record_lost_event( - "insufficient_data", data_category="profile" - ) - logger.debug("[Profiling] Discarding profile because insufficient samples.") - return False - - return True - - -class Scheduler(ABC): - mode: "ProfilerMode" = "unknown" - - def __init__(self, frequency: int) -> None: - self.interval = 1.0 / frequency - - self.sampler = self.make_sampler() - - # cap the number of new profiles at any time so it does not grow infinitely - self.new_profiles: "Deque[Profile]" = deque(maxlen=128) - self.active_profiles: "Set[Profile]" = set() - - def __enter__(self) -> "Scheduler": - self.setup() - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self.teardown() - - @abstractmethod - def setup(self) -> None: - pass - - @abstractmethod - def teardown(self) -> None: - pass - - def ensure_running(self) -> None: - """ - Ensure the scheduler is running. By default, this method is a no-op. - The method should be overridden by any implementation for which it is - relevant. - """ - return None - - def start_profiling(self, profile: "Profile") -> None: - self.ensure_running() - self.new_profiles.append(profile) - - def make_sampler(self) -> "Callable[..., None]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - def _sample_stack(*args: "Any", **kwargs: "Any") -> None: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - # make sure to clear the cache if we're not profiling so we dont - # keep a reference to the last stack of frames around - return - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - now = nanosecond_time() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - - inactive_profiles = [] - - for profile in self.active_profiles: - if profile.active: - profile.write(now, sample) - else: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - return _sample_stack - - -class ThreadScheduler(Scheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ProfilerMode" = "thread" - name = "sentry.profiler.ThreadScheduler" - - def __init__(self, frequency: int) -> None: - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[threading.Thread]" = None - self.pid: "Optional[int]" = None - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - """ - Check that the profiler has an active thread to run in, and start one if - that's not the case. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self.running - will be False after running this function. - """ - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - -class GeventScheduler(Scheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ProfilerMode" = "gevent" - name = "sentry.profiler.GeventScheduler" - - def __init__(self, frequency: int) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[_ThreadPool]" = None - self.pid: "Optional[int]" = None - - # This intentionally uses the gevent patched threading.Lock. - # The lock will be required when first trying to start profiles - # as we need to spawn the profiler thread from the greenlets. - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index fd0da3b6c0..06d3e7aff5 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -26,7 +26,6 @@ try_autostart_continuous_profiler, try_profile_lifecycle_trace_start, ) -from sentry_sdk.profiler.transaction_profiler import Profile from sentry_sdk.session import Session from sentry_sdk.traces import ( _DEFAULT_PARENT_SPAN, @@ -235,7 +234,6 @@ class Scope: "_session", "_attachments", "_force_auto_session_tracking", - "_profile", "_propagation_context", "client", "_type", @@ -302,8 +300,6 @@ def __copy__(self) -> "Scope": rv._force_auto_session_tracking = self._force_auto_session_tracking rv._attachments = self._attachments.copy() - rv._profile = self._profile - rv._last_event_id = self._last_event_id rv._flags = deepcopy(self._flags) @@ -758,8 +754,6 @@ def clear(self) -> None: self._session: "Optional[Session]" = None self._force_auto_session_tracking: "Optional[bool]" = None - self._profile: "Optional[Profile]" = None - self._propagation_context = None # self._last_event_id is only applicable to isolation scopes @@ -932,14 +926,6 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: span._attributes["sentry.segment.name.source"] ) - @property - def profile(self) -> "Optional[Profile]": - return self._profile - - @profile.setter - def profile(self, profile: "Optional[Profile]") -> None: - self._profile = profile - def set_tag(self, key: str, value: "Any") -> None: """ Sets a tag for a key to a specific value. @@ -1193,13 +1179,6 @@ def start_transaction( ) if transaction.sampled: - profile = Profile( - transaction.sampled, transaction._start_timestamp_monotonic_ns - ) - profile._set_initial_sampling_decision(sampling_context=sampling_context) - - transaction._profile = profile - transaction._continuous_profile = try_profile_lifecycle_trace_start() # Typically, the profiler is set when the transaction is created. But when @@ -1964,8 +1943,6 @@ def update_from_scope(self, scope: "Scope") -> None: self._span = scope._span if scope._attachments: self._attachments.extend(scope._attachments) - if scope._profile: - self._profile = scope._profile if scope._propagation_context: self._propagation_context = scope._propagation_context if scope._session: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 747bc7facc..8a7f17cb80 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -43,7 +43,6 @@ SamplingContext, ) from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - from sentry_sdk.profiler.transaction_profiler import Profile class SpanKwargs(TypedDict, total=False): trace_id: str @@ -801,7 +800,6 @@ class Transaction(Span): "sample_rate", "_measurements", "_contexts", - "_profile", "_continuous_profile", "_baggage", "_sample_rand", @@ -823,7 +821,6 @@ def __init__( # type: ignore[misc] self.parent_sampled = parent_sampled self._measurements: "Dict[str, MeasurementValue]" = {} self._contexts: "Dict[str, Any]" = {} - self._profile: "Optional[Profile]" = None self._continuous_profile: "Optional[ContinuousProfile]" = None self._baggage = baggage @@ -872,17 +869,11 @@ def __enter__(self) -> "Transaction": super().__enter__() - if self._profile is not None: - self._profile.__enter__() - return self def __exit__( self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" ) -> None: - if self._profile is not None: - self._profile.__exit__(ty, value, tb) - if self._continuous_profile is not None: self._continuous_profile.stop() @@ -1048,10 +1039,6 @@ def finish( if has_gen_ai_span: event["_has_gen_ai_span"] = True - if self._profile is not None and self._profile.valid(): - event["profile"] = self._profile - self._profile = None - event["measurements"] = self._measurements return scope.capture_event(event) diff --git a/tests/conftest.py b/tests/conftest.py index 6b406d6a06..8522511900 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -61,7 +61,6 @@ _installed_integrations, _processed_integrations, ) -from sentry_sdk.profiler import teardown_profiler from sentry_sdk.profiler.continuous_profiler import teardown_continuous_profiler from sentry_sdk.transport import Transport from sentry_sdk.utils import package_version, reraise @@ -776,13 +775,11 @@ def __ne__(self, test_obj): @pytest.fixture def teardown_profiling(): # Make sure that a previous test didn't leave the profiler running - teardown_profiler() teardown_continuous_profiler() yield - # Make sure that to shut down the profiler after the test - teardown_profiler() + # Make sure to shut down the profiler after the test teardown_continuous_profiler() diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 8c58a5c6a6..8ad22a1057 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -1,10 +1,8 @@ import asyncio import base64 import inspect -import json import os import sys -from unittest import mock import django import pytest @@ -197,85 +195,6 @@ async def test_async_views( } -@pytest.mark.parametrize("application", APPS) -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@pytest.mark.parametrize("middleware_spans", [False, True]) -@pytest.mark.asyncio -@pytest.mark.skipif( - django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" -) -@pytest.mark.parametrize("span_streaming", [True, False]) -async def test_active_thread_id( - sentry_init, - capture_envelopes, - capture_items, - teardown_profiling, - endpoint, - application, - middleware_spans, - span_streaming, -): - sentry_init( - integrations=[DjangoIntegration(middleware_spans=middleware_spans)], - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - trace_lifecycle="stream" if span_streaming else "static", - ) - - comm = HttpCommunicator(application, "GET", endpoint) - - if span_streaming: - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 - ): - items = capture_items("span") - response = await comm.get_response() - await comm.wait() - - assert response["status"] == 200, response["body"] - - data = json.loads(response["body"]) - - sentry_sdk.flush() - spans = [item.payload for item in items] - - for span in spans: - if span["is_segment"] is False: - continue - assert str(data["active"]) == span["attributes"]["thread.id"] - else: - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 - ): - envelopes = capture_envelopes() - response = await comm.get_response() - await comm.wait() - - assert response["status"] == 200, response["body"] - - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1 - - data = json.loads(response["body"]) - - for item in profiles: - transactions = item.payload.json["transactions"] - assert len(transactions) == 1 - assert str(data["active"]) == transactions[0]["active_thread_id"] - - transactions = [ - item for item in envelopes[0].items if item.type == "transaction" - ] - assert len(transactions) == 1 - - for item in transactions: - transaction = item.payload.json - trace_context = transaction["contexts"]["trace"] - assert str(data["active"]) == trace_context["data"]["thread.id"] - - @pytest.mark.asyncio @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 41424bc27f..2e7383e758 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -4,7 +4,6 @@ import os import threading import warnings -from unittest import mock import fastapi import pytest @@ -464,44 +463,6 @@ def test_legacy_setup( assert event["transaction"] == "/message/{message_id}" -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, endpoint): - sentry_init( - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - ) - app = fastapi_app_factory() - asgi_app = SentryAsgiMiddleware(app) - - envelopes = capture_envelopes() - - client = TestClient(asgi_app) - response = client.get(endpoint) - assert response.status_code == 200 - - data = json.loads(response.content) - - envelopes = [envelope for envelope in envelopes] - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1 - - for item in profiles: - transactions = item.payload.json["transactions"] - assert len(transactions) == 1 - assert str(data["active"]) == transactions[0]["active_thread_id"] - - transactions = [item for item in envelopes[0].items if item.type == "transaction"] - assert len(transactions) == 1 - - for item in transactions: - transaction = item.payload.json - trace_context = transaction["contexts"]["trace"] - assert str(data["active"]) == trace_context["data"]["thread.id"] - - @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) def test_active_thread_id_span_streaming(sentry_init, capture_items, endpoint): sentry_init( diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 286d6aeaff..59c947615f 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -1,8 +1,6 @@ import importlib -import json import sys import threading -from unittest import mock import pytest @@ -589,82 +587,6 @@ async def dispatch_request(self): assert event["transaction"] == "hello_class" -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@pytest.mark.asyncio -async def test_active_thread_id( - sentry_init, capture_envelopes, teardown_profiling, endpoint -): - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 - ): - sentry_init( - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - ) - app = quart_app_factory() - - envelopes = capture_envelopes() - - async with app.test_client() as client: - response = await client.get(endpoint) - assert response.status_code == 200 - - data = json.loads(await response.get_data(as_text=True)) - - envelopes = [envelope for envelope in envelopes] - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1, envelopes[0].items - - for item in profiles: - transactions = item.payload.json["transactions"] - assert len(transactions) == 1 - assert str(data["active"]) == transactions[0]["active_thread_id"] - - transactions = [ - item for item in envelopes[0].items if item.type == "transaction" - ] - assert len(transactions) == 1 - - for item in transactions: - transaction = item.payload.json - trace_context = transaction["contexts"]["trace"] - assert str(data["active"]) == trace_context["data"]["thread.id"] - - -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@pytest.mark.asyncio -async def test_active_thread_id_span_streaming( - sentry_init, capture_items, teardown_profiling, endpoint -): - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 - ): - sentry_init( - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - trace_lifecycle="stream", - ) - app = quart_app_factory() - - items = capture_items("span") - - async with app.test_client() as client: - response = await client.get(endpoint) - assert response.status_code == 200 - - data = json.loads(await response.get_data(as_text=True)) - - sentry_sdk.flush() - - spans = [item.payload for item in items] - assert len(spans) == 1 - - segment = spans[0] - assert str(data["active"]) == segment["attributes"]["thread.id"] - - @pytest.mark.asyncio async def test_span_origin(sentry_init, capture_events): sentry_init( diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index bf0d7b0993..2ef4a7ab57 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -7,7 +7,6 @@ import re import threading import warnings -from unittest import mock import pytest import starlette @@ -1453,44 +1452,6 @@ def test_legacy_setup( assert event["transaction"] == "/message/{message_id}" -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, endpoint): - sentry_init( - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - ) - app = starlette_app_factory() - asgi_app = SentryAsgiMiddleware(app) - - envelopes = capture_envelopes() - - client = TestClient(asgi_app) - response = client.get(endpoint) - assert response.status_code == 200 - - data = json.loads(response.content) - - envelopes = [envelope for envelope in envelopes] - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1 - - for item in profiles: - transactions = item.payload.json["transactions"] - assert len(transactions) == 1 - assert str(data["active"]) == transactions[0]["active_thread_id"] - - transactions = [item for item in envelopes[0].items if item.type == "transaction"] - assert len(transactions) == 1 - - for item in transactions: - transaction = item.payload.json - trace_context = transaction["contexts"]["trace"] - assert str(data["active"]) == trace_context["data"]["thread.id"] - - @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) def test_active_thread_id_span_streaming(sentry_init, capture_items, endpoint): sentry_init( diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index eb73be1499..ca7761a7b4 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -614,33 +614,6 @@ def sample_app(environ, start_response): assert sum(agg.get("crashed", 0) for agg in session_aggregates) == 1 -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_profile_sent( - sentry_init, - capture_envelopes, - teardown_profiling, -): - def test_app(environ, start_response): - start_response("200 OK", []) - return ["Go get the ball! Good dog!"] - - sentry_init( - traces_sample_rate=1.0, - _experiments={"profiles_sample_rate": 1.0}, - ) - app = SentryWsgiMiddleware(test_app) - envelopes = capture_envelopes() - - client = Client(app) - client.get("/") - - envelopes = [envelope for envelope in envelopes] - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1 - - @pytest.mark.parametrize("span_streaming", [True, False]) def test_span_origin_manual(sentry_init, capture_events, capture_items, span_streaming): def dogpark(environ, start_response): diff --git a/tests/profiler/test_transaction_profiler.py b/tests/profiler/test_transaction_profiler.py deleted file mode 100644 index e83f9c5dc3..0000000000 --- a/tests/profiler/test_transaction_profiler.py +++ /dev/null @@ -1,819 +0,0 @@ -import inspect -import os -import sys -import threading -import time -import warnings -from collections import defaultdict -from unittest import mock - -import pytest - -from sentry_sdk import start_transaction -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.profiler.transaction_profiler import ( - GeventScheduler, - Profile, - Scheduler, - ThreadScheduler, - setup_profiler, -) -from sentry_sdk.profiler.utils import ( - extract_frame, - extract_stack, - frame_id, - get_frame_name, -) - -try: - import gevent -except ImportError: - gevent = None - - -requires_gevent = pytest.mark.skipif(gevent is None, reason="gevent not enabled") - - -def process_test_sample(sample): - # insert a mock hashable for the stack - return [(tid, (stack, stack)) for tid, stack in sample] - - -def non_experimental_options(mode=None, sample_rate=None): - return {"profiler_mode": mode, "profiles_sample_rate": sample_rate} - - -def experimental_options(mode=None, sample_rate=None): - return { - "_experiments": {"profiler_mode": mode, "profiles_sample_rate": sample_rate} - } - - -@pytest.mark.parametrize( - "mode", - [pytest.param("foo")], -) -@pytest.mark.parametrize( - "make_options", - [ - pytest.param(experimental_options, id="experiment"), - pytest.param(non_experimental_options, id="non experimental"), - ], -) -def test_profiler_invalid_mode(mode, make_options, teardown_profiling): - with pytest.raises(ValueError): - setup_profiler(make_options(mode)) - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("sleep"), - pytest.param("gevent", marks=requires_gevent), - ], -) -@pytest.mark.parametrize( - "make_options", - [ - pytest.param(experimental_options, id="experiment"), - pytest.param(non_experimental_options, id="non experimental"), - ], -) -def test_profiler_valid_mode(mode, make_options, teardown_profiling): - # should not raise any exceptions - setup_profiler(make_options(mode)) - - -@pytest.mark.parametrize( - "make_options", - [ - pytest.param(experimental_options, id="experiment"), - pytest.param(non_experimental_options, id="non experimental"), - ], -) -def test_profiler_setup_twice(make_options, teardown_profiling): - # setting up the first time should return True to indicate success - assert setup_profiler(make_options()) - # setting up the second time should return False to indicate no-op - assert not setup_profiler(make_options()) - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("gevent", marks=requires_gevent), - ], -) -@pytest.mark.parametrize( - ("profiles_sample_rate", "profile_count"), - [ - pytest.param(1.00, 1, id="profiler sampled at 1.00"), - pytest.param(0.75, 1, id="profiler sampled at 0.75"), - pytest.param(0.25, 0, id="profiler sampled at 0.25"), - pytest.param(0.00, 0, id="profiler sampled at 0.00"), - pytest.param(None, 0, id="profiler not enabled"), - ], -) -@pytest.mark.parametrize( - "make_options", - [ - pytest.param(experimental_options, id="experiment"), - pytest.param(non_experimental_options, id="non experimental"), - ], -) -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_profiles_sample_rate( - sentry_init, - capture_envelopes, - capture_record_lost_event_calls, - teardown_profiling, - profiles_sample_rate, - profile_count, - make_options, - mode, -): - options = make_options(mode=mode, sample_rate=profiles_sample_rate) - sentry_init( - traces_sample_rate=1.0, - profiler_mode=options.get("profiler_mode"), - profiles_sample_rate=options.get("profiles_sample_rate"), - _experiments=options.get("_experiments", {}), - ) - - envelopes = capture_envelopes() - record_lost_event_calls = capture_record_lost_event_calls() - - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.random.random", return_value=0.5 - ): - with start_transaction(name="profiling"): - pass - - items = defaultdict(list) - for envelope in envelopes: - for item in envelope.items: - items[item.type].append(item) - - assert len(items["transaction"]) == 1 - assert len(items["profile"]) == profile_count - if profiles_sample_rate is None or profiles_sample_rate == 0: - assert record_lost_event_calls == [] - elif profile_count: - assert record_lost_event_calls == [] - else: - assert record_lost_event_calls == [("sample_rate", "profile", None, 1)] - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("gevent", marks=requires_gevent), - ], -) -@pytest.mark.parametrize( - ("profiles_sampler", "profile_count"), - [ - pytest.param(lambda _: 1.00, 1, id="profiler sampled at 1.00"), - pytest.param(lambda _: 0.75, 1, id="profiler sampled at 0.75"), - pytest.param(lambda _: 0.25, 0, id="profiler sampled at 0.25"), - pytest.param(lambda _: 0.00, 0, id="profiler sampled at 0.00"), - pytest.param(lambda _: None, 0, id="profiler not enabled"), - pytest.param( - lambda ctx: 1 if ctx["transaction_context"]["name"] == "profiling" else 0, - 1, - id="profiler sampled for transaction name", - ), - pytest.param( - lambda ctx: 0 if ctx["transaction_context"]["name"] == "profiling" else 1, - 0, - id="profiler not sampled for transaction name", - ), - pytest.param( - lambda _: "1", 0, id="profiler not sampled because string sample rate" - ), - pytest.param(lambda _: True, 1, id="profiler sampled at True"), - pytest.param(lambda _: False, 0, id="profiler sampled at False"), - ], -) -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_profiles_sampler( - sentry_init, - capture_envelopes, - capture_record_lost_event_calls, - teardown_profiling, - profiles_sampler, - profile_count, - mode, -): - sentry_init( - traces_sample_rate=1.0, - profiles_sampler=profiles_sampler, - ) - - envelopes = capture_envelopes() - record_lost_event_calls = capture_record_lost_event_calls() - - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.random.random", return_value=0.5 - ): - with start_transaction(name="profiling"): - pass - - items = defaultdict(list) - for envelope in envelopes: - for item in envelope.items: - items[item.type].append(item) - - assert len(items["transaction"]) == 1 - assert len(items["profile"]) == profile_count - if profile_count: - assert record_lost_event_calls == [] - else: - assert record_lost_event_calls == [("sample_rate", "profile", None, 1)] - - -def test_minimum_unique_samples_required( - sentry_init, - capture_envelopes, - capture_record_lost_event_calls, - teardown_profiling, -): - sentry_init( - traces_sample_rate=1.0, - _experiments={"profiles_sample_rate": 1.0}, - ) - - envelopes = capture_envelopes() - record_lost_event_calls = capture_record_lost_event_calls() - - with start_transaction(name="profiling"): - pass - - items = defaultdict(list) - for envelope in envelopes: - for item in envelope.items: - items[item.type].append(item) - - assert len(items["transaction"]) == 1 - # because we dont leave any time for the profiler to - # take any samples, it should be not be sent - assert len(items["profile"]) == 0 - assert record_lost_event_calls == [("insufficient_data", "profile", None, 1)] - - -@pytest.mark.forked -def test_profile_captured( - sentry_init, - capture_envelopes, - teardown_profiling, -): - sentry_init( - traces_sample_rate=1.0, - _experiments={"profiles_sample_rate": 1.0}, - ) - - envelopes = capture_envelopes() - - with start_transaction(name="profiling"): - time.sleep(0.05) - - items = defaultdict(list) - for envelope in envelopes: - for item in envelope.items: - items[item.type].append(item) - - assert len(items["transaction"]) == 1 - assert len(items["profile"]) == 1 - - -def get_frame(depth=1): - """ - This function is not exactly true to its name. Depending on - how it is called, the true depth of the stack can be deeper - than the argument implies. - """ - if depth <= 0: - raise ValueError("only positive integers allowed") - if depth > 1: - return get_frame(depth=depth - 1) - return inspect.currentframe() - - -class GetFrameBase: - def inherited_instance_method(self): - return inspect.currentframe() - - def inherited_instance_method_wrapped(self): - def wrapped(): - return inspect.currentframe() - - return wrapped - - @classmethod - def inherited_class_method(cls): - return inspect.currentframe() - - @classmethod - def inherited_class_method_wrapped(cls): - def wrapped(): - return inspect.currentframe() - - return wrapped - - @staticmethod - def inherited_static_method(): - return inspect.currentframe() - - -class GetFrame(GetFrameBase): - def instance_method(self): - return inspect.currentframe() - - def instance_method_wrapped(self): - def wrapped(): - return inspect.currentframe() - - return wrapped - - @classmethod - def class_method(cls): - return inspect.currentframe() - - @classmethod - def class_method_wrapped(cls): - def wrapped(): - return inspect.currentframe() - - return wrapped - - @staticmethod - def static_method(): - return inspect.currentframe() - - -@pytest.mark.parametrize( - ("frame", "frame_name"), - [ - pytest.param( - get_frame(), - "get_frame", - id="function", - ), - pytest.param( - (lambda: inspect.currentframe())(), - "", - id="lambda", - ), - pytest.param( - GetFrame().instance_method(), - "GetFrame.instance_method", - id="instance_method", - ), - pytest.param( - GetFrame().instance_method_wrapped()(), - ( - "wrapped" - if sys.version_info < (3, 11) - else "GetFrame.instance_method_wrapped..wrapped" - ), - id="instance_method_wrapped", - ), - pytest.param( - GetFrame().class_method(), - "GetFrame.class_method", - id="class_method", - ), - pytest.param( - GetFrame().class_method_wrapped()(), - ( - "wrapped" - if sys.version_info < (3, 11) - else "GetFrame.class_method_wrapped..wrapped" - ), - id="class_method_wrapped", - ), - pytest.param( - GetFrame().static_method(), - "static_method" if sys.version_info < (3, 11) else "GetFrame.static_method", - id="static_method", - ), - pytest.param( - GetFrame().inherited_instance_method(), - "GetFrameBase.inherited_instance_method", - id="inherited_instance_method", - ), - pytest.param( - GetFrame().inherited_instance_method_wrapped()(), - ( - "wrapped" - if sys.version_info < (3, 11) - else "GetFrameBase.inherited_instance_method_wrapped..wrapped" - ), - id="instance_method_wrapped", - ), - pytest.param( - GetFrame().inherited_class_method(), - "GetFrameBase.inherited_class_method", - id="inherited_class_method", - ), - pytest.param( - GetFrame().inherited_class_method_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" - ), - id="inherited_static_method", - ), - ], -) -def test_get_frame_name(frame, frame_name): - assert get_frame_name(frame) == frame_name - - -@pytest.mark.parametrize( - ("get_frame", "function"), - [ - pytest.param(lambda: get_frame(depth=1), "get_frame", id="simple"), - ], -) -def test_extract_frame(get_frame, function): - cwd = os.getcwd() - frame = get_frame() - extracted_frame = extract_frame(frame_id(frame), frame, cwd) - - # the abs_path should be equal toe the normalized path of the co_filename - assert extracted_frame["abs_path"] == os.path.normpath(frame.f_code.co_filename) - - # the module should be pull from this test module - assert extracted_frame["module"] == __name__ - - # the filename should be the file starting after the cwd - assert extracted_frame["filename"] == __file__[len(cwd) + 1 :] - - assert extracted_frame["function"] == function - - # the lineno will shift over time as this file is modified so just check - # that it is an int - assert isinstance(extracted_frame["lineno"], int) - - -@pytest.mark.parametrize( - ("depth", "max_stack_depth", "actual_depth"), - [ - pytest.param(1, 128, 1, id="less than"), - pytest.param(256, 128, 128, id="greater than"), - pytest.param(128, 128, 128, id="equals"), - ], -) -def test_extract_stack_with_max_depth(depth, max_stack_depth, actual_depth): - # introduce a lambda that we'll be looking for in the stack - frame = (lambda: get_frame(depth=depth))() - - # plus 1 because we introduced a lambda intentionally that we'll - # look for in the final stack to make sure its in the right position - base_stack_depth = len(inspect.stack()) + 1 - - # increase the max_depth by the `base_stack_depth` to account - # for the extra frames pytest will add - _, frame_ids, frames = extract_stack( - frame, - LRUCache(max_size=1), - max_stack_depth=max_stack_depth + base_stack_depth, - cwd=os.getcwd(), - ) - assert len(frame_ids) == base_stack_depth + actual_depth - assert len(frames) == base_stack_depth + actual_depth - - for i in range(actual_depth): - assert frames[i]["function"] == "get_frame", i - - # index 0 contains the inner most frame on the stack, so the lamdba - # should be at index `actual_depth` - if sys.version_info >= (3, 11): - assert ( - frames[actual_depth]["function"] - == "test_extract_stack_with_max_depth.." - ), actual_depth - else: - assert frames[actual_depth]["function"] == "", actual_depth - - -@pytest.mark.parametrize( - ("frame", "depth"), - [(get_frame(depth=1), len(inspect.stack()))], -) -def test_extract_stack_with_cache(frame, depth): - # make sure cache has enough room or this test will fail - cache = LRUCache(max_size=depth) - cwd = os.getcwd() - _, _, frames1 = extract_stack(frame, cache, cwd=cwd) - _, _, frames2 = extract_stack(frame, cache, cwd=cwd) - - assert len(frames1) > 0 - assert len(frames2) > 0 - assert len(frames1) == len(frames2) - for i, (frame1, frame2) in enumerate(zip(frames1, frames2)): - # DO NOT use `==` for the assertion here since we are - # testing for identity, and using `==` would test for - # equality which would always pass since we're extract - # the same stack. - assert frame1 is frame2, i - - -def get_scheduler_threads(scheduler): - return [thread for thread in threading.enumerate() if thread.name == scheduler.name] - - -@pytest.mark.parametrize( - ("scheduler_class",), - [ - pytest.param(ThreadScheduler, id="thread scheduler"), - pytest.param( - GeventScheduler, - marks=[ - requires_gevent, - pytest.mark.skip( - reason="cannot find this thread via threading.enumerate()" - ), - ], - id="gevent scheduler", - ), - ], -) -def test_thread_scheduler_single_background_thread(scheduler_class): - scheduler = scheduler_class(frequency=1000) - - # not yet setup, no scheduler threads yet - assert len(get_scheduler_threads(scheduler)) == 0 - - scheduler.setup() - - # setup but no profiles started so still no threads - assert len(get_scheduler_threads(scheduler)) == 0 - - scheduler.ensure_running() - - # the scheduler will start always 1 thread - assert len(get_scheduler_threads(scheduler)) == 1 - - scheduler.ensure_running() - - # the scheduler still only has 1 thread - assert len(get_scheduler_threads(scheduler)) == 1 - - scheduler.teardown() - - # once finished, the thread should stop - assert len(get_scheduler_threads(scheduler)) == 0 - - -@pytest.mark.parametrize( - ("scheduler_class",), - [ - pytest.param(ThreadScheduler, id="thread scheduler"), - pytest.param( - GeventScheduler, - marks=[ - requires_gevent, - pytest.mark.skip( - reason="cannot find this thread via threading.enumerate()" - ), - ], - id="gevent scheduler", - ), - ], -) -def test_thread_scheduler_no_thread_on_shutdown(scheduler_class): - scheduler = scheduler_class(frequency=1000) - - # not yet setup, no scheduler threads yet - assert len(get_scheduler_threads(scheduler)) == 0 - - scheduler.setup() - - # setup but no profiles started so still no threads - assert len(get_scheduler_threads(scheduler)) == 0 - - # mock RuntimeError as if the 3.12 interpreter was shutting down - with mock.patch( - "threading.Thread.start", - side_effect=RuntimeError("can't create new thread at interpreter shutdown"), - ): - scheduler.ensure_running() - - assert scheduler.running is False - - # still no thread - assert len(get_scheduler_threads(scheduler)) == 0 - - scheduler.teardown() - - assert len(get_scheduler_threads(scheduler)) == 0 - - -@pytest.mark.parametrize( - ("scheduler_class",), - [ - pytest.param(ThreadScheduler, id="thread scheduler"), - pytest.param(GeventScheduler, marks=requires_gevent, id="gevent scheduler"), - ], -) -@mock.patch("sentry_sdk.profiler.transaction_profiler.MAX_PROFILE_DURATION_NS", 1) -def test_max_profile_duration_reached(scheduler_class): - sample = [ - ( - "1", - extract_stack( - get_frame(), - LRUCache(max_size=1), - cwd=os.getcwd(), - ), - ), - ] - - with scheduler_class(frequency=1000) as scheduler: - with Profile(True, 0, scheduler=scheduler) as profile: - # profile just started, it's active - assert profile.active - - # write a sample at the start time, so still active - profile.write(profile.start_ns + 0, sample) - assert profile.active - - # write a sample at max time, so still active - profile.write(profile.start_ns + 1, sample) - assert profile.active - - # write a sample PAST the max time, so now inactive - profile.write(profile.start_ns + 2, sample) - assert not profile.active - - -class NoopScheduler(Scheduler): - def setup(self) -> None: - pass - - def teardown(self) -> None: - pass - - def ensure_running(self) -> None: - pass - - -current_thread = threading.current_thread() -thread_metadata = { - str(current_thread.ident): { - "name": str(current_thread.name), - }, -} - - -sample_stacks = [ - extract_stack( - get_frame(), - LRUCache(max_size=1), - max_stack_depth=1, - cwd=os.getcwd(), - ), - extract_stack( - get_frame(), - LRUCache(max_size=1), - max_stack_depth=2, - cwd=os.getcwd(), - ), -] - - -@pytest.mark.parametrize( - ("samples", "expected"), - [ - pytest.param( - [], - { - "frames": [], - "samples": [], - "stacks": [], - "thread_metadata": thread_metadata, - }, - id="empty", - ), - pytest.param( - [(6, [("1", sample_stacks[0])])], - { - "frames": [], - "samples": [], - "stacks": [], - "thread_metadata": thread_metadata, - }, - id="single sample out of range", - ), - pytest.param( - [(0, [("1", sample_stacks[0])])], - { - "frames": [sample_stacks[0][2][0]], - "samples": [ - { - "elapsed_since_start_ns": "0", - "thread_id": "1", - "stack_id": 0, - }, - ], - "stacks": [[0]], - "thread_metadata": thread_metadata, - }, - id="single sample in range", - ), - pytest.param( - [ - (0, [("1", sample_stacks[0])]), - (1, [("1", sample_stacks[0])]), - ], - { - "frames": [sample_stacks[0][2][0]], - "samples": [ - { - "elapsed_since_start_ns": "0", - "thread_id": "1", - "stack_id": 0, - }, - { - "elapsed_since_start_ns": "1", - "thread_id": "1", - "stack_id": 0, - }, - ], - "stacks": [[0]], - "thread_metadata": thread_metadata, - }, - id="two identical stacks", - ), - pytest.param( - [ - (0, [("1", sample_stacks[0])]), - (1, [("1", sample_stacks[1])]), - ], - { - "frames": [ - sample_stacks[0][2][0], - sample_stacks[1][2][0], - ], - "samples": [ - { - "elapsed_since_start_ns": "0", - "thread_id": "1", - "stack_id": 0, - }, - { - "elapsed_since_start_ns": "1", - "thread_id": "1", - "stack_id": 1, - }, - ], - "stacks": [[0], [1, 0]], - "thread_metadata": thread_metadata, - }, - id="two different stacks", - ), - ], -) -@mock.patch("sentry_sdk.profiler.transaction_profiler.MAX_PROFILE_DURATION_NS", 5) -def test_profile_processing( - DictionaryContaining, # noqa: N803 - samples, - expected, -): - with NoopScheduler(frequency=1000) as scheduler: - with Profile(True, 0, scheduler=scheduler) as profile: - for ts, sample in samples: - # force the sample to be written at a time relative to the - # start of the profile - now = profile.start_ns + ts - profile.write(now, sample) - - processed = profile.process() - - assert processed["thread_metadata"] == DictionaryContaining( - expected["thread_metadata"] - ) - assert processed["frames"] == expected["frames"] - assert processed["stacks"] == expected["stacks"] - assert processed["samples"] == expected["samples"] - - -def test_no_warning_without_hub(): - with warnings.catch_warnings(): - warnings.simplefilter("error") - Profile(True, 0) diff --git a/tests/test_client.py b/tests/test_client.py index c549e2328a..8a71dcc778 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1325,7 +1325,7 @@ def test_uwsgi_warnings(sentry_init, recwarn, opt, missing_flags): uwsgi = mock.MagicMock() uwsgi.opt = opt with mock.patch.dict("sys.modules", uwsgi=uwsgi): - sentry_init(profiles_sample_rate=1.0) + sentry_init() if missing_flags: assert len(recwarn) == 1 record = recwarn.pop() diff --git a/tests/test_envelope.py b/tests/test_envelope.py index 5549cc6e36..f6a7c4b541 100644 --- a/tests/test_envelope.py +++ b/tests/test_envelope.py @@ -250,7 +250,6 @@ def test_envelope_item_data_category_mapping(): ("session", "session"), ("attachment", "attachment"), ("client_report", "internal"), - ("profile", "profile"), ("profile_chunk", "profile_chunk"), ("check_in", "monitor"), ("unknown_type", "default"), From 17094d74ef69518bd1fbf0f7138b2116255d62ae Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 15:52:36 +0200 Subject: [PATCH 09/16] ref: Remove experimental potel integration (#6931) Closes https://github.com/getsentry/sentry-python/issues/5028 --- .github/workflows/test-integrations-misc.yml | 4 - MIGRATION_GUIDE.md | 1 + scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 8 -- .../split_tox_gh_actions.py | 1 - sentry_sdk/client.py | 15 +--- sentry_sdk/consts.py | 1 - .../integrations/opentelemetry/integration.py | 81 ------------------- setup.py | 1 - tests/conftest.py | 7 +- .../opentelemetry/test_experimental.py | 57 ------------- .../opentelemetry/test_propagator.py | 23 ------ tox.ini | 8 -- 13 files changed, 3 insertions(+), 205 deletions(-) delete mode 100644 sentry_sdk/integrations/opentelemetry/integration.py delete mode 100644 tests/integrations/opentelemetry/test_experimental.py diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index ebec367f0b..43e2e94f9e 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -49,10 +49,6 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-otlp" - - name: Test potel - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-potel" - name: Test pure_eval run: | set -x # print commands that are executed diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 930c6c654a..46f690878b 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -29,6 +29,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Transaction profiling and related code was removed. - 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. +- The experimental option `otel_powered_performance` has been removed together with the associated `OpenTelemetryIntegration` and `opentelemetry-experimental` extra. ## Deprecated diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index a8609dab50..6e8b81367b 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -71,7 +71,6 @@ "gevent", "opentelemetry", "otlp", - "potel", } # Free-threading is experimentally supported in 3.13, and officially supported in 3.14. diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 12a2038683..10b78c064f 100644 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -48,9 +48,6 @@ envlist = # OpenTelemetry with OTLP {py3.7,py3.9,py3.12,py3.13,py3.14}-otlp - # OpenTelemetry Experimental (POTel) - {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # === Integrations - Auto-generated === # These come from the populate_tox.py script. @@ -147,10 +144,6 @@ deps = otlp: opentelemetry-distro[otlp] otlp: responses - # OpenTelemetry Experimental (POTel) - potel: opentelemetry-distro - potel: pytest-forked - # === Integrations - Auto-generated === # These come from the populate_tox.py script. @@ -242,7 +235,6 @@ setenv = gcp: _TESTPATH=tests/integrations/gcp opentelemetry: _TESTPATH=tests/integrations/opentelemetry otlp: _TESTPATH=tests/integrations/otlp - potel: _TESTPATH=tests/integrations/opentelemetry socket: _TESTPATH=tests/integrations/socket # These TESTPATH definitions are auto-generated by toxgen diff --git a/scripts/split_tox_gh_actions/split_tox_gh_actions.py b/scripts/split_tox_gh_actions/split_tox_gh_actions.py index d6b5494515..9af495e3b4 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -165,7 +165,6 @@ "loguru", "opentelemetry", "otlp", - "potel", "pure_eval", "trytond", "typer", diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index cb3de4b382..3e1a2d4233 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -28,7 +28,7 @@ _resolve_data_collection, ) from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations +from sentry_sdk.integrations import setup_integrations from sentry_sdk.integrations.dedupe import DedupeIntegration from sentry_sdk.monitor import Monitor from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler @@ -675,19 +675,6 @@ def _record_lost_event( ) ) - if self.options["_experiments"].get("otel_powered_performance", False): - logger.debug( - "[OTel] Enabling experimental OTel-powered performance monitoring." - ) - self.options["instrumenter"] = INSTRUMENTER.OTEL - if ( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" - not in _DEFAULT_INTEGRATIONS - ): - _DEFAULT_INTEGRATIONS.append( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", - ) - self.integrations = setup_integrations( self.options["integrations"], with_defaults=self.options["default_integrations"], diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 36f7e4058e..e61623da9b 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -70,7 +70,6 @@ class CompressionAlgo(Enum): "record_sql_params": Optional[bool], "continuous_profiling_auto_start": Optional[bool], "continuous_profiling_mode": Optional[ContinuousProfilerMode], - "otel_powered_performance": Optional[bool], "transport_zlib_compression_level": Optional[int], "transport_compression_level": Optional[int], "transport_compression_algo": Optional[CompressionAlgo], diff --git a/sentry_sdk/integrations/opentelemetry/integration.py b/sentry_sdk/integrations/opentelemetry/integration.py deleted file mode 100644 index 472e8181f9..0000000000 --- a/sentry_sdk/integrations/opentelemetry/integration.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -IMPORTANT: The contents of this file are part of a proof of concept and as such -are experimental and not suitable for production use. They may be changed or -removed at any time without prior notice. -""" - -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - -try: - from opentelemetry import trace - from opentelemetry.propagate import set_global_textmap - from opentelemetry.sdk.trace import TracerProvider -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -try: - from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] - DjangoInstrumentor, - ) -except ImportError: - DjangoInstrumentor = None - - -CONFIGURABLE_INSTRUMENTATIONS = { - DjangoInstrumentor: {"is_sql_commentor_enabled": True}, -} - - -class OpenTelemetryIntegration(Integration): - identifier = "opentelemetry" - - @staticmethod - def setup_once() -> None: - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if has_span_streaming_enabled(options): - logger.warning( - "[OTel] OpenTelemetryIntegration is not compatible with span streaming " - "(trace_lifecycle='stream') and will be disabled." - ) - return - - warnings.warn( - "OpenTelemetryIntegration is deprecated. " - "Please use OTLPIntegration instead: " - "https://docs.sentry.io/platforms/python/integrations/otlp/", - DeprecationWarning, - stacklevel=2, - ) - - _setup_sentry_tracing() - # _setup_instrumentors() - - logger.debug("[OTel] Finished setting up OpenTelemetry integration") - - -def _setup_sentry_tracing() -> None: - provider = TracerProvider() - provider.add_span_processor(SentrySpanProcessor()) - trace.set_tracer_provider(provider) - set_global_textmap(SentryPropagator()) - - -def _setup_instrumentors() -> None: - for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): - if instrumentor is None: - continue - instrumentor().instrument(**kwargs) diff --git a/setup.py b/setup.py index 35d95ff636..0a129c5052 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,6 @@ def get_file_text(file_name): "openai": ["openai>=1.0.0", "tiktoken>=0.3.0"], "openfeature": ["openfeature-sdk>=0.7.1"], "opentelemetry": ["opentelemetry-distro>=0.35b0"], - "opentelemetry-experimental": ["opentelemetry-distro"], "opentelemetry-otlp": ["opentelemetry-distro[otlp]>=0.35b0"], "pure-eval": ["pure_eval", "executing", "asttokens"], "pydantic_ai": ["pydantic-ai>=1.0.0"], diff --git a/tests/conftest.py b/tests/conftest.py index 8522511900..04debe9b8c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -271,12 +271,7 @@ def reset_integrations(): but this also means some other stuff will be monkeypatched twice. """ global _DEFAULT_INTEGRATIONS, _processed_integrations - try: - _DEFAULT_INTEGRATIONS.remove( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" - ) - except ValueError: - pass + _processed_integrations.clear() _installed_integrations.clear() diff --git a/tests/integrations/opentelemetry/test_experimental.py b/tests/integrations/opentelemetry/test_experimental.py deleted file mode 100644 index 905ab0d5b4..0000000000 --- a/tests/integrations/opentelemetry/test_experimental.py +++ /dev/null @@ -1,57 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest - -from sentry_sdk.integrations.opentelemetry.integration import OpenTelemetryIntegration - - -@pytest.mark.forked -def test_integration_enabled_if_option_is_on(sentry_init, reset_integrations): - mocked_setup_sentry_tracing = MagicMock() - - with patch( - "sentry_sdk.integrations.opentelemetry.integration._setup_sentry_tracing", - mocked_setup_sentry_tracing, - ): - with pytest.warns(DeprecationWarning): - sentry_init(_experiments={"otel_powered_performance": True}) - mocked_setup_sentry_tracing.assert_called_once() - - -@pytest.mark.forked -def test_integration_not_enabled_if_option_is_off(sentry_init, reset_integrations): - mocked_setup_sentry_tracing = MagicMock() - - with patch( - "sentry_sdk.integrations.opentelemetry.integration._setup_sentry_tracing", - mocked_setup_sentry_tracing, - ): - sentry_init(_experiments={"otel_powered_performance": False}) - mocked_setup_sentry_tracing.assert_not_called() - - -@pytest.mark.forked -def test_integration_not_enabled_if_option_is_missing(sentry_init, reset_integrations): - mocked_setup_sentry_tracing = MagicMock() - - with patch( - "sentry_sdk.integrations.opentelemetry.integration._setup_sentry_tracing", - mocked_setup_sentry_tracing, - ): - sentry_init() - mocked_setup_sentry_tracing.assert_not_called() - - -@pytest.mark.forked -def test_integration_disabled_with_span_streaming(sentry_init, reset_integrations): - mocked_setup_sentry_tracing = MagicMock() - - with patch( - "sentry_sdk.integrations.opentelemetry.integration._setup_sentry_tracing", - mocked_setup_sentry_tracing, - ): - sentry_init( - integrations=[OpenTelemetryIntegration()], - trace_lifecycle="stream", - ) - mocked_setup_sentry_tracing.assert_not_called() diff --git a/tests/integrations/opentelemetry/test_propagator.py b/tests/integrations/opentelemetry/test_propagator.py index 1c3e301c3d..42ec4ee200 100644 --- a/tests/integrations/opentelemetry/test_propagator.py +++ b/tests/integrations/opentelemetry/test_propagator.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock import pytest -from opentelemetry import trace from opentelemetry.context import get_current from opentelemetry.trace import ( SpanContext, @@ -15,12 +14,8 @@ SENTRY_BAGGAGE_KEY, SENTRY_TRACE_KEY, ) -from sentry_sdk.integrations.opentelemetry.integration import ( - OpenTelemetryIntegration, -) from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor -from sentry_sdk.scope import global_event_processors from sentry_sdk.tracing_utils import Baggage @@ -302,21 +297,3 @@ def test_inject_sentry_span_baggage(): "baggage", baggage.serialize(), ) - - -def test_inject_no_memory_leak(): - - OpenTelemetryIntegration.setup_once() - - tracer = trace.get_tracer(__name__) - propagator = SentryPropagator() - - cnt_before = len(global_event_processors) - - with tracer.start_as_current_span("bar") as new_span: - context = set_span_in_context(new_span) - carrier = "any_carrier" - propagator.inject(carrier, context) - - cnt_after = len(global_event_processors) - assert cnt_after == cnt_before diff --git a/tox.ini b/tox.ini index 889af6d6bf..361a1ead98 100644 --- a/tox.ini +++ b/tox.ini @@ -48,9 +48,6 @@ envlist = # OpenTelemetry with OTLP {py3.7,py3.9,py3.12,py3.13,py3.14}-otlp - # OpenTelemetry Experimental (POTel) - {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # === Integrations - Auto-generated === # These come from the populate_tox.py script. @@ -467,10 +464,6 @@ deps = otlp: opentelemetry-distro[otlp] otlp: responses - # OpenTelemetry Experimental (POTel) - potel: opentelemetry-distro - potel: pytest-forked - # === Integrations - Auto-generated === # These come from the populate_tox.py script. @@ -18627,7 +18620,6 @@ setenv = gcp: _TESTPATH=tests/integrations/gcp opentelemetry: _TESTPATH=tests/integrations/opentelemetry otlp: _TESTPATH=tests/integrations/otlp - potel: _TESTPATH=tests/integrations/opentelemetry socket: _TESTPATH=tests/integrations/socket # These TESTPATH definitions are auto-generated by toxgen From 4702caa83dcd00bb6c10562b1aa282e09afbeda5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 16:42:57 +0200 Subject: [PATCH 10/16] ref: Remove old OTel support code (#6935) - Remove everything in `integrations/opentelemetry` (`SentrySpanProcessor`, `SentryPropagator`, etc.) - Remove associated test files and CI config - Move old propagator functions and consts that we were using in `OTLPIntegration` to the OTLP propagator directly - Remove `instrumenter` Note: `NoOpSpan` was not removed because it makes mypy blow up. Not worth the effort as we'll anyway get rid of it when dropping transaction based tracing. #### Issues Closes https://github.com/getsentry/sentry-python/issues/6932 --- .github/workflows/test-integrations-misc.yml | 4 - MIGRATION_GUIDE.md | 1 + scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 8 - .../split_tox_gh_actions.py | 1 - sentry_sdk/api.py | 6 +- sentry_sdk/client.py | 4 - sentry_sdk/consts.py | 7 - .../integrations/opentelemetry/__init__.py | 7 - .../integrations/opentelemetry/consts.py | 9 - .../integrations/opentelemetry/propagator.py | 128 ---- .../opentelemetry/span_processor.py | 407 ----------- sentry_sdk/integrations/otlp.py | 75 ++- sentry_sdk/scope.py | 22 +- sentry_sdk/tracing.py | 19 +- setup.py | 6 - tests/integrations/opentelemetry/__init__.py | 3 - .../opentelemetry/test_entry_points.py | 26 - .../opentelemetry/test_propagator.py | 299 --------- .../opentelemetry/test_span_processor.py | 635 ------------------ tests/integrations/otlp/test_otlp.py | 5 +- tests/tracing/test_noop_span.py | 52 -- tox.ini | 9 +- 23 files changed, 77 insertions(+), 1657 deletions(-) delete mode 100644 sentry_sdk/integrations/opentelemetry/__init__.py delete mode 100644 sentry_sdk/integrations/opentelemetry/consts.py delete mode 100644 sentry_sdk/integrations/opentelemetry/propagator.py delete mode 100644 sentry_sdk/integrations/opentelemetry/span_processor.py delete mode 100644 tests/integrations/opentelemetry/__init__.py delete mode 100644 tests/integrations/opentelemetry/test_entry_points.py delete mode 100644 tests/integrations/opentelemetry/test_propagator.py delete mode 100644 tests/integrations/opentelemetry/test_span_processor.py delete mode 100644 tests/tracing/test_noop_span.py diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index 43e2e94f9e..1de39fcadc 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -41,10 +41,6 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-loguru" - - name: Test opentelemetry - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-opentelemetry" - name: Test otlp run: | set -x # print commands that are executed diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 46f690878b..6809604e6e 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -28,6 +28,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Transaction profiling and related code was removed. - Removed the deprecated Hub class and all uses of hub throughout the SDK in arguments, options, etc. Use a scope instead. +- The `SentrySpanProcessor`, `SentryPropagator`, `instrumenter`, and associated OpenTelemetry compatibility code was removed along with the `opentelemetry` extra and the `SentryPropagator` entrypoint. Use the `OTLPIntegration` instead. - Removed the `auto_session_tracing` decorator. Use `track_session` instead. - The experimental option `otel_powered_performance` has been removed together with the associated `OpenTelemetryIntegration` and `opentelemetry-experimental` extra. diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 6e8b81367b..1fa9e765c5 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -69,7 +69,6 @@ "shadowed_module", "gcp", "gevent", - "opentelemetry", "otlp", } diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 10b78c064f..459fc00cfa 100644 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -42,9 +42,6 @@ envlist = # GCP {py3.7}-gcp - # OpenTelemetry (OTel) - {py3.7,py3.9,py3.12,py3.13,py3.14,py3.14t}-opentelemetry - # OpenTelemetry with OTLP {py3.7,py3.9,py3.12,py3.13,py3.14}-otlp @@ -136,10 +133,6 @@ deps = aws_lambda: uvicorn aws_lambda: pyyaml - # OpenTelemetry (OTel) - opentelemetry: opentelemetry-distro - opentelemetry: pytest-forked - # OpenTelemetry with OTLP otlp: opentelemetry-distro[otlp] otlp: responses @@ -233,7 +226,6 @@ setenv = aws_lambda: _TESTPATH=tests/integrations/aws_lambda cloud_resource_context: _TESTPATH=tests/integrations/cloud_resource_context gcp: _TESTPATH=tests/integrations/gcp - opentelemetry: _TESTPATH=tests/integrations/opentelemetry otlp: _TESTPATH=tests/integrations/otlp socket: _TESTPATH=tests/integrations/socket diff --git a/scripts/split_tox_gh_actions/split_tox_gh_actions.py b/scripts/split_tox_gh_actions/split_tox_gh_actions.py index 9af495e3b4..754315d72c 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -163,7 +163,6 @@ ], "Misc": [ "loguru", - "opentelemetry", "otlp", "pure_eval", "trytond", diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index d5f91fa145..4aa81e34ca 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -5,7 +5,6 @@ from sentry_sdk import Client, tracing_utils from sentry_sdk._init_implementation import init -from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.crons import monitor from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope from sentry_sdk.traces import StreamedSpan @@ -383,7 +382,6 @@ def start_span( @scopemethod def start_transaction( transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, custom_sampling_context: "Optional[SamplingContext]" = None, **kwargs: "Unpack[TransactionKwargs]", ) -> "Union[Transaction, NoOpSpan]": @@ -411,15 +409,13 @@ def start_transaction( :param transaction: The transaction to start. If omitted, we create and start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. :param custom_sampling_context: The transaction's custom sampling context. :param kwargs: Optional keyword arguments to be passed to the Transaction constructor. See :py:class:`sentry_sdk.tracing.Transaction` for available arguments. """ return get_current_scope().start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs + transaction, custom_sampling_context, **kwargs ) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 3e1a2d4233..fb315cd674 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -17,7 +17,6 @@ from sentry_sdk.consts import ( DEFAULT_MAX_VALUE_LENGTH, DEFAULT_OPTIONS, - INSTRUMENTER, SPANDATA, SPANSTATUS, VERSION, @@ -331,9 +330,6 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": if rv["server_name"] is None and hasattr(socket, "gethostname"): rv["server_name"] = socket.gethostname() - if rv["instrumenter"] is None: - rv["instrumenter"] = INSTRUMENTER.SENTRY - if rv["project_root"] is None: try: project_root = os.getcwd() diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index e61623da9b..c2500ca913 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -113,11 +113,6 @@ def __str__(self) -> str: return self.value -class INSTRUMENTER: - SENTRY = "sentry" - OTEL = "otel" - - class SPANNAME: DB_COMMIT = "COMMIT" DB_ROLLBACK = "ROLLBACK" @@ -1309,7 +1304,6 @@ def __init__( send_client_reports: bool = True, _experiments: "Experiments" = {}, # noqa: B006 proxy_headers: "Optional[Dict[str, str]]" = None, - instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, before_send_transaction: "Optional[TransactionProcessor]" = None, project_root: "Optional[str]" = None, enable_tracing: "Optional[bool]" = None, @@ -1716,7 +1710,6 @@ def __init__( :param spotlight: - :param instrumenter: :param enable_logs: Set `enable_logs` to True to enable the SDK to emit Sentry logs. Defaults to False. diff --git a/sentry_sdk/integrations/opentelemetry/__init__.py b/sentry_sdk/integrations/opentelemetry/__init__.py deleted file mode 100644 index d76b8058ee..0000000000 --- a/sentry_sdk/integrations/opentelemetry/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor - -__all__ = [ - "SentryPropagator", - "SentrySpanProcessor", -] diff --git a/sentry_sdk/integrations/opentelemetry/consts.py b/sentry_sdk/integrations/opentelemetry/consts.py deleted file mode 100644 index d6733036ea..0000000000 --- a/sentry_sdk/integrations/opentelemetry/consts.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable - -try: - from opentelemetry.context import create_key -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -SENTRY_TRACE_KEY = create_key("sentry-trace") -SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/sentry_sdk/integrations/opentelemetry/propagator.py b/sentry_sdk/integrations/opentelemetry/propagator.py deleted file mode 100644 index 5b5f13861d..0000000000 --- a/sentry_sdk/integrations/opentelemetry/propagator.py +++ /dev/null @@ -1,128 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.integrations.opentelemetry.span_processor import ( - SentrySpanProcessor, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data - -try: - from opentelemetry import trace - from opentelemetry.context import ( - Context, - get_current, - set_value, - ) - from opentelemetry.propagators.textmap import ( - CarrierT, - Getter, - Setter, - TextMapPropagator, - default_getter, - default_setter, - ) - from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - TraceFlags, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional, Set - - -class SentryPropagator(TextMapPropagator): - """ - Propagates tracing headers for Sentry's tracing system in a way OTel understands. - """ - - def extract( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - getter: "Getter[CarrierT]" = default_getter, - ) -> "Context": - if context is None: - context = get_current() - - sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) - if not sentry_trace: - return context - - sentrytrace = extract_sentrytrace_data(sentry_trace[0]) - if not sentrytrace: - return context - - context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) - - trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] - - span_context = SpanContext( - trace_id=int(trace_id, 16), # type: ignore - span_id=int(span_id, 16), # type: ignore - # we simulate a sampled trace on the otel side and leave the sampling to sentry - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - - baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) - - if baggage_header: - baggage = Baggage.from_incoming_header(baggage_header[0]) - else: - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and frozen and won't be populated as head SDK. - baggage = Baggage(sentry_items={}) - - baggage.freeze() - context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) - - span = NonRecordingSpan(span_context) - modified_context = trace.set_span_in_context(span, context) - return modified_context - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - if context is None: - context = get_current() - - current_span = trace.get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - span_id = trace.format_span_id(current_span_context.span_id) - - span_map = SentrySpanProcessor.otel_span_map - sentry_span = span_map.get(span_id, None) - if not sentry_span: - return - - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) - - if sentry_span.containing_transaction: - baggage = sentry_span.containing_transaction.get_baggage() - if baggage: - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - @property - def fields(self) -> "Set[str]": - return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/sentry_sdk/integrations/opentelemetry/span_processor.py b/sentry_sdk/integrations/opentelemetry/span_processor.py deleted file mode 100644 index 1efd2ac455..0000000000 --- a/sentry_sdk/integrations/opentelemetry/span_processor.py +++ /dev/null @@ -1,407 +0,0 @@ -from datetime import datetime, timezone -from time import time -from typing import TYPE_CHECKING, cast - -from urllib3.util import parse_url as urlparse - -from sentry_sdk import get_client, start_transaction -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing import Transaction - -try: - from opentelemetry.context import get_value - from opentelemetry.sdk.trace import ReadableSpan as OTelSpan - from opentelemetry.sdk.trace import SpanProcessor - from opentelemetry.semconv.trace import SpanAttributes - from opentelemetry.trace import ( - SpanKind, - format_span_id, - format_trace_id, - get_current_span, - ) - from opentelemetry.trace.span import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from opentelemetry import context as context_api - from opentelemetry.trace import SpanContext - - from sentry_sdk._types import Event, Hint - -OPEN_TELEMETRY_CONTEXT = "otel" -SPAN_MAX_TIME_OPEN_MINUTES = 10 -SPAN_ORIGIN = "auto.otel" - - -def link_trace_context_to_error_event( - event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" -) -> "Event": - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return event - - if hasattr(event, "type") and event["type"] == "transaction": - return event - - otel_span = get_current_span() - if not otel_span: - return event - - ctx = otel_span.get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return event - - sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) - if not sentry_span: - return event - - contexts = event.setdefault("contexts", {}) - contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) - - return event - - -class SentrySpanProcessor(SpanProcessor): - """ - Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. - """ - - # The mapping from otel span ids to sentry spans - otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} - - # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES - open_spans: "dict[int, set[str]]" = {} - - initialized: "bool" = False - - def __new__(cls) -> "SentrySpanProcessor": - if not hasattr(cls, "instance"): - cls.instance = super().__new__(cls) - - # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). - return cls.instance # type: ignore[misc] - - def __init__(self) -> None: - if self.initialized: - return - - @add_global_event_processor - def global_event_processor(event: "Event", hint: "Hint") -> "Event": - return link_trace_context_to_error_event(event, self.otel_span_map) - - self.initialized = True - - def _prune_old_spans(self: "SentrySpanProcessor") -> None: - """ - Prune spans that have been open for too long. - """ - current_time_minutes = int(time() / 60) - for span_start_minutes in list( - self.open_spans.keys() - ): # making a list because we change the dict - # prune empty open spans buckets - if self.open_spans[span_start_minutes] == set(): - self.open_spans.pop(span_start_minutes) - - # prune old buckets - elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: - for span_id in self.open_spans.pop(span_start_minutes): - self.otel_span_map.pop(span_id, None) - - def on_start( - self, - otel_span: "OTelSpan", - parent_context: "Optional[context_api.Context]" = None, - ) -> None: - client = get_client() - - if not client.parsed_dsn: - return - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - if self._is_sentry_span(otel_span): - return - - trace_data = self._get_trace_data( - span_context, otel_span.parent, parent_context - ) - - parent_span_id = trace_data["parent_span_id"] - sentry_parent_span = ( - self.otel_span_map.get(parent_span_id) if parent_span_id else None - ) - - start_timestamp = None - if otel_span.start_time is not None: - start_timestamp = datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span = None - if sentry_parent_span: - sentry_span = sentry_parent_span.start_child( - span_id=trace_data["span_id"], - name=otel_span.name, - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - else: - sentry_span = start_transaction( - name=otel_span.name, - span_id=trace_data["span_id"], - parent_span_id=parent_span_id, - trace_id=trace_data["trace_id"], - baggage=trace_data["baggage"], - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - - self.otel_span_map[trace_data["span_id"]] = sentry_span - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).add( - trace_data["span_id"] - ) - - self._prune_old_spans() - - def on_end(self, otel_span: "OTelSpan") -> None: - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - span_id = format_span_id(span_context.span_id) - sentry_span = self.otel_span_map.pop(span_id, None) - if not sentry_span: - return - - sentry_span.op = otel_span.name - - self._update_span_with_otel_status(sentry_span, otel_span) - - if isinstance(sentry_span, Transaction): - sentry_span.name = otel_span.name - sentry_span.set_context( - OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) - ) - self._update_transaction_with_otel_data(sentry_span, otel_span) - - else: - self._update_span_with_otel_data(sentry_span, otel_span) - - end_timestamp = None - if otel_span.end_time is not None: - end_timestamp = datetime.fromtimestamp( - otel_span.end_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span.finish(end_timestamp=end_timestamp) - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) - - self._prune_old_spans() - - def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: - """ - Break infinite loop: - HTTP requests to Sentry are caught by OTel and send again to Sentry. - """ - otel_span_url = None - if otel_span.attributes is not None: - otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) - otel_span_url = cast("Optional[str]", otel_span_url) - - parsed_dsn = get_client().parsed_dsn - dsn_url = parsed_dsn.netloc if parsed_dsn else None - - if otel_span_url and dsn_url and dsn_url in otel_span_url: - return True - - return False - - def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": - """ - Returns the OTel context for Sentry. - See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context - """ - ctx = {} - - if otel_span.attributes: - ctx["attributes"] = dict(otel_span.attributes) - - if otel_span.resource.attributes: - ctx["resource"] = dict(otel_span.resource.attributes) - - return ctx - - def _get_trace_data( - self, - span_context: "SpanContext", - parent_span_context: "Optional[SpanContext]", - parent_context: "Optional[context_api.Context]", - ) -> "dict[str, Any]": - """ - Extracts tracing information from one OTel span's context and its parent OTel context. - """ - trace_data: "dict[str, Any]" = {} - - span_id = format_span_id(span_context.span_id) - trace_data["span_id"] = span_id - - trace_id = format_trace_id(span_context.trace_id) - trace_data["trace_id"] = trace_id - - parent_span_id = ( - format_span_id(parent_span_context.span_id) if parent_span_context else None - ) - trace_data["parent_span_id"] = parent_span_id - - sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) - sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) - trace_data["parent_sampled"] = ( - sentry_trace_data["parent_sampled"] if sentry_trace_data else None - ) - - baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) - trace_data["baggage"] = baggage - - return trace_data - - def _update_span_with_otel_status( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Set the Sentry span status from the OTel span - """ - if otel_span.status.is_unset: - return - - if otel_span.status.is_ok: - sentry_span.set_status(SPANSTATUS.OK) - return - - sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - def _update_span_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Convert OTel span data and update the Sentry span with it. - This should eventually happen on the server when ingesting the spans. - """ - sentry_span.set_data("otel.kind", otel_span.kind) - - op = otel_span.name - description = otel_span.name - - if otel_span.attributes is not None: - for key, val in otel_span.attributes.items(): - sentry_span.set_data(key, val) - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - http_method = cast("Optional[str]", http_method) - - db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) - - if http_method: - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - description = http_method - - peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) - if peer_name: - description += " {}".format(peer_name) - - target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) - if target: - description += " {}".format(target) - - if not peer_name and not target: - url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) - url = cast("Optional[str]", url) - if url: - parsed_url = urlparse(url) - url = "{}://{}{}".format( - parsed_url.scheme, parsed_url.netloc, parsed_url.path - ) - description += " {}".format(url) - - status_code = otel_span.attributes.get( - SpanAttributes.HTTP_STATUS_CODE, None - ) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - elif db_query: - op = "db" - statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) - statement = cast("Optional[str]", statement) - if statement: - description = statement - - sentry_span.op = op - sentry_span.description = description - - def _update_transaction_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - if otel_span.attributes is None: - return - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - - if http_method: - status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - sentry_span.op = op diff --git a/sentry_sdk/integrations/otlp.py b/sentry_sdk/integrations/otlp.py index b91f2cfd21..5b26752700 100644 --- a/sentry_sdk/integrations/otlp.py +++ b/sentry_sdk/integrations/otlp.py @@ -6,7 +6,7 @@ BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, ) -from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data from sentry_sdk.utils import ( Dsn, capture_internal_exceptions, @@ -15,16 +15,22 @@ ) try: + from opentelemetry import trace from opentelemetry.context import ( Context, + create_key, get_current, get_value, + set_value, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.textmap import ( CarrierT, + Getter, Setter, + TextMapPropagator, + default_getter, default_setter, ) from opentelemetry.sdk.trace import Span, TracerProvider @@ -32,23 +38,27 @@ from opentelemetry.trace import ( INVALID_SPAN_ID, INVALID_TRACE_ID, + NonRecordingSpan, SpanContext, + TraceFlags, format_span_id, format_trace_id, get_current_span, get_tracer_provider, set_tracer_provider, ) - - from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY - from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator except ImportError: raise DidNotEnable("opentelemetry-distro[otlp] is not installed") + from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Dict, Optional, Tuple + from typing import Any, Dict, Optional, Set, Tuple + + +SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") +SENTRY_TRACE_KEY = create_key("sentry-trace") def otel_propagation_context() -> "Optional[Tuple[str, str]]": @@ -121,11 +131,8 @@ def _sentry_patched_record_exception( _sentry_patched_exception = True -class SentryOTLPPropagator(SentryPropagator): +class SentryOTLPPropagator(TextMapPropagator): """ - We need to override the inject of the older propagator since that - is SpanProcessor based. - !!! Note regarding baggage: We cannot meaningfully populate a new baggage as a head SDK when we are using OTLP since we don't have any sort of transaction semantic to @@ -134,6 +141,52 @@ class SentryOTLPPropagator(SentryPropagator): For incoming baggage, we just pass it on as is so that case is correctly handled. """ + def extract( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + getter: "Getter[CarrierT]" = default_getter, + ) -> "Context": + if context is None: + context = get_current() + + sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) + if not sentry_trace: + return context + + sentrytrace = extract_sentrytrace_data(sentry_trace[0]) + if not sentrytrace: + return context + + context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) + + trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] + + span_context = SpanContext( + trace_id=int(trace_id, 16), # type: ignore + span_id=int(span_id, 16), # type: ignore + # we simulate a sampled trace on the otel side and leave the sampling to sentry + trace_flags=TraceFlags(TraceFlags.SAMPLED), + is_remote=True, + ) + + baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) + + if baggage_header: + baggage = Baggage.from_incoming_header(baggage_header[0]) + else: + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and frozen and won't be populated as head SDK. + baggage = Baggage(sentry_items={}) + + baggage.freeze() + context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) + + span = NonRecordingSpan(span_context) + modified_context = trace.set_span_in_context(span, context) + return modified_context + def inject( self, carrier: "CarrierT", @@ -162,6 +215,10 @@ def inject( if baggage_data: setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + @property + def fields(self) -> "Set[str]": + return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} + def _to_traceparent(span_context: "SpanContext") -> str: """ diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 06d3e7aff5..d1be9f53db 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -17,7 +17,6 @@ from sentry_sdk.consts import ( DEFAULT_MAX_BREADCRUMBS, FALSE_VALUES, - INSTRUMENTER, SPANDATA, ) from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer @@ -1093,7 +1092,6 @@ def add_breadcrumb( def start_transaction( self, transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, custom_sampling_context: "Optional[SamplingContext]" = None, **kwargs: "Unpack[TransactionKwargs]", ) -> "Union[Transaction, NoOpSpan]": @@ -1121,8 +1119,6 @@ def start_transaction( :param transaction: The transaction to start. If omitted, we create and start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. :param custom_sampling_context: The transaction's custom sampling context. :param kwargs: Optional keyword arguments to be passed to the Transaction constructor. See :py:class:`sentry_sdk.tracing.Transaction` for @@ -1139,11 +1135,6 @@ def start_transaction( kwargs.setdefault("scope", self) - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - try_autostart_continuous_profiler() custom_sampling_context = custom_sampling_context or {} @@ -1194,9 +1185,7 @@ def start_transaction( return transaction - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": + def start_span(self, **kwargs: "Any") -> "Span": """ Start a span whose parent is the currently active span or transaction, if any. @@ -1211,10 +1200,6 @@ def start_span( one is not already in progress. For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. """ client = sentry_sdk.get_client() if has_span_streaming_enabled(client.options): @@ -1237,11 +1222,6 @@ def start_span( client = self.get_client() - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - # get current span or transaction span = self.span or self.get_isolation_scope().span if isinstance(span, StreamedSpan): diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 8a7f17cb80..001d66f40e 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, cast import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE +from sentry_sdk.consts import SPANDATA, SPANSTATUS, SPANTEMPLATE from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.utils import ( capture_internal_exceptions, @@ -412,19 +412,13 @@ def containing_transaction(self) -> "Optional[Transaction]": # referencing themselves) return self._containing_transaction - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": + def start_child(self, **kwargs: "Any") -> "Span": """ Start a sub-span from the current span or transaction. Takes the same arguments as the initializer of :py:class:`Span`. The trace id, sampling decision, transaction pointer, and span recorder are inherited from the current span/transaction. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. """ if kwargs.get("description") is not None: warnings.warn( @@ -433,11 +427,6 @@ def start_child( stacklevel=2, ) - configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - kwargs.setdefault("sampled", self.sampled) child = Span( @@ -1227,9 +1216,7 @@ def __repr__(self) -> str: def containing_transaction(self) -> "Optional[Transaction]": return None - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "NoOpSpan": + def start_child(self, **kwargs: "Any") -> "NoOpSpan": return NoOpSpan() def to_traceparent(self) -> str: diff --git a/setup.py b/setup.py index 0a129c5052..fa290a0962 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,6 @@ def get_file_text(file_name): "mcp": ["mcp>=1.15.0"], "openai": ["openai>=1.0.0", "tiktoken>=0.3.0"], "openfeature": ["openfeature-sdk>=0.7.1"], - "opentelemetry": ["opentelemetry-distro>=0.35b0"], "opentelemetry-otlp": ["opentelemetry-distro[otlp]>=0.35b0"], "pure-eval": ["pure_eval", "executing", "asttokens"], "pydantic_ai": ["pydantic-ai>=1.0.0"], @@ -90,11 +89,6 @@ def get_file_text(file_name): "unleash": ["UnleashClient>=6.0.1"], "google-genai": ["google-genai>=1.29.0"], }, - entry_points={ - "opentelemetry_propagator": [ - "sentry=sentry_sdk.integrations.opentelemetry:SentryPropagator" - ] - }, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", diff --git a/tests/integrations/opentelemetry/__init__.py b/tests/integrations/opentelemetry/__init__.py deleted file mode 100644 index 75763c2fee..0000000000 --- a/tests/integrations/opentelemetry/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import pytest - -pytest.importorskip("opentelemetry") diff --git a/tests/integrations/opentelemetry/test_entry_points.py b/tests/integrations/opentelemetry/test_entry_points.py deleted file mode 100644 index 77d9898278..0000000000 --- a/tests/integrations/opentelemetry/test_entry_points.py +++ /dev/null @@ -1,26 +0,0 @@ -try: - from importlib.metadata import entry_points -except ImportError: - from importlib_metadata import entry_points - -from sentry_sdk.integrations.opentelemetry import SentryPropagator - - -def test_propagator_registered_as_entry_point(): - all_eps = entry_points() - - if isinstance(all_eps, dict): - # Python 3.7-3.8 stdlib returns a dict keyed by group - matches = [ - ep - for ep in all_eps.get("opentelemetry_propagator", []) - if ep.name == "sentry" - ] - else: - matches = list(all_eps.select(group="opentelemetry_propagator", name="sentry")) - - assert len(matches) >= 1 - assert matches[0].value == "sentry_sdk.integrations.opentelemetry:SentryPropagator" - - loaded = matches[0].load() - assert loaded is SentryPropagator diff --git a/tests/integrations/opentelemetry/test_propagator.py b/tests/integrations/opentelemetry/test_propagator.py deleted file mode 100644 index 42ec4ee200..0000000000 --- a/tests/integrations/opentelemetry/test_propagator.py +++ /dev/null @@ -1,299 +0,0 @@ -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from opentelemetry.context import get_current -from opentelemetry.trace import ( - SpanContext, - TraceFlags, - set_span_in_context, -) -from opentelemetry.trace.propagation import get_current_span - -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor -from sentry_sdk.tracing_utils import Baggage - - -@pytest.mark.forked -def test_extract_no_context_no_sentry_trace_header(): - """ - No context and NO Sentry trace data in getter. - Extract should return empty context. - """ - carrier = None - context = None - getter = MagicMock() - getter.get.return_value = None - - modified_context = SentryPropagator().extract(carrier, context, getter) - - assert modified_context == {} - - -@pytest.mark.forked -def test_extract_context_no_sentry_trace_header(): - """ - Context but NO Sentry trace data in getter. - Extract should return context as is. - """ - carrier = None - context = {"some": "value"} - getter = MagicMock() - getter.get.return_value = None - - modified_context = SentryPropagator().extract(carrier, context, getter) - - assert modified_context == context - - -@pytest.mark.forked -def test_extract_empty_context_sentry_trace_header_no_baggage(): - """ - Empty context but Sentry trace data but NO Baggage in getter. - Extract should return context that has empty baggage in it and also a NoopSpan with span_id and trace_id. - """ - carrier = None - context = {} - getter = MagicMock() - getter.get.side_effect = [ - ["1234567890abcdef1234567890abcdef-1234567890abcdef-1"], - None, - ] - - modified_context = SentryPropagator().extract(carrier, context, getter) - - assert len(modified_context.keys()) == 3 - - assert modified_context[SENTRY_TRACE_KEY] == { - "trace_id": "1234567890abcdef1234567890abcdef", - "parent_span_id": "1234567890abcdef", - "parent_sampled": True, - } - assert modified_context[SENTRY_BAGGAGE_KEY].serialize() == "" - - span_context = get_current_span(modified_context).get_span_context() - assert span_context.span_id == int("1234567890abcdef", 16) - assert span_context.trace_id == int("1234567890abcdef1234567890abcdef", 16) - - -@pytest.mark.forked -def test_extract_context_sentry_trace_header_baggage(): - """ - Empty context but Sentry trace data and Baggage in getter. - Extract should return context that has baggage in it and also a NoopSpan with span_id and trace_id. - """ - baggage_header = ( - "other-vendor-value-1=foo;bar;baz, sentry-trace_id=771a43a4192642f0b136d5159a501700, " - "sentry-public_key=49d0f7386ad645858ae85020e393bef3, sentry-sample_rate=0.01337, " - "sentry-user_id=Am%C3%A9lie, other-vendor-value-2=foo;bar;" - ) - - carrier = None - context = {"some": "value"} - getter = MagicMock() - getter.get.side_effect = [ - ["1234567890abcdef1234567890abcdef-1234567890abcdef-1"], - [baggage_header], - ] - - modified_context = SentryPropagator().extract(carrier, context, getter) - - assert len(modified_context.keys()) == 4 - - assert modified_context[SENTRY_TRACE_KEY] == { - "trace_id": "1234567890abcdef1234567890abcdef", - "parent_span_id": "1234567890abcdef", - "parent_sampled": True, - } - - assert modified_context[SENTRY_BAGGAGE_KEY].serialize() == ( - "sentry-trace_id=771a43a4192642f0b136d5159a501700," - "sentry-public_key=49d0f7386ad645858ae85020e393bef3," - "sentry-sample_rate=0.01337,sentry-user_id=Am%C3%A9lie" - ) - - span_context = get_current_span(modified_context).get_span_context() - assert span_context.span_id == int("1234567890abcdef", 16) - assert span_context.trace_id == int("1234567890abcdef1234567890abcdef", 16) - - -@pytest.mark.forked -def test_inject_empty_otel_span_map(): - """ - Empty otel_span_map. - So there is no sentry_span to be found in inject() - and the function is returned early and no setters are called. - """ - carrier = None - context = get_current() - setter = MagicMock() - setter.set = MagicMock() - - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - span = MagicMock() - span.get_span_context.return_value = span_context - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span", - return_value=span, - ): - full_context = set_span_in_context(span, context) - SentryPropagator().inject(carrier, full_context, setter) - - setter.set.assert_not_called() - - -@pytest.mark.forked -def test_inject_sentry_span_no_baggage(): - """ - Inject a sentry span with no baggage. - """ - carrier = None - context = get_current() - setter = MagicMock() - setter.set = MagicMock() - - trace_id = "1234567890abcdef1234567890abcdef" - span_id = "1234567890abcdef" - - span_context = SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - span = MagicMock() - span.get_span_context.return_value = span_context - - sentry_span = MagicMock() - sentry_span.to_traceparent = mock.Mock( - return_value="1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ) - sentry_span.containing_transaction.get_baggage = mock.Mock(return_value=None) - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map[span_id] = sentry_span - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span", - return_value=span, - ): - full_context = set_span_in_context(span, context) - SentryPropagator().inject(carrier, full_context, setter) - - setter.set.assert_called_once_with( - carrier, - "sentry-trace", - "1234567890abcdef1234567890abcdef-1234567890abcdef-1", - ) - - -def test_inject_sentry_span_empty_baggage(): - """ - Inject a sentry span with no baggage. - """ - carrier = None - context = get_current() - setter = MagicMock() - setter.set = MagicMock() - - trace_id = "1234567890abcdef1234567890abcdef" - span_id = "1234567890abcdef" - - span_context = SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - span = MagicMock() - span.get_span_context.return_value = span_context - - sentry_span = MagicMock() - sentry_span.to_traceparent = mock.Mock( - return_value="1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ) - sentry_span.containing_transaction.get_baggage = mock.Mock(return_value=Baggage({})) - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map[span_id] = sentry_span - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span", - return_value=span, - ): - full_context = set_span_in_context(span, context) - SentryPropagator().inject(carrier, full_context, setter) - - setter.set.assert_called_once_with( - carrier, - "sentry-trace", - "1234567890abcdef1234567890abcdef-1234567890abcdef-1", - ) - - -def test_inject_sentry_span_baggage(): - """ - Inject a sentry span with baggage. - """ - carrier = None - context = get_current() - setter = MagicMock() - setter.set = MagicMock() - - trace_id = "1234567890abcdef1234567890abcdef" - span_id = "1234567890abcdef" - - span_context = SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - span = MagicMock() - span.get_span_context.return_value = span_context - - sentry_span = MagicMock() - sentry_span.to_traceparent = mock.Mock( - return_value="1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ) - sentry_items = { - "sentry-trace_id": "771a43a4192642f0b136d5159a501700", - "sentry-public_key": "49d0f7386ad645858ae85020e393bef3", - "sentry-sample_rate": 0.01337, - "sentry-user_id": "Amélie", - } - baggage = Baggage(sentry_items=sentry_items) - sentry_span.containing_transaction.get_baggage = MagicMock(return_value=baggage) - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map[span_id] = sentry_span - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span", - return_value=span, - ): - full_context = set_span_in_context(span, context) - SentryPropagator().inject(carrier, full_context, setter) - - setter.set.assert_any_call( - carrier, - "sentry-trace", - "1234567890abcdef1234567890abcdef-1234567890abcdef-1", - ) - - setter.set.assert_any_call( - carrier, - "baggage", - baggage.serialize(), - ) diff --git a/tests/integrations/opentelemetry/test_span_processor.py b/tests/integrations/opentelemetry/test_span_processor.py deleted file mode 100644 index abecee7301..0000000000 --- a/tests/integrations/opentelemetry/test_span_processor.py +++ /dev/null @@ -1,635 +0,0 @@ -import time -from datetime import datetime, timezone -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from opentelemetry.trace import SpanContext, SpanKind, Status, StatusCode - -import sentry_sdk -from sentry_sdk.integrations.opentelemetry.span_processor import ( - SentrySpanProcessor, - link_trace_context_to_error_event, -) -from sentry_sdk.scope import global_event_processors -from sentry_sdk.tracing import Span, Transaction -from sentry_sdk.tracing_utils import extract_sentrytrace_data -from sentry_sdk.utils import Dsn - - -def test_is_sentry_span(): - otel_span = MagicMock() - - span_processor = SentrySpanProcessor() - assert not span_processor._is_sentry_span(otel_span) - - client = MagicMock() - client.options = {"instrumenter": "otel"} - client.parsed_dsn = Dsn("https://1234567890abcdef@o123456.ingest.sentry.io/123456") - sentry_sdk.get_global_scope().set_client(client) - - assert not span_processor._is_sentry_span(otel_span) - - otel_span.attributes = { - "http.url": "https://example.com", - } - assert not span_processor._is_sentry_span(otel_span) - - otel_span.attributes = { - "http.url": "https://o123456.ingest.sentry.io/api/123/envelope", - } - assert span_processor._is_sentry_span(otel_span) - - -def test_get_otel_context(): - otel_span = MagicMock() - otel_span.attributes = {"foo": "bar"} - otel_span.resource = MagicMock() - otel_span.resource.attributes = {"baz": "qux"} - - span_processor = SentrySpanProcessor() - otel_context = span_processor._get_otel_context(otel_span) - - assert otel_context == { - "attributes": {"foo": "bar"}, - "resource": {"baz": "qux"}, - } - - -def test_get_trace_data_with_span_and_trace(): - otel_span = MagicMock() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = None - - parent_context = {} - - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] is None - assert sentry_trace_data["parent_sampled"] is None - assert sentry_trace_data["baggage"] is None - - -def test_get_trace_data_with_span_and_trace_and_parent(): - otel_span = MagicMock() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] == "abcdef1234567890" - assert sentry_trace_data["parent_sampled"] is None - assert sentry_trace_data["baggage"] is None - - -def test_get_trace_data_with_sentry_trace(): - otel_span = MagicMock() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.get_value", - side_effect=[ - extract_sentrytrace_data( - "1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ), - None, - ], - ): - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] == "abcdef1234567890" - assert sentry_trace_data["parent_sampled"] is True - assert sentry_trace_data["baggage"] is None - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.get_value", - side_effect=[ - extract_sentrytrace_data( - "1234567890abcdef1234567890abcdef-1234567890abcdef-0" - ), - None, - ], - ): - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] == "abcdef1234567890" - assert sentry_trace_data["parent_sampled"] is False - assert sentry_trace_data["baggage"] is None - - -def test_get_trace_data_with_sentry_trace_and_baggage(): - otel_span = MagicMock() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - baggage = ( - "sentry-trace_id=771a43a4192642f0b136d5159a501700," - "sentry-public_key=49d0f7386ad645858ae85020e393bef3," - "sentry-sample_rate=0.01337,sentry-user_id=Am%C3%A9lie" - ) - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.get_value", - side_effect=[ - extract_sentrytrace_data( - "1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ), - baggage, - ], - ): - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] == "abcdef1234567890" - assert sentry_trace_data["parent_sampled"] - assert sentry_trace_data["baggage"] == baggage - - -def test_update_span_with_otel_data_http_method(): - sentry_span = Span() - - otel_span = MagicMock() - otel_span.name = "Test OTel Span" - otel_span.kind = SpanKind.CLIENT - otel_span.attributes = { - "http.method": "GET", - "http.status_code": 429, - "http.status_text": "xxx", - "http.user_agent": "curl/7.64.1", - "net.peer.name": "example.com", - "http.target": "/", - } - - span_processor = SentrySpanProcessor() - span_processor._update_span_with_otel_data(sentry_span, otel_span) - - assert sentry_span.op == "http.client" - assert sentry_span.description == "GET example.com /" - assert sentry_span.status == "resource_exhausted" - - assert sentry_span._data["http.method"] == "GET" - assert sentry_span._data["http.response.status_code"] == 429 - assert sentry_span._data["http.status_text"] == "xxx" - assert sentry_span._data["http.user_agent"] == "curl/7.64.1" - assert sentry_span._data["net.peer.name"] == "example.com" - assert sentry_span._data["http.target"] == "/" - - -@pytest.mark.parametrize( - "otel_status, expected_status", - [ - pytest.param(Status(StatusCode.UNSET), None, id="unset"), - pytest.param(Status(StatusCode.OK), "ok", id="ok"), - pytest.param(Status(StatusCode.ERROR), "internal_error", id="error"), - ], -) -def test_update_span_with_otel_status(otel_status, expected_status): - sentry_span = Span() - - otel_span = MagicMock() - otel_span.name = "Test OTel Span" - otel_span.kind = SpanKind.INTERNAL - otel_span.status = otel_status - - span_processor = SentrySpanProcessor() - span_processor._update_span_with_otel_status(sentry_span, otel_span) - - assert sentry_span.get_trace_context().get("status") == expected_status - - -def test_update_span_with_otel_data_http_method2(): - sentry_span = Span() - - otel_span = MagicMock() - otel_span.name = "Test OTel Span" - otel_span.kind = SpanKind.SERVER - otel_span.attributes = { - "http.method": "GET", - "http.status_code": 429, - "http.status_text": "xxx", - "http.user_agent": "curl/7.64.1", - "http.url": "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef", - } - - span_processor = SentrySpanProcessor() - span_processor._update_span_with_otel_data(sentry_span, otel_span) - - assert sentry_span.op == "http.server" - assert sentry_span.description == "GET https://example.com/status/403" - assert sentry_span.status == "resource_exhausted" - - assert sentry_span._data["http.method"] == "GET" - assert sentry_span._data["http.response.status_code"] == 429 - assert sentry_span._data["http.status_text"] == "xxx" - assert sentry_span._data["http.user_agent"] == "curl/7.64.1" - assert ( - sentry_span._data["http.url"] - == "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef" - ) - - -def test_update_span_with_otel_data_db_query(): - sentry_span = Span() - - otel_span = MagicMock() - otel_span.name = "Test OTel Span" - otel_span.attributes = { - "db.system": "postgresql", - "db.statement": "SELECT * FROM table where pwd = '123456'", - } - - span_processor = SentrySpanProcessor() - span_processor._update_span_with_otel_data(sentry_span, otel_span) - - assert sentry_span.op == "db" - assert sentry_span.description == "SELECT * FROM table where pwd = '123456'" - - assert sentry_span._data["db.system"] == "postgresql" - assert ( - sentry_span._data["db.statement"] == "SELECT * FROM table where pwd = '123456'" - ) - - -def test_on_start_transaction(): - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.start_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - fake_start_transaction = MagicMock() - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - fake_client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456" - sentry_sdk.get_global_scope().set_client(fake_client) - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.start_transaction", - fake_start_transaction, - ): - span_processor = SentrySpanProcessor() - span_processor.on_start(otel_span, parent_context) - - fake_start_transaction.assert_called_once_with( - name="Sample OTel Span", - span_id="1234567890abcdef", - parent_span_id="abcdef1234567890", - trace_id="1234567890abcdef1234567890abcdef", - baggage=None, - start_timestamp=datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ), - instrumenter="otel", - origin="auto.otel", - ) - - assert len(span_processor.otel_span_map.keys()) == 1 - assert list(span_processor.otel_span_map.keys())[0] == "1234567890abcdef" - - -def test_on_start_child(): - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.start_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - fake_client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456" - sentry_sdk.get_global_scope().set_client(fake_client) - - fake_span = MagicMock() - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map["abcdef1234567890"] = fake_span - span_processor.on_start(otel_span, parent_context) - - fake_span.start_child.assert_called_once_with( - span_id="1234567890abcdef", - name="Sample OTel Span", - start_timestamp=datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ), - instrumenter="otel", - origin="auto.otel", - ) - - assert len(span_processor.otel_span_map.keys()) == 2 - assert "abcdef1234567890" in span_processor.otel_span_map.keys() - assert "1234567890abcdef" in span_processor.otel_span_map.keys() - - -def test_on_end_no_sentry_span(): - """ - If on_end is called on a span that is not in the otel_span_map, it should be a no-op. - """ - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.end_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map = {} - span_processor._get_otel_context = MagicMock() - span_processor._update_span_with_otel_data = MagicMock() - - span_processor.on_end(otel_span) - - span_processor._get_otel_context.assert_not_called() - span_processor._update_span_with_otel_data.assert_not_called() - - -def test_on_end_sentry_transaction(): - """ - Test on_end for a sentry Transaction. - """ - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.end_time = time.time_ns() - otel_span.status = Status(StatusCode.OK) - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - sentry_sdk.get_global_scope().set_client(fake_client) - - fake_sentry_span = MagicMock(spec=Transaction) - fake_sentry_span.set_context = MagicMock() - fake_sentry_span.finish = MagicMock() - - span_processor = SentrySpanProcessor() - span_processor._get_otel_context = MagicMock() - span_processor._update_span_with_otel_data = MagicMock() - span_processor.otel_span_map["1234567890abcdef"] = fake_sentry_span - - span_processor.on_end(otel_span) - - fake_sentry_span.set_context.assert_called_once() - span_processor._update_span_with_otel_data.assert_not_called() - fake_sentry_span.set_status.assert_called_once_with("ok") - fake_sentry_span.finish.assert_called_once() - - -def test_on_end_sentry_span(): - """ - Test on_end for a sentry Span. - """ - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.end_time = time.time_ns() - otel_span.status = Status(StatusCode.OK) - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - sentry_sdk.get_global_scope().set_client(fake_client) - - fake_sentry_span = MagicMock(spec=Span) - fake_sentry_span.set_context = MagicMock() - fake_sentry_span.finish = MagicMock() - - span_processor = SentrySpanProcessor() - span_processor._get_otel_context = MagicMock() - span_processor._update_span_with_otel_data = MagicMock() - span_processor.otel_span_map["1234567890abcdef"] = fake_sentry_span - - span_processor.on_end(otel_span) - - fake_sentry_span.set_context.assert_not_called() - span_processor._update_span_with_otel_data.assert_called_once_with( - fake_sentry_span, otel_span - ) - fake_sentry_span.set_status.assert_called_once_with("ok") - fake_sentry_span.finish.assert_called_once() - - -def test_link_trace_context_to_error_event(): - """ - Test that the trace context is added to the error event. - """ - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - sentry_sdk.get_global_scope().set_client(fake_client) - - span_id = "1234567890abcdef" - trace_id = "1234567890abcdef1234567890abcdef" - - fake_trace_context = { - "bla": "blub", - "foo": "bar", - "baz": 123, - } - - sentry_span = MagicMock() - sentry_span.get_trace_context = MagicMock(return_value=fake_trace_context) - - otel_span_map = { - span_id: sentry_span, - } - - span_context = SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - is_remote=True, - ) - otel_span = MagicMock() - otel_span.get_span_context = MagicMock(return_value=span_context) - - fake_event = {"event_id": "1234567890abcdef1234567890abcdef"} - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.get_current_span", - return_value=otel_span, - ): - event = link_trace_context_to_error_event(fake_event, otel_span_map) - - assert event - assert event == fake_event # the event is changed in place inside the function - assert "contexts" in event - assert "trace" in event["contexts"] - assert event["contexts"]["trace"] == fake_trace_context - - -def test_pruning_old_spans_on_start(): - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.start_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel", "debug": False} - fake_client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456" - sentry_sdk.get_global_scope().set_client(fake_client) - - span_processor = SentrySpanProcessor() - - span_processor.otel_span_map = { - "111111111abcdef": MagicMock(), # should stay - "2222222222abcdef": MagicMock(), # should go - "3333333333abcdef": MagicMock(), # should go - } - current_time_minutes = int(time.time() / 60) - span_processor.open_spans = { - current_time_minutes - 3: {"111111111abcdef"}, # should stay - current_time_minutes - 11: { - "2222222222abcdef", - "3333333333abcdef", - }, # should go - } - - span_processor.on_start(otel_span, parent_context) - assert sorted(list(span_processor.otel_span_map.keys())) == [ - "111111111abcdef", - "1234567890abcdef", - ] - assert sorted(list(span_processor.open_spans.values())) == [ - {"111111111abcdef"}, - {"1234567890abcdef"}, - ] - - -def test_pruning_old_spans_on_end(): - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.start_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - sentry_sdk.get_global_scope().set_client(fake_client) - - fake_sentry_span = MagicMock(spec=Span) - fake_sentry_span.set_context = MagicMock() - fake_sentry_span.finish = MagicMock() - - span_processor = SentrySpanProcessor() - span_processor._get_otel_context = MagicMock() - span_processor._update_span_with_otel_data = MagicMock() - - span_processor.otel_span_map = { - "111111111abcdef": MagicMock(), # should stay - "2222222222abcdef": MagicMock(), # should go - "3333333333abcdef": MagicMock(), # should go - "1234567890abcdef": fake_sentry_span, # should go (because it is closed) - } - current_time_minutes = int(time.time() / 60) - span_processor.open_spans = { - current_time_minutes: {"1234567890abcdef"}, # should go (because it is closed) - current_time_minutes - 3: {"111111111abcdef"}, # should stay - current_time_minutes - 11: { - "2222222222abcdef", - "3333333333abcdef", - }, # should go - } - - span_processor.on_end(otel_span) - assert sorted(list(span_processor.otel_span_map.keys())) == ["111111111abcdef"] - assert sorted(list(span_processor.open_spans.values())) == [{"111111111abcdef"}] - - -def test_no_memory_leak(): - span_processor_1 = SentrySpanProcessor() - cnt_before = len(global_event_processors) - - span_processor_2 = SentrySpanProcessor() - - cnt_after = len(global_event_processors) - assert span_processor_1 is span_processor_2 - assert cnt_before == cnt_after diff --git a/tests/integrations/otlp/test_otlp.py b/tests/integrations/otlp/test_otlp.py index 25e2a5f7fe..504507e274 100644 --- a/tests/integrations/otlp/test_otlp.py +++ b/tests/integrations/otlp/test_otlp.py @@ -16,7 +16,10 @@ ) from opentelemetry.util._once import Once -from sentry_sdk.integrations.otlp import OTLPIntegration, SentryOTLPPropagator +from sentry_sdk.integrations.otlp import ( + OTLPIntegration, + SentryOTLPPropagator, +) from sentry_sdk.scope import get_external_propagation_context original_propagator = get_global_textmap() diff --git a/tests/tracing/test_noop_span.py b/tests/tracing/test_noop_span.py deleted file mode 100644 index 36778cd485..0000000000 --- a/tests/tracing/test_noop_span.py +++ /dev/null @@ -1,52 +0,0 @@ -import sentry_sdk -from sentry_sdk.tracing import NoOpSpan - -# These tests make sure that the examples from the documentation [1] -# are working when OTel (OpenTelemetry) instrumentation is turned on, -# and therefore, the Sentry tracing should not do anything. -# -# 1: https://docs.sentry.io/platforms/python/performance/instrumentation/custom-instrumentation/ - - -def test_noop_start_transaction(sentry_init): - sentry_init(instrumenter="otel") - - with sentry_sdk.start_transaction( - op="task", name="test_transaction_name" - ) as transaction: - assert isinstance(transaction, NoOpSpan) - assert sentry_sdk.get_current_scope().span is transaction - - transaction.name = "new name" - - -def test_noop_start_span(sentry_init): - sentry_init(instrumenter="otel") - - with sentry_sdk.start_span(op="http", name="GET /") as span: - assert isinstance(span, NoOpSpan) - assert sentry_sdk.get_current_scope().span is span - - span.set_tag("http.response.status_code", 418) - span.set_data("http.entity_type", "teapot") - - -def test_noop_transaction_start_child(sentry_init): - sentry_init(instrumenter="otel") - - transaction = sentry_sdk.start_transaction(name="task") - assert isinstance(transaction, NoOpSpan) - - with transaction.start_child(op="child_task") as child: - assert isinstance(child, NoOpSpan) - assert sentry_sdk.get_current_scope().span is child - - -def test_noop_span_start_child(sentry_init): - sentry_init(instrumenter="otel") - span = sentry_sdk.start_span(name="task") - assert isinstance(span, NoOpSpan) - - with span.start_child(op="child_task") as child: - assert isinstance(child, NoOpSpan) - assert sentry_sdk.get_current_scope().span is child diff --git a/tox.ini b/tox.ini index 361a1ead98..7819947094 100644 --- a/tox.ini +++ b/tox.ini @@ -42,9 +42,6 @@ envlist = # GCP {py3.7}-gcp - # OpenTelemetry (OTel) - {py3.7,py3.9,py3.12,py3.13,py3.14,py3.14t}-opentelemetry - # OpenTelemetry with OTLP {py3.7,py3.9,py3.12,py3.13,py3.14}-otlp @@ -456,13 +453,10 @@ deps = aws_lambda: uvicorn aws_lambda: pyyaml - # OpenTelemetry (OTel) - opentelemetry: opentelemetry-distro - opentelemetry: pytest-forked - # OpenTelemetry with OTLP otlp: opentelemetry-distro[otlp] otlp: responses + otlp: pytest-forked # === Integrations - Auto-generated === # These come from the populate_tox.py script. @@ -18618,7 +18612,6 @@ setenv = aws_lambda: _TESTPATH=tests/integrations/aws_lambda cloud_resource_context: _TESTPATH=tests/integrations/cloud_resource_context gcp: _TESTPATH=tests/integrations/gcp - opentelemetry: _TESTPATH=tests/integrations/opentelemetry otlp: _TESTPATH=tests/integrations/otlp socket: _TESTPATH=tests/integrations/socket From 6e737c9e8e3fe932c0fb9437e49c7a826635bb77 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 30 Jul 2026 09:31:09 +0200 Subject: [PATCH 11/16] ref: Remove deprecated `set_measurement` in new major (#6941) ### Description The API is deprecated and slated for removal in 3.0. #### Issues Closes https://github.com/getsentry/sentry-python/issues/5019 #### Reminders - Please add tests to validate your changes, and lint your code using `uv run ruff`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr) --- MIGRATION_GUIDE.md | 1 + sentry_sdk/__init__.py | 1 - sentry_sdk/_types.py | 12 ------ sentry_sdk/api.py | 12 ------ sentry_sdk/tracing.py | 46 -------------------- tests/tracing/test_misc.py | 86 +------------------------------------- 6 files changed, 2 insertions(+), 156 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 6809604e6e..ab89d158df 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -30,6 +30,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Removed the deprecated Hub class and all uses of hub throughout the SDK in arguments, options, etc. Use a scope instead. - The `SentrySpanProcessor`, `SentryPropagator`, `instrumenter`, and associated OpenTelemetry compatibility code was removed along with the `opentelemetry` extra and the `SentryPropagator` entrypoint. Use the `OTLPIntegration` instead. - Removed the `auto_session_tracing` decorator. Use `track_session` instead. +- The deprecated `set_measurement` API was removed. - The experimental option `otel_powered_performance` has been removed together with the associated `OpenTelemetryIntegration` and `opentelemetry-experimental` extra. ## Deprecated diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index e47021ee1f..18b8739955 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -43,7 +43,6 @@ "set_context", "set_extra", "set_level", - "set_measurement", "set_tag", "set_tags", "set_user", diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 182a364130..b149097845 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -236,17 +236,6 @@ class DataCollection(TypedDict): "exbibyte", ] - FractionUnit = Literal["ratio", "percent"] - MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] - - MeasurementValue = TypedDict( - "MeasurementValue", - { - "value": float, - "unit": NotRequired[Optional[MeasurementUnit]], - }, - ) - Event = TypedDict( "Event", { @@ -268,7 +257,6 @@ class DataCollection(TypedDict): "level": LogLevelStr, "logentry": Mapping[str, object], "logger": str, - "measurements": dict[str, MeasurementValue], "message": str, "modules": dict[str, str], "monitor_config": Mapping[str, object], diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 4aa81e34ca..3b3f2e8125 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -34,7 +34,6 @@ ExcInfo, Hint, LogLevelStr, - MeasurementUnit, SamplingContext, ) from sentry_sdk.client import BaseClient @@ -79,7 +78,6 @@ def overload(x: "T") -> "T": "set_context", "set_extra", "set_level", - "set_measurement", "set_tag", "set_tags", "set_user", @@ -419,16 +417,6 @@ def start_transaction( ) -def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - transaction = get_current_scope().transaction - if transaction is not None: - transaction.set_measurement(name, value, unit) - - def get_current_span( scope: "Optional[Scope]" = None, ) -> "Optional[Span]": diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 001d66f40e..eef2e35929 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -38,8 +38,6 @@ from sentry_sdk._types import ( Event, - MeasurementUnit, - MeasurementValue, SamplingContext, ) from sentry_sdk.profiler.continuous_profiler import ContinuousProfile @@ -255,7 +253,6 @@ class Span: "sampled", "op", "description", - "_measurements", "start_timestamp", "_start_timestamp_monotonic_ns", "status", @@ -298,7 +295,6 @@ def __init__( self.status = status self.scope = scope self.origin = origin - self._measurements: "Dict[str, MeasurementValue]" = {} self._tags: "MutableMapping[str, str]" = {} self._data: "Dict[str, Any]" = {} self._containing_transaction = containing_transaction @@ -586,21 +582,6 @@ def set_flag(self, flag: str, result: bool) -> None: def set_status(self, value: str) -> None: self.status = value - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - def set_thread( self, thread_id: "Optional[int]", thread_name: "Optional[str]" ) -> None: @@ -696,9 +677,6 @@ def to_json(self) -> "Dict[str, Any]": # TODO-neel remove redundant tag in major self._tags["status"] = self.status - if len(self._measurements) > 0: - rv["measurements"] = self._measurements - tags = self._tags if tags: rv["tags"] = tags @@ -787,7 +765,6 @@ class Transaction(Span): "parent_sampled", # used to create baggage value for head SDKs in dynamic sampling "sample_rate", - "_measurements", "_contexts", "_continuous_profile", "_baggage", @@ -808,7 +785,6 @@ def __init__( # type: ignore[misc] self.source = source self.sample_rate: "Optional[float]" = None self.parent_sampled = parent_sampled - self._measurements: "Dict[str, MeasurementValue]" = {} self._contexts: "Dict[str, Any]" = {} self._continuous_profile: "Optional[ContinuousProfile]" = None self._baggage = baggage @@ -1028,25 +1004,8 @@ def finish( if has_gen_ai_span: event["_has_gen_ai_span"] = True - event["measurements"] = self._measurements - return scope.capture_event(event) - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - def set_context(self, key: str, value: "dict[str, Any]") -> None: """Sets a context. Transactions can have multiple contexts and they should follow the format described in the "Contexts Interface" @@ -1265,11 +1224,6 @@ def finish( ) -> "Optional[str]": pass - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - pass - def set_context(self, key: str, value: "dict[str, Any]") -> None: pass diff --git a/tests/tracing/test_misc.py b/tests/tracing/test_misc.py index 4fb881c9da..31cf4d55b2 100644 --- a/tests/tracing/test_misc.py +++ b/tests/tracing/test_misc.py @@ -4,7 +4,7 @@ import pytest import sentry_sdk -from sentry_sdk import set_measurement, start_span, start_transaction +from sentry_sdk import start_span, start_transaction from sentry_sdk.consts import MATCH_ALL from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span, Transaction @@ -260,90 +260,6 @@ def test_finds_non_orphan_span_on_scope_span_streaming(sentry_init): assert scope._span.name == "sniffing" -def test_set_measurement(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - - events = capture_events() - - transaction = start_transaction(name="measuring stuff") - - with pytest.raises(TypeError): - transaction.set_measurement() - - with pytest.raises(TypeError): - transaction.set_measurement("metric.foo") - - transaction.set_measurement("metric.foo", 123) - transaction.set_measurement("metric.bar", 456, unit="second") - transaction.set_measurement("metric.baz", 420.69, unit="custom") - transaction.set_measurement("metric.foobar", 12, unit="percent") - transaction.set_measurement("metric.foobar", 17.99, unit="percent") - - transaction.finish() - - (event,) = events - assert event["measurements"]["metric.foo"] == {"value": 123, "unit": ""} - assert event["measurements"]["metric.bar"] == {"value": 456, "unit": "second"} - assert event["measurements"]["metric.baz"] == {"value": 420.69, "unit": "custom"} - assert event["measurements"]["metric.foobar"] == {"value": 17.99, "unit": "percent"} - - -def test_set_measurement_public_api(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - - events = capture_events() - - with start_transaction(name="measuring stuff"): - set_measurement("metric.foo", 123) - set_measurement("metric.bar", 456, unit="second") - - (event,) = events - assert event["measurements"]["metric.foo"] == {"value": 123, "unit": ""} - assert event["measurements"]["metric.bar"] == {"value": 456, "unit": "second"} - - -def test_set_measurement_deprecated(sentry_init): - sentry_init(traces_sample_rate=1.0) - - with start_transaction(name="measuring stuff") as trx: - with pytest.warns(DeprecationWarning): - set_measurement("metric.foo", 123) - - with pytest.warns(DeprecationWarning): - trx.set_measurement("metric.bar", 456) - - with start_span(op="measuring span") as span: - with pytest.warns(DeprecationWarning): - span.set_measurement("metric.baz", 420.69, unit="custom") - - -def test_set_meaurement_compared_to_set_data(sentry_init, capture_events): - """ - This is just a test to see the difference - between measurements and data in the resulting event payload. - """ - sentry_init(traces_sample_rate=1.0) - - events = capture_events() - - with start_transaction(name="measuring stuff") as transaction: - transaction.set_measurement("metric.foo", 123) - transaction.set_data("metric.bar", 456) - - with start_span(op="measuring span") as span: - span.set_measurement("metric.baz", 420.69, unit="custom") - span.set_data("metric.qux", 789) - - (event,) = events - assert event["measurements"]["metric.foo"] == {"value": 123, "unit": ""} - assert event["contexts"]["trace"]["data"]["metric.bar"] == 456 - assert event["spans"][0]["measurements"]["metric.baz"] == { - "value": 420.69, - "unit": "custom", - } - assert event["spans"][0]["data"]["metric.qux"] == 789 - - @pytest.mark.parametrize( "trace_propagation_targets,url,expected_propagation_decision", [ From 0125904bf51b097020ec6ae81a38c46ea1e069d1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 30 Jul 2026 10:21:25 +0200 Subject: [PATCH 12/16] ref: Remove `push_scope`, `configure_scope` in new major (#6944) ### Description Remove the deprecated API. #### Issues Closes https://github.com/getsentry/sentry-python/issues/5018 --- MIGRATION_GUIDE.md | 1 + docs/api.rst | 3 -- sentry_sdk/__init__.py | 2 - sentry_sdk/api.py | 95 +----------------------------------------- tests/test_api.py | 14 ------- tests/test_basics.py | 18 -------- tests/test_client.py | 22 ---------- 7 files changed, 2 insertions(+), 153 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index ab89d158df..a068711fbb 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -26,6 +26,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## Removed +- The deprecated `push_scope` and `configure_scope` APIs have been removed. Use `with new_scope():` to push a new scope and `scope = get_current_scope()` to retrieve the current scope instead. - Transaction profiling and related code was removed. - Removed the deprecated Hub class and all uses of hub throughout the SDK in arguments, options, etc. Use a scope instead. - The `SentrySpanProcessor`, `SentryPropagator`, `instrumenter`, and associated OpenTelemetry compatibility code was removed along with the `opentelemetry` extra and the `SentryPropagator` entrypoint. Use the `OTLPIntegration` instead. diff --git a/docs/api.rst b/docs/api.rst index 802abee75d..e9ed33246a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -62,7 +62,4 @@ Client Management Managing Scope (advanced) ========================= -.. autofunction:: sentry_sdk.api.configure_scope -.. autofunction:: sentry_sdk.api.push_scope - .. autofunction:: sentry_sdk.api.new_scope diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 18b8739955..483e80bda7 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -21,7 +21,6 @@ "capture_event", "capture_exception", "capture_message", - "configure_scope", "continue_trace", "flush", "flush_async", @@ -36,7 +35,6 @@ "isolation_scope", "last_event_id", "new_scope", - "push_scope", "remove_attribute", "set_attribute", "set_attributes", diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 3b3f2e8125..51faebebf7 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,12 +1,11 @@ import inspect import warnings -from contextlib import contextmanager from typing import TYPE_CHECKING from sentry_sdk import Client, tracing_utils from sentry_sdk._init_implementation import init from sentry_sdk.crons import monitor -from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope +from sentry_sdk.scope import Scope, isolation_scope, new_scope from sentry_sdk.traces import StreamedSpan from sentry_sdk.traces import get_current_span as _get_current_streamed_span from sentry_sdk.tracing import NoOpSpan, Transaction, trace @@ -16,9 +15,7 @@ from typing import ( Any, Callable, - ContextManager, Dict, - Generator, Optional, TypeVar, Union, @@ -56,7 +53,6 @@ def overload(x: "T") -> "T": "capture_event", "capture_exception", "capture_message", - "configure_scope", "continue_trace", "flush", "flush_async", @@ -71,7 +67,6 @@ def overload(x: "T") -> "T": "isolation_scope", "last_event_id", "new_scope", - "push_scope", "remove_attribute", "set_attribute", "set_attributes", @@ -203,94 +198,6 @@ def add_breadcrumb( return get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) -@overload -def configure_scope() -> "ContextManager[Scope]": - pass - - -@overload -def configure_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def configure_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - warnings.warn( - "sentry_sdk.configure_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-configuring", - DeprecationWarning, - stacklevel=2, - ) - - scope = get_isolation_scope() - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - -@overload -def push_scope() -> "ContextManager[Scope]": - pass - - -@overload -def push_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def push_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - warnings.warn( - "sentry_sdk.push_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-pushing", - DeprecationWarning, - stacklevel=2, - ) - - if callback is not None: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - with push_scope() as scope: - callback(scope) - return None - - return _ScopeManager() - - @scopemethod def set_attribute(attribute: str, value: "Any") -> None: """ diff --git a/tests/test_api.py b/tests/test_api.py index c25ed3397d..4c83f03cb6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,7 +6,6 @@ import sentry_sdk from sentry_sdk import ( capture_exception, - configure_scope, continue_trace, get_baggage, get_client, @@ -16,7 +15,6 @@ get_isolation_scope, get_traceparent, is_initialized, - push_scope, set_tags, start_transaction, ) @@ -314,18 +312,6 @@ def test_set_tags(sentry_init, capture_events): }, "Updating tags with empty dict changed tags" -def test_configure_scope_deprecation(): - with pytest.warns(DeprecationWarning): - with configure_scope(): - ... - - -def test_push_scope_deprecation(): - with pytest.warns(DeprecationWarning): - with push_scope(): - ... - - def test_init_context_manager_deprecation(): with pytest.warns(DeprecationWarning): with sentry_sdk.init(): diff --git a/tests/test_basics.py b/tests/test_basics.py index 2942206b86..ada3f569dc 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -19,7 +19,6 @@ isolation_scope, last_event_id, new_scope, - push_scope, start_transaction, ) from sentry_sdk.integrations import ( @@ -294,23 +293,6 @@ def before_breadcrumb(crumb, hint): add_breadcrumb(crumb=dict(foo=42)) -def test_push_scope(sentry_init, capture_events, suppress_deprecation_warnings): - sentry_init() - events = capture_events() - - with push_scope() as scope: - scope.level = "warning" - try: - 1 / 0 - except Exception as e: - capture_exception(e) - - (event,) = events - - assert event["level"] == "warning" - assert "exception" in event - - def test_breadcrumbs(sentry_init, capture_events): sentry_init(max_breadcrumbs=10) events = capture_events() diff --git a/tests/test_client.py b/tests/test_client.py index 8a71dcc778..e654a1ede1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -19,7 +19,6 @@ capture_event, capture_exception, capture_message, - configure_scope, set_tag, start_transaction, ) @@ -652,27 +651,6 @@ def test_client_debug_option_disabled(with_client, sentry_init, caplog): assert "OK" not in caplog.text -@pytest.mark.skip( - reason="New behavior in SDK 2.0: You have a scope before init and add data to it." -) -def test_scope_initialized_before_client(sentry_init, capture_events): - """ - This is a consequence of how configure_scope() works. We must - make `configure_scope()` a noop if no client is configured. Even - if the user later configures a client: We don't know that. - """ - with configure_scope() as scope: - scope.set_tag("foo", 42) - - sentry_init() - - events = capture_events() - capture_message("hi") - (event,) = events - - assert "tags" not in event - - def test_weird_chars(sentry_init, capture_events): sentry_init() events = capture_events() From 5ea58ef083421c710da559be5fa32cbf8c141984 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 30 Jul 2026 10:22:28 +0200 Subject: [PATCH 13/16] ref: Remove `enable_tracing` in new major (#6943) --- MIGRATION_GUIDE.md | 1 + sentry_sdk/client.py | 10 ------ sentry_sdk/consts.py | 3 -- sentry_sdk/tracing_utils.py | 9 ++--- .../celery/test_update_celery_task_headers.py | 36 ------------------- tests/test_basics.py | 27 -------------- tests/test_client.py | 6 ---- 7 files changed, 4 insertions(+), 88 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index a068711fbb..9eacd0c7e5 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -26,6 +26,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## Removed +- The `enable_tracing` option was removed. Use `traces_sample_rate=1.0` instead. - The deprecated `push_scope` and `configure_scope` APIs have been removed. Use `with new_scope():` to push a new scope and `scope = get_current_scope()` to retrieve the current scope instead. - Transaction profiling and related code was removed. - Removed the deprecated Hub class and all uses of hub throughout the SDK in arguments, options, etc. Use a scope instead. diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index fb315cd674..f865cbac97 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -338,9 +338,6 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": rv["project_root"] = project_root - if rv["enable_tracing"] is True and rv["traces_sample_rate"] is None: - rv["traces_sample_rate"] = 1.0 - rv["data_collection"] = _resolve_data_collection(rv) # Do not add the event scrubber if data collection is enabled as it can remove data that's @@ -369,13 +366,6 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False ) - if rv["enable_tracing"] is not None: - warnings.warn( - "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", - DeprecationWarning, - stacklevel=2, - ) - if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): warnings.warn( "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index c2500ca913..a288c54f72 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1306,7 +1306,6 @@ def __init__( proxy_headers: "Optional[Dict[str, str]]" = None, before_send_transaction: "Optional[TransactionProcessor]" = None, project_root: "Optional[str]" = None, - enable_tracing: "Optional[bool]" = None, include_local_variables: "Optional[bool]" = True, include_source_context: "Optional[bool]" = True, trace_propagation_targets: "Optional[Sequence[str]]" = [ # noqa: B006 @@ -1702,8 +1701,6 @@ def __init__( :param profile_session_sample_rate: - :param enable_tracing: - :param propagate_traces: :param auto_session_tracking: diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index d75d94efd6..105edb0ec7 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -95,17 +95,14 @@ def __iter__(self) -> "Generator[str, None, None]": def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: """ Returns True if either traces_sample_rate or traces_sampler is - defined and enable_tracing is set and not false. + defined. """ if options is None: return False return bool( - options.get("enable_tracing") is not False - and ( - options.get("traces_sample_rate") is not None - or options.get("traces_sampler") is not None - ) + options.get("traces_sample_rate") is not None + or options.get("traces_sampler") is not None ) diff --git a/tests/integrations/celery/test_update_celery_task_headers.py b/tests/integrations/celery/test_update_celery_task_headers.py index 18f2f96bbf..4418653885 100644 --- a/tests/integrations/celery/test_update_celery_task_headers.py +++ b/tests/integrations/celery/test_update_celery_task_headers.py @@ -189,39 +189,3 @@ def test_celery_trace_propagation_traces_sample_rate( else: assert "sentry-monitor-start-timestamp-s" not in outgoing_headers assert "sentry-monitor-start-timestamp-s" not in outgoing_headers["headers"] - - -@pytest.mark.parametrize( - "enable_tracing,monitor_beat_tasks", - list(itertools.product([None, True, False], [True, False])), -) -def test_celery_trace_propagation_enable_tracing( - sentry_init, enable_tracing, monitor_beat_tasks -): - """ - The celery integration does not check the traces_sample_rate. - By default traces_sample_rate is None which means "do not propagate traces". - But the celery integration does not check this value. - The Celery integration has its own mechanism to propagate traces: - https://docs.sentry.io/platforms/python/integrations/celery/#distributed-traces - """ - sentry_init(enable_tracing=enable_tracing) - - headers = {} - span = None - - scope = sentry_sdk.get_isolation_scope() - - outgoing_headers = _update_celery_task_headers(headers, span, monitor_beat_tasks) - - assert outgoing_headers["sentry-trace"] == scope.get_traceparent() - assert outgoing_headers["headers"]["sentry-trace"] == scope.get_traceparent() - assert outgoing_headers["baggage"] == scope.get_baggage().serialize() - assert outgoing_headers["headers"]["baggage"] == scope.get_baggage().serialize() - - if monitor_beat_tasks: - assert "sentry-monitor-start-timestamp-s" in outgoing_headers - assert "sentry-monitor-start-timestamp-s" in outgoing_headers["headers"] - else: - assert "sentry-monitor-start-timestamp-s" not in outgoing_headers - assert "sentry-monitor-start-timestamp-s" not in outgoing_headers["headers"] diff --git a/tests/test_basics.py b/tests/test_basics.py index ada3f569dc..06b51d9867 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -31,7 +31,6 @@ from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.stdlib import StdlibIntegration from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.tracing_utils import has_tracing_enabled from sentry_sdk.utils import datetime_from_isoformat, get_sdk_name, reraise @@ -248,32 +247,6 @@ def do_this(): assert crumb["type"] == "default" -@pytest.mark.parametrize( - "enable_tracing, traces_sample_rate, tracing_enabled, updated_traces_sample_rate", - [ - (None, None, False, None), - (False, 0.0, False, 0.0), - (False, 1.0, False, 1.0), - (None, 1.0, True, 1.0), - (True, 1.0, True, 1.0), - (None, 0.0, True, 0.0), # We use this as - it's configured but turned off - (True, 0.0, True, 0.0), # We use this as - it's configured but turned off - (True, None, True, 1.0), - ], -) -def test_option_enable_tracing( - sentry_init, - enable_tracing, - traces_sample_rate, - tracing_enabled, - updated_traces_sample_rate, -): - sentry_init(enable_tracing=enable_tracing, traces_sample_rate=traces_sample_rate) - options = sentry_sdk.get_client().options - assert has_tracing_enabled(options) is tracing_enabled - assert options["traces_sample_rate"] == updated_traces_sample_rate - - def test_breadcrumb_arguments(sentry_init, capture_events): assert_hint = {"bar": 42} diff --git a/tests/test_client.py b/tests/test_client.py index e654a1ede1..82ea2fcf73 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1471,12 +1471,6 @@ def test_dropped_transaction(sentry_init, capture_record_lost_event_calls, test_ test_config.run(sentry_init, capture_record_lost_event_calls) -@pytest.mark.parametrize("enable_tracing", [True, False]) -def test_enable_tracing_deprecated(sentry_init, enable_tracing): - with pytest.warns(DeprecationWarning): - sentry_init(enable_tracing=enable_tracing) - - def test_ignore_spans_warns_without_streaming(sentry_init): with pytest.warns(UserWarning, match=r"`ignore_spans` parameter only works"): sentry_init(ignore_spans=["/health"], trace_lifecycle="static") From f67953a061f3e9dc324498136f8ac24548952b4f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 30 Jul 2026 10:38:53 +0200 Subject: [PATCH 14/16] ref: Remove `propagate_traces` in new major (#6945) --- MIGRATION_GUIDE.md | 1 + sentry_sdk/consts.py | 3 --- sentry_sdk/scope.py | 7 ------- tests/tracing/test_integration_tests.py | 14 -------------- 4 files changed, 1 insertion(+), 24 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 9eacd0c7e5..f0af1e5df5 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -34,6 +34,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Removed the `auto_session_tracing` decorator. Use `track_session` instead. - The deprecated `set_measurement` API was removed. - The experimental option `otel_powered_performance` has been removed together with the associated `OpenTelemetryIntegration` and `opentelemetry-experimental` extra. +- The deprecated `propagate_traces` option has been removed. Use `trace_propagation_targets` instead, which gives you more power over trace propagation. Note that only the top-level `init` option was removed; the `propagate_traces` option of the Celery integration remains available. ## Deprecated diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index a288c54f72..1a205a31de 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1291,7 +1291,6 @@ def __init__( debug: "Optional[bool]" = None, attach_stacktrace: bool = False, ca_certs: "Optional[str]" = None, - propagate_traces: bool = True, traces_sample_rate: "Optional[float]" = None, trace_lifecycle: "Optional[Literal['static', 'stream']]" = None, traces_sampler: "Optional[TracesSampler]" = None, @@ -1701,8 +1700,6 @@ def __init__( :param profile_session_sample_rate: - :param propagate_traces: - :param auto_session_tracking: :param spotlight: diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index d1be9f53db..4ac12e9310 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -683,13 +683,6 @@ def iter_trace_propagation_headers( If no span is given, the trace data is taken from the scope. """ client = self.get_client() - if not client.options.get("propagate_traces"): - warnings.warn( - "The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.", - DeprecationWarning, - stacklevel=2, - ) - return span = kwargs.pop("span", None) if not span: diff --git a/tests/tracing/test_integration_tests.py b/tests/tracing/test_integration_tests.py index a7d59d2741..a363a64982 100644 --- a/tests/tracing/test_integration_tests.py +++ b/tests/tracing/test_integration_tests.py @@ -313,20 +313,6 @@ def test_continue_trace_span_streaming( assert message_payload["message"] == "hello" -@pytest.mark.parametrize("sample_rate", [0.0, 1.0]) -def test_propagate_traces_deprecation_warning(sentry_init, sample_rate): - sentry_init(traces_sample_rate=sample_rate, propagate_traces=False) - - with start_transaction(name="hi"): - with start_span() as old_span: - with pytest.warns(DeprecationWarning): - dict( - sentry_sdk.get_current_scope().iter_trace_propagation_headers( - old_span - ) - ) - - @pytest.mark.parametrize("sample_rate", [0.5, 1.0]) def test_dynamic_sampling_head_sdk_creates_dsc( sentry_init, capture_envelopes, sample_rate, monkeypatch From 2a338a0eb834fbc75baf14c52061df30f4927c5a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 30 Jul 2026 11:01:28 +0200 Subject: [PATCH 15/16] ref: Drop support for legacy `failed_request_status_codes` format in new major (#6948) ### Description When `failed_request_status_codes` was first introduced, it accepted a different format. The format was then changed, while the old format was deprecated. Drop support for the old format now. #### Issues Closes https://github.com/getsentry/sentry-python/issues/5017 --- MIGRATION_GUIDE.md | 1 + sentry_sdk/_types.py | 4 +- sentry_sdk/integrations/_wsgi_common.py | 37 +-------- sentry_sdk/integrations/starlette.py | 26 +------ tests/integrations/fastapi/test_fastapi.py | 43 ----------- .../integrations/starlette/test_starlette.py | 76 ------------------- 6 files changed, 7 insertions(+), 180 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index f0af1e5df5..def77740de 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -34,6 +34,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Removed the `auto_session_tracing` decorator. Use `track_session` instead. - The deprecated `set_measurement` API was removed. - The experimental option `otel_powered_performance` has been removed together with the associated `OpenTelemetryIntegration` and `opentelemetry-experimental` extra. +- The `failed_request_status_codes` integration option now only supports a set of integers as input. Lists of integers or containers of integers are no longer supported. - The deprecated `propagate_traces` option has been removed. Use `trace_propagation_targets` instead, which gives you more power over trace propagation. Note that only the top-level `init` option was removed; the `propagate_traces` option of the Celery integration remains available. ## Deprecated diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index b149097845..3e5abae915 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -138,7 +138,7 @@ def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue": if TYPE_CHECKING: - from collections.abc import Container, MutableMapping, Sequence + from collections.abc import MutableMapping, Sequence from datetime import datetime from types import TracebackType from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type @@ -452,8 +452,6 @@ class DataCollection(TypedDict): total=False, ) - HttpStatusCodeRange = Union[int, Container[int]] - class TextPart(TypedDict): type: Literal["text"] content: str diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 50db021a62..a538363b8c 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -5,7 +5,7 @@ from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE from sentry_sdk.data_collection import _apply_key_value_collection_filtering from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger +from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled try: from django.http.request import RawPostDataException @@ -20,7 +20,7 @@ if TYPE_CHECKING: from typing import Any, Dict, Mapping, MutableMapping, Optional, Union - from sentry_sdk._types import Event, HttpStatusCodeRange + from sentry_sdk._types import Event SENSITIVE_ENV_KEYS = ( @@ -247,36 +247,3 @@ def _filter_headers( ) for k, v in headers.items() } - - -def _in_http_status_code_range( - code: object, code_ranges: "list[HttpStatusCodeRange]" -) -> bool: - for target in code_ranges: - if isinstance(target, int): - if code == target: - return True - continue - - try: - if code in target: - return True - except TypeError: - logger.warning( - "failed_request_status_codes has to be a list of integers or containers" - ) - - return False - - -class HttpCodeRangeContainer: - """ - Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int]. - Used for backwards compatibility with the old `failed_request_status_codes` option. - """ - - def __init__(self, code_ranges: "list[HttpStatusCodeRange]") -> None: - self._code_ranges = code_ranges - - def __contains__(self, item: object) -> bool: - return _in_http_status_code_range(item, self._code_ranges) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index f82ee7b955..054cea6c12 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -1,7 +1,6 @@ import functools import json import sys -import warnings from collections.abc import Set from copy import deepcopy from json import JSONDecodeError @@ -19,7 +18,6 @@ from sentry_sdk.integrations._asgi_common import _RootPathInPath from sentry_sdk.integrations._wsgi_common import ( DEFAULT_HTTP_METHODS_TO_CAPTURE, - HttpCodeRangeContainer, _is_json_content_type, request_body_within_bounds, ) @@ -51,10 +49,9 @@ Dict, Optional, Tuple, - Union, ) - from sentry_sdk._types import Event, HttpStatusCodeRange + from sentry_sdk._types import Event try: import starlette from starlette import __version__ as STARLETTE_VERSION @@ -113,7 +110,7 @@ class StarletteIntegration(Integration): def __init__( self, transaction_style: str = "url", - failed_request_status_codes: "Union[Set[int], list[HttpStatusCodeRange], None]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, middleware_spans: bool = False, http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, ): @@ -126,24 +123,7 @@ def __init__( self.middleware_spans = middleware_spans self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - if isinstance(failed_request_status_codes, Set): - self.failed_request_status_codes: "Container[int]" = ( - failed_request_status_codes - ) - else: - warnings.warn( - "Passing a list or None for failed_request_status_codes is deprecated. " - "Please pass a set of int instead.", - DeprecationWarning, - stacklevel=2, - ) - - if failed_request_status_codes is None: - self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES - else: - self.failed_request_status_codes = HttpCodeRangeContainer( - failed_request_status_codes - ) + self.failed_request_status_codes: "Container[int]" = failed_request_status_codes @staticmethod def setup_once() -> None: diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 2e7383e758..02d761baf9 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -56,7 +56,6 @@ ) from tests.integrations.conftest import parametrize_test_configurable_status_codes -from tests.integrations.starlette import test_starlette def fastapi_app_factory(): @@ -889,48 +888,6 @@ def test_transaction_name_in_middleware( ) -@test_starlette.parametrize_test_configurable_status_codes_deprecated -def test_configurable_status_codes_deprecated( - sentry_init, - capture_events, - failed_request_status_codes, - status_code, - expected_error, -): - with pytest.warns(DeprecationWarning): - starlette_integration = StarletteIntegration( - failed_request_status_codes=failed_request_status_codes - ) - - with pytest.warns(DeprecationWarning): - fast_api_integration = FastApiIntegration( - failed_request_status_codes=failed_request_status_codes - ) - - sentry_init( - integrations=[ - starlette_integration, - fast_api_integration, - ] - ) - - events = capture_events() - - app = FastAPI() - - @app.get("/error") - async def _error(): - raise HTTPException(status_code) - - client = TestClient(app) - client.get("/error") - - if expected_error: - assert len(events) == 1 - else: - assert not events - - @pytest.mark.skipif( FASTAPI_VERSION < (0, 80), reason="Requires FastAPI >= 0.80, because earlier versions do not support HTTP 'HEAD' requests", diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 5b5532a67b..14b827da5d 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -1743,82 +1743,6 @@ def test_span_origin(sentry_init, capture_events, capture_items, span_streaming) assert span["origin"] == "auto.http.starlette" -class NonIterableContainer: - """Wraps any container and makes it non-iterable. - - Used to test backwards compatibility with our old way of defining failed_request_status_codes, which allowed - passing in a list of (possibly non-iterable) containers. The Python standard library does not provide any built-in - non-iterable containers, so we have to define our own. - """ - - def __init__(self, inner): - self.inner = inner - - def __contains__(self, item): - return item in self.inner - - -parametrize_test_configurable_status_codes_deprecated = pytest.mark.parametrize( - "failed_request_status_codes,status_code,expected_error", - [ - (None, 500, True), - (None, 400, False), - ([500, 501], 500, True), - ([500, 501], 401, False), - ([range(400, 499)], 401, True), - ([range(400, 499)], 500, False), - ([range(400, 499), range(500, 599)], 300, False), - ([range(400, 499), range(500, 599)], 403, True), - ([range(400, 499), range(500, 599)], 503, True), - ([range(400, 403), 500, 501], 401, True), - ([range(400, 403), 500, 501], 405, False), - ([range(400, 403), 500, 501], 501, True), - ([range(400, 403), 500, 501], 503, False), - ([], 500, False), - ([NonIterableContainer(range(500, 600))], 500, True), - ([NonIterableContainer(range(500, 600))], 404, False), - ], -) -"""Test cases for configurable status codes (deprecated API). -Also used by the FastAPI tests. -""" - - -@parametrize_test_configurable_status_codes_deprecated -def test_configurable_status_codes_deprecated( - sentry_init, - capture_events, - failed_request_status_codes, - status_code, - expected_error, -): - with pytest.warns(DeprecationWarning): - starlette_integration = StarletteIntegration( - failed_request_status_codes=failed_request_status_codes - ) - - sentry_init(integrations=[starlette_integration]) - - events = capture_events() - - async def _error(request): - raise HTTPException(status_code) - - app = starlette.applications.Starlette( - routes=[ - starlette.routing.Route("/error", _error, methods=["GET"]), - ], - ) - - client = TestClient(app) - client.get("/error") - - if expected_error: - assert len(events) == 1 - else: - assert not events - - @pytest.mark.skipif( STARLETTE_VERSION < (0, 21), reason="Requires Starlette >= 0.21, because earlier versions do not support HTTP 'HEAD' requests", From 4373f14ad24d0ad6da58915e1e09198da286ea42 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 30 Jul 2026 14:08:41 +0200 Subject: [PATCH 16/16] ref: Slim down extras in new major (#6942) ### Description Most of the entries in our extras list serve as a way to communicate/enforce the lower boundary of the respective framework that we support. This creates a parallel system to the version checks we already have in each integration. Some extras, however, define extra dependencies or specific extras that are required for an integration to work correctly (e.g. the Flask integration needs `blinker` to work properly). In that case, keep the extra. #### Issues Closes https://github.com/getsentry/sentry-python/issues/6259 --- MIGRATION_GUIDE.md | 1 + setup.py | 46 ++++++---------------------------------------- 2 files changed, 7 insertions(+), 40 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index def77740de..5921ff2992 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -34,6 +34,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Removed the `auto_session_tracing` decorator. Use `track_session` instead. - The deprecated `set_measurement` API was removed. - The experimental option `otel_powered_performance` has been removed together with the associated `OpenTelemetryIntegration` and `opentelemetry-experimental` extra. +- A number of extras (installable via `sentry-sdk[extra-name]`) has been removed. Use the base package (`sentry-sdk`) instead; there is no difference in functionality. The following extras have been removed: `aiohttp`, `anthropic`, `arq`, `asyncpg`, `beam`, `bottle`, `celery`, `celery-redbeat`, `chalice`, `clickhouse-driver`, `django`, `falcon`, `fastapi`, `google-genai`, `httpx`, `huey`, `huggingface_hub`, `langchain`, `langgraph`, `launchdarkly`, `litellm`, `litestar`, `loguru`, `mcp`, `openai`, `openfeature`, `pydantic_ai`, `pymongo`, `pyspark`, `rq`, `sanic`, `sqlalchemy`, `starlette`, `starlite`, `statsig`, `tornado`, `unleash`. - The `failed_request_status_codes` integration option now only supports a set of integers as input. Lists of integers or containers of integers are no longer supported. - The deprecated `propagate_traces` option has been removed. Use `trace_propagation_targets` instead, which gives you more power over trace propagation. Note that only the top-level `init` option was removed; the `propagate_traces` option of the Celery integration remains available. diff --git a/setup.py b/setup.py index fa290a0962..79a4c56431 100644 --- a/setup.py +++ b/setup.py @@ -44,50 +44,16 @@ def get_file_text(file_name): "certifi", ], extras_require={ - "aiohttp": ["aiohttp>=3.5"], - "anthropic": ["anthropic>=0.16"], - "arq": ["arq>=0.23"], - "asyncpg": ["asyncpg>=0.23"], - "beam": ["apache-beam>=2.12"], - "bottle": ["bottle>=0.12.13"], - "celery": ["celery>=3"], - "celery-redbeat": ["celery-redbeat>=2"], - "chalice": ["chalice>=1.16.0"], - "clickhouse-driver": ["clickhouse-driver>=0.2.0"], - "django": ["django>=1.8"], - "falcon": ["falcon>=1.4"], - "fastapi": ["fastapi>=0.79.0"], - "flask": ["flask>=0.11", "blinker>=1.1", "markupsafe"], + # Only add an extra if our integration needs something additional to + # work with the framework (a dependency or an extra). For instance, + # our Flask integration has a hard dependency on blinker. + "asyncio": ["httpcore[asyncio]==1.*"], + "flask": ["flask", "blinker>=1.1", "markupsafe"], "grpcio": ["grpcio>=1.21.1", "protobuf>=3.8.0"], "http2": ["httpcore[http2]==1.*"], - "asyncio": ["httpcore[asyncio]==1.*"], - "httpx": ["httpx>=0.16.0"], - "huey": ["huey>=2"], - "huggingface_hub": ["huggingface_hub>=0.22"], - "langchain": ["langchain>=0.0.210"], - "langgraph": ["langgraph>=0.6.6"], - "launchdarkly": ["launchdarkly-server-sdk>=9.8.0"], - "litellm": ["litellm>=1.77.5,!=1.82.7,!=1.82.8"], - "litestar": ["litestar>=2.0.0"], - "loguru": ["loguru>=0.5"], - "mcp": ["mcp>=1.15.0"], - "openai": ["openai>=1.0.0", "tiktoken>=0.3.0"], - "openfeature": ["openfeature-sdk>=0.7.1"], "opentelemetry-otlp": ["opentelemetry-distro[otlp]>=0.35b0"], "pure-eval": ["pure_eval", "executing", "asttokens"], - "pydantic_ai": ["pydantic-ai>=1.0.0"], - "pymongo": ["pymongo>=3.1"], - "pyspark": ["pyspark>=2.4.4"], - "quart": ["quart>=0.16.1", "blinker>=1.1"], - "rq": ["rq>=0.6"], - "sanic": ["sanic>=0.8"], - "sqlalchemy": ["sqlalchemy>=1.2"], - "starlette": ["starlette>=0.19.1"], - "starlite": ["starlite>=1.48"], - "statsig": ["statsig>=0.55.3"], - "tornado": ["tornado>=6"], - "unleash": ["UnleashClient>=6.0.1"], - "google-genai": ["google-genai>=1.29.0"], + "quart": ["quart", "blinker>=1.1"], }, classifiers=[ "Development Status :: 5 - Production/Stable",