Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/integrations/sanic.py: 7%
183 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import sys
2import weakref
3from inspect import isawaitable
5from sentry_sdk._compat import urlparse, reraise
6from sentry_sdk.hub import Hub
7from sentry_sdk.tracing import TRANSACTION_SOURCE_COMPONENT
8from sentry_sdk.utils import (
9 capture_internal_exceptions,
10 event_from_exception,
11 HAS_REAL_CONTEXTVARS,
12 CONTEXTVARS_ERROR_MESSAGE,
13)
14from sentry_sdk.integrations import Integration, DidNotEnable
15from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers
16from sentry_sdk.integrations.logging import ignore_logger
18from sentry_sdk._types import MYPY
20if MYPY: 20 ↛ 21line 20 didn't jump to line 21, because the condition on line 20 was never true
21 from typing import Any
22 from typing import Callable
23 from typing import Optional
24 from typing import Union
25 from typing import Tuple
26 from typing import Dict
28 from sanic.request import Request, RequestParameters
30 from sentry_sdk._types import Event, EventProcessor, Hint
31 from sanic.router import Route
33try:
34 from sanic import Sanic, __version__ as SANIC_VERSION
35 from sanic.exceptions import SanicException
36 from sanic.router import Router
37 from sanic.handlers import ErrorHandler
38except ImportError:
39 raise DidNotEnable("Sanic not installed")
41old_error_handler_lookup = ErrorHandler.lookup
42old_handle_request = Sanic.handle_request
43old_router_get = Router.get
45try:
46 # This method was introduced in Sanic v21.9
47 old_startup = Sanic._startup
48except AttributeError:
49 pass
52class SanicIntegration(Integration):
53 identifier = "sanic"
54 version = (0, 0) # type: Tuple[int, ...]
56 @staticmethod
57 def setup_once():
58 # type: () -> None
60 try:
61 SanicIntegration.version = tuple(map(int, SANIC_VERSION.split(".")))
62 except (TypeError, ValueError):
63 raise DidNotEnable("Unparsable Sanic version: {}".format(SANIC_VERSION))
65 if SanicIntegration.version < (0, 8):
66 raise DidNotEnable("Sanic 0.8 or newer required.")
68 if not HAS_REAL_CONTEXTVARS:
69 # We better have contextvars or we're going to leak state between
70 # requests.
71 raise DidNotEnable(
72 "The sanic integration for Sentry requires Python 3.7+ "
73 " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE
74 )
76 if SANIC_VERSION.startswith("0.8."):
77 # Sanic 0.8 and older creates a logger named "root" and puts a
78 # stringified version of every exception in there (without exc_info),
79 # which our error deduplication can't detect.
80 #
81 # We explicitly check the version here because it is a very
82 # invasive step to ignore this logger and not necessary in newer
83 # versions at all.
84 #
85 # https://github.com/huge-success/sanic/issues/1332
86 ignore_logger("root")
88 if SanicIntegration.version < (21, 9):
89 _setup_legacy_sanic()
90 return
92 _setup_sanic()
95class SanicRequestExtractor(RequestExtractor):
96 def content_length(self):
97 # type: () -> int
98 if self.request.body is None:
99 return 0
100 return len(self.request.body)
102 def cookies(self):
103 # type: () -> Dict[str, str]
104 return dict(self.request.cookies)
106 def raw_data(self):
107 # type: () -> bytes
108 return self.request.body
110 def form(self):
111 # type: () -> RequestParameters
112 return self.request.form
114 def is_json(self):
115 # type: () -> bool
116 raise NotImplementedError()
118 def json(self):
119 # type: () -> Optional[Any]
120 return self.request.json
122 def files(self):
123 # type: () -> RequestParameters
124 return self.request.files
126 def size_of_file(self, file):
127 # type: (Any) -> int
128 return len(file.body or ())
131def _setup_sanic():
132 # type: () -> None
133 Sanic._startup = _startup
134 ErrorHandler.lookup = _sentry_error_handler_lookup
137def _setup_legacy_sanic():
138 # type: () -> None
139 Sanic.handle_request = _legacy_handle_request
140 Router.get = _legacy_router_get
141 ErrorHandler.lookup = _sentry_error_handler_lookup
144async def _startup(self):
145 # type: (Sanic) -> None
146 # This happens about as early in the lifecycle as possible, just after the
147 # Request object is created. The body has not yet been consumed.
148 self.signal("http.lifecycle.request")(_hub_enter)
150 # This happens after the handler is complete. In v21.9 this signal is not
151 # dispatched when there is an exception. Therefore we need to close out
152 # and call _hub_exit from the custom exception handler as well.
153 # See https://github.com/sanic-org/sanic/issues/2297
154 self.signal("http.lifecycle.response")(_hub_exit)
156 # This happens inside of request handling immediately after the route
157 # has been identified by the router.
158 self.signal("http.routing.after")(_set_transaction)
160 # The above signals need to be declared before this can be called.
161 await old_startup(self)
164async def _hub_enter(request):
165 # type: (Request) -> None
166 hub = Hub.current
167 request.ctx._sentry_do_integration = (
168 hub.get_integration(SanicIntegration) is not None
169 )
171 if not request.ctx._sentry_do_integration:
172 return
174 weak_request = weakref.ref(request)
175 request.ctx._sentry_hub = Hub(hub)
176 request.ctx._sentry_hub.__enter__()
178 with request.ctx._sentry_hub.configure_scope() as scope:
179 scope.clear_breadcrumbs()
180 scope.add_event_processor(_make_request_processor(weak_request))
183async def _hub_exit(request, **_):
184 # type: (Request, **Any) -> None
185 request.ctx._sentry_hub.__exit__(None, None, None)
188async def _set_transaction(request, route, **kwargs):
189 # type: (Request, Route, **Any) -> None
190 hub = Hub.current
191 if hub.get_integration(SanicIntegration) is not None:
192 with capture_internal_exceptions():
193 with hub.configure_scope() as scope:
194 route_name = route.name.replace(request.app.name, "").strip(".")
195 scope.set_transaction_name(
196 route_name, source=TRANSACTION_SOURCE_COMPONENT
197 )
200def _sentry_error_handler_lookup(self, exception, *args, **kwargs):
201 # type: (Any, Exception, *Any, **Any) -> Optional[object]
202 _capture_exception(exception)
203 old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs)
205 if old_error_handler is None:
206 return None
208 if Hub.current.get_integration(SanicIntegration) is None:
209 return old_error_handler
211 async def sentry_wrapped_error_handler(request, exception):
212 # type: (Request, Exception) -> Any
213 try:
214 response = old_error_handler(request, exception)
215 if isawaitable(response):
216 response = await response
217 return response
218 except Exception:
219 # Report errors that occur in Sanic error handler. These
220 # exceptions will not even show up in Sanic's
221 # `sanic.exceptions` logger.
222 exc_info = sys.exc_info()
223 _capture_exception(exc_info)
224 reraise(*exc_info)
225 finally:
226 # As mentioned in previous comment in _startup, this can be removed
227 # after https://github.com/sanic-org/sanic/issues/2297 is resolved
228 if SanicIntegration.version == (21, 9):
229 await _hub_exit(request)
231 return sentry_wrapped_error_handler
234async def _legacy_handle_request(self, request, *args, **kwargs):
235 # type: (Any, Request, *Any, **Any) -> Any
236 hub = Hub.current
237 if hub.get_integration(SanicIntegration) is None:
238 return old_handle_request(self, request, *args, **kwargs)
240 weak_request = weakref.ref(request)
242 with Hub(hub) as hub:
243 with hub.configure_scope() as scope:
244 scope.clear_breadcrumbs()
245 scope.add_event_processor(_make_request_processor(weak_request))
247 response = old_handle_request(self, request, *args, **kwargs)
248 if isawaitable(response):
249 response = await response
251 return response
254def _legacy_router_get(self, *args):
255 # type: (Any, Union[Any, Request]) -> Any
256 rv = old_router_get(self, *args)
257 hub = Hub.current
258 if hub.get_integration(SanicIntegration) is not None:
259 with capture_internal_exceptions():
260 with hub.configure_scope() as scope:
261 if SanicIntegration.version and SanicIntegration.version >= (21, 3):
262 # Sanic versions above and including 21.3 append the app name to the
263 # route name, and so we need to remove it from Route name so the
264 # transaction name is consistent across all versions
265 sanic_app_name = self.ctx.app.name
266 sanic_route = rv[0].name
268 if sanic_route.startswith("%s." % sanic_app_name):
269 # We add a 1 to the len of the sanic_app_name because there is a dot
270 # that joins app name and the route name
271 # Format: app_name.route_name
272 sanic_route = sanic_route[len(sanic_app_name) + 1 :]
274 scope.set_transaction_name(
275 sanic_route, source=TRANSACTION_SOURCE_COMPONENT
276 )
277 else:
278 scope.set_transaction_name(
279 rv[0].__name__, source=TRANSACTION_SOURCE_COMPONENT
280 )
282 return rv
285def _capture_exception(exception):
286 # type: (Union[Tuple[Optional[type], Optional[BaseException], Any], BaseException]) -> None
287 hub = Hub.current
288 integration = hub.get_integration(SanicIntegration)
289 if integration is None:
290 return
292 # If an integration is there, a client has to be there.
293 client = hub.client # type: Any
295 with capture_internal_exceptions():
296 event, hint = event_from_exception(
297 exception,
298 client_options=client.options,
299 mechanism={"type": "sanic", "handled": False},
300 )
301 hub.capture_event(event, hint=hint)
304def _make_request_processor(weak_request):
305 # type: (Callable[[], Request]) -> EventProcessor
306 def sanic_processor(event, hint):
307 # type: (Event, Optional[Hint]) -> Optional[Event]
309 try:
310 if hint and issubclass(hint["exc_info"][0], SanicException):
311 return None
312 except KeyError:
313 pass
315 request = weak_request()
316 if request is None:
317 return event
319 with capture_internal_exceptions():
320 extractor = SanicRequestExtractor(request)
321 extractor.extract_into_event(event)
323 request_info = event["request"]
324 urlparts = urlparse.urlsplit(request.url)
326 request_info["url"] = "%s://%s%s" % (
327 urlparts.scheme,
328 urlparts.netloc,
329 urlparts.path,
330 )
332 request_info["query_string"] = urlparts.query
333 request_info["method"] = request.method
334 request_info["env"] = {"REMOTE_ADDR": request.remote_addr}
335 request_info["headers"] = _filter_headers(dict(request.headers))
337 return event
339 return sanic_processor