flask.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import sentry_sdk
  2. from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
  3. from sentry_sdk.integrations._wsgi_common import (
  4. DEFAULT_HTTP_METHODS_TO_CAPTURE,
  5. RequestExtractor,
  6. )
  7. from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
  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. package_version,
  15. )
  16. from typing import TYPE_CHECKING
  17. if TYPE_CHECKING:
  18. from typing import Any, Callable, Dict, Union
  19. from sentry_sdk._types import Event, EventProcessor
  20. from sentry_sdk.integrations.wsgi import _ScopedResponse
  21. from werkzeug.datastructures import FileStorage, ImmutableMultiDict
  22. try:
  23. import flask_login # type: ignore
  24. except ImportError:
  25. flask_login = None
  26. try:
  27. from flask import Flask, Request # type: ignore
  28. from flask import request as flask_request
  29. from flask.signals import (
  30. before_render_template,
  31. got_request_exception,
  32. request_started,
  33. )
  34. from markupsafe import Markup
  35. except ImportError:
  36. raise DidNotEnable("Flask is not installed")
  37. try:
  38. import blinker # noqa
  39. except ImportError:
  40. raise DidNotEnable("blinker is not installed")
  41. TRANSACTION_STYLE_VALUES = ("endpoint", "url")
  42. class FlaskIntegration(Integration):
  43. identifier = "flask"
  44. origin = f"auto.http.{identifier}"
  45. transaction_style = ""
  46. def __init__(
  47. self,
  48. transaction_style: str = "endpoint",
  49. http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE,
  50. ) -> 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. self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
  58. @staticmethod
  59. def setup_once() -> None:
  60. try:
  61. from quart import Quart # type: ignore
  62. if Flask == Quart:
  63. # This is Quart masquerading as Flask, don't enable the Flask
  64. # integration. See https://github.com/getsentry/sentry-python/issues/2709
  65. raise DidNotEnable(
  66. "This is not a Flask app but rather Quart pretending to be Flask"
  67. )
  68. except ImportError:
  69. pass
  70. version = package_version("flask")
  71. _check_minimum_version(FlaskIntegration, version)
  72. before_render_template.connect(_add_sentry_trace)
  73. request_started.connect(_request_started)
  74. got_request_exception.connect(_capture_exception)
  75. old_app = Flask.__call__
  76. def sentry_patched_wsgi_app(
  77. self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]"
  78. ) -> "_ScopedResponse":
  79. if sentry_sdk.get_client().get_integration(FlaskIntegration) is None:
  80. return old_app(self, environ, start_response)
  81. integration = sentry_sdk.get_client().get_integration(FlaskIntegration)
  82. middleware = SentryWsgiMiddleware(
  83. lambda *a, **kw: old_app(self, *a, **kw),
  84. span_origin=FlaskIntegration.origin,
  85. http_methods_to_capture=(
  86. integration.http_methods_to_capture
  87. if integration
  88. else DEFAULT_HTTP_METHODS_TO_CAPTURE
  89. ),
  90. )
  91. return middleware(environ, start_response)
  92. Flask.__call__ = sentry_patched_wsgi_app
  93. def _add_sentry_trace(
  94. sender: "Flask", template: "Any", context: "Dict[str, Any]", **extra: "Any"
  95. ) -> None:
  96. if "sentry_trace" in context:
  97. return
  98. scope = sentry_sdk.get_current_scope()
  99. trace_meta = Markup(scope.trace_propagation_meta())
  100. context["sentry_trace"] = trace_meta # for backwards compatibility
  101. context["sentry_trace_meta"] = trace_meta
  102. def _set_transaction_name_and_source(
  103. scope: "sentry_sdk.Scope", transaction_style: str, request: "Request"
  104. ) -> None:
  105. try:
  106. name_for_style = {
  107. "url": request.url_rule.rule,
  108. "endpoint": request.url_rule.endpoint,
  109. }
  110. scope.set_transaction_name(
  111. name_for_style[transaction_style],
  112. source=SOURCE_FOR_STYLE[transaction_style],
  113. )
  114. except Exception:
  115. pass
  116. def _request_started(app: "Flask", **kwargs: "Any") -> None:
  117. integration = sentry_sdk.get_client().get_integration(FlaskIntegration)
  118. if integration is None:
  119. return
  120. request = flask_request._get_current_object()
  121. # Set the transaction name and source here,
  122. # but rely on WSGI middleware to actually start the transaction
  123. _set_transaction_name_and_source(
  124. sentry_sdk.get_current_scope(), integration.transaction_style, request
  125. )
  126. scope = sentry_sdk.get_isolation_scope()
  127. evt_processor = _make_request_event_processor(app, request, integration)
  128. scope.add_event_processor(evt_processor)
  129. class FlaskRequestExtractor(RequestExtractor):
  130. def env(self) -> "Dict[str, str]":
  131. return self.request.environ
  132. def cookies(self) -> "Dict[Any, Any]":
  133. return {
  134. k: v[0] if isinstance(v, list) and len(v) == 1 else v
  135. for k, v in self.request.cookies.items()
  136. }
  137. def raw_data(self) -> bytes:
  138. return self.request.get_data()
  139. def form(self) -> "ImmutableMultiDict[str, Any]":
  140. return self.request.form
  141. def files(self) -> "ImmutableMultiDict[str, Any]":
  142. return self.request.files
  143. def is_json(self) -> bool:
  144. return self.request.is_json
  145. def json(self) -> "Any":
  146. return self.request.get_json(silent=True)
  147. def size_of_file(self, file: "FileStorage") -> int:
  148. return file.content_length
  149. def _make_request_event_processor(
  150. app: "Flask", request: "Callable[[], Request]", integration: "FlaskIntegration"
  151. ) -> "EventProcessor":
  152. def inner(event: "Event", hint: "dict[str, Any]") -> "Event":
  153. # if the request is gone we are fine not logging the data from
  154. # it. This might happen if the processor is pushed away to
  155. # another thread.
  156. if request is None:
  157. return event
  158. with capture_internal_exceptions():
  159. FlaskRequestExtractor(request).extract_into_event(event)
  160. if should_send_default_pii():
  161. with capture_internal_exceptions():
  162. _add_user_to_event(event)
  163. return event
  164. return inner
  165. @ensure_integration_enabled(FlaskIntegration)
  166. def _capture_exception(
  167. sender: "Flask", exception: "Union[ValueError, BaseException]", **kwargs: "Any"
  168. ) -> None:
  169. event, hint = event_from_exception(
  170. exception,
  171. client_options=sentry_sdk.get_client().options,
  172. mechanism={"type": "flask", "handled": False},
  173. )
  174. sentry_sdk.capture_event(event, hint=hint)
  175. def _add_user_to_event(event: "Event") -> None:
  176. if flask_login is None:
  177. return
  178. user = flask_login.current_user
  179. if user is None:
  180. return
  181. with capture_internal_exceptions():
  182. # Access this object as late as possible as accessing the user
  183. # is relatively costly
  184. user_info = event.setdefault("user", {})
  185. try:
  186. user_info.setdefault("id", user.get_id())
  187. # TODO: more configurable user attrs here
  188. except AttributeError:
  189. # might happen if:
  190. # - flask_login could not be imported
  191. # - flask_login is not configured
  192. # - no user is logged in
  193. pass
  194. # The following attribute accesses are ineffective for the general
  195. # Flask-Login case, because the User interface of Flask-Login does not
  196. # care about anything but the ID. However, Flask-User (based on
  197. # Flask-Login) documents a few optional extra attributes.
  198. #
  199. # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names
  200. try:
  201. user_info.setdefault("email", user.email)
  202. except Exception:
  203. pass
  204. try:
  205. user_info.setdefault("username", user.username)
  206. except Exception:
  207. pass