From 33ec70b92a26e714c739b8453e40f9c7e75f1559 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 7 Nov 2023 10:56:30 +0100 Subject: [PATCH 01/21] Run tests on Django 5.0 --- tox.ini | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index d19607563c..44fd60fb37 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}-django-v{5.0} # Falcon {py2.7,py3.5,py3.6,py3.7}-falcon-v{1.4} @@ -295,10 +297,10 @@ deps = 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 +315,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 From 4043eb009b98619c80014de589fb32b73044e130 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 8 Nov 2023 12:40:59 +0100 Subject: [PATCH 02/21] fix sync receiver patching --- .../integrations/django/signals_handlers.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py index 87b6b22ff8..50d1b55ded 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import +from django import VERSION as DJANGO_VERSION from django.dispatch import Signal from sentry_sdk import Hub @@ -50,9 +51,14 @@ def patch_signals(): def _sentry_live_receivers(self, sender): # type: (Signal, 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 +75,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 From aaaa910ff8a10843a591aa678d31088b10601a42 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 8 Nov 2023 13:08:14 +0100 Subject: [PATCH 03/21] reorganize --- sentry_sdk/integrations/django/__init__.py | 8 +++- sentry_sdk/integrations/django/asgi.py | 47 +++++++++++++++++-- .../integrations/django/signals_handlers.py | 27 +++++------ 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index c82ef4f148..34aa7fc474 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -242,7 +242,13 @@ def _django_queryset_repr(value, hint): patch_django_middlewares() patch_views() patch_templates() - patch_signals() + + if DJANGO_VERSION >= (5, 0): + from sentry_sdk.integrations.django.asgi import patch_signals_async + + patch_signals_async() + else: + patch_signals() if patch_caching is not None: patch_caching() diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index 41ebe18e62..fbe1448058 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -8,16 +8,19 @@ import asyncio +from django.dispatch import Signal + from sentry_sdk import Hub, _functools +from sentry_sdk._functools import wraps from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.consts import OP from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.integrations.django.signals_handlers import _get_receiver_name + if TYPE_CHECKING: - from typing import Any - from typing import Union - from typing import Callable + from typing import Any, Callable, List, Tuple, Union from django.http.response import HttpResponse @@ -153,3 +156,41 @@ async def __acall__(self, *args, **kwargs): return await f(*args, **kwargs) return SentryASGIMixin + + +def patch_signals_async(): + # type: () -> None + """Patch django signal receivers to create a span.""" + from sentry_sdk.integrations.django import DjangoIntegration + + old_live_receivers = Signal._live_receivers + + def _sentry_live_receivers(self, sender): + # type: (Signal, Any) -> Tuple[List[Callable[..., Any]], List[Callable[..., Any]]] + hub = Hub.current + + sync_receivers, async_receivers = old_live_receivers(self, sender) + + def sentry_sync_receiver_wrapper(receiver): + # type: (Callable[..., Any]) -> Callable[..., Any] + @wraps(receiver) + def wrapper(*args, **kwargs): + # type: (Any, Any) -> Any + signal_name = _get_receiver_name(receiver) + with hub.start_span( + op=OP.EVENT_DJANGO, + description=signal_name, + ) as span: + span.set_data("signal", signal_name) + return receiver(*args, **kwargs) + + return wrapper + + integration = hub.get_integration(DjangoIntegration) + if integration and integration.signals_spans: + for idx, receiver in enumerate(sync_receivers): + sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) + + return sync_receivers, async_receivers + + Signal._live_receivers = _sentry_live_receivers diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py index 50d1b55ded..bcd7f632e7 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -from django import VERSION as DJANGO_VERSION from django.dispatch import Signal from sentry_sdk import Hub @@ -11,9 +10,7 @@ if TYPE_CHECKING: - from typing import Any - from typing import Callable - from typing import List + from typing import Any, Callable, List def _get_receiver_name(receiver): @@ -43,7 +40,12 @@ 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 is used for Django<5.0. Django>=5.0 introduced async receivers; see + patch_signals_async in asgi.py. + """ from sentry_sdk.integrations.django import DjangoIntegration old_live_receivers = Signal._live_receivers @@ -52,11 +54,7 @@ def _sentry_live_receivers(self, sender): # type: (Signal, Any) -> List[Callable[..., Any]] hub = Hub.current - if DJANGO_VERSION >= (5, 0): - sync_receivers, async_receivers = old_live_receivers(self, sender) - else: - sync_receivers = old_live_receivers(self, sender) - async_receivers = [] + receivers = old_live_receivers(self, sender) def sentry_sync_receiver_wrapper(receiver): # type: (Callable[..., Any]) -> Callable[..., Any] @@ -75,12 +73,9 @@ def wrapper(*args, **kwargs): integration = hub.get_integration(DjangoIntegration) if integration and integration.signals_spans: - for idx, receiver in enumerate(sync_receivers): - sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) + for idx, receiver in enumerate(receivers): + receivers[idx] = sentry_sync_receiver_wrapper(receiver) - if DJANGO_VERSION >= (5, 0): - return sync_receivers, async_receivers - else: - return sync_receivers + return receivers Signal._live_receivers = _sentry_live_receivers From b01a39850f4d0c24f93d58c395179ab7931ccd40 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 8 Nov 2023 13:49:58 +0100 Subject: [PATCH 04/21] also patch async receivers --- sentry_sdk/integrations/django/asgi.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index fbe1448058..81904a9baa 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -186,10 +186,27 @@ def wrapper(*args, **kwargs): return wrapper + def sentry_async_receiver_wrapper(receiver): + # type: (Callable[..., Any]) -> Callable[..., Any] + @wraps(receiver) + async def wrapper(*args, **kwargs): + # type: (Any, Any) -> Any + signal_name = _get_receiver_name(receiver) + with hub.start_span( + op=OP.EVENT_DJANGO, + description=signal_name, + ) as span: + span.set_data("signal", signal_name) + return await receiver(*args, **kwargs) + + return wrapper + integration = hub.get_integration(DjangoIntegration) if integration and integration.signals_spans: for idx, receiver in enumerate(sync_receivers): sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) + for idx, receiver in enumerate(async_receivers): + async_receivers[idx] = sentry_async_receiver_wrapper(receiver) return sync_receivers, async_receivers From 4c23cae3f843d92c28d77d831be17afd3ae0762b Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 8 Nov 2023 13:59:52 +0100 Subject: [PATCH 05/21] test 5.0 on 3.12 as well --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 44fd60fb37..51cfaf53e1 100644 --- a/tox.ini +++ b/tox.ini @@ -80,7 +80,7 @@ envlist = # - 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}-django-v{5.0} + {py3.10,py3.11,py3.12}-django-v{5.0} # Falcon {py2.7,py3.5,py3.6,py3.7}-falcon-v{1.4} From d357101080a500293b3c5a99ee4a2c2f71162491 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 8 Nov 2023 14:40:06 +0100 Subject: [PATCH 06/21] Make reading the request body work in Django ASGI apps. --- sentry_sdk/integrations/django/__init__.py | 12 ++-- sentry_sdk/integrations/django/asgi.py | 69 +++++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 73908bc333..d09c57539e 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -36,6 +36,7 @@ from django import VERSION as DJANGO_VERSION from django.conf import settings as django_settings from django.core import signals + from django.core.handlers.asgi import ASGIRequest from django.conf import settings try: @@ -410,7 +411,7 @@ def _before_get_response(request): _set_transaction_name_and_source(scope, integration.transaction_style, request) scope.add_event_processor( - _make_event_processor(weakref.ref(request), integration) + _make_wsgi_request_event_processor(weakref.ref(request), integration) ) @@ -462,9 +463,9 @@ def sentry_patched_get_response(self, request): patch_get_response_async(BaseHandler, _before_get_response) -def _make_event_processor(weak_request, integration): +def _make_wsgi_request_event_processor(weak_request, integration): # type: (Callable[[], WSGIRequest], DjangoIntegration) -> EventProcessor - def event_processor(event, hint): + def wsgi_request_event_processor(event, hint): # 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 @@ -473,6 +474,9 @@ def event_processor(event, hint): if request is None: return event + if type(request) == ASGIRequest: + return event + try: drf_request = request._sentry_drf_request_backref() if drf_request is not None: @@ -489,7 +493,7 @@ def event_processor(event, hint): return event - return event_processor + return wsgi_request_event_processor def _got_request_exception(request=None, **kwargs): diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index 41ebe18e62..e4446d462f 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -11,16 +11,56 @@ from sentry_sdk import Hub, _functools from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.consts import OP +from sentry_sdk.hub import _should_send_default_pii 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 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: (Callable[[], ASGIRequest], DjangoIntegration) -> EventProcessor + def asgi_request_event_processor(event, hint): + # 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. + from sentry_sdk.integrations.django import ( + DjangoRequestExtractor, + _set_user_info, + ) + + if request is None: + return event + + if type(request) == WSGIRequest: + return event + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if _should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return asgi_request_event_processor + def patch_django_asgi_handler_impl(cls): # type: (Any) -> None @@ -31,16 +71,43 @@ def patch_django_asgi_handler_impl(cls): async def sentry_patched_asgi_handler(self, scope, receive, send): # type: (Any, Any, Any, Any) -> Any - if Hub.current.get_integration(DjangoIntegration) is None: + hub = Hub.current + integration = hub.get_integration(DjangoIntegration) + if integration is None: return await old_app(self, scope, receive, send) middleware = SentryAsgiMiddleware( old_app.__get__(self, cls), unsafe_context_data=True )._run_asgi3 + return await middleware(scope, receive, send) cls.__call__ = sentry_patched_asgi_handler + old_create_request = cls.create_request + + def sentry_patched_create_request(self, *args, **kwargs): + hub = Hub.current + integration = hub.get_integration(DjangoIntegration) + if integration is None: + return old_create_request(self, *args, **kwargs) + + with hub.configure_scope() as scope: + request, error_response = old_create_request(self, *args, **kwargs) + + # read the body once, to signal Django to cache the body stream + # so we can read the body in our event processor + # (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) + ) + + return request, error_response + + cls.create_request = sentry_patched_create_request + def patch_get_response_async(cls, _before_get_response): # type: (Any, Any) -> None From 1656a83e0d145aae0293960ee1528525e2be359f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 8 Nov 2023 16:32:25 +0100 Subject: [PATCH 07/21] Fixed some tests for Django<3 --- sentry_sdk/integrations/django/__init__.py | 11 +++++-- sentry_sdk/integrations/django/asgi.py | 36 ++++++++++++---------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index d09c57539e..25c85be828 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -36,7 +36,6 @@ from django import VERSION as DJANGO_VERSION from django.conf import settings as django_settings from django.core import signals - from django.core.handlers.asgi import ASGIRequest from django.conf import settings try: @@ -48,6 +47,13 @@ from django.urls import Resolver404 except ImportError: from django.core.urlresolvers import Resolver404 + + # Only available in Django 3.0+ + try: + from django.core.handlers.asgi import ASGIRequest + except ModuleNotFoundError: + ASGIRequest = None + except ImportError: raise DidNotEnable("Django not installed") @@ -474,7 +480,8 @@ def wsgi_request_event_processor(event, hint): if request is None: return event - if type(request) == ASGIRequest: + django_3 = ASGIRequest is not None + if django_3 and type(request) == ASGIRequest: return event try: diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index e4446d462f..cb92667e5e 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -84,29 +84,31 @@ async def sentry_patched_asgi_handler(self, scope, receive, send): cls.__call__ = sentry_patched_asgi_handler - old_create_request = cls.create_request + modern_django_asgi_support = hasattr(cls, "create_request") + if modern_django_asgi_support: + old_create_request = cls.create_request - def sentry_patched_create_request(self, *args, **kwargs): - hub = Hub.current - integration = hub.get_integration(DjangoIntegration) - if integration is None: - return old_create_request(self, *args, **kwargs) + def sentry_patched_create_request(self, *args, **kwargs): + hub = Hub.current + integration = hub.get_integration(DjangoIntegration) + if integration is None: + return old_create_request(self, *args, **kwargs) - with hub.configure_scope() as scope: - request, error_response = old_create_request(self, *args, **kwargs) + with hub.configure_scope() as scope: + request, error_response = old_create_request(self, *args, **kwargs) - # read the body once, to signal Django to cache the body stream - # so we can read the body in our event processor - # (otherwise Django closes the body stream and makes it impossible to read it again) - _ = request.body + # read the body once, to signal Django to cache the body stream + # so we can read the body in our event processor + # (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, integration) + ) - return request, error_response + return request, error_response - cls.create_request = sentry_patched_create_request + cls.create_request = sentry_patched_create_request def patch_get_response_async(cls, _before_get_response): From 2b815904f2f6680c16b5499afe3fa021f35d18ee Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 8 Nov 2023 16:39:45 +0100 Subject: [PATCH 08/21] Make mypy happy --- sentry_sdk/integrations/django/asgi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index cb92667e5e..f6e609c718 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -89,6 +89,7 @@ async def sentry_patched_asgi_handler(self, scope, receive, send): old_create_request = cls.create_request def sentry_patched_create_request(self, *args, **kwargs): + # type: (Any, *Any, **Any) -> Any hub = Hub.current integration = hub.get_integration(DjangoIntegration) if integration is None: From 4add097d86358bde35219cde46c2b80a03b49289 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 8 Nov 2023 16:54:53 +0100 Subject: [PATCH 09/21] Make old Djangos happy --- sentry_sdk/integrations/django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 25c85be828..9e0d1664f8 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -51,7 +51,7 @@ # Only available in Django 3.0+ try: from django.core.handlers.asgi import ASGIRequest - except ModuleNotFoundError: + except Exception: ASGIRequest = None except ImportError: From faf73a96eb1a8509b3c951b6e6c91505fa973c6b Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 8 Nov 2023 17:07:06 +0100 Subject: [PATCH 10/21] missing deps? --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 51cfaf53e1..8da78550ef 100644 --- a/tox.ini +++ b/tox.ini @@ -288,6 +288,8 @@ deps = # Django django: psycopg2-binary django: Werkzeug<2.1.0 + django: channels + django: daphne 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}: pytest-asyncio From 96a4d46330413e9afc06fc9c7be5ddbef956ecf3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 8 Nov 2023 17:16:50 +0100 Subject: [PATCH 11/21] missing deps --- tox.ini | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index c0ec2527a6..59e8fe19f5 100644 --- a/tox.ini +++ b/tox.ini @@ -288,12 +288,10 @@ deps = # Django django: psycopg2-binary django: Werkzeug<2.1.0 - django: channels - django: daphne 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}: 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}: channels[daphne]>2 + {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,5.0}: 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,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 From 5da9af2caff378f49d30e8d323e8094125a332ed Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 8 Nov 2023 19:14:23 +0100 Subject: [PATCH 12/21] Added tests --- tests/integrations/django/asgi/test_asgi.py | 46 +++++++++++++++++++++ tests/integrations/django/myapp/urls.py | 5 +++ tests/integrations/django/myapp/views.py | 8 ++++ 3 files changed, 59 insertions(+) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 85921cf364..e1f6bddefd 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -7,6 +7,11 @@ from sentry_sdk.integrations.django import DjangoIntegration from tests.integrations.django.myapp.asgi import channels_application +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse + try: from unittest import mock # python 3.3 and above except ImportError: @@ -353,3 +358,44 @@ async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_e assert msg_event["contexts"]["trace"]["trace_id"] == trace_id assert error_event["contexts"]["trace"]["trace_id"] == trace_id + + +@pytest.mark.parametrize("application", APPS) +@pytest.mark.parametrize( + "body,expected_return_data", + [ + ( + b'{"username":"xyz","password":"xyz"}', + {"username": "xyz", "password": "xyz"}, + ), + (b"hello", ""), + (b"", None), + ], +) +@pytest.mark.asyncio +async def test_asgi_request_body( + sentry_init, capture_envelopes, application, body, expected_return_data +): + sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) + + envelopes = capture_envelopes() + + comm = HttpCommunicator( + application, + method="POST", + path=reverse("post_echo_async"), + body=body, + headers=[(b"content-type", b"application/json")], + ) + response = await comm.get_response() + + assert response["status"] == 200 + assert response["body"] == body + + (envelope,) = envelopes + event = envelope.get_event() + + if expected_return_data is not None: + assert event["request"]["data"] == expected_return_data + else: + assert "data" not in event["request"] diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py index 2a4535e588..be5a40239e 100644 --- a/tests/integrations/django/myapp/urls.py +++ b/tests/integrations/django/myapp/urls.py @@ -82,6 +82,11 @@ def path(path, *args, **kwargs): path("async/thread_ids", views.thread_ids_async, name="thread_ids_async") ) +if views.post_echo_async is not None: + urlpatterns.append( + path("post_echo_async", views.post_echo_async, name="post_echo_async") + ) + # rest framework try: urlpatterns.append( diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index 1e909f2b38..6362adc121 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -235,7 +235,15 @@ def thread_ids_sync(*args, **kwargs): }) return HttpResponse(response)""" ) + + exec( + """@csrf_exempt +def post_echo_async(request): + sentry_sdk.capture_message("hi") + return HttpResponse(request.body)""" + ) else: async_message = None my_async_view = None thread_ids_async = None + post_echo_async = None From ed6afc5702afd99f178c357e74ce2915fc42db42 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 8 Nov 2023 19:21:46 +0100 Subject: [PATCH 13/21] Added comment to make it easier to comprehend --- sentry_sdk/integrations/django/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 9e0d1664f8..95f18d00ab 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -482,6 +482,7 @@ def wsgi_request_event_processor(event, hint): django_3 = ASGIRequest is not None if django_3 and type(request) == ASGIRequest: + # We have a `asgi_request_event_processor` for this. return event try: From 3f0aa00c63ea5248e013b018e9f36ce2390a52cd Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 8 Nov 2023 19:38:43 +0100 Subject: [PATCH 14/21] Omit test in old Django versions --- tests/integrations/django/asgi/test_asgi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index e1f6bddefd..57145b698d 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -373,6 +373,9 @@ async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_e ], ) @pytest.mark.asyncio +@pytest.mark.skipif( + django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" +) async def test_asgi_request_body( sentry_init, capture_envelopes, application, body, expected_return_data ): From 619c105eb29190a8fd2c0707d297281746f1b741 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Fri, 10 Nov 2023 13:20:22 +0100 Subject: [PATCH 15/21] add missing 3.12 tests --- tox.ini | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 59e8fe19f5..072b561b07 100644 --- a/tox.ini +++ b/tox.ini @@ -290,13 +290,12 @@ 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,5.0}: 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,5.0}: 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,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 From 8edee83d7b9a980e9dc34e6cc2b755148e9471a9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 13 Nov 2023 12:24:24 +0100 Subject: [PATCH 16/21] new test makes other tests fail, run separately --- tests/integrations/django/asgi/test_asgi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 57145b698d..48637e429b 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -373,6 +373,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" ) From 7f2871e5fee0a4da3896ed8967645aa91fa63a58 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 13 Nov 2023 12:28:16 +0100 Subject: [PATCH 17/21] stop async tests from influencing each other --- tests/integrations/django/asgi/test_asgi.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 48637e429b..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" ) From 2f69be8dba17cb7fd628e6f999da3624efc67ddb Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 13 Nov 2023 14:57:16 +0100 Subject: [PATCH 18/21] do not wrap async receivers --- sentry_sdk/integrations/django/__init__.py | 8 +------ sentry_sdk/integrations/django/asgi.py | 9 +++---- .../integrations/django/signals_handlers.py | 24 ++++++++++++------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 3d889513f3..95f18d00ab 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -249,13 +249,7 @@ def _django_queryset_repr(value, hint): patch_django_middlewares() patch_views() patch_templates() - - if DJANGO_VERSION >= (5, 0): - from sentry_sdk.integrations.django.asgi import patch_signals_async - - patch_signals_async() - else: - patch_signals() + patch_signals() if patch_caching is not None: patch_caching() diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index 422b6c03d9..060d81b7e2 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -28,12 +28,11 @@ 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] # if the request is gone we are fine not logging the data from @@ -103,9 +102,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 bcd7f632e7..c8084d3679 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -7,10 +7,11 @@ 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, Callable, List + from typing import Any, Callable, List, Tuple, Union def _get_receiver_name(receiver): @@ -43,18 +44,22 @@ def patch_signals(): """ Patch django signal receivers to create a span. - This is used for Django<5.0. Django>=5.0 introduced async receivers; see - patch_signals_async in asgi.py. + 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) + 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] @@ -73,9 +78,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_sync_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 From 8e3172ba99bc0ee83b1b22f1e6f20bb0fe3ddad6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 13 Nov 2023 15:07:39 +0100 Subject: [PATCH 19/21] remove unused code --- sentry_sdk/integrations/django/asgi.py | 60 +------------------------- 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index 060d81b7e2..f4845c219b 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -9,21 +9,18 @@ import asyncio from django.core.handlers.wsgi import WSGIRequest -from django.dispatch import Signal from sentry_sdk import Hub, _functools -from sentry_sdk._functools import wraps from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.consts import OP from sentry_sdk.hub import _should_send_default_pii from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.integrations.django.signals_handlers import _get_receiver_name from sentry_sdk.utils import capture_internal_exceptions if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Tuple, Union + from typing import Any, Callable, Dict, Union from django.core.handlers.asgi import ASGIRequest from django.http.response import HttpResponse @@ -220,58 +217,3 @@ async def __acall__(self, *args, **kwargs): return await f(*args, **kwargs) return SentryASGIMixin - - -def patch_signals_async(): - # type: () -> None - """Patch django signal receivers to create a span.""" - from sentry_sdk.integrations.django import DjangoIntegration - - old_live_receivers = Signal._live_receivers - - def _sentry_live_receivers(self, sender): - # type: (Signal, Any) -> Tuple[List[Callable[..., Any]], List[Callable[..., Any]]] - hub = Hub.current - - sync_receivers, async_receivers = old_live_receivers(self, sender) - - def sentry_sync_receiver_wrapper(receiver): - # type: (Callable[..., Any]) -> Callable[..., Any] - @wraps(receiver) - def wrapper(*args, **kwargs): - # type: (Any, Any) -> Any - signal_name = _get_receiver_name(receiver) - with hub.start_span( - op=OP.EVENT_DJANGO, - description=signal_name, - ) as span: - span.set_data("signal", signal_name) - return receiver(*args, **kwargs) - - return wrapper - - def sentry_async_receiver_wrapper(receiver): - # type: (Callable[..., Any]) -> Callable[..., Any] - @wraps(receiver) - async def wrapper(*args, **kwargs): - # type: (Any, Any) -> Any - signal_name = _get_receiver_name(receiver) - with hub.start_span( - op=OP.EVENT_DJANGO, - description=signal_name, - ) as span: - span.set_data("signal", signal_name) - return await receiver(*args, **kwargs) - - return wrapper - - integration = hub.get_integration(DjangoIntegration) - if integration and integration.signals_spans: - for idx, receiver in enumerate(sync_receivers): - sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) - for idx, receiver in enumerate(async_receivers): - async_receivers[idx] = sentry_async_receiver_wrapper(receiver) - - return sync_receivers, async_receivers - - Signal._live_receivers = _sentry_live_receivers From 67615f6c38e01662b86d817123ebdaf8c4abdabc Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 14 Nov 2023 13:25:36 +0100 Subject: [PATCH 20/21] remove deprecated types --- sentry_sdk/integrations/django/asgi.py | 5 +++-- sentry_sdk/integrations/django/signals_handlers.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index f4845c219b..bd785a23c2 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -20,7 +20,8 @@ if TYPE_CHECKING: - from typing import Any, Callable, Dict, Union + from collections.abc import Callable + from typing import Any, Union from django.core.handlers.asgi import ASGIRequest from django.http.response import HttpResponse @@ -31,7 +32,7 @@ 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. diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py index c8084d3679..4d61b9f7a6 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: - from typing import Any, Callable, List, Tuple, Union + from typing import Any, Callable, Union def _get_receiver_name(receiver): @@ -52,7 +52,7 @@ def patch_signals(): old_live_receivers = Signal._live_receivers def _sentry_live_receivers(self, sender): - # type: (Signal, Any) -> Union[Tuple[List[Callable[..., Any]], List[Callable[..., Any]]], List[Callable[..., Any]]] + # type: (Signal, Any) -> Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]] hub = Hub.current if DJANGO_VERSION >= (5, 0): From ada4a8ed95cc9f1fc811ec271b6fe81e247443ee Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 14 Nov 2023 13:26:30 +0100 Subject: [PATCH 21/21] new Callable --- sentry_sdk/integrations/django/signals_handlers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py index 4d61b9f7a6..097a56c8aa 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -11,7 +11,8 @@ if TYPE_CHECKING: - from typing import Any, Callable, Union + from collections.abc import Callable + from typing import Any, Union def _get_receiver_name(receiver):