Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/integrations/django/__init__.py: 50%

301 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1# -*- coding: utf-8 -*- 

2from __future__ import absolute_import 

3 

4import sys 

5import threading 

6import weakref 

7 

8from sentry_sdk._types import MYPY 

9from sentry_sdk.hub import Hub, _should_send_default_pii 

10from sentry_sdk.scope import add_global_event_processor 

11from sentry_sdk.serializer import add_global_repr_processor 

12from sentry_sdk.tracing import SOURCE_FOR_STYLE 

13from sentry_sdk.tracing_utils import record_sql_queries 

14from sentry_sdk.utils import ( 

15 HAS_REAL_CONTEXTVARS, 

16 CONTEXTVARS_ERROR_MESSAGE, 

17 logger, 

18 capture_internal_exceptions, 

19 event_from_exception, 

20 transaction_from_function, 

21 walk_exception_chain, 

22) 

23from sentry_sdk.integrations import Integration, DidNotEnable 

24from sentry_sdk.integrations.logging import ignore_logger 

25from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware 

26from sentry_sdk.integrations._wsgi_common import RequestExtractor 

27 

28try: 

29 from django import VERSION as DJANGO_VERSION 

30 from django.core import signals 

31 

32 try: 

33 from django.urls import resolve 

34 except ImportError: 

35 from django.core.urlresolvers import resolve 

36except ImportError: 

37 raise DidNotEnable("Django not installed") 

38 

39 

40from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER 

41from sentry_sdk.integrations.django.templates import ( 

42 get_template_frame_from_exception, 

43 patch_templates, 

44) 

45from sentry_sdk.integrations.django.middleware import patch_django_middlewares 

46from sentry_sdk.integrations.django.views import patch_views 

47 

48 

49if MYPY: 49 ↛ 50line 49 didn't jump to line 50, because the condition on line 49 was never true

50 from typing import Any 

51 from typing import Callable 

52 from typing import Dict 

53 from typing import Optional 

54 from typing import Union 

55 from typing import List 

56 

57 from django.core.handlers.wsgi import WSGIRequest 

58 from django.http.response import HttpResponse 

59 from django.http.request import QueryDict 

60 from django.utils.datastructures import MultiValueDict 

61 

62 from sentry_sdk.scope import Scope 

63 from sentry_sdk.integrations.wsgi import _ScopedResponse 

64 from sentry_sdk._types import Event, Hint, EventProcessor, NotImplementedType 

65 

66 

67if DJANGO_VERSION < (1, 10): 67 ↛ 69line 67 didn't jump to line 69, because the condition on line 67 was never true

68 

69 def is_authenticated(request_user): 

70 # type: (Any) -> bool 

71 return request_user.is_authenticated() 

72 

73else: 

74 

75 def is_authenticated(request_user): 

76 # type: (Any) -> bool 

77 return request_user.is_authenticated 

78 

79 

80TRANSACTION_STYLE_VALUES = ("function_name", "url") 

81 

82 

83class DjangoIntegration(Integration): 

84 identifier = "django" 

85 

86 transaction_style = "" 

87 middleware_spans = None 

88 

89 def __init__(self, transaction_style="url", middleware_spans=True): 

90 # type: (str, bool) -> None 

91 if transaction_style not in TRANSACTION_STYLE_VALUES: 91 ↛ 92line 91 didn't jump to line 92, because the condition on line 91 was never true

92 raise ValueError( 

93 "Invalid value for transaction_style: %s (must be in %s)" 

94 % (transaction_style, TRANSACTION_STYLE_VALUES) 

95 ) 

96 self.transaction_style = transaction_style 

97 self.middleware_spans = middleware_spans 

98 

99 @staticmethod 

100 def setup_once(): 

101 # type: () -> None 

102 

103 if DJANGO_VERSION < (1, 8): 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true

104 raise DidNotEnable("Django 1.8 or newer is required.") 

105 

106 install_sql_hook() 

107 # Patch in our custom middleware. 

108 

109 # logs an error for every 500 

110 ignore_logger("django.server") 

111 ignore_logger("django.request") 

112 

113 from django.core.handlers.wsgi import WSGIHandler 

114 

115 old_app = WSGIHandler.__call__ 

116 

117 def sentry_patched_wsgi_handler(self, environ, start_response): 

118 # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse 

119 if Hub.current.get_integration(DjangoIntegration) is None: 

120 return old_app(self, environ, start_response) 

121 

122 bound_old_app = old_app.__get__(self, WSGIHandler) 

123 

124 from django.conf import settings 

125 

126 use_x_forwarded_for = settings.USE_X_FORWARDED_HOST 

127 

128 return SentryWsgiMiddleware(bound_old_app, use_x_forwarded_for)( 

129 environ, start_response 

130 ) 

131 

132 WSGIHandler.__call__ = sentry_patched_wsgi_handler 

133 

134 _patch_get_response() 

135 

136 _patch_django_asgi_handler() 

137 

138 signals.got_request_exception.connect(_got_request_exception) 

139 

140 @add_global_event_processor 

141 def process_django_templates(event, hint): 

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

143 if hint is None: 

144 return event 

145 

146 exc_info = hint.get("exc_info", None) 

147 

148 if exc_info is None: 

149 return event 

150 

151 exception = event.get("exception", None) 

152 

153 if exception is None: 

154 return event 

155 

156 values = exception.get("values", None) 

157 

158 if values is None: 

159 return event 

160 

161 for exception, (_, exc_value, _) in zip( 

162 reversed(values), walk_exception_chain(exc_info) 

163 ): 

164 frame = get_template_frame_from_exception(exc_value) 

165 if frame is not None: 

166 frames = exception.get("stacktrace", {}).get("frames", []) 

167 

168 for i in reversed(range(len(frames))): 

169 f = frames[i] 

170 if ( 

171 f.get("function") in ("Parser.parse", "parse", "render") 

172 and f.get("module") == "django.template.base" 

173 ): 

174 i += 1 

175 break 

176 else: 

177 i = len(frames) 

178 

179 frames.insert(i, frame) 

180 

181 return event 

182 

183 @add_global_repr_processor 

184 def _django_queryset_repr(value, hint): 

185 # type: (Any, Dict[str, Any]) -> Union[NotImplementedType, str] 

186 try: 

187 # Django 1.6 can fail to import `QuerySet` when Django settings 

188 # have not yet been initialized. 

189 # 

190 # If we fail to import, return `NotImplemented`. It's at least 

191 # unlikely that we have a query set in `value` when importing 

192 # `QuerySet` fails. 

193 from django.db.models.query import QuerySet 

194 except Exception: 

195 return NotImplemented 

196 

197 if not isinstance(value, QuerySet) or value._result_cache: 

198 return NotImplemented 

199 

200 # Do not call Hub.get_integration here. It is intentional that 

201 # running under a new hub does not suddenly start executing 

202 # querysets. This might be surprising to the user but it's likely 

203 # less annoying. 

204 

205 return "<%s from %s at 0x%x>" % ( 

206 value.__class__.__name__, 

207 value.__module__, 

208 id(value), 

209 ) 

210 

211 _patch_channels() 

212 patch_django_middlewares() 

213 patch_views() 

214 patch_templates() 

215 

216 

217_DRF_PATCHED = False 

218_DRF_PATCH_LOCK = threading.Lock() 

219 

220 

221def _patch_drf(): 

222 # type: () -> None 

223 """ 

224 Patch Django Rest Framework for more/better request data. DRF's request 

225 type is a wrapper around Django's request type. The attribute we're 

226 interested in is `request.data`, which is a cached property containing a 

227 parsed request body. Reading a request body from that property is more 

228 reliable than reading from any of Django's own properties, as those don't 

229 hold payloads in memory and therefore can only be accessed once. 

230 

231 We patch the Django request object to include a weak backreference to the 

232 DRF request object, such that we can later use either in 

233 `DjangoRequestExtractor`. 

234 

235 This function is not called directly on SDK setup, because importing almost 

236 any part of Django Rest Framework will try to access Django settings (where 

237 `sentry_sdk.init()` might be called from in the first place). Instead we 

238 run this function on every request and do the patching on the first 

239 request. 

240 """ 

241 

242 global _DRF_PATCHED 

243 

244 if _DRF_PATCHED: 

245 # Double-checked locking 

246 return 

247 

248 with _DRF_PATCH_LOCK: 

249 if _DRF_PATCHED: 249 ↛ 250line 249 didn't jump to line 250, because the condition on line 249 was never true

250 return 

251 

252 # We set this regardless of whether the code below succeeds or fails. 

253 # There is no point in trying to patch again on the next request. 

254 _DRF_PATCHED = True 

255 

256 with capture_internal_exceptions(): 

257 try: 

258 from rest_framework.views import APIView # type: ignore 

259 except ImportError: 

260 pass 

261 else: 

262 old_drf_initial = APIView.initial 

263 

264 def sentry_patched_drf_initial(self, request, *args, **kwargs): 

265 # type: (APIView, Any, *Any, **Any) -> Any 

266 with capture_internal_exceptions(): 

267 request._request._sentry_drf_request_backref = weakref.ref( 

268 request 

269 ) 

270 pass 

271 return old_drf_initial(self, request, *args, **kwargs) 

272 

273 APIView.initial = sentry_patched_drf_initial 

274 

275 

276def _patch_channels(): 

277 # type: () -> None 

278 try: 

279 from channels.http import AsgiHandler # type: ignore 

280 except ImportError: 

281 return 

282 

283 if not HAS_REAL_CONTEXTVARS: 

284 # We better have contextvars or we're going to leak state between 

285 # requests. 

286 # 

287 # We cannot hard-raise here because channels may not be used at all in 

288 # the current process. That is the case when running traditional WSGI 

289 # workers in gunicorn+gevent and the websocket stuff in a separate 

290 # process. 

291 logger.warning( 

292 "We detected that you are using Django channels 2.0." 

293 + CONTEXTVARS_ERROR_MESSAGE 

294 ) 

295 

296 from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl 

297 

298 patch_channels_asgi_handler_impl(AsgiHandler) 

299 

300 

301def _patch_django_asgi_handler(): 

302 # type: () -> None 

303 try: 

304 from django.core.handlers.asgi import ASGIHandler 

305 except ImportError: 

306 return 

307 

308 if not HAS_REAL_CONTEXTVARS: 308 ↛ 314line 308 didn't jump to line 314, because the condition on line 308 was never true

309 # We better have contextvars or we're going to leak state between 

310 # requests. 

311 # 

312 # We cannot hard-raise here because Django's ASGI stuff may not be used 

313 # at all. 

314 logger.warning( 

315 "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE 

316 ) 

317 

318 from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl 

319 

320 patch_django_asgi_handler_impl(ASGIHandler) 

321 

322 

323def _set_transaction_name_and_source(scope, transaction_style, request): 

324 # type: (Scope, str, WSGIRequest) -> None 

325 try: 

326 transaction_name = "" 

327 if transaction_style == "function_name": 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true

328 fn = resolve(request.path).func 

329 transaction_name = ( 

330 transaction_from_function(getattr(fn, "view_class", fn)) or "" 

331 ) 

332 

333 elif transaction_style == "url": 333 ↛ 341line 333 didn't jump to line 341, because the condition on line 333 was never false

334 if hasattr(request, "urlconf"): 334 ↛ 335line 334 didn't jump to line 335, because the condition on line 334 was never true

335 transaction_name = LEGACY_RESOLVER.resolve( 

336 request.path_info, urlconf=request.urlconf 

337 ) 

338 else: 

339 transaction_name = LEGACY_RESOLVER.resolve(request.path_info) 

340 

341 scope.set_transaction_name( 

342 transaction_name, 

343 source=SOURCE_FOR_STYLE[transaction_style], 

344 ) 

345 except Exception: 

346 pass 

347 

348 

349def _before_get_response(request): 

350 # type: (WSGIRequest) -> None 

351 hub = Hub.current 

352 integration = hub.get_integration(DjangoIntegration) 

353 if integration is None: 353 ↛ 354line 353 didn't jump to line 354, because the condition on line 353 was never true

354 return 

355 

356 _patch_drf() 

357 

358 with hub.configure_scope() as scope: 

359 # Rely on WSGI middleware to start a trace 

360 _set_transaction_name_and_source(scope, integration.transaction_style, request) 

361 

362 scope.add_event_processor( 

363 _make_event_processor(weakref.ref(request), integration) 

364 ) 

365 

366 

367def _attempt_resolve_again(request, scope, transaction_style): 

368 # type: (WSGIRequest, Scope, str) -> None 

369 """ 

370 Some django middlewares overwrite request.urlconf 

371 so we need to respect that contract, 

372 so we try to resolve the url again. 

373 """ 

374 if not hasattr(request, "urlconf"): 374 ↛ 377line 374 didn't jump to line 377, because the condition on line 374 was never false

375 return 

376 

377 _set_transaction_name_and_source(scope, transaction_style, request) 

378 

379 

380def _after_get_response(request): 

381 # type: (WSGIRequest) -> None 

382 hub = Hub.current 

383 integration = hub.get_integration(DjangoIntegration) 

384 if integration is None or integration.transaction_style != "url": 384 ↛ 385line 384 didn't jump to line 385, because the condition on line 384 was never true

385 return 

386 

387 with hub.configure_scope() as scope: 

388 _attempt_resolve_again(request, scope, integration.transaction_style) 

389 

390 

391def _patch_get_response(): 

392 # type: () -> None 

393 """ 

394 patch get_response, because at that point we have the Django request object 

395 """ 

396 from django.core.handlers.base import BaseHandler 

397 

398 old_get_response = BaseHandler.get_response 

399 

400 def sentry_patched_get_response(self, request): 

401 # type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException] 

402 _before_get_response(request) 

403 rv = old_get_response(self, request) 

404 _after_get_response(request) 

405 return rv 

406 

407 BaseHandler.get_response = sentry_patched_get_response 

408 

409 if hasattr(BaseHandler, "get_response_async"): 409 ↛ exitline 409 didn't return from function '_patch_get_response', because the condition on line 409 was never false

410 from sentry_sdk.integrations.django.asgi import patch_get_response_async 

411 

412 patch_get_response_async(BaseHandler, _before_get_response) 

413 

414 

415def _make_event_processor(weak_request, integration): 

416 # type: (Callable[[], WSGIRequest], DjangoIntegration) -> EventProcessor 

417 def event_processor(event, hint): 

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

419 # if the request is gone we are fine not logging the data from 

420 # it. This might happen if the processor is pushed away to 

421 # another thread. 

422 request = weak_request() 

423 if request is None: 

424 return event 

425 

426 try: 

427 drf_request = request._sentry_drf_request_backref() 

428 if drf_request is not None: 

429 request = drf_request 

430 except AttributeError: 

431 pass 

432 

433 with capture_internal_exceptions(): 

434 DjangoRequestExtractor(request).extract_into_event(event) 

435 

436 if _should_send_default_pii(): 

437 with capture_internal_exceptions(): 

438 _set_user_info(request, event) 

439 

440 return event 

441 

442 return event_processor 

443 

444 

445def _got_request_exception(request=None, **kwargs): 

446 # type: (WSGIRequest, **Any) -> None 

447 hub = Hub.current 

448 integration = hub.get_integration(DjangoIntegration) 

449 if integration is not None: 

450 

451 if request is not None and integration.transaction_style == "url": 

452 with hub.configure_scope() as scope: 

453 _attempt_resolve_again(request, scope, integration.transaction_style) 

454 

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

456 client = hub.client # type: Any 

457 

458 event, hint = event_from_exception( 

459 sys.exc_info(), 

460 client_options=client.options, 

461 mechanism={"type": "django", "handled": False}, 

462 ) 

463 hub.capture_event(event, hint=hint) 

464 

465 

466class DjangoRequestExtractor(RequestExtractor): 

467 def env(self): 

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

469 return self.request.META 

470 

471 def cookies(self): 

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

473 return self.request.COOKIES 

474 

475 def raw_data(self): 

476 # type: () -> bytes 

477 return self.request.body 

478 

479 def form(self): 

480 # type: () -> QueryDict 

481 return self.request.POST 

482 

483 def files(self): 

484 # type: () -> MultiValueDict 

485 return self.request.FILES 

486 

487 def size_of_file(self, file): 

488 # type: (Any) -> int 

489 return file.size 

490 

491 def parsed_body(self): 

492 # type: () -> Optional[Dict[str, Any]] 

493 try: 

494 return self.request.data 

495 except AttributeError: 

496 return RequestExtractor.parsed_body(self) 

497 

498 

499def _set_user_info(request, event): 

500 # type: (WSGIRequest, Dict[str, Any]) -> None 

501 user_info = event.setdefault("user", {}) 

502 

503 user = getattr(request, "user", None) 

504 

505 if user is None or not is_authenticated(user): 

506 return 

507 

508 try: 

509 user_info.setdefault("id", str(user.pk)) 

510 except Exception: 

511 pass 

512 

513 try: 

514 user_info.setdefault("email", user.email) 

515 except Exception: 

516 pass 

517 

518 try: 

519 user_info.setdefault("username", user.get_username()) 

520 except Exception: 

521 pass 

522 

523 

524def install_sql_hook(): 

525 # type: () -> None 

526 """If installed this causes Django's queries to be captured.""" 

527 try: 

528 from django.db.backends.utils import CursorWrapper 

529 except ImportError: 

530 from django.db.backends.util import CursorWrapper 

531 

532 try: 

533 # django 1.6 and 1.7 compatability 

534 from django.db.backends import BaseDatabaseWrapper 

535 except ImportError: 

536 # django 1.8 or later 

537 from django.db.backends.base.base import BaseDatabaseWrapper 

538 

539 try: 

540 real_execute = CursorWrapper.execute 

541 real_executemany = CursorWrapper.executemany 

542 real_connect = BaseDatabaseWrapper.connect 

543 except AttributeError: 

544 # This won't work on Django versions < 1.6 

545 return 

546 

547 def execute(self, sql, params=None): 

548 # type: (CursorWrapper, Any, Optional[Any]) -> Any 

549 hub = Hub.current 

550 if hub.get_integration(DjangoIntegration) is None: 550 ↛ 551line 550 didn't jump to line 551, because the condition on line 550 was never true

551 return real_execute(self, sql, params) 

552 

553 with record_sql_queries( 

554 hub, self.cursor, sql, params, paramstyle="format", executemany=False 

555 ): 

556 return real_execute(self, sql, params) 

557 

558 def executemany(self, sql, param_list): 

559 # type: (CursorWrapper, Any, List[Any]) -> Any 

560 hub = Hub.current 

561 if hub.get_integration(DjangoIntegration) is None: 

562 return real_executemany(self, sql, param_list) 

563 

564 with record_sql_queries( 

565 hub, self.cursor, sql, param_list, paramstyle="format", executemany=True 

566 ): 

567 return real_executemany(self, sql, param_list) 

568 

569 def connect(self): 

570 # type: (BaseDatabaseWrapper) -> None 

571 hub = Hub.current 

572 if hub.get_integration(DjangoIntegration) is None: 572 ↛ 573line 572 didn't jump to line 573, because the condition on line 572 was never true

573 return real_connect(self) 

574 

575 with capture_internal_exceptions(): 

576 hub.add_breadcrumb(message="connect", category="query") 

577 

578 with hub.start_span(op="db", description="connect"): 

579 return real_connect(self) 

580 

581 CursorWrapper.execute = execute 

582 CursorWrapper.executemany = executemany 

583 BaseDatabaseWrapper.connect = connect 

584 ignore_logger("django.db.backends")