Repository: getsentry/sentry-python

Issue:
0.12.0 breaks Django function-based middleware
Similar to #504, but a different stack trace:

    AttributeError: 'method-wrapper' object has no attribute '__module__'
      File "django/core/handlers/exception.py", line 41, in inner
        response = get_response(request)
      File "functools.py", line 33, in update_wrapper
        setattr(wrapper, attr, getattr(wrapped, attr))

According to sentry (kind-of neat how I get this in this case...), the `get_response` object at that point in time is `<sentry_sdk.integrations.django.middleware.AuditMiddleware object at 0x7f37d64d4450>`.

This problem only occurs in 0.12.0 and newer, and with Django 1.11.x


Discussion:
>`sentry_sdk.integrations.django.middleware.AuditMiddleware`

I am highly confused, such a class does not exist, so I believe the repr is buggy because of our monkeypatch. Could you figure out where AuditMiddleware really comes from? There must be something peculiar about it that makes our monkeypatch break the world.
Ah, I do have an AuditMiddleware installed. 

I am running some tests to see if this is triggered by the middleware span stuff, but I need to wait until tomorrow to get it onto our staging environment. 
It appears that it is not directly caused by the middleware span feature. I'll have to try to get the error to occur in development to be able to troubleshoot it further.
FWIW, the `AuditMiddleware` looks like:

    def AuditMiddleware(get_response):
        def middleware(request):
            from .receivers import set_audit_variables
            set_audit_variables(request)
            return get_response(request)

        return middleware

So, it's different to the rest of my middleware that all inherit from `django.utils.deprecation.MiddlewareMixin`.
So, it seems that using a middleware function is the problem. I'll try to work up a test case.

Resolving patch:
diff --git a/sentry_sdk/integrations/django/middleware.py b/sentry_sdk/integrations/django/middleware.py
--- a/sentry_sdk/integrations/django/middleware.py
+++ b/sentry_sdk/integrations/django/middleware.py
@@ -7,7 +7,11 @@
 from django import VERSION as DJANGO_VERSION
 
 from sentry_sdk import Hub
-from sentry_sdk.utils import ContextVar, transaction_from_function
+from sentry_sdk.utils import (
+    ContextVar,
+    transaction_from_function,
+    capture_internal_exceptions,
+)
 
 from sentry_sdk._types import MYPY
 
@@ -64,29 +68,36 @@ def _wrap_middleware(middleware, middleware_name):
 
     def _get_wrapped_method(old_method):
         # type: (F) -> F
-        @wraps(old_method)
-        def sentry_wrapped_method(*args, **kwargs):
-            # type: (*Any, **Any) -> Any
-            hub = Hub.current
-            integration = hub.get_integration(DjangoIntegration)
-            if integration is None or not integration.middleware_spans:
-                return old_method(*args, **kwargs)
-
-            function_name = transaction_from_function(old_method)
-
-            description = middleware_name
-            function_basename = getattr(old_method, "__name__", None)
-            if function_basename:
-                description = "{}.{}".format(description, function_basename)
-
-            with hub.start_span(
-                op="django.middleware", description=description
-            ) as span:
-                span.set_tag("django.function_name", function_name)
-                span.set_tag("django.middleware_name", middleware_name)
-                return old_method(*args, **kwargs)
-
-        return sentry_wrapped_method  # type: ignore
+        with capture_internal_exceptions():
+
+            def sentry_wrapped_method(*args, **kwargs):
+                # type: (*Any, **Any) -> Any
+                hub = Hub.current
+                integration = hub.get_integration(DjangoIntegration)
+                if integration is None or not integration.middleware_spans:
+                    return old_method(*args, **kwargs)
+
+                function_name = transaction_from_function(old_method)
+
+                description = middleware_name
+                function_basename = getattr(old_method, "__name__", None)
+                if function_basename:
+                    description = "{}.{}".format(description, function_basename)
+
+                with hub.start_span(
+                    op="django.middleware", description=description
+                ) as span:
+                    span.set_tag("django.function_name", function_name)
+                    span.set_tag("django.middleware_name", middleware_name)
+                    return old_method(*args, **kwargs)
+
+            try:
+                # fails for __call__ of function on Python 2 (see py2.7-django-1.11)
+                return wraps(old_method)(sentry_wrapped_method)  # type: ignore
+            except Exception:
+                return sentry_wrapped_method  # type: ignore
+
+        return old_method
 
     class SentryWrappingMiddleware(object):
         def __init__(self, *args, **kwargs):


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
@@ -66,6 +66,13 @@ def process_response(self, request, response):
         return response
 
 
+def TestFunctionMiddleware(get_response):
+    def middleware(request):
+        return get_response(request)
+
+    return middleware
+
+
 MIDDLEWARE_CLASSES = [
     "django.contrib.sessions.middleware.SessionMiddleware",
     "django.contrib.auth.middleware.AuthenticationMiddleware",
@@ -73,7 +80,9 @@ def process_response(self, request, response):
 ]
 
 if MiddlewareMixin is not object:
-    MIDDLEWARE = MIDDLEWARE_CLASSES
+    MIDDLEWARE = MIDDLEWARE_CLASSES + [
+        "tests.integrations.django.myapp.settings.TestFunctionMiddleware"
+    ]
 
 
 ROOT_URLCONF = "tests.integrations.django.myapp.urls"
diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py
--- a/tests/integrations/django/test_basic.py
+++ b/tests/integrations/django/test_basic.py
@@ -519,6 +519,7 @@ def test_middleware_spans(sentry_init, client, capture_events):
 
     if DJANGO_VERSION >= (1, 10):
         reference_value = [
+            "tests.integrations.django.myapp.settings.TestFunctionMiddleware.__call__",
             "tests.integrations.django.myapp.settings.TestMiddleware.__call__",
             "django.contrib.auth.middleware.AuthenticationMiddleware.__call__",
             "django.contrib.sessions.middleware.SessionMiddleware.__call__",