1from __future__ import absolute_import 2 3from sentry_sdk.hub import Hub 4from sentry_sdk.utils import ( 5 capture_internal_exceptions, 6 event_from_exception, 7 transaction_from_function, 8) 9from sentry_sdk.integrations import Integration, DidNotEnable 10from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware 11from sentry_sdk.integrations._wsgi_common import RequestExtractor 12 13from sentry_sdk._types import MYPY 14 15if MYPY: 16 from sentry_sdk.integrations.wsgi import _ScopedResponse 17 from typing import Any 18 from typing import Dict 19 from typing import Callable 20 from typing import Optional 21 from bottle import FileUpload, FormsDict, LocalRequest # type: ignore 22 23 from sentry_sdk._types import EventProcessor 24 25try: 26 from bottle import ( 27 Bottle, 28 Route, 29 request as bottle_request, 30 HTTPResponse, 31 __version__ as BOTTLE_VERSION, 32 ) 33except ImportError: 34 raise DidNotEnable("Bottle not installed") 35 36 37TRANSACTION_STYLE_VALUES = ("endpoint", "url") 38 39 40class BottleIntegration(Integration): 41 identifier = "bottle" 42 43 transaction_style = None 44 45 def __init__(self, transaction_style="endpoint"): 46 # type: (str) -> None 47 48 if transaction_style not in TRANSACTION_STYLE_VALUES: 49 raise ValueError( 50 "Invalid value for transaction_style: %s (must be in %s)" 51 % (transaction_style, TRANSACTION_STYLE_VALUES) 52 ) 53 self.transaction_style = transaction_style 54 55 @staticmethod 56 def setup_once(): 57 # type: () -> None 58 59 try: 60 version = tuple(map(int, BOTTLE_VERSION.split("."))) 61 except (TypeError, ValueError): 62 raise DidNotEnable("Unparseable Bottle version: {}".format(version)) 63 64 if version < (0, 12): 65 raise DidNotEnable("Bottle 0.12 or newer required.") 66 67 # monkey patch method Bottle.__call__ 68 old_app = Bottle.__call__ 69 70 def sentry_patched_wsgi_app(self, environ, start_response): 71 # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse 72 73 hub = Hub.current 74 integration = hub.get_integration(BottleIntegration) 75 if integration is None: 76 return old_app(self, environ, start_response) 77 78 return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))( 79 environ, start_response 80 ) 81 82 Bottle.__call__ = sentry_patched_wsgi_app 83 84 # monkey patch method Bottle._handle 85 old_handle = Bottle._handle 86 87 def _patched_handle(self, environ): 88 # type: (Bottle, Dict[str, Any]) -> Any 89 hub = Hub.current 90 integration = hub.get_integration(BottleIntegration) 91 if integration is None: 92 return old_handle(self, environ) 93 94 # create new scope 95 scope_manager = hub.push_scope() 96 97 with scope_manager: 98 app = self 99 with hub.configure_scope() as scope: 100 scope._name = "bottle" 101 scope.add_event_processor( 102 _make_request_event_processor(app, bottle_request, integration) 103 ) 104 res = old_handle(self, environ) 105 106 # scope cleanup 107 return res 108 109 Bottle._handle = _patched_handle 110 111 # monkey patch method Route._make_callback 112 old_make_callback = Route._make_callback 113 114 def patched_make_callback(self, *args, **kwargs): 115 # type: (Route, *object, **object) -> Any 116 hub = Hub.current 117 integration = hub.get_integration(BottleIntegration) 118 prepared_callback = old_make_callback(self, *args, **kwargs) 119 if integration is None: 120 return prepared_callback 121 122 # If an integration is there, a client has to be there. 123 client = hub.client # type: Any 124 125 def wrapped_callback(*args, **kwargs): 126 # type: (*object, **object) -> Any 127 128 try: 129 res = prepared_callback(*args, **kwargs) 130 except HTTPResponse: 131 raise 132 except Exception as exception: 133 event, hint = event_from_exception( 134 exception, 135 client_options=client.options, 136 mechanism={"type": "bottle", "handled": False}, 137 ) 138 hub.capture_event(event, hint=hint) 139 raise exception 140 141 return res 142 143 return wrapped_callback 144 145 Route._make_callback = patched_make_callback 146 147 148class BottleRequestExtractor(RequestExtractor): 149 def env(self): 150 # type: () -> Dict[str, str] 151 return self.request.environ 152 153 def cookies(self): 154 # type: () -> Dict[str, str] 155 return self.request.cookies 156 157 def raw_data(self): 158 # type: () -> bytes 159 return self.request.body.read() 160 161 def form(self): 162 # type: () -> FormsDict 163 if self.is_json(): 164 return None 165 return self.request.forms.decode() 166 167 def files(self): 168 # type: () -> Optional[Dict[str, str]] 169 if self.is_json(): 170 return None 171 172 return self.request.files 173 174 def size_of_file(self, file): 175 # type: (FileUpload) -> int 176 return file.content_length 177 178 179def _make_request_event_processor(app, request, integration): 180 # type: (Bottle, LocalRequest, BottleIntegration) -> EventProcessor 181 def inner(event, hint): 182 # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] 183 184 try: 185 if integration.transaction_style == "endpoint": 186 event["transaction"] = request.route.name or transaction_from_function( 187 request.route.callback 188 ) 189 elif integration.transaction_style == "url": 190 event["transaction"] = request.route.rule 191 except Exception: 192 pass 193 194 with capture_internal_exceptions(): 195 BottleRequestExtractor(request).extract_into_event(event) 196 197 return event 198 199 return inner 200