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