Skip to content

Commit dad343e

Browse files
authored
feat(profiling): Set active thread id for quart (#1830)
Following up to #1824 to set the active thread id for quart.
1 parent a135fd6 commit dad343e

File tree

2 files changed

+103
-9
lines changed

2 files changed

+103
-9
lines changed

sentry_sdk/integrations/quart.py

+59-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import absolute_import
22

3+
import inspect
4+
import threading
5+
36
from sentry_sdk.hub import _should_send_default_pii, Hub
47
from sentry_sdk.integrations import DidNotEnable, Integration
58
from sentry_sdk.integrations._wsgi_common import _filter_headers
@@ -11,6 +14,7 @@
1114
event_from_exception,
1215
)
1316

17+
from sentry_sdk._functools import wraps
1418
from sentry_sdk._types import TYPE_CHECKING
1519

1620
if TYPE_CHECKING:
@@ -34,13 +38,15 @@
3438
request,
3539
websocket,
3640
)
41+
from quart.scaffold import Scaffold # type: ignore
3742
from quart.signals import ( # type: ignore
3843
got_background_exception,
3944
got_request_exception,
4045
got_websocket_exception,
4146
request_started,
4247
websocket_started,
4348
)
49+
from quart.utils import is_coroutine_function # type: ignore
4450
except ImportError:
4551
raise DidNotEnable("Quart is not installed")
4652

@@ -71,18 +77,62 @@ def setup_once():
7177
got_request_exception.connect(_capture_exception)
7278
got_websocket_exception.connect(_capture_exception)
7379

74-
old_app = Quart.__call__
80+
patch_asgi_app()
81+
patch_scaffold_route()
82+
83+
84+
def patch_asgi_app():
85+
# type: () -> None
86+
old_app = Quart.__call__
87+
88+
async def sentry_patched_asgi_app(self, scope, receive, send):
89+
# type: (Any, Any, Any, Any) -> Any
90+
if Hub.current.get_integration(QuartIntegration) is None:
91+
return await old_app(self, scope, receive, send)
92+
93+
middleware = SentryAsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))
94+
middleware.__call__ = middleware._run_asgi3
95+
return await middleware(scope, receive, send)
96+
97+
Quart.__call__ = sentry_patched_asgi_app
98+
99+
100+
def patch_scaffold_route():
101+
# type: () -> None
102+
old_route = Scaffold.route
103+
104+
def _sentry_route(*args, **kwargs):
105+
# type: (*Any, **Any) -> Any
106+
old_decorator = old_route(*args, **kwargs)
107+
108+
def decorator(old_func):
109+
# type: (Any) -> Any
110+
111+
if inspect.isfunction(old_func) and not is_coroutine_function(old_func):
112+
113+
@wraps(old_func)
114+
def _sentry_func(*args, **kwargs):
115+
# type: (*Any, **Any) -> Any
116+
hub = Hub.current
117+
integration = hub.get_integration(QuartIntegration)
118+
if integration is None:
119+
return old_func(*args, **kwargs)
120+
121+
with hub.configure_scope() as sentry_scope:
122+
if sentry_scope.profile is not None:
123+
sentry_scope.profile.active_thread_id = (
124+
threading.current_thread().ident
125+
)
126+
127+
return old_func(*args, **kwargs)
128+
129+
return old_decorator(_sentry_func)
75130

76-
async def sentry_patched_asgi_app(self, scope, receive, send):
77-
# type: (Any, Any, Any, Any) -> Any
78-
if Hub.current.get_integration(QuartIntegration) is None:
79-
return await old_app(self, scope, receive, send)
131+
return old_decorator(old_func)
80132

81-
middleware = SentryAsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))
82-
middleware.__call__ = middleware._run_asgi3
83-
return await middleware(scope, receive, send)
133+
return decorator
84134

85-
Quart.__call__ = sentry_patched_asgi_app
135+
Scaffold.route = _sentry_route
86136

87137

88138
def _set_transaction_name_and_source(scope, transaction_style, request):

tests/integrations/quart/test_quart.py

+44
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import json
2+
import threading
3+
14
import pytest
25
import pytest_asyncio
36

@@ -41,6 +44,20 @@ async def hi_with_id(message_id):
4144
capture_message("hi with id")
4245
return "ok with id"
4346

47+
@app.get("/sync/thread_ids")
48+
def _thread_ids_sync():
49+
return {
50+
"main": str(threading.main_thread().ident),
51+
"active": str(threading.current_thread().ident),
52+
}
53+
54+
@app.get("/async/thread_ids")
55+
async def _thread_ids_async():
56+
return {
57+
"main": str(threading.main_thread().ident),
58+
"active": str(threading.current_thread().ident),
59+
}
60+
4461
return app
4562

4663

@@ -523,3 +540,30 @@ async def dispatch_request(self):
523540

524541
assert event["message"] == "hi"
525542
assert event["transaction"] == "hello_class"
543+
544+
545+
@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"])
546+
async def test_active_thread_id(sentry_init, capture_envelopes, endpoint, app):
547+
sentry_init(
548+
traces_sample_rate=1.0,
549+
_experiments={"profiles_sample_rate": 1.0},
550+
)
551+
552+
envelopes = capture_envelopes()
553+
554+
async with app.test_client() as client:
555+
response = await client.get(endpoint)
556+
assert response.status_code == 200
557+
558+
data = json.loads(response.content)
559+
560+
envelopes = [envelope for envelope in envelopes]
561+
assert len(envelopes) == 1
562+
563+
profiles = [item for item in envelopes[0].items if item.type == "profile"]
564+
assert len(profiles) == 1
565+
566+
for profile in profiles:
567+
transactions = profile.payload.json["transactions"]
568+
assert len(transactions) == 1
569+
assert str(data["active"]) == transactions[0]["active_thread_id"]

0 commit comments

Comments
 (0)