flask.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  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="endpoint", # type: str
  49. http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
  50. ):
  51. # type: (...) -> None
  52. if transaction_style not in TRANSACTION_STYLE_VALUES:
  53. raise ValueError(
  54. "Invalid value for transaction_style: %s (must be in %s)"
  55. % (transaction_style, TRANSACTION_STYLE_VALUES)
  56. )
  57. self.transaction_style = transaction_style
  58. self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
  59. @staticmethod
  60. def setup_once():
  61. # type: () -> None
  62. try:
  63. from quart import Quart # type: ignore
  64. if Flask == Quart:
  65. # This is Quart masquerading as Flask, don't enable the Flask
  66. # integration. See https://github.com/getsentry/sentry-python/issues/2709
  67. raise DidNotEnable(
  68. "This is not a Flask app but rather Quart pretending to be Flask"
  69. )
  70. except ImportError:
  71. pass
  72. version = package_version("flask")
  73. _check_minimum_version(FlaskIntegration, version)
  74. before_render_template.connect(_add_sentry_trace)
  75. request_started.connect(_request_started)
  76. got_request_exception.connect(_capture_exception)
  77. old_app = Flask.__call__
  78. def sentry_patched_wsgi_app(self, environ, start_response):
  79. # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse
  80. if sentry_sdk.get_client().get_integration(FlaskIntegration) is None:
  81. return old_app(self, environ, start_response)
  82. integration = sentry_sdk.get_client().get_integration(FlaskIntegration)
  83. middleware = SentryWsgiMiddleware(
  84. lambda *a, **kw: old_app(self, *a, **kw),
  85. span_origin=FlaskIntegration.origin,
  86. http_methods_to_capture=(
  87. integration.http_methods_to_capture
  88. if integration
  89. else DEFAULT_HTTP_METHODS_TO_CAPTURE
  90. ),
  91. )
  92. return middleware(environ, start_response)
  93. Flask.__call__ = sentry_patched_wsgi_app
  94. def _add_sentry_trace(sender, template, context, **extra):
  95. # type: (Flask, Any, Dict[str, Any], **Any) -> 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(scope, transaction_style, request):
  103. # type: (sentry_sdk.Scope, str, Request) -> None
  104. try:
  105. name_for_style = {
  106. "url": request.url_rule.rule,
  107. "endpoint": request.url_rule.endpoint,
  108. }
  109. scope.set_transaction_name(
  110. name_for_style[transaction_style],
  111. source=SOURCE_FOR_STYLE[transaction_style],
  112. )
  113. except Exception:
  114. pass
  115. def _request_started(app, **kwargs):
  116. # type: (Flask, **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):
  131. # type: () -> Dict[str, str]
  132. return self.request.environ
  133. def cookies(self):
  134. # type: () -> Dict[Any, Any]
  135. return {
  136. k: v[0] if isinstance(v, list) and len(v) == 1 else v
  137. for k, v in self.request.cookies.items()
  138. }
  139. def raw_data(self):
  140. # type: () -> bytes
  141. return self.request.get_data()
  142. def form(self):
  143. # type: () -> ImmutableMultiDict[str, Any]
  144. return self.request.form
  145. def files(self):
  146. # type: () -> ImmutableMultiDict[str, Any]
  147. return self.request.files
  148. def is_json(self):
  149. # type: () -> bool
  150. return self.request.is_json
  151. def json(self):
  152. # type: () -> Any
  153. return self.request.get_json(silent=True)
  154. def size_of_file(self, file):
  155. # type: (FileStorage) -> int
  156. return file.content_length
  157. def _make_request_event_processor(app, request, integration):
  158. # type: (Flask, Callable[[], Request], FlaskIntegration) -> EventProcessor
  159. def inner(event, hint):
  160. # type: (Event, dict[str, Any]) -> Event
  161. # if the request is gone we are fine not logging the data from
  162. # it. This might happen if the processor is pushed away to
  163. # another thread.
  164. if request is None:
  165. return event
  166. with capture_internal_exceptions():
  167. FlaskRequestExtractor(request).extract_into_event(event)
  168. if should_send_default_pii():
  169. with capture_internal_exceptions():
  170. _add_user_to_event(event)
  171. return event
  172. return inner
  173. @ensure_integration_enabled(FlaskIntegration)
  174. def _capture_exception(sender, exception, **kwargs):
  175. # type: (Flask, Union[ValueError, BaseException], **Any) -> None
  176. event, hint = event_from_exception(
  177. exception,
  178. client_options=sentry_sdk.get_client().options,
  179. mechanism={"type": "flask", "handled": False},
  180. )
  181. sentry_sdk.capture_event(event, hint=hint)
  182. def _add_user_to_event(event):
  183. # type: (Event) -> None
  184. if flask_login is None:
  185. return
  186. user = flask_login.current_user
  187. if user is None:
  188. return
  189. with capture_internal_exceptions():
  190. # Access this object as late as possible as accessing the user
  191. # is relatively costly
  192. user_info = event.setdefault("user", {})
  193. try:
  194. user_info.setdefault("id", user.get_id())
  195. # TODO: more configurable user attrs here
  196. except AttributeError:
  197. # might happen if:
  198. # - flask_login could not be imported
  199. # - flask_login is not configured
  200. # - no user is logged in
  201. pass
  202. # The following attribute accesses are ineffective for the general
  203. # Flask-Login case, because the User interface of Flask-Login does not
  204. # care about anything but the ID. However, Flask-User (based on
  205. # Flask-Login) documents a few optional extra attributes.
  206. #
  207. # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names
  208. try:
  209. user_info.setdefault("email", user.email)
  210. except Exception:
  211. pass
  212. try:
  213. user_info.setdefault("username", user.username)
  214. except Exception:
  215. pass