diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/integrations/flask.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/sentry_sdk/integrations/flask.py | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/flask.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/flask.py new file mode 100644 index 00000000..f45ec6db --- /dev/null +++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/flask.py @@ -0,0 +1,275 @@ +import sentry_sdk +from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + RequestExtractor, +) +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Union + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + from werkzeug.datastructures import FileStorage, ImmutableMultiDict + + +try: + import flask_login # type: ignore +except ImportError: + flask_login = None + +try: + from flask import Flask, Request # type: ignore + from flask import request as flask_request + from flask.signals import ( + before_render_template, + got_request_exception, + request_started, + ) + from markupsafe import Markup +except ImportError: + raise DidNotEnable("Flask is not installed") + +try: + import blinker # noqa +except ImportError: + raise DidNotEnable("blinker is not installed") + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class FlaskIntegration(Integration): + identifier = "flask" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style="endpoint", # type: str + http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...] + ): + # type: (...) -> None + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + @staticmethod + def setup_once(): + # type: () -> None + try: + from quart import Quart # type: ignore + + if Flask == Quart: + # This is Quart masquerading as Flask, don't enable the Flask + # integration. See https://github.com/getsentry/sentry-python/issues/2709 + raise DidNotEnable( + "This is not a Flask app but rather Quart pretending to be Flask" + ) + except ImportError: + pass + + version = package_version("flask") + _check_minimum_version(FlaskIntegration, version) + + before_render_template.connect(_add_sentry_trace) + request_started.connect(_request_started) + got_request_exception.connect(_capture_exception) + + old_app = Flask.__call__ + + def sentry_patched_wsgi_app(self, environ, start_response): + # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse + if sentry_sdk.get_client().get_integration(FlaskIntegration) is None: + return old_app(self, environ, start_response) + + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + + middleware = SentryWsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=FlaskIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + ) + return middleware(environ, start_response) + + Flask.__call__ = sentry_patched_wsgi_app + + +def _add_sentry_trace(sender, template, context, **extra): + # type: (Flask, Any, Dict[str, Any], **Any) -> None + if "sentry_trace" in context: + return + + scope = sentry_sdk.get_current_scope() + trace_meta = Markup(scope.trace_propagation_meta()) + context["sentry_trace"] = trace_meta # for backwards compatibility + context["sentry_trace_meta"] = trace_meta + + +def _set_transaction_name_and_source(scope, transaction_style, request): + # type: (sentry_sdk.Scope, str, Request) -> None + try: + name_for_style = { + "url": request.url_rule.rule, + "endpoint": request.url_rule.endpoint, + } + scope.set_transaction_name( + name_for_style[transaction_style], + source=SOURCE_FOR_STYLE[transaction_style], + ) + except Exception: + pass + + +def _request_started(app, **kwargs): + # type: (Flask, **Any) -> None + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + if integration is None: + return + + request = flask_request._get_current_object() + + # Set the transaction name and source here, + # but rely on WSGI middleware to actually start the transaction + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + + scope = sentry_sdk.get_isolation_scope() + evt_processor = _make_request_event_processor(app, request, integration) + scope.add_event_processor(evt_processor) + + +class FlaskRequestExtractor(RequestExtractor): + def env(self): + # type: () -> Dict[str, str] + return self.request.environ + + def cookies(self): + # type: () -> Dict[Any, Any] + return { + k: v[0] if isinstance(v, list) and len(v) == 1 else v + for k, v in self.request.cookies.items() + } + + def raw_data(self): + # type: () -> bytes + return self.request.get_data() + + def form(self): + # type: () -> ImmutableMultiDict[str, Any] + return self.request.form + + def files(self): + # type: () -> ImmutableMultiDict[str, Any] + return self.request.files + + def is_json(self): + # type: () -> bool + return self.request.is_json + + def json(self): + # type: () -> Any + return self.request.get_json(silent=True) + + def size_of_file(self, file): + # type: (FileStorage) -> int + return file.content_length + + +def _make_request_event_processor(app, request, integration): + # type: (Flask, Callable[[], Request], FlaskIntegration) -> EventProcessor + + def inner(event, hint): + # type: (Event, dict[str, Any]) -> Event + + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + FlaskRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _add_user_to_event(event) + + return event + + return inner + + +@ensure_integration_enabled(FlaskIntegration) +def _capture_exception(sender, exception, **kwargs): + # type: (Flask, Union[ValueError, BaseException], **Any) -> None + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "flask", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _add_user_to_event(event): + # type: (Event) -> None + if flask_login is None: + return + + user = flask_login.current_user + if user is None: + return + + with capture_internal_exceptions(): + # Access this object as late as possible as accessing the user + # is relatively costly + + user_info = event.setdefault("user", {}) + + try: + user_info.setdefault("id", user.get_id()) + # TODO: more configurable user attrs here + except AttributeError: + # might happen if: + # - flask_login could not be imported + # - flask_login is not configured + # - no user is logged in + pass + + # The following attribute accesses are ineffective for the general + # Flask-Login case, because the User interface of Flask-Login does not + # care about anything but the ID. However, Flask-User (based on + # Flask-Login) documents a few optional extra attributes. + # + # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names + + try: + user_info.setdefault("email", user.email) + except Exception: + pass + + try: + user_info.setdefault("username", user.username) + except Exception: + pass |