Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,7 @@ class OP:
SUBPROCESS_WAIT = "subprocess.wait"
SUBPROCESS_COMMUNICATE = "subprocess.communicate"
TEMPLATE_RENDER = "template.render"
VIEW_AUTHENTICATE = "view.authenticate"
VIEW_RENDER = "view.render"
VIEW_RESPONSE_RENDER = "view.response.render"
WEBSOCKET_SERVER = "websocket.server"
Expand Down
41 changes: 41 additions & 0 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ def _patch_drf() -> None:
DRF request object, such that we can later use either in
`DjangoRequestExtractor`.

We also patch DRF's authentication to create a span, so that the work done
by the configured authentication classes (which often involves database
queries) doesn't show up as part of the view itself.

This function is not called directly on SDK setup, because importing almost
any part of Django Rest Framework will try to access Django settings (where
`sentry_sdk.init()` might be called from in the first place). Instead we
Expand Down Expand Up @@ -339,6 +343,43 @@ def sentry_patched_drf_initial(

APIView.initial = sentry_patched_drf_initial

with capture_internal_exceptions():
try:
from rest_framework.request import Request # type: ignore
except ImportError:
pass
else:
old_drf_authenticate = Request._authenticate

def sentry_patched_drf_authenticate(self: "Request") -> "Any":
client = sentry_sdk.get_client()
integration = client.get_integration(DjangoIntegration)
# Nothing to time if there are no authenticators configured
# for this view.
if integration is None or not getattr(self, "authenticators", None):
return old_drf_authenticate(self)

if has_span_streaming_enabled(client.options):
if sentry_sdk.traces.get_current_span() is None:
return old_drf_authenticate(self)
with sentry_sdk.traces.start_span(
name="authenticate",
attributes={
"sentry.op": OP.VIEW_AUTHENTICATE,
"sentry.origin": DjangoIntegration.origin,
},
):
return old_drf_authenticate(self)
else:
with sentry_sdk.start_span(
op=OP.VIEW_AUTHENTICATE,
name="authenticate",
origin=DjangoIntegration.origin,
):
return old_drf_authenticate(self)

Request._authenticate = sentry_patched_drf_authenticate


def _patch_channels() -> None:
try:
Expand Down
14 changes: 14 additions & 0 deletions tests/integrations/django/myapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,20 @@ def path(path, *args, **kwargs):
)
)
urlpatterns.append(path("rest-hello", views.rest_hello, name="rest_hello"))
urlpatterns.append(
path(
"rest-authenticated-hello",
views.rest_authenticated_hello,
name="rest_authenticated_hello",
)
)
urlpatterns.append(
path(
"rest-unauthenticated-hello",
views.rest_unauthenticated_hello,
name="rest_unauthenticated_hello",
)
)
urlpatterns.append(
path("rest-json-response", views.rest_json_response, name="rest_json_response")
)
Expand Down
17 changes: 16 additions & 1 deletion tests/integrations/django/myapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,24 @@
)

try:
from rest_framework.decorators import api_view
from rest_framework.authentication import BaseAuthentication
from rest_framework.decorators import api_view, authentication_classes
from rest_framework.response import Response

class DummyAuthentication(BaseAuthentication):
def authenticate(self, request):
return None

@api_view(["GET"])
@authentication_classes([DummyAuthentication])
def rest_authenticated_hello(request):
return HttpResponse("ok")

@api_view(["GET"])
@authentication_classes([])
def rest_unauthenticated_hello(request):
return HttpResponse("ok")

@api_view(["POST"])
def rest_framework_exc(request):
1 / 0
Expand Down
84 changes: 84 additions & 0 deletions tests/integrations/django/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1642,6 +1642,90 @@ def test_rest_framework_basic(
assert event["request"]["headers"]["Content-Type"] == ct


@pytest.mark.parametrize("span_streaming", [True, False])
def test_rest_framework_authentication_span(
sentry_init,
client,
capture_events,
capture_items,
render_span_tree,
span_streaming,
):
pytest.importorskip("rest_framework")
sentry_init(
integrations=[
DjangoIntegration(middleware_spans=False, signals_spans=False),
],
traces_sample_rate=1.0,
trace_lifecycle="stream" if span_streaming else "static",
)
if span_streaming:
items = capture_items("span")

client.get(reverse("rest_authenticated_hello"))

sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]

assert (
render_span_tree(spans)
== """\
- sentry.op="http.server": name="/rest-authenticated-hello"
- sentry.op="view.authenticate": name="authenticate"\
"""
)
else:
events = capture_events()

client.get(reverse("rest_authenticated_hello"))

(transaction,) = events

assert (
render_span_tree(transaction["spans"], transaction["contexts"]["trace"])
== """\
- op="http.server": description=null
- op="view.authenticate": description="authenticate"\
"""
)


@pytest.mark.parametrize("span_streaming", [True, False])
def test_rest_framework_authentication_span_without_authenticators(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
pytest.importorskip("rest_framework")
sentry_init(
integrations=[
DjangoIntegration(middleware_spans=False, signals_spans=False),
],
traces_sample_rate=1.0,
trace_lifecycle="stream" if span_streaming else "static",
)
if span_streaming:
items = capture_items("span")

client.get(reverse("rest_unauthenticated_hello"))

sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]

# only the root span
assert len(spans) == 1
else:
events = capture_events()

client.get(reverse("rest_unauthenticated_hello"))

(transaction,) = events

assert transaction["spans"] == []


@pytest.mark.parametrize(
"endpoint", ["rest_permission_denied_exc", "permission_denied_exc"]
)
Expand Down