Skip to content

Commit f07a08c

Browse files
authored
Add Starlette/FastAPI template tag for adding sentry tracing information (#2225)
Adding sentry_trace_meta to template context so meta tags including Sentry trace information can be rendered using {{ sentry_trace_meta }} in the Jinja templates in Starlette and FastAPI.
1 parent 1eb9600 commit f07a08c

File tree

6 files changed

+138
-20
lines changed

6 files changed

+138
-20
lines changed

sentry_sdk/integrations/starlette.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
AnnotatedValue,
2020
capture_internal_exceptions,
2121
event_from_exception,
22+
parse_version,
2223
transaction_from_function,
2324
)
2425

@@ -29,6 +30,7 @@
2930

3031
try:
3132
import starlette # type: ignore
33+
from starlette import __version__ as STARLETTE_VERSION
3234
from starlette.applications import Starlette # type: ignore
3335
from starlette.datastructures import UploadFile # type: ignore
3436
from starlette.middleware import Middleware # type: ignore
@@ -77,10 +79,20 @@ def __init__(self, transaction_style="url"):
7779
@staticmethod
7880
def setup_once():
7981
# type: () -> None
82+
version = parse_version(STARLETTE_VERSION)
83+
84+
if version is None:
85+
raise DidNotEnable(
86+
"Unparsable Starlette version: {}".format(STARLETTE_VERSION)
87+
)
88+
8089
patch_middlewares()
8190
patch_asgi_app()
8291
patch_request_response()
8392

93+
if version >= (0, 24):
94+
patch_templates()
95+
8496

8597
def _enable_span_for_middleware(middleware_class):
8698
# type: (Any) -> type
@@ -456,6 +468,47 @@ def event_processor(event, hint):
456468
starlette.routing.request_response = _sentry_request_response
457469

458470

471+
def patch_templates():
472+
# type: () -> None
473+
474+
# If markupsafe is not installed, then Jinja2 is not installed
475+
# (markupsafe is a dependency of Jinja2)
476+
# In this case we do not need to patch the Jinja2Templates class
477+
try:
478+
from markupsafe import Markup
479+
except ImportError:
480+
return # Nothing to do
481+
482+
from starlette.templating import Jinja2Templates # type: ignore
483+
484+
old_jinja2templates_init = Jinja2Templates.__init__
485+
486+
not_yet_patched = "_sentry_jinja2templates_init" not in str(
487+
old_jinja2templates_init
488+
)
489+
490+
if not_yet_patched:
491+
492+
def _sentry_jinja2templates_init(self, *args, **kwargs):
493+
# type: (Jinja2Templates, *Any, **Any) -> None
494+
def add_sentry_trace_meta(request):
495+
# type: (Request) -> Dict[str, Any]
496+
hub = Hub.current
497+
trace_meta = Markup(hub.trace_propagation_meta())
498+
return {
499+
"sentry_trace_meta": trace_meta,
500+
}
501+
502+
kwargs.setdefault("context_processors", [])
503+
504+
if add_sentry_trace_meta not in kwargs["context_processors"]:
505+
kwargs["context_processors"].append(add_sentry_trace_meta)
506+
507+
return old_jinja2templates_init(self, *args, **kwargs)
508+
509+
Jinja2Templates.__init__ = _sentry_jinja2templates_init
510+
511+
459512
class StarletteRequestExtractor:
460513
"""
461514
Extracts useful information from the Starlette request

tests/integrations/django/test_basic.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import
22

33
import json
4+
import re
45
import pytest
56
import random
67
from functools import partial
@@ -707,23 +708,26 @@ def test_read_request(sentry_init, client, capture_events):
707708

708709

709710
def test_template_tracing_meta(sentry_init, client, capture_events):
710-
sentry_init(integrations=[DjangoIntegration()], traces_sample_rate=1.0)
711+
sentry_init(integrations=[DjangoIntegration()])
711712
events = capture_events()
712713

713-
# The view will capture_message the sentry-trace and baggage information
714714
content, _, _ = client.get(reverse("template_test3"))
715715
rendered_meta = b"".join(content).decode("utf-8")
716716

717717
traceparent, baggage = events[0]["message"].split("\n")
718-
expected_meta = (
719-
'<meta name="sentry-trace" content="%s"><meta name="baggage" content="%s">\n'
720-
% (
721-
traceparent,
722-
baggage,
723-
)
718+
assert traceparent != ""
719+
assert baggage != ""
720+
721+
match = re.match(
722+
r'^<meta name="sentry-trace" content="([^\"]*)"><meta name="baggage" content="([^\"]*)">\n',
723+
rendered_meta,
724724
)
725+
assert match is not None
726+
assert match.group(1) == traceparent
725727

726-
assert rendered_meta == expected_meta
728+
# Python 2 does not preserve sort order
729+
rendered_baggage = match.group(2)
730+
assert sorted(rendered_baggage.split(",")) == sorted(baggage.split(","))
727731

728732

729733
@pytest.mark.parametrize("with_executing_integration", [[], [ExecutingIntegration()]])

tests/integrations/flask/test_flask.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import re
23
import pytest
34
import logging
45

@@ -809,8 +810,8 @@ def dispatch_request(self):
809810
@pytest.mark.parametrize(
810811
"template_string", ["{{ sentry_trace }}", "{{ sentry_trace_meta }}"]
811812
)
812-
def test_sentry_trace_context(sentry_init, app, capture_events, template_string):
813-
sentry_init(integrations=[flask_sentry.FlaskIntegration()], traces_sample_rate=1.0)
813+
def test_template_tracing_meta(sentry_init, app, capture_events, template_string):
814+
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
814815
events = capture_events()
815816

816817
@app.route("/")
@@ -825,14 +826,19 @@ def index():
825826

826827
rendered_meta = response.data.decode("utf-8")
827828
traceparent, baggage = events[0]["message"].split("\n")
828-
expected_meta = (
829-
'<meta name="sentry-trace" content="%s"><meta name="baggage" content="%s">'
830-
% (
831-
traceparent,
832-
baggage,
833-
)
834-
)
835-
assert rendered_meta == expected_meta
829+
assert traceparent != ""
830+
assert baggage != ""
831+
832+
match = re.match(
833+
r'^<meta name="sentry-trace" content="([^\"]*)"><meta name="baggage" content="([^\"]*)">',
834+
rendered_meta,
835+
)
836+
assert match is not None
837+
assert match.group(1) == traceparent
838+
839+
# Python 2 does not preserve sort order
840+
rendered_baggage = match.group(2)
841+
assert sorted(rendered_baggage.split(",")) == sorted(baggage.split(","))
836842

837843

838844
def test_dont_override_sentry_trace_context(sentry_init, app):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ sentry_trace_meta }}

tests/integrations/starlette/test_starlette.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import json
55
import logging
66
import os
7+
import re
78
import threading
89

910
import pytest
1011

1112
from sentry_sdk import last_event_id, capture_exception
1213
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
14+
from sentry_sdk.utils import parse_version
1315

1416
try:
1517
from unittest import mock # python 3.3 and above
@@ -33,7 +35,7 @@
3335
from starlette.middleware.authentication import AuthenticationMiddleware
3436
from starlette.testclient import TestClient
3537

36-
STARLETTE_VERSION = tuple([int(x) for x in starlette.__version__.split(".")])
38+
STARLETTE_VERSION = parse_version(starlette.__version__)
3739

3840
PICTURE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "photo.jpg")
3941

@@ -93,7 +95,16 @@ async def _mock_receive(msg):
9395
return msg
9496

9597

98+
from sentry_sdk import Hub
99+
from starlette.templating import Jinja2Templates
100+
101+
96102
def starlette_app_factory(middleware=None, debug=True):
103+
template_dir = os.path.join(
104+
os.getcwd(), "tests", "integrations", "starlette", "templates"
105+
)
106+
templates = Jinja2Templates(directory=template_dir)
107+
97108
async def _homepage(request):
98109
1 / 0
99110
return starlette.responses.JSONResponse({"status": "ok"})
@@ -125,6 +136,16 @@ async def _thread_ids_async(request):
125136
}
126137
)
127138

139+
async def _render_template(request):
140+
hub = Hub.current
141+
capture_message(hub.get_traceparent() + "\n" + hub.get_baggage())
142+
143+
template_context = {
144+
"request": request,
145+
"msg": "Hello Template World!",
146+
}
147+
return templates.TemplateResponse("trace_meta.html", template_context)
148+
128149
app = starlette.applications.Starlette(
129150
debug=debug,
130151
routes=[
@@ -134,6 +155,7 @@ async def _thread_ids_async(request):
134155
starlette.routing.Route("/message/{message_id}", _message_with_id),
135156
starlette.routing.Route("/sync/thread_ids", _thread_ids_sync),
136157
starlette.routing.Route("/async/thread_ids", _thread_ids_async),
158+
starlette.routing.Route("/render_template", _render_template),
137159
],
138160
middleware=middleware,
139161
)
@@ -902,3 +924,34 @@ async def _error(request):
902924
event = events[0]
903925
assert event["request"]["data"] == {"password": "[Filtered]"}
904926
assert event["request"]["headers"]["authorization"] == "[Filtered]"
927+
928+
929+
@pytest.mark.skipif(STARLETTE_VERSION < (0, 24), reason="Requires Starlette >= 0.24")
930+
def test_template_tracing_meta(sentry_init, capture_events):
931+
sentry_init(
932+
auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request.
933+
integrations=[StarletteIntegration()],
934+
)
935+
events = capture_events()
936+
937+
app = starlette_app_factory()
938+
939+
client = TestClient(app)
940+
response = client.get("/render_template")
941+
assert response.status_code == 200
942+
943+
rendered_meta = response.text
944+
traceparent, baggage = events[0]["message"].split("\n")
945+
assert traceparent != ""
946+
assert baggage != ""
947+
948+
match = re.match(
949+
r'^<meta name="sentry-trace" content="([^\"]*)"><meta name="baggage" content="([^\"]*)">',
950+
rendered_meta,
951+
)
952+
assert match is not None
953+
assert match.group(1) == traceparent
954+
955+
# Python 2 does not preserve sort order
956+
rendered_baggage = match.group(2)
957+
assert sorted(rendered_baggage.split(",")) == sorted(baggage.split(","))

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ deps =
412412
starlette: python-multipart
413413
starlette: requests
414414
starlette: httpx
415+
starlette: jinja2
415416
starlette-v0.20: starlette>=0.20.0,<0.21.0
416417
starlette-v0.22: starlette>=0.22.0,<0.23.0
417418
starlette-v0.24: starlette>=0.24.0,<0.25.0

0 commit comments

Comments
 (0)