| import functools |
|
|
| import sentry_sdk |
| from sentry_sdk.tracing import SOURCE_FOR_STYLE |
| from sentry_sdk.utils import ( |
| capture_internal_exceptions, |
| ensure_integration_enabled, |
| event_from_exception, |
| parse_version, |
| transaction_from_function, |
| ) |
| from sentry_sdk.integrations import ( |
| Integration, |
| DidNotEnable, |
| _DEFAULT_FAILED_REQUEST_STATUS_CODES, |
| _check_minimum_version, |
| ) |
| from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware |
| from sentry_sdk.integrations._wsgi_common import RequestExtractor |
|
|
| from typing import TYPE_CHECKING |
|
|
| if TYPE_CHECKING: |
| from collections.abc import Set |
|
|
| from sentry_sdk.integrations.wsgi import _ScopedResponse |
| from typing import Any |
| from typing import Dict |
| from typing import Callable |
| from typing import Optional |
| from bottle import FileUpload, FormsDict, LocalRequest |
|
|
| from sentry_sdk._types import EventProcessor, Event |
|
|
| try: |
| from bottle import ( |
| Bottle, |
| HTTPResponse, |
| Route, |
| request as bottle_request, |
| __version__ as BOTTLE_VERSION, |
| ) |
| except ImportError: |
| raise DidNotEnable("Bottle not installed") |
|
|
|
|
| TRANSACTION_STYLE_VALUES = ("endpoint", "url") |
|
|
|
|
| class BottleIntegration(Integration): |
| identifier = "bottle" |
| origin = f"auto.http.{identifier}" |
|
|
| transaction_style = "" |
|
|
| def __init__( |
| self, |
| transaction_style="endpoint", |
| *, |
| failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, |
| ): |
| |
|
|
| 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.failed_request_status_codes = failed_request_status_codes |
|
|
| @staticmethod |
| def setup_once(): |
| |
| version = parse_version(BOTTLE_VERSION) |
| _check_minimum_version(BottleIntegration, version) |
|
|
| old_app = Bottle.__call__ |
|
|
| @ensure_integration_enabled(BottleIntegration, old_app) |
| def sentry_patched_wsgi_app(self, environ, start_response): |
| |
| middleware = SentryWsgiMiddleware( |
| lambda *a, **kw: old_app(self, *a, **kw), |
| span_origin=BottleIntegration.origin, |
| ) |
|
|
| return middleware(environ, start_response) |
|
|
| Bottle.__call__ = sentry_patched_wsgi_app |
|
|
| old_handle = Bottle._handle |
|
|
| @functools.wraps(old_handle) |
| def _patched_handle(self, environ): |
| |
| integration = sentry_sdk.get_client().get_integration(BottleIntegration) |
| if integration is None: |
| return old_handle(self, environ) |
|
|
| scope = sentry_sdk.get_isolation_scope() |
| scope._name = "bottle" |
| scope.add_event_processor( |
| _make_request_event_processor(self, bottle_request, integration) |
| ) |
| res = old_handle(self, environ) |
|
|
| return res |
|
|
| Bottle._handle = _patched_handle |
|
|
| old_make_callback = Route._make_callback |
|
|
| @functools.wraps(old_make_callback) |
| def patched_make_callback(self, *args, **kwargs): |
| |
| prepared_callback = old_make_callback(self, *args, **kwargs) |
|
|
| integration = sentry_sdk.get_client().get_integration(BottleIntegration) |
| if integration is None: |
| return prepared_callback |
|
|
| def wrapped_callback(*args, **kwargs): |
| |
| try: |
| res = prepared_callback(*args, **kwargs) |
| except Exception as exception: |
| _capture_exception(exception, handled=False) |
| raise exception |
|
|
| if ( |
| isinstance(res, HTTPResponse) |
| and res.status_code in integration.failed_request_status_codes |
| ): |
| _capture_exception(res, handled=True) |
|
|
| return res |
|
|
| return wrapped_callback |
|
|
| Route._make_callback = patched_make_callback |
|
|
|
|
| class BottleRequestExtractor(RequestExtractor): |
| def env(self): |
| |
| return self.request.environ |
|
|
| def cookies(self): |
| |
| return self.request.cookies |
|
|
| def raw_data(self): |
| |
| return self.request.body.read() |
|
|
| def form(self): |
| |
| if self.is_json(): |
| return None |
| return self.request.forms.decode() |
|
|
| def files(self): |
| |
| if self.is_json(): |
| return None |
|
|
| return self.request.files |
|
|
| def size_of_file(self, file): |
| |
| return file.content_length |
|
|
|
|
| def _set_transaction_name_and_source(event, transaction_style, request): |
| |
| name = "" |
|
|
| if transaction_style == "url": |
| try: |
| name = request.route.rule or "" |
| except RuntimeError: |
| pass |
|
|
| elif transaction_style == "endpoint": |
| try: |
| name = ( |
| request.route.name |
| or transaction_from_function(request.route.callback) |
| or "" |
| ) |
| except RuntimeError: |
| pass |
|
|
| event["transaction"] = name |
| event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} |
|
|
|
|
| def _make_request_event_processor(app, request, integration): |
| |
|
|
| def event_processor(event, hint): |
| |
| _set_transaction_name_and_source(event, integration.transaction_style, request) |
|
|
| with capture_internal_exceptions(): |
| BottleRequestExtractor(request).extract_into_event(event) |
|
|
| return event |
|
|
| return event_processor |
|
|
|
|
| def _capture_exception(exception, handled): |
| |
| event, hint = event_from_exception( |
| exception, |
| client_options=sentry_sdk.get_client().options, |
| mechanism={"type": "bottle", "handled": handled}, |
| ) |
| sentry_sdk.capture_event(event, hint=hint) |
|
|