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

1import sys 

2import weakref 

3from inspect import isawaitable 

4 

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 

17 

18from sentry_sdk._types import MYPY 

19 

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 

27 

28 from sanic.request import Request, RequestParameters 

29 

30 from sentry_sdk._types import Event, EventProcessor, Hint 

31 from sanic.router import Route 

32 

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") 

40 

41old_error_handler_lookup = ErrorHandler.lookup 

42old_handle_request = Sanic.handle_request 

43old_router_get = Router.get 

44 

45try: 

46 # This method was introduced in Sanic v21.9 

47 old_startup = Sanic._startup 

48except AttributeError: 

49 pass 

50 

51 

52class SanicIntegration(Integration): 

53 identifier = "sanic" 

54 version = (0, 0) # type: Tuple[int, ...] 

55 

56 @staticmethod 

57 def setup_once(): 

58 # type: () -> None 

59 

60 try: 

61 SanicIntegration.version = tuple(map(int, SANIC_VERSION.split("."))) 

62 except (TypeError, ValueError): 

63 raise DidNotEnable("Unparsable Sanic version: {}".format(SANIC_VERSION)) 

64 

65 if SanicIntegration.version < (0, 8): 

66 raise DidNotEnable("Sanic 0.8 or newer required.") 

67 

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 ) 

75 

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") 

87 

88 if SanicIntegration.version < (21, 9): 

89 _setup_legacy_sanic() 

90 return 

91 

92 _setup_sanic() 

93 

94 

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) 

101 

102 def cookies(self): 

103 # type: () -> Dict[str, str] 

104 return dict(self.request.cookies) 

105 

106 def raw_data(self): 

107 # type: () -> bytes 

108 return self.request.body 

109 

110 def form(self): 

111 # type: () -> RequestParameters 

112 return self.request.form 

113 

114 def is_json(self): 

115 # type: () -> bool 

116 raise NotImplementedError() 

117 

118 def json(self): 

119 # type: () -> Optional[Any] 

120 return self.request.json 

121 

122 def files(self): 

123 # type: () -> RequestParameters 

124 return self.request.files 

125 

126 def size_of_file(self, file): 

127 # type: (Any) -> int 

128 return len(file.body or ()) 

129 

130 

131def _setup_sanic(): 

132 # type: () -> None 

133 Sanic._startup = _startup 

134 ErrorHandler.lookup = _sentry_error_handler_lookup 

135 

136 

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 

142 

143 

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) 

149 

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) 

155 

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) 

159 

160 # The above signals need to be declared before this can be called. 

161 await old_startup(self) 

162 

163 

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 ) 

170 

171 if not request.ctx._sentry_do_integration: 

172 return 

173 

174 weak_request = weakref.ref(request) 

175 request.ctx._sentry_hub = Hub(hub) 

176 request.ctx._sentry_hub.__enter__() 

177 

178 with request.ctx._sentry_hub.configure_scope() as scope: 

179 scope.clear_breadcrumbs() 

180 scope.add_event_processor(_make_request_processor(weak_request)) 

181 

182 

183async def _hub_exit(request, **_): 

184 # type: (Request, **Any) -> None 

185 request.ctx._sentry_hub.__exit__(None, None, None) 

186 

187 

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 ) 

198 

199 

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) 

204 

205 if old_error_handler is None: 

206 return None 

207 

208 if Hub.current.get_integration(SanicIntegration) is None: 

209 return old_error_handler 

210 

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) 

230 

231 return sentry_wrapped_error_handler 

232 

233 

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) 

239 

240 weak_request = weakref.ref(request) 

241 

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

246 

247 response = old_handle_request(self, request, *args, **kwargs) 

248 if isawaitable(response): 

249 response = await response 

250 

251 return response 

252 

253 

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 

267 

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 :] 

273 

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 ) 

281 

282 return rv 

283 

284 

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 

291 

292 # If an integration is there, a client has to be there. 

293 client = hub.client # type: Any 

294 

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) 

302 

303 

304def _make_request_processor(weak_request): 

305 # type: (Callable[[], Request]) -> EventProcessor 

306 def sanic_processor(event, hint): 

307 # type: (Event, Optional[Hint]) -> Optional[Event] 

308 

309 try: 

310 if hint and issubclass(hint["exc_info"][0], SanicException): 

311 return None 

312 except KeyError: 

313 pass 

314 

315 request = weak_request() 

316 if request is None: 

317 return event 

318 

319 with capture_internal_exceptions(): 

320 extractor = SanicRequestExtractor(request) 

321 extractor.extract_into_event(event) 

322 

323 request_info = event["request"] 

324 urlparts = urlparse.urlsplit(request.url) 

325 

326 request_info["url"] = "%s://%s%s" % ( 

327 urlparts.scheme, 

328 urlparts.netloc, 

329 urlparts.path, 

330 ) 

331 

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

336 

337 return event 

338 

339 return sanic_processor