Skip to content

Commit

Permalink
feat(integrations): Support Django 5.0 (#2490)
Browse files Browse the repository at this point in the history
Fix the way we wrap signal receivers: Django 5.0 introduced async receivers and changed the signature of the `Signal._live_receivers` method to return both sync and async receivers.

We'll need to change the Django version in tox.ini to 5.0 once it's been released. At the moment we're using the 5.0b1 release.
  • Loading branch information
sentrivana authored Nov 14, 2023
1 parent cb7299a commit 44b0244
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 30 deletions.
21 changes: 8 additions & 13 deletions sentry_sdk/integrations/django/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand Down
33 changes: 23 additions & 10 deletions sentry_sdk/integrations/django/signals_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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
11 changes: 11 additions & 0 deletions tests/integrations/django/asgi/test_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"
)
Expand All @@ -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"
)
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"
)
Expand Down
17 changes: 10 additions & 7 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 44b0244

Please sign in to comment.