Skip to content

Commit

Permalink
feat(profiling): Set active thread id for quart
Browse files Browse the repository at this point in the history
Following up to #1824 to set the active thread id for quart.
  • Loading branch information
Zylphrex authored and sl0thentr0py committed Mar 6, 2023
1 parent a135fd6 commit 1f60e7b
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 9 deletions.
68 changes: 59 additions & 9 deletions sentry_sdk/integrations/quart.py
@@ -1,5 +1,8 @@
from __future__ import absolute_import

import inspect
import threading

from sentry_sdk.hub import _should_send_default_pii, Hub
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.integrations._wsgi_common import _filter_headers
Expand All @@ -11,6 +14,7 @@
event_from_exception,
)

from sentry_sdk._functools import wraps
from sentry_sdk._types import TYPE_CHECKING

if TYPE_CHECKING:
Expand All @@ -34,13 +38,15 @@
request,
websocket,
)
from quart.scaffold import Scaffold # type: ignore
from quart.signals import ( # type: ignore
got_background_exception,
got_request_exception,
got_websocket_exception,
request_started,
websocket_started,
)
from quart.utils import is_coroutine_function # type: ignore
except ImportError:
raise DidNotEnable("Quart is not installed")

Expand Down Expand Up @@ -71,18 +77,62 @@ def setup_once():
got_request_exception.connect(_capture_exception)
got_websocket_exception.connect(_capture_exception)

old_app = Quart.__call__
patch_asgi_app()
patch_scaffold_route()


def patch_asgi_app():
# type: () -> None
old_app = Quart.__call__

async def sentry_patched_asgi_app(self, scope, receive, send):
# type: (Any, Any, Any, Any) -> Any
if Hub.current.get_integration(QuartIntegration) is None:
return await old_app(self, scope, receive, send)

Check warning on line 91 in sentry_sdk/integrations/quart.py

View check run for this annotation

Codecov / codecov/patch/python

sentry_sdk/integrations/quart.py#L91

Added line #L91 was not covered by tests

middleware = SentryAsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))
middleware.__call__ = middleware._run_asgi3
return await middleware(scope, receive, send)

Quart.__call__ = sentry_patched_asgi_app


def patch_scaffold_route():
# type: () -> None
old_route = Scaffold.route

def _sentry_route(*args, **kwargs):
# type: (*Any, **Any) -> Any
old_decorator = old_route(*args, **kwargs)

def decorator(old_func):
# type: (Any) -> Any

if inspect.isfunction(old_func) and not is_coroutine_function(old_func):

@wraps(old_func)
def _sentry_func(*args, **kwargs):
# type: (*Any, **Any) -> Any
hub = Hub.current
integration = hub.get_integration(QuartIntegration)

Check warning on line 117 in sentry_sdk/integrations/quart.py

View check run for this annotation

Codecov / codecov/patch/python

sentry_sdk/integrations/quart.py#L116-L117

Added lines #L116 - L117 were not covered by tests
if integration is None:
return old_func(*args, **kwargs)

Check warning on line 119 in sentry_sdk/integrations/quart.py

View check run for this annotation

Codecov / codecov/patch/python

sentry_sdk/integrations/quart.py#L119

Added line #L119 was not covered by tests

with hub.configure_scope() as sentry_scope:
if sentry_scope.profile is not None:
sentry_scope.profile.active_thread_id = (

Check warning on line 123 in sentry_sdk/integrations/quart.py

View check run for this annotation

Codecov / codecov/patch/python

sentry_sdk/integrations/quart.py#L123

Added line #L123 was not covered by tests
threading.current_thread().ident
)

return old_func(*args, **kwargs)

Check warning on line 127 in sentry_sdk/integrations/quart.py

View check run for this annotation

Codecov / codecov/patch/python

sentry_sdk/integrations/quart.py#L127

Added line #L127 was not covered by tests

return old_decorator(_sentry_func)

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

middleware = SentryAsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))
middleware.__call__ = middleware._run_asgi3
return await middleware(scope, receive, send)
return decorator

Quart.__call__ = sentry_patched_asgi_app
Scaffold.route = _sentry_route


def _set_transaction_name_and_source(scope, transaction_style, request):
Expand Down
44 changes: 44 additions & 0 deletions tests/integrations/quart/test_quart.py
@@ -1,3 +1,6 @@
import json
import threading

import pytest
import pytest_asyncio

Expand Down Expand Up @@ -41,6 +44,20 @@ async def hi_with_id(message_id):
capture_message("hi with id")
return "ok with id"

@app.get("/sync/thread_ids")
def _thread_ids_sync():
return {
"main": str(threading.main_thread().ident),
"active": str(threading.current_thread().ident),
}

@app.get("/async/thread_ids")
async def _thread_ids_async():
return {
"main": str(threading.main_thread().ident),
"active": str(threading.current_thread().ident),
}

return app


Expand Down Expand Up @@ -523,3 +540,30 @@ async def dispatch_request(self):

assert event["message"] == "hi"
assert event["transaction"] == "hello_class"


@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"])
async def test_active_thread_id(sentry_init, capture_envelopes, endpoint, app):
sentry_init(
traces_sample_rate=1.0,
_experiments={"profiles_sample_rate": 1.0},
)

envelopes = capture_envelopes()

async with app.test_client() as client:
response = await client.get(endpoint)
assert response.status_code == 200

data = json.loads(response.content)

envelopes = [envelope for envelope in envelopes]
assert len(envelopes) == 1

profiles = [item for item in envelopes[0].items if item.type == "profile"]
assert len(profiles) == 1

for profile in profiles:
transactions = profile.payload.json["transactions"]
assert len(transactions) == 1
assert str(data["active"]) == transactions[0]["active_thread_id"]

0 comments on commit 1f60e7b

Please sign in to comment.