quart.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import asyncio
  2. import inspect
  3. from functools import wraps
  4. import sentry_sdk
  5. from sentry_sdk.integrations import DidNotEnable, Integration
  6. from sentry_sdk.integrations._wsgi_common import _filter_headers
  7. from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
  8. from sentry_sdk.scope import should_send_default_pii
  9. from sentry_sdk.tracing import SOURCE_FOR_STYLE
  10. from sentry_sdk.utils import (
  11. capture_internal_exceptions,
  12. ensure_integration_enabled,
  13. event_from_exception,
  14. )
  15. from typing import TYPE_CHECKING
  16. if TYPE_CHECKING:
  17. from typing import Any
  18. from typing import Union
  19. from sentry_sdk._types import Event, EventProcessor
  20. try:
  21. import quart_auth # type: ignore
  22. except ImportError:
  23. quart_auth = None
  24. try:
  25. from quart import ( # type: ignore
  26. has_request_context,
  27. has_websocket_context,
  28. Request,
  29. Quart,
  30. request,
  31. websocket,
  32. )
  33. from quart.signals import ( # type: ignore
  34. got_background_exception,
  35. got_request_exception,
  36. got_websocket_exception,
  37. request_started,
  38. websocket_started,
  39. )
  40. except ImportError:
  41. raise DidNotEnable("Quart is not installed")
  42. else:
  43. # Quart 0.19 is based on Flask and hence no longer has a Scaffold
  44. try:
  45. from quart.scaffold import Scaffold # type: ignore
  46. except ImportError:
  47. from flask.sansio.scaffold import Scaffold # type: ignore
  48. TRANSACTION_STYLE_VALUES = ("endpoint", "url")
  49. class QuartIntegration(Integration):
  50. identifier = "quart"
  51. origin = f"auto.http.{identifier}"
  52. transaction_style = ""
  53. def __init__(self, transaction_style: str = "endpoint") -> None:
  54. if transaction_style not in TRANSACTION_STYLE_VALUES:
  55. raise ValueError(
  56. "Invalid value for transaction_style: %s (must be in %s)"
  57. % (transaction_style, TRANSACTION_STYLE_VALUES)
  58. )
  59. self.transaction_style = transaction_style
  60. @staticmethod
  61. def setup_once() -> None:
  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. patch_asgi_app()
  68. patch_scaffold_route()
  69. def patch_asgi_app() -> None:
  70. old_app = Quart.__call__
  71. async def sentry_patched_asgi_app(
  72. self: "Any", scope: "Any", receive: "Any", send: "Any"
  73. ) -> "Any":
  74. if sentry_sdk.get_client().get_integration(QuartIntegration) is None:
  75. return await old_app(self, scope, receive, send)
  76. middleware = SentryAsgiMiddleware(
  77. lambda *a, **kw: old_app(self, *a, **kw),
  78. span_origin=QuartIntegration.origin,
  79. asgi_version=3,
  80. )
  81. return await middleware(scope, receive, send)
  82. Quart.__call__ = sentry_patched_asgi_app
  83. def patch_scaffold_route() -> None:
  84. old_route = Scaffold.route
  85. def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any":
  86. old_decorator = old_route(*args, **kwargs)
  87. def decorator(old_func: "Any") -> "Any":
  88. if inspect.isfunction(old_func) and not asyncio.iscoroutinefunction(
  89. old_func
  90. ):
  91. @wraps(old_func)
  92. @ensure_integration_enabled(QuartIntegration, old_func)
  93. def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any":
  94. current_scope = sentry_sdk.get_current_scope()
  95. if current_scope.transaction is not None:
  96. current_scope.transaction.update_active_thread()
  97. sentry_scope = sentry_sdk.get_isolation_scope()
  98. if sentry_scope.profile is not None:
  99. sentry_scope.profile.update_active_thread_id()
  100. return old_func(*args, **kwargs)
  101. return old_decorator(_sentry_func)
  102. return old_decorator(old_func)
  103. return decorator
  104. Scaffold.route = _sentry_route
  105. def _set_transaction_name_and_source(
  106. scope: "sentry_sdk.Scope", transaction_style: str, request: "Request"
  107. ) -> None:
  108. try:
  109. name_for_style = {
  110. "url": request.url_rule.rule,
  111. "endpoint": request.url_rule.endpoint,
  112. }
  113. scope.set_transaction_name(
  114. name_for_style[transaction_style],
  115. source=SOURCE_FOR_STYLE[transaction_style],
  116. )
  117. except Exception:
  118. pass
  119. async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None:
  120. integration = sentry_sdk.get_client().get_integration(QuartIntegration)
  121. if integration is None:
  122. return
  123. if has_request_context():
  124. request_websocket = request._get_current_object()
  125. if has_websocket_context():
  126. request_websocket = websocket._get_current_object()
  127. # Set the transaction name here, but rely on ASGI middleware
  128. # to actually start the transaction
  129. _set_transaction_name_and_source(
  130. sentry_sdk.get_current_scope(), integration.transaction_style, request_websocket
  131. )
  132. scope = sentry_sdk.get_isolation_scope()
  133. evt_processor = _make_request_event_processor(app, request_websocket, integration)
  134. scope.add_event_processor(evt_processor)
  135. def _make_request_event_processor(
  136. app: "Quart", request: "Request", integration: "QuartIntegration"
  137. ) -> "EventProcessor":
  138. def inner(event: "Event", hint: "dict[str, Any]") -> "Event":
  139. # if the request is gone we are fine not logging the data from
  140. # it. This might happen if the processor is pushed away to
  141. # another thread.
  142. if request is None:
  143. return event
  144. with capture_internal_exceptions():
  145. # TODO: Figure out what to do with request body. Methods on request
  146. # are async, but event processors are not.
  147. request_info = event.setdefault("request", {})
  148. request_info["url"] = request.url
  149. request_info["query_string"] = request.query_string
  150. request_info["method"] = request.method
  151. request_info["headers"] = _filter_headers(dict(request.headers))
  152. if should_send_default_pii():
  153. request_info["env"] = {"REMOTE_ADDR": request.access_route[0]}
  154. _add_user_to_event(event)
  155. return event
  156. return inner
  157. async def _capture_exception(
  158. sender: "Quart", exception: "Union[ValueError, BaseException]", **kwargs: "Any"
  159. ) -> None:
  160. integration = sentry_sdk.get_client().get_integration(QuartIntegration)
  161. if integration is None:
  162. return
  163. event, hint = event_from_exception(
  164. exception,
  165. client_options=sentry_sdk.get_client().options,
  166. mechanism={"type": "quart", "handled": False},
  167. )
  168. sentry_sdk.capture_event(event, hint=hint)
  169. def _add_user_to_event(event: "Event") -> None:
  170. if quart_auth is None:
  171. return
  172. user = quart_auth.current_user
  173. if user is None:
  174. return
  175. with capture_internal_exceptions():
  176. user_info = event.setdefault("user", {})
  177. user_info["id"] = quart_auth.current_user._auth_id