diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 008dc386bb..3ef21a55ca 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -37,7 +37,10 @@ from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER -from sentry_sdk.integrations.django.templates import get_template_frame_from_exception +from sentry_sdk.integrations.django.templates import ( + get_template_frame_from_exception, + patch_templates, +) from sentry_sdk.integrations.django.middleware import patch_django_middlewares from sentry_sdk.integrations.django.views import patch_views @@ -201,6 +204,7 @@ def _django_queryset_repr(value, hint): _patch_channels() patch_django_middlewares() patch_views() + patch_templates() _DRF_PATCHED = False diff --git a/sentry_sdk/integrations/django/templates.py b/sentry_sdk/integrations/django/templates.py index 2285644909..51d3a2b3b0 100644 --- a/sentry_sdk/integrations/django/templates.py +++ b/sentry_sdk/integrations/django/templates.py @@ -1,5 +1,7 @@ from django.template import TemplateSyntaxError +from django import VERSION as DJANGO_VERSION +from sentry_sdk import _functools, Hub from sentry_sdk._types import MYPY if MYPY: @@ -40,6 +42,60 @@ def get_template_frame_from_exception(exc_value): return None +def _get_template_name_description(template_name): + if isinstance(template_name, (list, tuple)): + if template_name: + return "[%s, ...]".format(template_name[0]) + else: + return template_name + + +def patch_templates(): + # type: () -> None + from django.template.response import SimpleTemplateResponse + from sentry_sdk.integrations.django import DjangoIntegration + + real_rendered_content = SimpleTemplateResponse.rendered_content + + @property # type: ignore + def rendered_content(self): + # type: (SimpleTemplateResponse) -> str + hub = Hub.current + if hub.get_integration(DjangoIntegration) is None: + return real_rendered_content.fget(self) + + with hub.start_span( + op="django.template.render", + description=_get_template_name_description(self.template_name), + ) as span: + span.set_data("context", self.context_data) + return real_rendered_content.fget(self) + + SimpleTemplateResponse.rendered_content = rendered_content + + if DJANGO_VERSION < (1, 7): + return + import django.shortcuts + + real_render = django.shortcuts.render + + @_functools.wraps(real_render) + def render(request, template_name, context=None, *args, **kwargs): + # type: (django.http.HttpRequest, str, Optional[Dict[str, Any]], *Any, **Any) -> django.http.HttpResponse + hub = Hub.current + if hub.get_integration(DjangoIntegration) is None: + return real_render(request, template_name, context, *args, **kwargs) + + with hub.start_span( + op="django.template.render", + description=_get_template_name_description(template_name), + ) as span: + span.set_data("context", context) + return real_render(request, template_name, context, *args, **kwargs) + + django.shortcuts.render = render + + def _get_template_frame_from_debug(debug): # type: (Dict[str, Any]) -> Dict[str, Any] if debug is None: diff --git a/tests/integrations/django/myapp/templates/user_name.html b/tests/integrations/django/myapp/templates/user_name.html new file mode 100644 index 0000000000..970107349f --- /dev/null +++ b/tests/integrations/django/myapp/templates/user_name.html @@ -0,0 +1 @@ +{{ request.user }}: {{ user_age }} diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py index 5131d8674f..9427499dcf 100644 --- a/tests/integrations/django/myapp/urls.py +++ b/tests/integrations/django/myapp/urls.py @@ -45,6 +45,8 @@ def path(path, *args, **kwargs): ), path("post-echo", views.post_echo, name="post_echo"), path("template-exc", views.template_exc, name="template_exc"), + path("template-test", views.template_test, name="template_test"), + path("template-test2", views.template_test2, name="template_test2"), path( "permission-denied-exc", views.permission_denied_exc, diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index 1c78837ee4..29a4859b5d 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -4,6 +4,7 @@ from django.core.exceptions import PermissionDenied from django.http import HttpResponse, HttpResponseNotFound, HttpResponseServerError from django.shortcuts import render +from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.views.generic import ListView @@ -114,6 +115,16 @@ def template_exc(request, *args, **kwargs): return render(request, "error.html") +@csrf_exempt +def template_test(request, *args, **kwargs): + return render(request, "user_name.html", {"user_age": 20}) + + +@csrf_exempt +def template_test2(request, *args, **kwargs): + return TemplateResponse(request, ("user_name.html", "another_template.html"), {"user_age": 25}) + + @csrf_exempt def permission_denied_exc(*args, **kwargs): raise PermissionDenied("bye") diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index c42ab3d9e4..7cbbeaae87 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -518,6 +518,26 @@ def test_does_not_capture_403(sentry_init, client, capture_events, endpoint): assert not events +def test_render_spans(sentry_init, client, capture_events, render_span_tree): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + ) + views_tests = [ + (reverse("template_test2"), '- op="django.template.render": description="[user_name.html, ...]"'), + ] + if DJANGO_VERSION >= (1, 7): + views_tests.append( + (reverse("template_test"), '- op="django.template.render": description="user_name.html"'), + ) + + for url, expected_line in views_tests: + events = capture_events() + _content, status, _headers = client.get(url) + transaction = events[0] + assert expected_line in render_span_tree(transaction) + + def test_middleware_spans(sentry_init, client, capture_events, render_span_tree): sentry_init( integrations=[DjangoIntegration()],