10000 feat: Django template source (#231) · etherscan-io/sentry-python@bacfab5 · GitHub
[go: up one dir, main page]

Skip to content

Commit bacfab5

Browse files
authored
feat: Django template source (getsentry#231)
1 parent fefff6c commit bacfab5

File tree

8 files changed

+201
-8
lines changed

8 files changed

+201
-8
lines changed

sentry_sdk/integrations/django/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,21 @@ def sql_to_string(sql):
2929

3030
from sentry_sdk import Hub
3131
from sentry_sdk.hub import _should_send_default_pii
32+
from sentry_sdk.scope import add_global_event_processor
3233
from sentry_sdk.utils import (
3334
capture_internal_exceptions,
3435
event_from_exception,
3536
safe_repr,
3637
format_and_strip,
3738
transaction_from_function,
39+
walk_exception_chain,
3840
)
3941
from sentry_sdk.integrations import Integration
4042
from sentry_sdk.integrations.logging import ignore_logger
4143
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
4244
from sentry_sdk.integrations._wsgi_common import RequestExtractor
4345
from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
46+
from sentry_sdk.integrations.django.templates import get_template_frame_from_exception
4447

4548

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

113116
signals.got_request_exception.connect(_got_request_exception)
114117

118+
@add_global_event_processor
119+
def process_django_templates(event, hint):
120+
if hint.get("exc_info", None) is None:
121+
return event
122+
123+
if "exception" not in event:
124+
return event
125+
126+
exception = event["exception"]
127+
128+
if "values" not in exception:
129+
return event
130+
131+
for exception, (_, exc_value, _) in zip(
132+
exception["values"], walk_exception_chain(hint["exc_info"])
133+
):
134+
frame = get_template_frame_from_exception(exc_value)
135+
if frame is not None:
136+
frames = exception.setdefault("stacktrace", {}).setdefault(
137+
"frames", []
138+
)
139+
frames.append(frame)
140+
141+
return event
142+
115143

116144
def _make_event_processor(weak_request, integration):
117145
def event_processor(event, hint):
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from django.template import TemplateSyntaxError
2+
3+
try:
4+
# support Django 1.9
5+
from django.template.base import Origin
6+
except ImportError:
7+
# backward compatibility
8+
from django.template.loader import LoaderOrigin as Origin
9+
10+
11+
def get_template_frame_from_exception(exc_value):
12+
# As of Django 1.9 or so the new template debug thing showed up.
13+
if hasattr(exc_value, "template_debug"):
14+
return _get_template_frame_from_debug(exc_value.template_debug)
15+
16+
# As of r16833 (Django) all exceptions may contain a
17+
# ``django_template_source`` attribute (rather than the legacy
18+
# ``TemplateSyntaxError.source`` check)
19+
if hasattr(exc_value, "django_template_source"):
20+
return _get_template_frame_from_source(exc_value.django_template_source)
21+
22+
if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"):
23+
source = exc_value.source
24+
if isinstance(source, (tuple, list)) and isinstance(source[0], Origin):
25+
return _get_template_frame_from_source(source)
26+
27+
28+
def _get_template_frame_from_debug(debug):
29+
if debug is None:
30+
return None
31+
32+
lineno = debug["line"]
33+
filename = debug["name"]
34+
if filename is None:
35+
filename = "<django template>"
36+
37+
pre_context = []
38+
post_context = []
39+
context_line = None
40+
41+
for i, line in debug["source_lines"]:
42+
if i < lineno:
43+
pre_context.append(line)
44+
elif i > lineno:
45+
post_context.append(line)
46+
else:
47+
context_line = line
48+
49+
return {
50+
"filename": filename,
51+
"lineno": lineno,
52+
"pre_context": pre_context[-5:],
53+
"post_context": post_context[:5],
54+
"context_line": context_line,
55+
}
56+
57+
58+
def _linebreak_iter(template_source):
59+
yield 0
60+
p = template_source.find("\n")
61+
while p >= 0:
62+
yield p + 1
63+
p = template_source.find("\n", p + 1)
64+
65+
66+
def _get_template_frame_from_source(source):
67+
if not source:
68+
return None
69+
70+
origin, (start, end) = source
71+
filename = getattr(origin, "loadname", None)
72+
if filename is None:
73+
filename = "<django template>"
74+
template_source = origin.reload()
75+
lineno = None
76+
upto = 0
77+
pre_context = []
78+
post_context = []
79+
context_line = None
80+
81+
for num, next in enumerate(_linebreak_iter(template_source)):
82+
line = template_source[upto:next]
83+
if start >= upto and end <= next:
84+
lineno = num
85+
context_line = line
86+
elif lineno is None:
87+
pre_context.append(line)
88+
else:
89+
post_context.append(line)
90+
91+
upto = next
92+
93+
if context_line is None or lineno is None:
94+
return None
95+
96+
return {
97+
"filename": filename,
98+
"lineno": lineno,
99+
"pre_context": pre_context[-5:],
100+
"post_context": post_context[:5],
101+
"context_line": context_line,
102+
}

sentry_sdk/utils.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -427,21 +427,29 @@ def single_exception_from_error_tuple(
427427
}
428428

429429

430-
def exceptions_from_error_tuple(exc_info, client_options=None, mechanism=None):
430+
def walk_exception_chain(exc_info):
431431
exc_type, exc_value, tb = exc_info
432-
rv = []
432+
433433
while exc_type is not None:
434-
rv.append(
435-
single_exception_from_error_tuple(
436-
exc_type, exc_value, tb, client_options, mechanism
437-
)
438-
)
434+
yield exc_type, exc_value, tb
435+
439436
cause = getattr(exc_value, "__cause__", None)
440437
if cause is None:
441438
break
442439
exc_type = type(cause)
443440
exc_value = cause
444441
tb = getattr(cause, "__traceback__", None)
442+
443+
444+
def exceptions_from_error_tuple(exc_info, client_options=None, mechanism=None):
445+
exc_type, exc_value, tb = exc_info
446+
rv = []
447+
for exc_type, exc_value, tb in walk_exception_chain(exc_info):
448+
rv.append(
449+
single_exception_from_error_tuple(
450+
exc_type, exc_value, tb, client_options, mechanism
451+
)
452+
)
445453
return rv
446454

447455

tests/integrations/django/myapp/settings.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,13 @@ def process_response(self, request, response):
7575
"DIRS": [],
7676
"APP_DIRS": True,
7777
"OPTIONS": {
78+
"debug": True,
7879
"context_processors": [
7980
"django.template.context_processors.debug",
8081
"django.template.context_processors.request",
8182
"django.contrib.auth.context_processors.auth",
8283
"django.contrib.messages.context_processors.messages",
83-
]
84+
],
8485
},
8586
}
8687
]
@@ -120,6 +121,8 @@ def process_response(self, request, response):
120121

121122
USE_TZ = True
122123

124+
TEMPLATE_DEBUG = True
125+
123126

124127
# Static files (CSS, JavaScript, Images)
125128
# https://docs.djangoproject.com/en/2.0/howto/static-files/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
1
2+
2
3+
3
4+
4
5+
5
6+
6
7+
7
8+
8
9+
9
10+
{% invalid template tag %}
11+
11
12+
12
13+
13
14+
14
15+
15
16+
16
17+
17
18+
18
19+
19
20+
20

tests/integrations/django/myapp/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
path("mylogin", views.mylogin, name="mylogin"),
3030
path("classbased", views.ClassBasedView.as_view(), name="classbased"),
3131
path("post-echo", views.post_echo, name="post_echo"),
32+
path("template-exc", views.template_exc, name="template_exc"),
3233
]
3334

3435
handler500 = views.handler500

tests/integrations/django/myapp/views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.contrib.auth import login
22
from django.contrib.auth.models import User
33
from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
4+
from django.shortcuts import render_to_response
45
from django.views.generic import ListView
56

67
import sentry_sdk
@@ -42,3 +43,7 @@ def post_echo(request):
4243
def handler404(*args, **kwargs):
4344
sentry_sdk.capture_message("not found", level="error")
4445
return HttpResponseNotFound("404")
46+
47+
48+
def template_exc(*args, **kwargs):
49+
return render_to_response("error.html")

tests/integrations/django/test_basic.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,29 @@ def test_request_body(sentry_init, client, capture_events):
288288
assert event["message"] == "hi"
289289
assert event["request"]["data"] == {"hey": 42}
290290
assert "" not in event
291+
292+
293+
def test_template_exception(sentry_init, client, capture_events):
294+
sentry_init(integrations=[DjangoIntegration()])
295+
events = capture_events()
296+
297+
content, status, headers = client.get(reverse("template_exc"))
298+
assert status.lower() == "500 internal server error"
299+
300+
event, = events
301+
exception, = event["exception"]["values"]
302+
303+
frames = [
304+
f
305+
for f in exception["stacktrace"]["frames"]
306+
if not f["filename"].startswith("django/")
307+
]
308+
view_frame, template_frame = frames[-2:]
309+
310+
assert template_frame["context_line"] == "{% invalid template tag %}\n"
311+
assert template_frame["pre_context"] == ["5\n", "6\n", "7\n", "8\n", "9\n"]
312+
313+
assert template_frame["post_context"] == ["11\n", "12\n", "13\n", "14\n", "15\n"]
314+
assert template_frame["lineno"] == 10
315+
assert template_frame["in_app"]
316+
assert template_frame["filename"].endswith("error.html")

0 commit comments

Comments
 (0)
0