Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make reading the request body work in Django ASGI apps. #2495

Merged
merged 15 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,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 Exception:
ASGIRequest = None

Check warning on line 55 in sentry_sdk/integrations/django/__init__.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/__init__.py#L52-L55

Added lines #L52 - L55 were not covered by tests

except ImportError:
raise DidNotEnable("Django not installed")

Expand Down Expand Up @@ -410,7 +417,7 @@
_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)
)


Expand Down Expand Up @@ -462,9 +469,9 @@
patch_get_response_async(BaseHandler, _before_get_response)


def _make_event_processor(weak_request, integration):
def _make_wsgi_request_event_processor(weak_request, integration):

Check warning on line 472 in sentry_sdk/integrations/django/__init__.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/__init__.py#L472

Added line #L472 was not covered by tests
# type: (Callable[[], WSGIRequest], DjangoIntegration) -> EventProcessor
def event_processor(event, hint):
def wsgi_request_event_processor(event, hint):

Check warning on line 474 in sentry_sdk/integrations/django/__init__.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/__init__.py#L474

Added line #L474 was not covered by tests
# 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
Expand All @@ -473,6 +480,11 @@
if request is None:
return event

django_3 = ASGIRequest is not None

Check warning on line 483 in sentry_sdk/integrations/django/__init__.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/__init__.py#L483

Added line #L483 was not covered by tests
if django_3 and type(request) == ASGIRequest:
# We have a `asgi_request_event_processor` for this.
return event

Check warning on line 486 in sentry_sdk/integrations/django/__init__.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/__init__.py#L486

Added line #L486 was not covered by tests

try:
drf_request = request._sentry_drf_request_backref()
if drf_request is not None:
Expand All @@ -489,7 +501,7 @@

return event

return event_processor
return wsgi_request_event_processor

Check warning on line 504 in sentry_sdk/integrations/django/__init__.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/__init__.py#L504

Added line #L504 was not covered by tests


def _got_request_exception(request=None, **kwargs):
Expand Down
72 changes: 71 additions & 1 deletion sentry_sdk/integrations/django/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 14 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L14

Added line #L14 was not covered by tests

from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from sentry_sdk.utils import capture_internal_exceptions

Check warning on line 17 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L17

Added line #L17 was not covered by tests

from django.core.handlers.wsgi import WSGIRequest

Check warning on line 19 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L19

Added line #L19 was not covered by tests


if TYPE_CHECKING:
from typing import Any
from typing import Dict

Check warning on line 24 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L24

Added line #L24 was not covered by tests
from typing import Union
from typing import Callable

from django.core.handlers.asgi import ASGIRequest

Check warning on line 28 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L28

Added line #L28 was not covered by tests
from django.http.response import HttpResponse

from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk._types import EventProcessor

Check warning on line 32 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L31-L32

Added lines #L31 - L32 were not covered by tests


def _make_asgi_request_event_processor(request, integration):

Check warning on line 35 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L35

Added line #L35 was not covered by tests
# type: (ASGIRequest, DjangoIntegration) -> EventProcessor
def asgi_request_event_processor(event, hint):

Check warning on line 37 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L37

Added line #L37 was not covered by tests
# 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 (

Check warning on line 42 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L42

Added line #L42 was not covered by tests
DjangoRequestExtractor,
_set_user_info,
)

if request is None:
return event

Check warning on line 48 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L48

Added line #L48 was not covered by tests

if type(request) == WSGIRequest:
return event

Check warning on line 51 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L51

Added line #L51 was not covered by tests

with capture_internal_exceptions():
DjangoRequestExtractor(request).extract_into_event(event)

Check warning on line 54 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L54

Added line #L54 was not covered by tests

if _should_send_default_pii():
with capture_internal_exceptions():
_set_user_info(request, event)

Check warning on line 58 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L58

Added line #L58 was not covered by tests

return event

Check warning on line 60 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L60

Added line #L60 was not covered by tests

return asgi_request_event_processor

Check warning on line 62 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L62

Added line #L62 was not covered by tests


def patch_django_asgi_handler_impl(cls):
# type: (Any) -> None
Expand All @@ -31,16 +71,46 @@

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)

Check warning on line 75 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L74-L75

Added lines #L74 - L75 were not covered by tests
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

modern_django_asgi_support = hasattr(cls, "create_request")

Check warning on line 87 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L87

Added line #L87 was not covered by tests
if modern_django_asgi_support:
old_create_request = cls.create_request

Check warning on line 89 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L89

Added line #L89 was not covered by tests

def sentry_patched_create_request(self, *args, **kwargs):

Check warning on line 91 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L91

Added line #L91 was not covered by tests
# type: (Any, *Any, **Any) -> Any
hub = Hub.current
integration = hub.get_integration(DjangoIntegration)

Check warning on line 94 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L93-L94

Added lines #L93 - L94 were not covered by tests
if integration is None:
return old_create_request(self, *args, **kwargs)

Check warning on line 96 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L96

Added line #L96 was not covered by tests

with hub.configure_scope() as scope:
request, error_response = old_create_request(self, *args, **kwargs)

Check warning on line 99 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L99

Added line #L99 was not covered by tests

# 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

Check warning on line 104 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L104

Added line #L104 was not covered by tests

scope.add_event_processor(

Check warning on line 106 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L106

Added line #L106 was not covered by tests
_make_asgi_request_event_processor(request, integration)
)

return request, error_response

Check warning on line 110 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L110

Added line #L110 was not covered by tests

cls.create_request = sentry_patched_create_request

Check warning on line 112 in sentry_sdk/integrations/django/asgi.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/django/asgi.py#L112

Added line #L112 was not covered by tests


def patch_get_response_async(cls, _before_get_response):
# type: (Any, Any) -> None
Expand Down
49 changes: 49 additions & 0 deletions tests/integrations/django/asgi/test_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -353,3 +358,47 @@ 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
@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
):
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"]
5 changes: 5 additions & 0 deletions tests/integrations/django/myapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions tests/integrations/django/myapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ 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}: 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}: 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

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
Expand Down
Loading