Skip to content

Commit f0bbd04

Browse files
untitakerwilliam chusentry-bot
authored
fix: Fix crash with Django 3.1 async views (#851)
Co-authored-by: william chu <[email protected]> Co-authored-by: sentry-bot <[email protected]>
1 parent b2badef commit f0bbd04

File tree

5 files changed

+101
-18
lines changed

5 files changed

+101
-18
lines changed

sentry_sdk/integrations/django/asgi.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
`django.core.handlers.asgi`.
77
"""
88

9-
from sentry_sdk import Hub
9+
from sentry_sdk import Hub, _functools
1010
from sentry_sdk._types import MYPY
1111

12-
from sentry_sdk.integrations.django import DjangoIntegration
1312
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
1413

1514
if MYPY:
@@ -21,6 +20,9 @@
2120

2221
def patch_django_asgi_handler_impl(cls):
2322
# type: (Any) -> None
23+
24+
from sentry_sdk.integrations.django import DjangoIntegration
25+
2426
old_app = cls.__call__
2527

2628
async def sentry_patched_asgi_handler(self, scope, receive, send):
@@ -50,6 +52,9 @@ async def sentry_patched_get_response_async(self, request):
5052

5153
def patch_channels_asgi_handler_impl(cls):
5254
# type: (Any) -> None
55+
56+
from sentry_sdk.integrations.django import DjangoIntegration
57+
5358
old_app = cls.__call__
5459

5560
async def sentry_patched_asgi_handler(self, receive, send):
@@ -64,3 +69,17 @@ async def sentry_patched_asgi_handler(self, receive, send):
6469
return await middleware(self.scope)(receive, send)
6570

6671
cls.__call__ = sentry_patched_asgi_handler
72+
73+
74+
def wrap_async_view(hub, callback):
75+
# type: (Hub, Any) -> Any
76+
@_functools.wraps(callback)
77+
async def sentry_wrapped_callback(request, *args, **kwargs):
78+
# type: (Any, *Any, **Any) -> Any
79+
80+
with hub.start_span(
81+
op="django.view", description=request.resolver_match.view_name
82+
):
83+
return await callback(request, *args, **kwargs)
84+
85+
return sentry_wrapped_callback

sentry_sdk/integrations/django/views.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
from typing import Any
77

88

9+
try:
10+
from asyncio import iscoroutinefunction
11+
except ImportError:
12+
iscoroutinefunction = None # type: ignore
13+
14+
15+
try:
16+
from sentry_sdk.integrations.django.asgi import wrap_async_view
17+
except (ImportError, SyntaxError):
18+
wrap_async_view = None # type: ignore
19+
20+
921
def patch_views():
1022
# type: () -> None
1123

@@ -27,17 +39,31 @@ def sentry_patched_make_view_atomic(self, *args, **kwargs):
2739

2840
if integration is not None and integration.middleware_spans:
2941

30-
@_functools.wraps(callback)
31-
def sentry_wrapped_callback(request, *args, **kwargs):
32-
# type: (Any, *Any, **Any) -> Any
33-
with hub.start_span(
34-
op="django.view", description=request.resolver_match.view_name
35-
):
36-
return callback(request, *args, **kwargs)
42+
if (
43+
iscoroutinefunction is not None
44+
and wrap_async_view is not None
45+
and iscoroutinefunction(callback)
46+
):
47+
sentry_wrapped_callback = wrap_async_view(hub, callback)
48+
else:
49+
sentry_wrapped_callback = _wrap_sync_view(hub, callback)
3750

3851
else:
3952
sentry_wrapped_callback = callback
4053

4154
return sentry_wrapped_callback
4255

4356
BaseHandler.make_view_atomic = sentry_patched_make_view_atomic
57+
58+
59+
def _wrap_sync_view(hub, callback):
60+
# type: (Hub, Any) -> Any
61+
@_functools.wraps(callback)
62+
def sentry_wrapped_callback(request, *args, **kwargs):
63+
# type: (Any, *Any, **Any) -> Any
64+
with hub.start_span(
65+
op="django.view", description=request.resolver_match.view_name
66+
):
67+
return callback(request, *args, **kwargs)
68+
69+
return sentry_wrapped_callback

tests/integrations/django/asgi/test_asgi.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import pytest
2-
31
import django
4-
2+
import pytest
53
from channels.testing import HttpCommunicator
6-
74
from sentry_sdk import capture_message
85
from sentry_sdk.integrations.django import DjangoIntegration
9-
106
from tests.integrations.django.myapp.asgi import channels_application
117

128
APPS = [channels_application]
@@ -18,7 +14,7 @@
1814

1915
@pytest.mark.parametrize("application", APPS)
2016
@pytest.mark.asyncio
21-
async def test_basic(sentry_init, capture_events, application, request):
17+
async def test_basic(sentry_init, capture_events, application):
2218
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
2319

2420
events = capture_events()
@@ -46,3 +42,29 @@ async def test_basic(sentry_init, capture_events, application, request):
4642
capture_message("hi")
4743
event = events[-1]
4844
assert "request" not in event
45+
46+
47+
@pytest.mark.parametrize("application", APPS)
48+
@pytest.mark.asyncio
49+
@pytest.mark.skipif(
50+
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
51+
)
52+
async def test_async_views(sentry_init, capture_events, application):
53+
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
54+
55+
events = capture_events()
56+
57+
comm = HttpCommunicator(application, "GET", "/async_message")
58+
response = await comm.get_response()
59+
assert response["status"] == 200
60+
61+
(event,) = events
62+
63+
assert event["transaction"] == "/async_message"
64+
assert event["request"] == {
65+
"cookies": {},
66+
"headers": {},
67+
"method": "GET",
68+
"query_string": None,
69+
"url": "/async_message",
70+
}

tests/integrations/django/myapp/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ def path(path, *args, **kwargs):
5757
),
5858
]
5959

60+
# async views
61+
if views.async_message is not None:
62+
urlpatterns.append(path("async_message", views.async_message, name="async_message"))
6063

64+
# rest framework
6165
try:
6266
urlpatterns.append(
6367
path("rest-framework-exc", views.rest_framework_exc, name="rest_framework_exc")

tests/integrations/django/myapp/views.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
from django import VERSION
12
from django.contrib.auth import login
23
from django.contrib.auth.models import User
34
from django.core.exceptions import PermissionDenied
4-
from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
5+
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseServerError
56
from django.shortcuts import render
6-
from django.views.generic import ListView
7-
from django.views.decorators.csrf import csrf_exempt
87
from django.utils.decorators import method_decorator
8+
from django.views.decorators.csrf import csrf_exempt
9+
from django.views.generic import ListView
910

1011
try:
1112
from rest_framework.decorators import api_view
@@ -120,3 +121,14 @@ def permission_denied_exc(*args, **kwargs):
120121

121122
def csrf_hello_not_exempt(*args, **kwargs):
122123
return HttpResponse("ok")
124+
125+
126+
if VERSION >= (3, 1):
127+
# Use exec to produce valid Python 2
128+
exec(
129+
"""async def async_message(request):
130+
sentry_sdk.capture_message("hi")
131+
return HttpResponse("ok")"""
132+
)
133+
else:
134+
async_message = None

0 commit comments

Comments
 (0)