Repository: getsentry/sentry-python

Issue:
Django view functions replaced by wrapper
Django 2.2.16
Sentry 0.16.4+

When using sentry 0.16.4+, the resolver has been patched to return phony view functions that wrap calling the actual view function. This breaks anything that uses the resolver matched view function to compare to an imported function, eg we have middleware acting as a guardian with code like this:

```python
@custom_login_required_decorator
def the_view(request):
    request.session["skip"] = 1
    return redirect(reverse("homepage"))


def can_skip(request):
    return "skip" in request.session


class Guardian:
    def process_view(self, request, view_func, view_args, view_kwargs):
        if can_skip(request) or view_func == the_view:
            return
        return redirect(reverse("the_view"))
```

With sentry 0.16.3 (and every version of sentry-sdk before that, and with raven...), this worked fine. With sentry 0.16.4+, the view_func has been replaced. I'm pretty sure that 193f591 is responsible (and this is by design...). If this is the only way of achieving the functionality you need from the views, then at the very least this deficiency needs to be documented prominently - [here perhaps](https://docs.sentry.io/platforms/python/guides/django/#behavior)

With sentry 0.17.8 enabled:
```
(Pdb) p view_func                                                      
<function set_client_code at 0x7fc81f1c26a8>                           
(Pdb) p set_client_code                                                
<portal.utils.login_utils._ExtendedCheckLogin object at 0x7fc8210b54a8>
```

With sentry 0.17.8, but disabled:
```
(Pdb) p view_func                                                      
<portal.utils.login_utils._ExtendedCheckLogin object at 0x7fc63afbb128>
(Pdb) p set_client_code                                                
<portal.utils.login_utils._ExtendedCheckLogin object at 0x7fc63afbb128>
```

With sentry 0.16.3 enabled:
```
(Pdb) p view_func                                                      
<portal.utils.login_utils._ExtendedCheckLogin object at 0x7f3324e504a8>
(Pdb) p set_client_code                                                
<portal.utils.login_utils._ExtendedCheckLogin object at 0x7f3324e504a8>
```


Discussion:
I see. I think we will need to figure out another way to wrap the view function then. Thanks for bringing this up!

Resolving patch:
diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py
--- a/sentry_sdk/integrations/django/__init__.py
+++ b/sentry_sdk/integrations/django/__init__.py
@@ -39,7 +39,7 @@
 from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
 from sentry_sdk.integrations.django.templates import get_template_frame_from_exception
 from sentry_sdk.integrations.django.middleware import patch_django_middlewares
-from sentry_sdk.integrations.django.views import patch_resolver
+from sentry_sdk.integrations.django.views import patch_views
 
 
 if MYPY:
@@ -200,7 +200,7 @@ def _django_queryset_repr(value, hint):
 
         _patch_channels()
         patch_django_middlewares()
-        patch_resolver()
+        patch_views()
 
 
 _DRF_PATCHED = False
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
@@ -5,63 +5,39 @@
 if MYPY:
     from typing import Any
 
-    from django.urls.resolvers import ResolverMatch
 
-
-def patch_resolver():
+def patch_views():
     # type: () -> None
-    try:
-        from django.urls.resolvers import URLResolver
-    except ImportError:
-        try:
-            from django.urls.resolvers import RegexURLResolver as URLResolver
-        except ImportError:
-            from django.core.urlresolvers import RegexURLResolver as URLResolver
 
+    from django.core.handlers.base import BaseHandler
     from sentry_sdk.integrations.django import DjangoIntegration
 
-    old_resolve = URLResolver.resolve
-
-    def resolve(self, path):
-        # type: (URLResolver, Any) -> ResolverMatch
-        hub = Hub.current
-        integration = hub.get_integration(DjangoIntegration)
-
-        if integration is None or not integration.middleware_spans:
-            return old_resolve(self, path)
+    old_make_view_atomic = BaseHandler.make_view_atomic
 
-        return _wrap_resolver_match(hub, old_resolve(self, path))
+    @_functools.wraps(old_make_view_atomic)
+    def sentry_patched_make_view_atomic(self, *args, **kwargs):
+        # type: (Any, *Any, **Any) -> Any
+        callback = old_make_view_atomic(self, *args, **kwargs)
 
-    URLResolver.resolve = resolve
+        # XXX: The wrapper function is created for every request. Find more
+        # efficient way to wrap views (or build a cache?)
 
+        hub = Hub.current
+        integration = hub.get_integration(DjangoIntegration)
 
-def _wrap_resolver_match(hub, resolver_match):
-    # type: (Hub, ResolverMatch) -> ResolverMatch
-
-    # XXX: The wrapper function is created for every request. Find more
-    # efficient way to wrap views (or build a cache?)
-
-    old_callback = resolver_match.func
+        if integration is not None and integration.middleware_spans:
 
-    # Explicitly forward `csrf_exempt` in case it is not an attribute in
-    # callback.__dict__, but rather a class attribute (on a class
-    # implementing __call__) such as this:
-    #
-    #     class Foo(object):
-    #         csrf_exempt = True
-    #
-    #         def __call__(self, request): ...
-    #
-    # We have had this in the Sentry codebase (for no good reason, but
-    # nevertheless we broke user code)
-    assigned = _functools.WRAPPER_ASSIGNMENTS + ("csrf_exempt",)
+            @_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)
 
-    @_functools.wraps(old_callback, assigned=assigned)
-    def callback(*args, **kwargs):
-        # type: (*Any, **Any) -> Any
-        with hub.start_span(op="django.view", description=resolver_match.view_name):
-            return old_callback(*args, **kwargs)
+        else:
+            sentry_wrapped_callback = callback
 
-    resolver_match.func = callback
+        return sentry_wrapped_callback
 
-    return resolver_match
+    BaseHandler.make_view_atomic = sentry_patched_make_view_atomic


Test patch:
diff --git a/tests/integrations/django/myapp/settings.py b/tests/integrations/django/myapp/settings.py
--- a/tests/integrations/django/myapp/settings.py
+++ b/tests/integrations/django/myapp/settings.py
@@ -59,6 +59,11 @@
 
 class TestMiddleware(MiddlewareMixin):
     def process_request(self, request):
+        # https://github.com/getsentry/sentry-python/issues/837 -- We should
+        # not touch the resolver_match because apparently people rely on it.
+        if request.resolver_match:
+            assert not getattr(request.resolver_match.callback, "__wrapped__", None)
+
         if "middleware-exc" in request.path:
             1 / 0