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
36 changes: 36 additions & 0 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,13 +382,49 @@ def _patch_django_asgi_handler() -> None:
patch_django_asgi_handler_impl(ASGIHandler)


def _unwrap_django_ninja_view(fn: "Callable[..., Any]", request: "WSGIRequest") -> "Callable[..., Any]":
"""
Unwrap django-ninja PathView to get the actual endpoint function.

Django-ninja wraps endpoint functions in PathView.get_view() which returns
a sync_view_wrapper or async_view_wrapper. These wrappers have a closure
that contains the PathView instance, which has the list of operations.
We need to find the operation that matches the request method to get the
actual endpoint function.
"""
try:
# Check if this is a django-ninja view wrapper by looking for PathView in closure
if (
hasattr(fn, "__closure__")
and fn.__closure__
and hasattr(fn, "__qualname__")
and "PathView.get_view" in fn.__qualname__
):
# Extract PathView from the closure
for cell in fn.__closure__:
path_view = cell.cell_contents
# Check if this is actually a PathView instance
if (
type(path_view).__name__ == "PathView"
and hasattr(path_view, "operations")
):
# Find the operation matching the request method
for operation in path_view.operations:
if request.method in operation.methods:
return operation.view_func
except Exception:
pass
return fn


def _set_transaction_name_and_source(
scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest"
) -> None:
try:
transaction_name = None
if transaction_style == "function_name":
fn = resolve(request.path).func
fn = _unwrap_django_ninja_view(fn, request)
transaction_name = transaction_from_function(getattr(fn, "view_class", fn))

elif transaction_style == "url":
Expand Down
4 changes: 4 additions & 0 deletions tests/integrations/django/myapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,9 @@ def path(path, *args, **kwargs):
except AttributeError:
pass

# django-ninja
if views.ninja_api is not None:
urlpatterns.append(path("ninja/", views.ninja_api.urls))

handler500 = views.handler500
handler404 = views.handler404
26 changes: 26 additions & 0 deletions tests/integrations/django/myapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,3 +370,29 @@ def send_myapp_custom_signal(request):
myapp_custom_signal.send(sender="hello")
myapp_custom_signal_silenced.send(sender="hello")
return HttpResponse("ok")


# django-ninja integration
try:
from ninja import NinjaAPI

ninja_api = NinjaAPI()

@ninja_api.get("/ninja-hello")
def ninja_hello(request):
"""Ninja hello endpoint"""
return {"message": "Hello from Ninja"}

@ninja_api.post("/ninja-hello")
def ninja_hello_post(request):
"""Ninja hello POST endpoint"""
return {"message": "POST Hello from Ninja"}

@ninja_api.get("/ninja-message")
def ninja_message(request):
"""Ninja message endpoint that captures a message"""
capture_message("ninja message")
return {"message": "Ninja message sent"}

except ImportError:
ninja_api = None
78 changes: 78 additions & 0 deletions tests/integrations/django/test_ninja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Tests for django-ninja integration with transaction_style="function_name"."""

import pytest

django = pytest.importorskip("django")
ninja = pytest.importorskip("ninja")

from sentry_sdk.integrations.django import DjangoIntegration


@pytest.mark.parametrize(
"transaction_style,url,expected_transaction,expected_source",
[
(
"function_name",
"/ninja/ninja-message",
"tests.integrations.django.myapp.views.ninja_message",
"component",
),
(
"url",
"/ninja/ninja-message",
"/ninja/ninja-message",
"route",
),
],
)
def test_ninja_transaction_style(
sentry_init,
client,
capture_events,
transaction_style,
url,
expected_transaction,
expected_source,
):
"""Test that django-ninja endpoints work correctly with different transaction styles."""
sentry_init(
integrations=[DjangoIntegration(transaction_style=transaction_style)],
send_default_pii=True,
)
events = capture_events()

# Make the request
response = client.get(url)
assert response.status_code == 200

# Check the captured event
(event,) = events
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}


def test_ninja_multiple_methods_same_path(
sentry_init,
client,
capture_events,
):
"""Test that ninja endpoints with multiple HTTP methods on same path work correctly."""
sentry_init(
integrations=[DjangoIntegration(transaction_style="function_name")],
send_default_pii=True,
)
events = capture_events()

# Test GET
response = client.get("/ninja/ninja-hello")
assert response.status_code == 200

# Test POST - note that ninja-hello has both GET and POST handlers
response = client.post("/ninja/ninja-hello")
assert response.status_code == 200

# We should have 0 events because ninja-hello doesn't capture messages
# This test just validates that the endpoints don't error and can be properly
# resolved by the integration without crashing
assert len(events) == 0