Skip to content
Merged
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
28 changes: 28 additions & 0 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@ def sql_to_string(sql):

from sentry_sdk import Hub
from sentry_sdk.hub import _should_send_default_pii
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
safe_repr,
format_and_strip,
transaction_from_function,
walk_exception_chain,
)
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
from sentry_sdk.integrations.django.templates import get_template_frame_from_exception


if DJANGO_VERSION < (1, 10):
Expand Down Expand Up @@ -112,6 +115,31 @@ def sentry_patched_get_response(self, request):

signals.got_request_exception.connect(_got_request_exception)

@add_global_event_processor
def process_django_templates(event, hint):
if hint.get("exc_info", None) is None:
return event

if "exception" not in event:
return event

exception = event["exception"]

if "values" not in exception:
return event

for exception, (_, exc_value, _) in zip(
exception["values"], walk_exception_chain(hint["exc_info"])
):
frame = get_template_frame_from_exception(exc_value)
if frame is not None:
frames = exception.setdefault("stacktrace", {}).setdefault(
"frames", []
)
frames.append(frame)

return event


def _make_event_processor(weak_request, integration):
def event_processor(event, hint):
Expand Down
102 changes: 102 additions & 0 deletions sentry_sdk/integrations/django/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from django.template import TemplateSyntaxError

try:
# support Django 1.9
from django.template.base import Origin
except ImportError:
# backward compatibility
from django.template.loader import LoaderOrigin as Origin


def get_template_frame_from_exception(exc_value):
# As of Django 1.9 or so the new template debug thing showed up.
if hasattr(exc_value, "template_debug"):
return _get_template_frame_from_debug(exc_value.template_debug)

# As of r16833 (Django) all exceptions may contain a
# ``django_template_source`` attribute (rather than the legacy
# ``TemplateSyntaxError.source`` check)
if hasattr(exc_value, "django_template_source"):
return _get_template_frame_from_source(exc_value.django_template_source)

if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"):
source = exc_value.source
if isinstance(source, (tuple, list)) and isinstance(source[0], Origin):
return _get_template_frame_from_source(source)


def _get_template_frame_from_debug(debug):
if debug is None:
return None

lineno = debug["line"]
filename = debug["name"]
if filename is None:
filename = "<django template>"

pre_context = []
post_context = []
context_line = None

for i, line in debug["source_lines"]:
if i < lineno:
pre_context.append(line)
elif i > lineno:
post_context.append(line)
else:
context_line = line

return {
"filename": filename,
"lineno": lineno,
"pre_context": pre_context[-5:],
"post_context": post_context[:5],
"context_line": context_line,
}


def _linebreak_iter(template_source):
yield 0
p = template_source.find("\n")
while p >= 0:
yield p + 1
p = template_source.find("\n", p + 1)


def _get_template_frame_from_source(source):
if not source:
return None

origin, (start, end) = source
filename = getattr(origin, "loadname", None)
if filename is None:
filename = "<django template>"
template_source = origin.reload()
lineno = None
upto = 0
pre_context = []
post_context = []
context_line = None

for num, next in enumerate(_linebreak_iter(template_source)):
line = template_source[upto:next]
if start >= upto and end <= next:
lineno = num
context_line = line
elif lineno is None:
pre_context.append(line)
else:
post_context.append(line)

upto = next

if context_line is None or lineno is None:
return None

return {
"filename": filename,
"lineno": lineno,
"pre_context": pre_context[-5:],
"post_context": post_context[:5],
"context_line": context_line,
}
22 changes: 15 additions & 7 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,21 +427,29 @@ def single_exception_from_error_tuple(
}


def exceptions_from_error_tuple(exc_info, client_options=None, mechanism=None):
def walk_exception_chain(exc_info):
exc_type, exc_value, tb = exc_info
rv = []

while exc_type is not None:
rv.append(
single_exception_from_error_tuple(
exc_type, exc_value, tb, client_options, mechanism
)
)
yield exc_type, exc_value, tb

cause = getattr(exc_value, "__cause__", None)
if cause is None:
break
exc_type = type(cause)
exc_value = cause
tb = getattr(cause, "__traceback__", None)


def exceptions_from_error_tuple(exc_info, client_options=None, mechanism=None):
exc_type, exc_value, tb = exc_info
rv = []
for exc_type, exc_value, tb in walk_exception_chain(exc_info):
rv.append(
single_exception_from_error_tuple(
exc_type, exc_value, tb, client_options, mechanism
)
)
return rv


Expand Down
5 changes: 4 additions & 1 deletion tests/integrations/django/myapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,13 @@ def process_response(self, request, response):
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"debug": True,
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
],
},
}
]
Expand Down Expand Up @@ -120,6 +121,8 @@ def process_response(self, request, response):

USE_TZ = True

TEMPLATE_DEBUG = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
Expand Down
20 changes: 20 additions & 0 deletions tests/integrations/django/myapp/templates/error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
1
2
3
4
5
6
7
8
9
{% invalid template tag %}
11
12
13
14
15
16
17
18
19
20
1 change: 1 addition & 0 deletions tests/integrations/django/myapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
path("mylogin", views.mylogin, name="mylogin"),
path("classbased", views.ClassBasedView.as_view(), name="classbased"),
path("post-echo", views.post_echo, name="post_echo"),
path("template-exc", views.template_exc, name="template_exc"),
]

handler500 = views.handler500
Expand Down
5 changes: 5 additions & 0 deletions tests/integrations/django/myapp/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.auth import login
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
from django.shortcuts import render_to_response
from django.views.generic import ListView

import sentry_sdk
Expand Down Expand Up @@ -42,3 +43,7 @@ def post_echo(request):
def handler404(*args, **kwargs):
sentry_sdk.capture_message("not found", level="error")
return HttpResponseNotFound("404")


def template_exc(*args, **kwargs):
return render_to_response("error.html")
26 changes: 26 additions & 0 deletions tests/integrations/django/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,29 @@ def test_request_body(sentry_init, client, capture_events):
assert event["message"] == "hi"
assert event["request"]["data"] == {"hey": 42}
assert "" not in event


def test_template_exception(sentry_init, client, capture_events):
sentry_init(integrations=[DjangoIntegration()])
events = capture_events()

content, status, headers = client.get(reverse("template_exc"))
assert status.lower() == "500 internal server error"

event, = events
exception, = event["exception"]["values"]

frames = [
f
for f in exception["stacktrace"]["frames"]
if not f["filename"].startswith("django/")
]
view_frame, template_frame = frames[-2:]

assert template_frame["context_line"] == "{% invalid template tag %}\n"
assert template_frame["pre_context"] == ["5\n", "6\n", "7\n", "8\n", "9\n"]

assert template_frame["post_context"] == ["11\n", "12\n", "13\n", "14\n", "15\n"]
assert template_frame["lineno"] == 10
assert template_frame["in_app"]
assert template_frame["filename"].endswith("error.html")