Skip to content

Commit 0adc26c

Browse files
authored
fix(django): Support for Django 3.1 (#707)
Django 3.1a1 adds more parameters to load_middleware which we do not really care about. Django 3.1a1 starts executing exception handlers in a random thread/with the wrong context. Turns out they have their own implementation of context local that is necessary to be able to find the right hub. See also getsentry/sentry-docs#1721 More support is required for supporting async middlewares once Django 3.1 comes out but this should unbreak basic usage of the sdk. Fix #704
1 parent 5b5bf34 commit 0adc26c

File tree

7 files changed

+98
-57
lines changed

7 files changed

+98
-57
lines changed

mypy.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,5 @@ ignore_missing_imports = True
4646
ignore_missing_imports = True
4747
[mypy-pyspark.*]
4848
ignore_missing_imports = True
49+
[mypy-asgiref.*]
50+
ignore_missing_imports = True

sentry_sdk/integrations/django/__init__.py

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -120,39 +120,9 @@ def sentry_patched_wsgi_handler(self, environ, start_response):
120120

121121
WSGIHandler.__call__ = sentry_patched_wsgi_handler
122122

123-
_patch_django_asgi_handler()
124-
125-
# patch get_response, because at that point we have the Django request
126-
# object
127-
from django.core.handlers.base import BaseHandler
128-
129-
old_get_response = BaseHandler.get_response
130-
131-
def sentry_patched_get_response(self, request):
132-
# type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException]
133-
hub = Hub.current
134-
integration = hub.get_integration(DjangoIntegration)
135-
if integration is not None:
136-
_patch_drf()
137-
138-
with hub.configure_scope() as scope:
139-
# Rely on WSGI middleware to start a trace
140-
try:
141-
if integration.transaction_style == "function_name":
142-
scope.transaction = transaction_from_function(
143-
resolve(request.path).func
144-
)
145-
elif integration.transaction_style == "url":
146-
scope.transaction = LEGACY_RESOLVER.resolve(request.path)
147-
except Exception:
148-
pass
149-
150-
scope.add_event_processor(
151-
_make_event_processor(weakref.ref(request), integration)
152-
)
153-
return old_get_response(self, request)
123+
_patch_get_response()
154124

155-
BaseHandler.get_response = sentry_patched_get_response
125+
_patch_django_asgi_handler()
156126

157127
signals.got_request_exception.connect(_got_request_exception)
158128

@@ -337,6 +307,54 @@ def _patch_django_asgi_handler():
337307
patch_django_asgi_handler_impl(ASGIHandler)
338308

339309

310+
def _before_get_response(request):
311+
# type: (WSGIRequest) -> None
312+
hub = Hub.current
313+
integration = hub.get_integration(DjangoIntegration)
314+
if integration is None:
315+
return
316+
317+
_patch_drf()
318+
319+
with hub.configure_scope() as scope:
320+
# Rely on WSGI middleware to start a trace
321+
try:
322+
if integration.transaction_style == "function_name":
323+
scope.transaction = transaction_from_function(
324+
resolve(request.path).func
325+
)
326+
elif integration.transaction_style == "url":
327+
scope.transaction = LEGACY_RESOLVER.resolve(request.path)
328+
except Exception:
329+
pass
330+
331+
scope.add_event_processor(
332+
_make_event_processor(weakref.ref(request), integration)
333+
)
334+
335+
336+
def _patch_get_response():
337+
# type: () -> None
338+
"""
339+
patch get_response, because at that point we have the Django request object
340+
"""
341+
from django.core.handlers.base import BaseHandler
342+
343+
old_get_response = BaseHandler.get_response
344+
345+
def sentry_patched_get_response(self, request):
346+
# type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException]
347+
_before_get_response(request)
348+
return old_get_response(self, request)
349+
350+
BaseHandler.get_response = sentry_patched_get_response
351+
352+
if hasattr(BaseHandler, "get_response_async"):
353+
from sentry_sdk.integrations.django.asgi import patch_get_response_async
354+
355+
patch_get_response_async(BaseHandler, _before_get_response)
356+
357+
340358
def _make_event_processor(weak_request, integration):
341359
# type: (Callable[[], WSGIRequest], DjangoIntegration) -> EventProcessor
342360
def event_processor(event, hint):

sentry_sdk/integrations/django/asgi.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
if MYPY:
1616
from typing import Any
17+
from typing import Union
18+
19+
from django.http.response import HttpResponse
1720

1821

1922
def patch_django_asgi_handler_impl(cls):
@@ -33,6 +36,18 @@ async def sentry_patched_asgi_handler(self, scope, receive, send):
3336
cls.__call__ = sentry_patched_asgi_handler
3437

3538

39+
def patch_get_response_async(cls, _before_get_response):
40+
# type: (Any, Any) -> None
41+
old_get_response_async = cls.get_response_async
42+
43+
async def sentry_patched_get_response_async(self, request):
44+
# type: (Any, Any) -> Union[HttpResponse, BaseException]
45+
_before_get_response(request)
46+
return await old_get_response_async(self, request)
47+
48+
cls.get_response_async = sentry_patched_get_response_async
49+
50+
3651
def patch_channels_asgi_handler_impl(cls):
3752
# type: (Any) -> None
3853
old_app = cls.__call__

sentry_sdk/integrations/django/middleware.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ def sentry_patched_import_string(dotted_path):
4949

5050
old_load_middleware = base.BaseHandler.load_middleware
5151

52-
def sentry_patched_load_middleware(self):
53-
# type: (base.BaseHandler) -> Any
52+
def sentry_patched_load_middleware(*args, **kwargs):
53+
# type: (Any, Any) -> Any
5454
_import_string_should_wrap_middleware.set(True)
5555
try:
56-
return old_load_middleware(self)
56+
return old_load_middleware(*args, **kwargs)
5757
finally:
5858
_import_string_should_wrap_middleware.set(False)
5959

sentry_sdk/utils.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,27 @@ def _is_contextvars_broken():
751751
return False
752752

753753

754+
def _make_threadlocal_contextvars(local):
755+
# type: (type) -> type
756+
class ContextVar(object):
757+
# Super-limited impl of ContextVar
758+
759+
def __init__(self, name):
760+
# type: (str) -> None
761+
self._name = name
762+
self._local = local()
763+
764+
def get(self, default):
765+
# type: (Any) -> Any
766+
return getattr(self._local, "value", default)
767+
768+
def set(self, value):
769+
# type: (Any) -> None
770+
self._local.value = value
771+
772+
return ContextVar
773+
774+
754775
def _get_contextvars():
755776
# type: () -> Tuple[bool, type]
756777
"""
@@ -786,23 +807,7 @@ def _get_contextvars():
786807

787808
from threading import local
788809

789-
class ContextVar(object):
790-
# Super-limited impl of ContextVar
791-
792-
def __init__(self, name):
793-
# type: (str) -> None
794-
self._name = name
795-
self._local = local()
796-
797-
def get(self, default):
798-
# type: (Any) -> Any
799-
return getattr(self._local, "value", default)
800-
801-
def set(self, value):
802-
# type: (Any) -> None
803-
self._local.value = value
804-
805-
return False, ContextVar
810+
return False, _make_threadlocal_contextvars(local)
806811

807812

808813
HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()

tests/integrations/django/asgi/test_asgi.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818

1919
@pytest.mark.parametrize("application", APPS)
2020
@pytest.mark.asyncio
21-
async def test_basic(sentry_init, capture_events, application):
21+
async def test_basic(sentry_init, capture_events, application, request):
2222
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
23+
2324
events = capture_events()
2425

2526
comm = HttpCommunicator(application, "GET", "/view-exc?test=query")

tox.ini

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ envlist =
7373
deps =
7474
-r test-requirements.txt
7575

76-
django-{1.11,2.0,2.1,2.2,3.0}: djangorestframework>=3.0.0,<4.0.0
77-
py3.7-django-{1.11,2.0,2.1,2.2,3.0}: channels>2
78-
py3.7-django-{1.11,2.0,2.1,2.2,3.0}: pytest-asyncio==0.10.0
79-
{py2.7,py3.7}-django-{1.11,2.2,3.0}: psycopg2-binary
76+
django-{1.11,2.0,2.1,2.2,3.0,dev}: djangorestframework>=3.0.0,<4.0.0
77+
{py3.7,py3.8}-django-{1.11,2.0,2.1,2.2,3.0,dev}: channels>2
78+
{py3.7,py3.8}-django-{1.11,2.0,2.1,2.2,3.0,dev}: pytest-asyncio==0.10.0
79+
{py2.7,py3.7,py3.8}-django-{1.11,2.2,3.0,dev}: psycopg2-binary
8080

8181
django-{1.6,1.7,1.8}: pytest-django<3.0
8282
django-{1.9,1.10,1.11,2.0,2.1,2.2,3.0,dev}: pytest-django>=3.0

0 commit comments

Comments
 (0)