diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index 48b27c50c8..bd785a23c2 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -8,6 +8,8 @@ import asyncio +from django.core.handlers.wsgi import WSGIRequest + from sentry_sdk import Hub, _functools from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.consts import OP @@ -16,26 +18,21 @@ from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.utils import capture_internal_exceptions -from django.core.handlers.wsgi import WSGIRequest - if TYPE_CHECKING: - from typing import Any - from typing import Dict - from typing import Union - from typing import Callable + from collections.abc import Callable + from typing import Any, Union from django.core.handlers.asgi import ASGIRequest from django.http.response import HttpResponse - from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk._types import EventProcessor -def _make_asgi_request_event_processor(request, integration): - # type: (ASGIRequest, DjangoIntegration) -> EventProcessor +def _make_asgi_request_event_processor(request): + # type: (ASGIRequest) -> EventProcessor def asgi_request_event_processor(event, hint): - # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] + # type: (dict[str, Any], dict[str, Any]) -> dict[str, Any] # if the request is gone we are fine not logging the data from # it. This might happen if the processor is pushed away to # another thread. @@ -103,9 +100,7 @@ def sentry_patched_create_request(self, *args, **kwargs): # (otherwise Django closes the body stream and makes it impossible to read it again) _ = request.body - scope.add_event_processor( - _make_asgi_request_event_processor(request, integration) - ) + scope.add_event_processor(_make_asgi_request_event_processor(request)) return request, error_response diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py index 87b6b22ff8..097a56c8aa 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -7,12 +7,12 @@ from sentry_sdk._functools import wraps from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.consts import OP +from sentry_sdk.integrations.django import DJANGO_VERSION if TYPE_CHECKING: - from typing import Any - from typing import Callable - from typing import List + from collections.abc import Callable + from typing import Any, Union def _get_receiver_name(receiver): @@ -42,17 +42,27 @@ def _get_receiver_name(receiver): def patch_signals(): # type: () -> None - """Patch django signal receivers to create a span""" + """ + Patch django signal receivers to create a span. + + This only wraps sync receivers. Django>=5.0 introduced async receivers, but + since we don't create transactions for ASGI Django, we don't wrap them. + """ from sentry_sdk.integrations.django import DjangoIntegration old_live_receivers = Signal._live_receivers def _sentry_live_receivers(self, sender): - # type: (Signal, Any) -> List[Callable[..., Any]] + # type: (Signal, Any) -> Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]] hub = Hub.current - receivers = old_live_receivers(self, sender) - def sentry_receiver_wrapper(receiver): + if DJANGO_VERSION >= (5, 0): + sync_receivers, async_receivers = old_live_receivers(self, sender) + else: + sync_receivers = old_live_receivers(self, sender) + async_receivers = [] + + def sentry_sync_receiver_wrapper(receiver): # type: (Callable[..., Any]) -> Callable[..., Any] @wraps(receiver) def wrapper(*args, **kwargs): @@ -69,9 +79,12 @@ def wrapper(*args, **kwargs): integration = hub.get_integration(DjangoIntegration) if integration and integration.signals_spans: - for idx, receiver in enumerate(receivers): - receivers[idx] = sentry_receiver_wrapper(receiver) + for idx, receiver in enumerate(sync_receivers): + sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) - return receivers + if DJANGO_VERSION >= (5, 0): + return sync_receivers, async_receivers + else: + return sync_receivers Signal._live_receivers = _sentry_live_receivers diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 57145b698d..c7f5f1dfd9 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -26,6 +26,7 @@ @pytest.mark.parametrize("application", APPS) @pytest.mark.asyncio +@pytest.mark.forked async def test_basic(sentry_init, capture_events, application): sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) @@ -58,6 +59,7 @@ async def test_basic(sentry_init, capture_events, application): @pytest.mark.parametrize("application", APPS) @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -85,6 +87,7 @@ async def test_async_views(sentry_init, capture_events, application): @pytest.mark.parametrize("application", APPS) @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -119,6 +122,7 @@ async def test_active_thread_id(sentry_init, capture_envelopes, endpoint, applic @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -152,6 +156,7 @@ async def test_async_views_concurrent_execution(sentry_init, settings): @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -189,6 +194,7 @@ async def test_async_middleware_that_is_function_concurrent_execution( @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -238,6 +244,7 @@ async def test_async_middleware_spans( @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -267,6 +274,7 @@ async def test_has_trace_if_performance_enabled(sentry_init, capture_events): @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -293,6 +301,7 @@ async def test_has_trace_if_performance_disabled(sentry_init, capture_events): @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -328,6 +337,7 @@ async def test_trace_from_headers_if_performance_enabled(sentry_init, capture_ev @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) @@ -373,6 +383,7 @@ async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_e ], ) @pytest.mark.asyncio +@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) diff --git a/tox.ini b/tox.ini index d5e0d753a9..072b561b07 100644 --- a/tox.ini +++ b/tox.ini @@ -79,6 +79,8 @@ envlist = {py3.6,py3.7,py3.8,py3.9,py3.10,py3.11}-django-v{3.2} # - Django 4.x {py3.8,py3.9,py3.10,py3.11,py3.12}-django-v{4.0,4.1,4.2} + # - Django 5.x + {py3.10,py3.11,py3.12}-django-v{5.0} # Falcon {py2.7,py3.5,py3.6,py3.7}-falcon-v{1.4} @@ -288,17 +290,16 @@ deps = django: Werkzeug<2.1.0 django-v{1.11,2.0,2.1,2.2,3.0,3.1,3.2}: djangorestframework>=3.0.0,<4.0.0 - {py3.7,py3.8,py3.9,py3.10,py3.11}-django-v{1.11,2.0,2.1,2.2,3.0,3.1,3.2,4.0,4.1,4.2}: pytest-asyncio - {py3.7,py3.8,py3.9,py3.10,py3.11}-django-v{1.11,2.0,2.1,2.2,3.0,3.1,3.2,4.0,4.1,4.2}: channels[daphne]>2 + {py3.7,py3.8,py3.9,py3.10,py3.11,py3.12}-django-v{1.11,2.0,2.1,2.2,3.0,3.1,3.2,4.0,4.1,4.2,5.0}: pytest-asyncio + {py3.7,py3.8,py3.9,py3.10,py3.11,py3.12}-django-v{1.11,2.0,2.1,2.2,3.0,3.1,3.2,4.0,4.1,4.2,5.0}: channels[daphne]>2 django-v{1.8,1.9,1.10,1.11,2.0,2.1}: pytest-django<4.0 django-v{2.2,3.0,3.1,3.2}: pytest-django>=4.0 django-v{2.2,3.0,3.1,3.2}: Werkzeug<2.0 - - django-v{4.0,4.1,4.2}: djangorestframework - django-v{4.0,4.1,4.2}: pytest-asyncio - django-v{4.0,4.1,4.2}: pytest-django - django-v{4.0,4.1,4.2}: Werkzeug + django-v{4.0,4.1,4.2,5.0}: djangorestframework + django-v{4.0,4.1,4.2,5.0}: pytest-asyncio + django-v{4.0,4.1,4.2,5.0}: pytest-django + django-v{4.0,4.1,4.2,5.0}: Werkzeug django-v1.8: Django>=1.8,<1.9 django-v1.9: Django>=1.9,<1.10 @@ -313,6 +314,8 @@ deps = django-v4.0: Django>=4.0,<4.1 django-v4.1: Django>=4.1,<4.2 django-v4.2: Django>=4.2,<4.3 + # TODO: change to final when available + django-v5.0: Django==5.0b1 # Falcon falcon-v1.4: falcon>=1.4,<1.5