Skip to content

Commit 20f0a76

Browse files
authored
feat(django): Pick custom urlconf up from request if any (#1308)
Django middlewares sometimes can override `request.urlconf` which we also need to respect in our transaction name resolving. This fixes an issue (WEB-530) with a customer using `django-tenants` where all their transactions were named `Generic WSGI request` due to the default url resolution failing.
1 parent f92e970 commit 20f0a76

File tree

5 files changed

+114
-13
lines changed

5 files changed

+114
-13
lines changed

sentry_sdk/integrations/django/__init__.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,31 @@ def _before_get_response(request):
346346
)
347347

348348

349+
def _after_get_response(request):
350+
# type: (WSGIRequest) -> None
351+
"""
352+
Some django middlewares overwrite request.urlconf
353+
so we need to respect that contract,
354+
so we try to resolve the url again.
355+
"""
356+
if not hasattr(request, "urlconf"):
357+
return
358+
359+
hub = Hub.current
360+
integration = hub.get_integration(DjangoIntegration)
361+
if integration is None or integration.transaction_style != "url":
362+
return
363+
364+
with hub.configure_scope() as scope:
365+
try:
366+
scope.transaction = LEGACY_RESOLVER.resolve(
367+
request.path_info,
368+
urlconf=request.urlconf,
369+
)
370+
except Exception:
371+
pass
372+
373+
349374
def _patch_get_response():
350375
# type: () -> None
351376
"""
@@ -358,7 +383,9 @@ def _patch_get_response():
358383
def sentry_patched_get_response(self, request):
359384
# type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException]
360385
_before_get_response(request)
361-
return old_get_response(self, request)
386+
rv = old_get_response(self, request)
387+
_after_get_response(request)
388+
return rv
362389

363390
BaseHandler.get_response = sentry_patched_get_response
364391

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""myapp URL Configuration
2+
3+
The `urlpatterns` list routes URLs to views. For more information please see:
4+
https://docs.djangoproject.com/en/2.0/topics/http/urls/
5+
Examples:
6+
Function views
7+
1. Add an import: from my_app import views
8+
2. Add a URL to urlpatterns: path('', views.home, name='home')
9+
Class-based views
10+
1. Add an import: from other_app.views import Home
11+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12+
Including another URLconf
13+
1. Import the include() function: from django.urls import include, path
14+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15+
"""
16+
from __future__ import absolute_import
17+
18+
try:
19+
from django.urls import path
20+
except ImportError:
21+
from django.conf.urls import url
22+
23+
def path(path, *args, **kwargs):
24+
return url("^{}$".format(path), *args, **kwargs)
25+
26+
27+
from . import views
28+
29+
urlpatterns = [
30+
path("custom/ok", views.custom_ok, name="custom_ok"),
31+
]
Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
1-
import asyncio
2-
from django.utils.decorators import sync_and_async_middleware
1+
import django
32

3+
if django.VERSION >= (3, 1):
4+
import asyncio
5+
from django.utils.decorators import sync_and_async_middleware
46

5-
@sync_and_async_middleware
6-
def simple_middleware(get_response):
7-
if asyncio.iscoroutinefunction(get_response):
7+
@sync_and_async_middleware
8+
def simple_middleware(get_response):
9+
if asyncio.iscoroutinefunction(get_response):
810

9-
async def middleware(request):
10-
response = await get_response(request)
11-
return response
11+
async def middleware(request):
12+
response = await get_response(request)
13+
return response
1214

13-
else:
15+
else:
1416

15-
def middleware(request):
16-
response = get_response(request)
17-
return response
17+
def middleware(request):
18+
response = get_response(request)
19+
return response
20+
21+
return middleware
22+
23+
24+
def custom_urlconf_middleware(get_response):
25+
def middleware(request):
26+
request.urlconf = "tests.integrations.django.myapp.custom_urls"
27+
response = get_response(request)
28+
return response
1829

1930
return middleware

tests/integrations/django/myapp/views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ def template_test(request, *args, **kwargs):
120120
return render(request, "user_name.html", {"user_age": 20})
121121

122122

123+
@csrf_exempt
124+
def custom_ok(request, *args, **kwargs):
125+
return HttpResponse("custom ok")
126+
127+
123128
@csrf_exempt
124129
def template_test2(request, *args, **kwargs):
125130
return TemplateResponse(

tests/integrations/django/test_basic.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,3 +755,30 @@ def test_csrf(sentry_init, client):
755755
content, status, _headers = client.post(reverse("message"))
756756
assert status.lower() == "200 ok"
757757
assert b"".join(content) == b"ok"
758+
759+
760+
@pytest.mark.skipif(DJANGO_VERSION < (2, 0), reason="Requires Django > 2.0")
761+
def test_custom_urlconf_middleware(
762+
settings, sentry_init, client, capture_events, render_span_tree
763+
):
764+
"""
765+
Some middlewares (for instance in django-tenants) overwrite request.urlconf.
766+
Test that the resolver picks up the correct urlconf for transaction naming.
767+
"""
768+
urlconf = "tests.integrations.django.myapp.middleware.custom_urlconf_middleware"
769+
settings.ROOT_URLCONF = ""
770+
settings.MIDDLEWARE.insert(0, urlconf)
771+
client.application.load_middleware()
772+
773+
sentry_init(integrations=[DjangoIntegration()], traces_sample_rate=1.0)
774+
events = capture_events()
775+
776+
content, status, _headers = client.get("/custom/ok")
777+
assert status.lower() == "200 ok"
778+
assert b"".join(content) == b"custom ok"
779+
780+
(event,) = events
781+
assert event["transaction"] == "/custom/ok"
782+
assert "custom_urlconf_middleware" in render_span_tree(event)
783+
784+
settings.MIDDLEWARE.pop(0)

0 commit comments

Comments
 (0)