Skip to content

Commit cd4585a

Browse files
authored
fix(django): Delay patching of DRF (getsentry#398)
* fix(django): Delay patching of DRF Fix getsentry#397 * fix: Update doc comments * fix: Workaround for Django 1.6
1 parent d29ff87 commit cd4585a

File tree

3 files changed

+82
-22
lines changed

3 files changed

+82
-22
lines changed

sentry_sdk/integrations/django/__init__.py

+71-20
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from __future__ import absolute_import
33

44
import sys
5+
import threading
56
import weakref
67

78
from django import VERSION as DJANGO_VERSION # type: ignore
8-
from django.db.models.query import QuerySet # type: ignore
99
from django.core import signals # type: ignore
1010

1111
if False:
@@ -116,6 +116,8 @@ def sentry_patched_get_response(self, request):
116116
hub = Hub.current
117117
integration = hub.get_integration(DjangoIntegration)
118118
if integration is not None:
119+
_patch_drf()
120+
119121
with hub.configure_scope() as scope:
120122
scope.add_event_processor(
121123
_make_event_processor(weakref.ref(request), integration)
@@ -124,25 +126,6 @@ def sentry_patched_get_response(self, request):
124126

125127
BaseHandler.get_response = sentry_patched_get_response
126128

127-
try:
128-
from rest_framework.views import APIView # type: ignore
129-
except ImportError:
130-
pass
131-
else:
132-
# DRF's request type (which wraps the Django request and proxies
133-
# all attrs) has some attributes such as `data` which buffer
134-
# request data. We want to use those in the RequestExtractor to
135-
# get body data more reliably.
136-
old_drf_initial = APIView.initial
137-
138-
def sentry_patched_drf_initial(self, request, *args, **kwargs):
139-
with capture_internal_exceptions():
140-
request._request._sentry_drf_request_backref = weakref.ref(request)
141-
pass
142-
return old_drf_initial(self, request, *args, **kwargs)
143-
144-
APIView.initial = sentry_patched_drf_initial
145-
146129
signals.got_request_exception.connect(_got_request_exception)
147130

148131
@add_global_event_processor
@@ -190,6 +173,17 @@ def process_django_templates(event, hint):
190173

191174
@add_global_repr_processor
192175
def _django_queryset_repr(value, hint):
176+
try:
177+
# Django 1.6 can fail to import `QuerySet` when Django settings
178+
# have not yet been initialized.
179+
#
180+
# If we fail to import, return `NotImplemented`. It's at least
181+
# unlikely that we have a query set in `value` when importing
182+
# `QuerySet` fails.
183+
from django.db.models.query import QuerySet # type: ignore
184+
except Exception:
185+
return NotImplemented
186+
193187
if not isinstance(value, QuerySet) or value._result_cache:
194188
return NotImplemented
195189

@@ -205,6 +199,63 @@ def _django_queryset_repr(value, hint):
205199
)
206200

207201

202+
_DRF_PATCHED = False
203+
_DRF_PATCH_LOCK = threading.Lock()
204+
205+
206+
def _patch_drf():
207+
"""
208+
Patch Django Rest Framework for more/better request data. DRF's request
209+
type is a wrapper around Django's request type. The attribute we're
210+
interested in is `request.data`, which is a cached property containing a
211+
parsed request body. Reading a request body from that property is more
212+
reliable than reading from any of Django's own properties, as those don't
213+
hold payloads in memory and therefore can only be accessed once.
214+
215+
We patch the Django request object to include a weak backreference to the
216+
DRF request object, such that we can later use either in
217+
`DjangoRequestExtractor`.
218+
219+
This function is not called directly on SDK setup, because importing almost
220+
any part of Django Rest Framework will try to access Django settings (where
221+
`sentry_sdk.init()` might be called from in the first place). Instead we
222+
run this function on every request and do the patching on the first
223+
request.
224+
"""
225+
226+
global _DRF_PATCHED
227+
228+
if _DRF_PATCHED:
229+
# Double-checked locking
230+
return
231+
232+
with _DRF_PATCH_LOCK:
233+
if _DRF_PATCHED:
234+
return
235+
236+
# We set this regardless of whether the code below succeeds or fails.
237+
# There is no point in trying to patch again on the next request.
238+
_DRF_PATCHED = True
239+
240+
with capture_internal_exceptions():
241+
try:
242+
from rest_framework.views import APIView # type: ignore
243+
except ImportError:
244+
pass
245+
else:
246+
old_drf_initial = APIView.initial
247+
248+
def sentry_patched_drf_initial(self, request, *args, **kwargs):
249+
with capture_internal_exceptions():
250+
request._request._sentry_drf_request_backref = weakref.ref(
251+
request
252+
)
253+
pass
254+
return old_drf_initial(self, request, *args, **kwargs)
255+
256+
APIView.initial = sentry_patched_drf_initial
257+
258+
208259
def _make_event_processor(weak_request, integration):
209260
# type: (Callable[[], WSGIRequest], DjangoIntegration) -> Callable
210261
def event_processor(event, hint):

tests/integrations/django/myapp/settings.py

+9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
https://docs.djangoproject.com/en/2.0/ref/settings/
1111
"""
1212

13+
14+
# We shouldn't access settings while setting up integrations. Initialize SDK
15+
# here to provoke any errors that might occur.
16+
import sentry_sdk
17+
from sentry_sdk.integrations.django import DjangoIntegration
18+
19+
sentry_sdk.init(integrations=[DjangoIntegration()])
20+
21+
1322
import os
1423

1524
try:

tests/integrations/django/test_basic.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def test_sql_queries(sentry_init, capture_events):
179179

180180
event, = events
181181

182-
crumb, = event["breadcrumbs"]
182+
crumb = event["breadcrumbs"][-1]
183183

184184
assert crumb["message"] == """SELECT count(*) FROM people_person WHERE foo = 123"""
185185

@@ -297,7 +297,7 @@ def test_sql_queries_large_params(sentry_init, capture_events):
297297

298298
event, = events
299299

300-
crumb, = event["breadcrumbs"]
300+
crumb = event["breadcrumbs"][-1]
301301
assert crumb["message"] == (
302302
"SELECT count(*) FROM people_person WHERE foo = '%s... and bar IS NULL"
303303
% ("x" * 124,)

0 commit comments

Comments
 (0)