Repository: getsentry/sentry-python

Issue:
Django 3.1 async views do not work
When using sentry versions greater than 0.16.3, (tested on 0.18.0), Django 3.1 aysnc views do not work.

```
log.py                     224 ERROR    Internal Server Error: /async_ok
Traceback (most recent call last):
  File "/Users/williamchu/dev/sentry-python/.tox/py3.8-django-3.1/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/williamchu/dev/sentry-python/.tox/py3.8-django-3.1/lib/python3.8/site-packages/django/core/handlers/base.py", line 186, in _get_response
    self.check_response(response, callback)
  File "/Users/williamchu/dev/sentry-python/.tox/py3.8-django-3.1/lib/python3.8/site-packages/django/core/handlers/base.py", line 312, in check_response
    raise ValueError(
ValueError: The view tests.integrations.django.myapp.views.async_ok didn't return an HttpResponse object. It returned an unawaited coroutine instead. You may need to add an 'await' into your view.
```

I have made a branch with a test case to demonstrate this: https://github.com/uptickmetachu/sentry-python/tree/django3.1-test-async-view




Discussion:

Resolving patch:
diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py
--- a/sentry_sdk/integrations/django/asgi.py
+++ b/sentry_sdk/integrations/django/asgi.py
@@ -6,10 +6,9 @@
 `django.core.handlers.asgi`.
 """
 
-from sentry_sdk import Hub
+from sentry_sdk import Hub, _functools
 from sentry_sdk._types import MYPY
 
-from sentry_sdk.integrations.django import DjangoIntegration
 from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
 
 if MYPY:
@@ -21,6 +20,9 @@
 
 def patch_django_asgi_handler_impl(cls):
     # type: (Any) -> None
+
+    from sentry_sdk.integrations.django import DjangoIntegration
+
     old_app = cls.__call__
 
     async def sentry_patched_asgi_handler(self, scope, receive, send):
@@ -50,6 +52,9 @@ async def sentry_patched_get_response_async(self, request):
 
 def patch_channels_asgi_handler_impl(cls):
     # type: (Any) -> None
+
+    from sentry_sdk.integrations.django import DjangoIntegration
+
     old_app = cls.__call__
 
     async def sentry_patched_asgi_handler(self, receive, send):
@@ -64,3 +69,17 @@ async def sentry_patched_asgi_handler(self, receive, send):
         return await middleware(self.scope)(receive, send)
 
     cls.__call__ = sentry_patched_asgi_handler
+
+
+def wrap_async_view(hub, callback):
+    # type: (Hub, Any) -> Any
+    @_functools.wraps(callback)
+    async def sentry_wrapped_callback(request, *args, **kwargs):
+        # type: (Any, *Any, **Any) -> Any
+
+        with hub.start_span(
+            op="django.view", description=request.resolver_match.view_name
+        ):
+            return await callback(request, *args, **kwargs)
+
+    return sentry_wrapped_callback
diff --git a/sentry_sdk/integrations/django/views.py b/sentry_sdk/integrations/django/views.py
--- a/sentry_sdk/integrations/django/views.py
+++ b/sentry_sdk/integrations/django/views.py
@@ -6,6 +6,18 @@
     from typing import Any
 
 
+try:
+    from asyncio import iscoroutinefunction
+except ImportError:
+    iscoroutinefunction = None  # type: ignore
+
+
+try:
+    from sentry_sdk.integrations.django.asgi import wrap_async_view
+except (ImportError, SyntaxError):
+    wrap_async_view = None  # type: ignore
+
+
 def patch_views():
     # type: () -> None
 
@@ -27,13 +39,14 @@ def sentry_patched_make_view_atomic(self, *args, **kwargs):
 
         if integration is not None and integration.middleware_spans:
 
-            @_functools.wraps(callback)
-            def sentry_wrapped_callback(request, *args, **kwargs):
-                # type: (Any, *Any, **Any) -> Any
-                with hub.start_span(
-                    op="django.view", description=request.resolver_match.view_name
-                ):
-                    return callback(request, *args, **kwargs)
+            if (
+                iscoroutinefunction is not None
+                and wrap_async_view is not None
+                and iscoroutinefunction(callback)
+            ):
+                sentry_wrapped_callback = wrap_async_view(hub, callback)
+            else:
+                sentry_wrapped_callback = _wrap_sync_view(hub, callback)
 
         else:
             sentry_wrapped_callback = callback
@@ -41,3 +54,16 @@ def sentry_wrapped_callback(request, *args, **kwargs):
         return sentry_wrapped_callback
 
     BaseHandler.make_view_atomic = sentry_patched_make_view_atomic
+
+
+def _wrap_sync_view(hub, callback):
+    # type: (Hub, Any) -> Any
+    @_functools.wraps(callback)
+    def sentry_wrapped_callback(request, *args, **kwargs):
+        # type: (Any, *Any, **Any) -> Any
+        with hub.start_span(
+            op="django.view", description=request.resolver_match.view_name
+        ):
+            return callback(request, *args, **kwargs)
+
+    return sentry_wrapped_callback


Test patch:
diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py
--- a/tests/integrations/django/asgi/test_asgi.py
+++ b/tests/integrations/django/asgi/test_asgi.py
@@ -1,12 +1,8 @@
-import pytest
-
 import django
-
+import pytest
 from channels.testing import HttpCommunicator
-
 from sentry_sdk import capture_message
 from sentry_sdk.integrations.django import DjangoIntegration
-
 from tests.integrations.django.myapp.asgi import channels_application
 
 APPS = [channels_application]
@@ -18,7 +14,7 @@
 
 @pytest.mark.parametrize("application", APPS)
 @pytest.mark.asyncio
-async def test_basic(sentry_init, capture_events, application, request):
+async def test_basic(sentry_init, capture_events, application):
     sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
 
     events = capture_events()
@@ -46,3 +42,29 @@ async def test_basic(sentry_init, capture_events, application, request):
     capture_message("hi")
     event = events[-1]
     assert "request" not in event
+
+
+@pytest.mark.parametrize("application", APPS)
+@pytest.mark.asyncio
+@pytest.mark.skipif(
+    django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
+)
+async def test_async_views(sentry_init, capture_events, application):
+    sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
+
+    events = capture_events()
+
+    comm = HttpCommunicator(application, "GET", "/async_message")
+    response = await comm.get_response()
+    assert response["status"] == 200
+
+    (event,) = events
+
+    assert event["transaction"] == "/async_message"
+    assert event["request"] == {
+        "cookies": {},
+        "headers": {},
+        "method": "GET",
+        "query_string": None,
+        "url": "/async_message",
+    }
diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py
--- a/tests/integrations/django/myapp/urls.py
+++ b/tests/integrations/django/myapp/urls.py
@@ -57,7 +57,11 @@ def path(path, *args, **kwargs):
     ),
 ]
 
+# async views
+if views.async_message is not None:
+    urlpatterns.append(path("async_message", views.async_message, name="async_message"))
 
+# rest framework
 try:
     urlpatterns.append(
         path("rest-framework-exc", views.rest_framework_exc, name="rest_framework_exc")
diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py
--- a/tests/integrations/django/myapp/views.py
+++ b/tests/integrations/django/myapp/views.py
@@ -1,11 +1,12 @@
+from django import VERSION
 from django.contrib.auth import login
 from django.contrib.auth.models import User
 from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
+from django.http import HttpResponse, HttpResponseNotFound, HttpResponseServerError
 from django.shortcuts import render
-from django.views.generic import ListView
-from django.views.decorators.csrf import csrf_exempt
 from django.utils.decorators import method_decorator
+from django.views.decorators.csrf import csrf_exempt
+from django.views.generic import ListView
 
 try:
     from rest_framework.decorators import api_view
@@ -120,3 +121,14 @@ def permission_denied_exc(*args, **kwargs):
 
 def csrf_hello_not_exempt(*args, **kwargs):
     return HttpResponse("ok")
+
+
+if VERSION >= (3, 1):
+    # Use exec to produce valid Python 2
+    exec(
+        """async def async_message(request):
+    sentry_sdk.capture_message("hi")
+    return HttpResponse("ok")"""
+    )
+else:
+    async_message = None