Skip to content

Commit

Permalink
Add Starlette/FastAPI template tag for adding sentry tracing informat…
Browse files Browse the repository at this point in the history
…ion (#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.
  • Loading branch information
antonpirker committed Jul 6, 2023
1 parent 1eb9600 commit f07a08c
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 20 deletions.
53 changes: 53 additions & 0 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
AnnotatedValue,
capture_internal_exceptions,
event_from_exception,
parse_version,
transaction_from_function,
)

Expand All @@ -29,6 +30,7 @@

try:
import starlette # type: ignore
from starlette import __version__ as STARLETTE_VERSION
from starlette.applications import Starlette # type: ignore
from starlette.datastructures import UploadFile # type: ignore
from starlette.middleware import Middleware # type: ignore
Expand Down Expand Up @@ -77,10 +79,20 @@ def __init__(self, transaction_style="url"):
@staticmethod
def setup_once():
# type: () -> None
version = parse_version(STARLETTE_VERSION)

if version is None:
raise DidNotEnable(
"Unparsable Starlette version: {}".format(STARLETTE_VERSION)
)

patch_middlewares()
patch_asgi_app()
patch_request_response()

if version >= (0, 24):
patch_templates()


def _enable_span_for_middleware(middleware_class):
# type: (Any) -> type
Expand Down Expand Up @@ -456,6 +468,47 @@ def event_processor(event, hint):
starlette.routing.request_response = _sentry_request_response


def patch_templates():
# type: () -> None

# If markupsafe is not installed, then Jinja2 is not installed
# (markupsafe is a dependency of Jinja2)
# In this case we do not need to patch the Jinja2Templates class
try:
from markupsafe import Markup
except ImportError:
return # Nothing to do

from starlette.templating import Jinja2Templates # type: ignore

old_jinja2templates_init = Jinja2Templates.__init__

not_yet_patched = "_sentry_jinja2templates_init" not in str(
old_jinja2templates_init
)

if not_yet_patched:

def _sentry_jinja2templates_init(self, *args, **kwargs):
# type: (Jinja2Templates, *Any, **Any) -> None
def add_sentry_trace_meta(request):
# type: (Request) -> Dict[str, Any]
hub = Hub.current
trace_meta = Markup(hub.trace_propagation_meta())
return {
"sentry_trace_meta": trace_meta,
}

kwargs.setdefault("context_processors", [])

if add_sentry_trace_meta not in kwargs["context_processors"]:
kwargs["context_processors"].append(add_sentry_trace_meta)

return old_jinja2templates_init(self, *args, **kwargs)

Jinja2Templates.__init__ = _sentry_jinja2templates_init


class StarletteRequestExtractor:
"""
Extracts useful information from the Starlette request
Expand Down
22 changes: 13 additions & 9 deletions tests/integrations/django/test_basic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import absolute_import

import json
import re
import pytest
import random
from functools import partial
Expand Down Expand Up @@ -707,23 +708,26 @@ def test_read_request(sentry_init, client, capture_events):


def test_template_tracing_meta(sentry_init, client, capture_events):
sentry_init(integrations=[DjangoIntegration()], traces_sample_rate=1.0)
sentry_init(integrations=[DjangoIntegration()])
events = capture_events()

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

traceparent, baggage = events[0]["message"].split("\n")
expected_meta = (
'<meta name="sentry-trace" content="%s"><meta name="baggage" content="%s">\n'
% (
traceparent,
baggage,
)
assert traceparent != ""
assert baggage != ""

match = re.match(
r'^<meta name="sentry-trace" content="([^\"]*)"><meta name="baggage" content="([^\"]*)">\n',
rendered_meta,
)
assert match is not None
assert match.group(1) == traceparent

assert rendered_meta == expected_meta
# Python 2 does not preserve sort order
rendered_baggage = match.group(2)
assert sorted(rendered_baggage.split(",")) == sorted(baggage.split(","))


@pytest.mark.parametrize("with_executing_integration", [[], [ExecutingIntegration()]])
Expand Down
26 changes: 16 additions & 10 deletions tests/integrations/flask/test_flask.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import re
import pytest
import logging

Expand Down Expand Up @@ -809,8 +810,8 @@ def dispatch_request(self):
@pytest.mark.parametrize(
"template_string", ["{{ sentry_trace }}", "{{ sentry_trace_meta }}"]
)
def test_sentry_trace_context(sentry_init, app, capture_events, template_string):
sentry_init(integrations=[flask_sentry.FlaskIntegration()], traces_sample_rate=1.0)
def test_template_tracing_meta(sentry_init, app, capture_events, template_string):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
events = capture_events()

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

rendered_meta = response.data.decode("utf-8")
traceparent, baggage = events[0]["message"].split("\n")
expected_meta = (
'<meta name="sentry-trace" content="%s"><meta name="baggage" content="%s">'
% (
traceparent,
baggage,
)
)
assert rendered_meta == expected_meta
assert traceparent != ""
assert baggage != ""

match = re.match(
r'^<meta name="sentry-trace" content="([^\"]*)"><meta name="baggage" content="([^\"]*)">',
rendered_meta,
)
assert match is not None
assert match.group(1) == traceparent

# Python 2 does not preserve sort order
rendered_baggage = match.group(2)
assert sorted(rendered_baggage.split(",")) == sorted(baggage.split(","))


def test_dont_override_sentry_trace_context(sentry_init, app):
Expand Down
1 change: 1 addition & 0 deletions tests/integrations/starlette/templates/trace_meta.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ sentry_trace_meta }}
55 changes: 54 additions & 1 deletion tests/integrations/starlette/test_starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import json
import logging
import os
import re
import threading

import pytest

from sentry_sdk import last_event_id, capture_exception
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from sentry_sdk.utils import parse_version

try:
from unittest import mock # python 3.3 and above
Expand All @@ -33,7 +35,7 @@
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.testclient import TestClient

STARLETTE_VERSION = tuple([int(x) for x in starlette.__version__.split(".")])
STARLETTE_VERSION = parse_version(starlette.__version__)

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

Expand Down Expand Up @@ -93,7 +95,16 @@ async def _mock_receive(msg):
return msg


from sentry_sdk import Hub
from starlette.templating import Jinja2Templates


def starlette_app_factory(middleware=None, debug=True):
template_dir = os.path.join(
os.getcwd(), "tests", "integrations", "starlette", "templates"
)
templates = Jinja2Templates(directory=template_dir)

async def _homepage(request):
1 / 0
return starlette.responses.JSONResponse({"status": "ok"})
Expand Down Expand Up @@ -125,6 +136,16 @@ async def _thread_ids_async(request):
}
)

async def _render_template(request):
hub = Hub.current
capture_message(hub.get_traceparent() + "\n" + hub.get_baggage())

template_context = {
"request": request,
"msg": "Hello Template World!",
}
return templates.TemplateResponse("trace_meta.html", template_context)

app = starlette.applications.Starlette(
debug=debug,
routes=[
Expand All @@ -134,6 +155,7 @@ async def _thread_ids_async(request):
starlette.routing.Route("/message/{message_id}", _message_with_id),
starlette.routing.Route("/sync/thread_ids", _thread_ids_sync),
starlette.routing.Route("/async/thread_ids", _thread_ids_async),
starlette.routing.Route("/render_template", _render_template),
],
middleware=middleware,
)
Expand Down Expand Up @@ -902,3 +924,34 @@ async def _error(request):
event = events[0]
assert event["request"]["data"] == {"password": "[Filtered]"}
assert event["request"]["headers"]["authorization"] == "[Filtered]"


@pytest.mark.skipif(STARLETTE_VERSION < (0, 24), reason="Requires Starlette >= 0.24")
def test_template_tracing_meta(sentry_init, capture_events):
sentry_init(
auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request.
integrations=[StarletteIntegration()],
)
events = capture_events()

app = starlette_app_factory()

client = TestClient(app)
response = client.get("/render_template")
assert response.status_code == 200

rendered_meta = response.text
traceparent, baggage = events[0]["message"].split("\n")
assert traceparent != ""
assert baggage != ""

match = re.match(
r'^<meta name="sentry-trace" content="([^\"]*)"><meta name="baggage" content="([^\"]*)">',
rendered_meta,
)
assert match is not None
assert match.group(1) == traceparent

# Python 2 does not preserve sort order
rendered_baggage = match.group(2)
assert sorted(rendered_baggage.split(",")) == sorted(baggage.split(","))
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ deps =
starlette: python-multipart
starlette: requests
starlette: httpx
starlette: jinja2
starlette-v0.20: starlette>=0.20.0,<0.21.0
starlette-v0.22: starlette>=0.22.0,<0.23.0
starlette-v0.24: starlette>=0.24.0,<0.25.0
Expand Down

0 comments on commit f07a08c

Please sign in to comment.