Skip to content

Commit ea6b0ee

Browse files
committed
feat(integrations): Add span for DRF authentication
1 parent 10b4f5b commit ea6b0ee

5 files changed

Lines changed: 156 additions & 1 deletion

File tree

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,7 @@ class OP:
12571257
SUBPROCESS_WAIT = "subprocess.wait"
12581258
SUBPROCESS_COMMUNICATE = "subprocess.communicate"
12591259
TEMPLATE_RENDER = "template.render"
1260+
VIEW_AUTHENTICATE = "view.authenticate"
12601261
VIEW_RENDER = "view.render"
12611262
VIEW_RESPONSE_RENDER = "view.response.render"
12621263
WEBSOCKET_SERVER = "websocket.server"

sentry_sdk/integrations/django/__init__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,10 @@ def _patch_drf() -> None:
298298
DRF request object, such that we can later use either in
299299
`DjangoRequestExtractor`.
300300
301+
We also patch DRF's authentication to create a span, so that the work done
302+
by the configured authentication classes (which often involves database
303+
queries) doesn't show up as part of the view itself.
304+
301305
This function is not called directly on SDK setup, because importing almost
302306
any part of Django Rest Framework will try to access Django settings (where
303307
`sentry_sdk.init()` might be called from in the first place). Instead we
@@ -339,6 +343,43 @@ def sentry_patched_drf_initial(
339343

340344
APIView.initial = sentry_patched_drf_initial
341345

346+
with capture_internal_exceptions():
347+
try:
348+
from rest_framework.request import Request # type: ignore
349+
except ImportError:
350+
pass
351+
else:
352+
old_drf_authenticate = Request._authenticate
353+
354+
def sentry_patched_drf_authenticate(self: "Request") -> "Any":
355+
client = sentry_sdk.get_client()
356+
integration = client.get_integration(DjangoIntegration)
357+
# Nothing to time if there are no authenticators configured
358+
# for this view.
359+
if integration is None or not getattr(self, "authenticators", None):
360+
return old_drf_authenticate(self)
361+
362+
if has_span_streaming_enabled(client.options):
363+
if sentry_sdk.traces.get_current_span() is None:
364+
return old_drf_authenticate(self)
365+
with sentry_sdk.traces.start_span(
366+
name="authenticate",
367+
attributes={
368+
"sentry.op": OP.VIEW_AUTHENTICATE,
369+
"sentry.origin": DjangoIntegration.origin,
370+
},
371+
):
372+
return old_drf_authenticate(self)
373+
else:
374+
with sentry_sdk.start_span(
375+
op=OP.VIEW_AUTHENTICATE,
376+
name="authenticate",
377+
origin=DjangoIntegration.origin,
378+
):
379+
return old_drf_authenticate(self)
380+
381+
Request._authenticate = sentry_patched_drf_authenticate
382+
342383

343384
def _patch_channels() -> None:
344385
try:

tests/integrations/django/myapp/urls.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,20 @@ def path(path, *args, **kwargs):
150150
)
151151
)
152152
urlpatterns.append(path("rest-hello", views.rest_hello, name="rest_hello"))
153+
urlpatterns.append(
154+
path(
155+
"rest-authenticated-hello",
156+
views.rest_authenticated_hello,
157+
name="rest_authenticated_hello",
158+
)
159+
)
160+
urlpatterns.append(
161+
path(
162+
"rest-unauthenticated-hello",
163+
views.rest_unauthenticated_hello,
164+
name="rest_unauthenticated_hello",
165+
)
166+
)
153167
urlpatterns.append(
154168
path("rest-json-response", views.rest_json_response, name="rest_json_response")
155169
)

tests/integrations/django/myapp/views.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,24 @@
2222
)
2323

2424
try:
25-
from rest_framework.decorators import api_view
25+
from rest_framework.authentication import BaseAuthentication
26+
from rest_framework.decorators import api_view, authentication_classes
2627
from rest_framework.response import Response
2728

29+
class DummyAuthentication(BaseAuthentication):
30+
def authenticate(self, request):
31+
return None
32+
33+
@api_view(["GET"])
34+
@authentication_classes([DummyAuthentication])
35+
def rest_authenticated_hello(request):
36+
return HttpResponse("ok")
37+
38+
@api_view(["GET"])
39+
@authentication_classes([])
40+
def rest_unauthenticated_hello(request):
41+
return HttpResponse("ok")
42+
2843
@api_view(["POST"])
2944
def rest_framework_exc(request):
3045
1 / 0

tests/integrations/django/test_basic.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,6 +1642,90 @@ def test_rest_framework_basic(
16421642
assert event["request"]["headers"]["Content-Type"] == ct
16431643

16441644

1645+
@pytest.mark.parametrize("span_streaming", [True, False])
1646+
def test_rest_framework_authentication_span(
1647+
sentry_init,
1648+
client,
1649+
capture_events,
1650+
capture_items,
1651+
render_span_tree,
1652+
span_streaming,
1653+
):
1654+
pytest.importorskip("rest_framework")
1655+
sentry_init(
1656+
integrations=[
1657+
DjangoIntegration(middleware_spans=False, signals_spans=False),
1658+
],
1659+
traces_sample_rate=1.0,
1660+
trace_lifecycle="stream" if span_streaming else "static",
1661+
)
1662+
if span_streaming:
1663+
items = capture_items("span")
1664+
1665+
client.get(reverse("rest_authenticated_hello"))
1666+
1667+
sentry_sdk.flush()
1668+
spans = [item.payload for item in items if item.type == "span"]
1669+
1670+
assert (
1671+
render_span_tree(spans)
1672+
== """\
1673+
- sentry.op="http.server": name="/rest-authenticated-hello"
1674+
- sentry.op="view.authenticate": name="authenticate"\
1675+
"""
1676+
)
1677+
else:
1678+
events = capture_events()
1679+
1680+
client.get(reverse("rest_authenticated_hello"))
1681+
1682+
(transaction,) = events
1683+
1684+
assert (
1685+
render_span_tree(transaction["spans"], transaction["contexts"]["trace"])
1686+
== """\
1687+
- op="http.server": description=null
1688+
- op="view.authenticate": description="authenticate"\
1689+
"""
1690+
)
1691+
1692+
1693+
@pytest.mark.parametrize("span_streaming", [True, False])
1694+
def test_rest_framework_authentication_span_without_authenticators(
1695+
sentry_init,
1696+
client,
1697+
capture_events,
1698+
capture_items,
1699+
span_streaming,
1700+
):
1701+
pytest.importorskip("rest_framework")
1702+
sentry_init(
1703+
integrations=[
1704+
DjangoIntegration(middleware_spans=False, signals_spans=False),
1705+
],
1706+
traces_sample_rate=1.0,
1707+
trace_lifecycle="stream" if span_streaming else "static",
1708+
)
1709+
if span_streaming:
1710+
items = capture_items("span")
1711+
1712+
client.get(reverse("rest_unauthenticated_hello"))
1713+
1714+
sentry_sdk.flush()
1715+
spans = [item.payload for item in items if item.type == "span"]
1716+
1717+
# only the root span
1718+
assert len(spans) == 1
1719+
else:
1720+
events = capture_events()
1721+
1722+
client.get(reverse("rest_unauthenticated_hello"))
1723+
1724+
(transaction,) = events
1725+
1726+
assert transaction["spans"] == []
1727+
1728+
16451729
@pytest.mark.parametrize(
16461730
"endpoint", ["rest_permission_denied_exc", "permission_denied_exc"]
16471731
)

0 commit comments

Comments
 (0)