Skip to content

Commit 2246620

Browse files
authored
feat(quart): Add a Quart integration (#1248)
This is based on the Flask integration but includes background and websocket exceptions, and works with asgi.
1 parent 412c44a commit 2246620

File tree

5 files changed

+690
-0
lines changed

5 files changed

+690
-0
lines changed

sentry_sdk/integrations/quart.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
from __future__ import absolute_import
2+
3+
from sentry_sdk.hub import _should_send_default_pii, Hub
4+
from sentry_sdk.integrations import DidNotEnable, Integration
5+
from sentry_sdk.integrations._wsgi_common import _filter_headers
6+
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
7+
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
8+
9+
from sentry_sdk._types import MYPY
10+
11+
if MYPY:
12+
from typing import Any
13+
from typing import Dict
14+
from typing import Union
15+
16+
from sentry_sdk._types import EventProcessor
17+
18+
try:
19+
import quart_auth # type: ignore
20+
except ImportError:
21+
quart_auth = None
22+
23+
try:
24+
from quart import ( # type: ignore
25+
Request,
26+
Quart,
27+
_request_ctx_stack,
28+
_websocket_ctx_stack,
29+
_app_ctx_stack,
30+
)
31+
from quart.signals import ( # type: ignore
32+
got_background_exception,
33+
got_request_exception,
34+
got_websocket_exception,
35+
request_started,
36+
websocket_started,
37+
)
38+
except ImportError:
39+
raise DidNotEnable("Quart is not installed")
40+
41+
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
42+
43+
44+
class QuartIntegration(Integration):
45+
identifier = "quart"
46+
47+
transaction_style = None
48+
49+
def __init__(self, transaction_style="endpoint"):
50+
# type: (str) -> None
51+
if transaction_style not in TRANSACTION_STYLE_VALUES:
52+
raise ValueError(
53+
"Invalid value for transaction_style: %s (must be in %s)"
54+
% (transaction_style, TRANSACTION_STYLE_VALUES)
55+
)
56+
self.transaction_style = transaction_style
57+
58+
@staticmethod
59+
def setup_once():
60+
# type: () -> None
61+
62+
request_started.connect(_request_websocket_started)
63+
websocket_started.connect(_request_websocket_started)
64+
got_background_exception.connect(_capture_exception)
65+
got_request_exception.connect(_capture_exception)
66+
got_websocket_exception.connect(_capture_exception)
67+
68+
old_app = Quart.__call__
69+
70+
async def sentry_patched_asgi_app(self, scope, receive, send):
71+
# type: (Any, Any, Any, Any) -> Any
72+
if Hub.current.get_integration(QuartIntegration) is None:
73+
return await old_app(self, scope, receive, send)
74+
75+
middleware = SentryAsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))
76+
middleware.__call__ = middleware._run_asgi3
77+
return await middleware(scope, receive, send)
78+
79+
Quart.__call__ = sentry_patched_asgi_app
80+
81+
82+
def _request_websocket_started(sender, **kwargs):
83+
# type: (Quart, **Any) -> None
84+
hub = Hub.current
85+
integration = hub.get_integration(QuartIntegration)
86+
if integration is None:
87+
return
88+
89+
app = _app_ctx_stack.top.app
90+
with hub.configure_scope() as scope:
91+
if _request_ctx_stack.top is not None:
92+
request_websocket = _request_ctx_stack.top.request
93+
if _websocket_ctx_stack.top is not None:
94+
request_websocket = _websocket_ctx_stack.top.websocket
95+
96+
# Set the transaction name here, but rely on ASGI middleware
97+
# to actually start the transaction
98+
try:
99+
if integration.transaction_style == "endpoint":
100+
scope.transaction = request_websocket.url_rule.endpoint
101+
elif integration.transaction_style == "url":
102+
scope.transaction = request_websocket.url_rule.rule
103+
except Exception:
104+
pass
105+
106+
evt_processor = _make_request_event_processor(
107+
app, request_websocket, integration
108+
)
109+
scope.add_event_processor(evt_processor)
110+
111+
112+
def _make_request_event_processor(app, request, integration):
113+
# type: (Quart, Request, QuartIntegration) -> EventProcessor
114+
def inner(event, hint):
115+
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
116+
# if the request is gone we are fine not logging the data from
117+
# it. This might happen if the processor is pushed away to
118+
# another thread.
119+
if request is None:
120+
return event
121+
122+
with capture_internal_exceptions():
123+
# TODO: Figure out what to do with request body. Methods on request
124+
# are async, but event processors are not.
125+
126+
request_info = event.setdefault("request", {})
127+
request_info["url"] = request.url
128+
request_info["query_string"] = request.query_string
129+
request_info["method"] = request.method
130+
request_info["headers"] = _filter_headers(dict(request.headers))
131+
132+
if _should_send_default_pii():
133+
request_info["env"] = {"REMOTE_ADDR": request.access_route[0]}
134+
_add_user_to_event(event)
135+
136+
return event
137+
138+
return inner
139+
140+
141+
def _capture_exception(sender, exception, **kwargs):
142+
# type: (Quart, Union[ValueError, BaseException], **Any) -> None
143+
hub = Hub.current
144+
if hub.get_integration(QuartIntegration) is None:
145+
return
146+
147+
# If an integration is there, a client has to be there.
148+
client = hub.client # type: Any
149+
150+
event, hint = event_from_exception(
151+
exception,
152+
client_options=client.options,
153+
mechanism={"type": "quart", "handled": False},
154+
)
155+
156+
hub.capture_event(event, hint=hint)
157+
158+
159+
def _add_user_to_event(event):
160+
# type: (Dict[str, Any]) -> None
161+
if quart_auth is None:
162+
return
163+
164+
user = quart_auth.current_user
165+
if user is None:
166+
return
167+
168+
with capture_internal_exceptions():
169+
user_info = event.setdefault("user", {})
170+
171+
user_info["id"] = quart_auth.current_user._auth_id

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def get_file_text(file_name):
4040
install_requires=["urllib3>=1.10.0", "certifi"],
4141
extras_require={
4242
"flask": ["flask>=0.11", "blinker>=1.1"],
43+
"quart": ["quart>=0.16.1", "blinker>=1.1"],
4344
"bottle": ["bottle>=0.12.13"],
4445
"falcon": ["falcon>=1.4"],
4546
"django": ["django>=1.8"],

tests/integrations/quart/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pytest
2+
3+
quart = pytest.importorskip("quart")

0 commit comments

Comments
 (0)