| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- 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
|