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
« 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
4import sys
5import threading
6import weakref
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
28try:
29 from django import VERSION as DJANGO_VERSION
30 from django.core import signals
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")
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
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
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
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
67if DJANGO_VERSION < (1, 10): 67 ↛ 69line 67 didn't jump to line 69, because the condition on line 67 was never true
69 def is_authenticated(request_user):
70 # type: (Any) -> bool
71 return request_user.is_authenticated()
73else:
75 def is_authenticated(request_user):
76 # type: (Any) -> bool
77 return request_user.is_authenticated
80TRANSACTION_STYLE_VALUES = ("function_name", "url")
83class DjangoIntegration(Integration):
84 identifier = "django"
86 transaction_style = ""
87 middleware_spans = None
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
99 @staticmethod
100 def setup_once():
101 # type: () -> None
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.")
106 install_sql_hook()
107 # Patch in our custom middleware.
109 # logs an error for every 500
110 ignore_logger("django.server")
111 ignore_logger("django.request")
113 from django.core.handlers.wsgi import WSGIHandler
115 old_app = WSGIHandler.__call__
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)
122 bound_old_app = old_app.__get__(self, WSGIHandler)
124 from django.conf import settings
126 use_x_forwarded_for = settings.USE_X_FORWARDED_HOST
128 return SentryWsgiMiddleware(bound_old_app, use_x_forwarded_for)(
129 environ, start_response
130 )
132 WSGIHandler.__call__ = sentry_patched_wsgi_handler
134 _patch_get_response()
136 _patch_django_asgi_handler()
138 signals.got_request_exception.connect(_got_request_exception)
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
146 exc_info = hint.get("exc_info", None)
148 if exc_info is None:
149 return event
151 exception = event.get("exception", None)
153 if exception is None:
154 return event
156 values = exception.get("values", None)
158 if values is None:
159 return event
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", [])
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)
179 frames.insert(i, frame)
181 return event
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
197 if not isinstance(value, QuerySet) or value._result_cache:
198 return NotImplemented
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.
205 return "<%s from %s at 0x%x>" % (
206 value.__class__.__name__,
207 value.__module__,
208 id(value),
209 )
211 _patch_channels()
212 patch_django_middlewares()
213 patch_views()
214 patch_templates()
217_DRF_PATCHED = False
218_DRF_PATCH_LOCK = threading.Lock()
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.
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`.
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 """
242 global _DRF_PATCHED
244 if _DRF_PATCHED:
245 # Double-checked locking
246 return
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
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
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
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)
273 APIView.initial = sentry_patched_drf_initial
276def _patch_channels():
277 # type: () -> None
278 try:
279 from channels.http import AsgiHandler # type: ignore
280 except ImportError:
281 return
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 )
296 from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl
298 patch_channels_asgi_handler_impl(AsgiHandler)
301def _patch_django_asgi_handler():
302 # type: () -> None
303 try:
304 from django.core.handlers.asgi import ASGIHandler
305 except ImportError:
306 return
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 )
318 from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl
320 patch_django_asgi_handler_impl(ASGIHandler)
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 )
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)
341 scope.set_transaction_name(
342 transaction_name,
343 source=SOURCE_FOR_STYLE[transaction_style],
344 )
345 except Exception:
346 pass
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
356 _patch_drf()
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)
362 scope.add_event_processor(
363 _make_event_processor(weakref.ref(request), integration)
364 )
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
377 _set_transaction_name_and_source(scope, transaction_style, request)
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
387 with hub.configure_scope() as scope:
388 _attempt_resolve_again(request, scope, integration.transaction_style)
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
398 old_get_response = BaseHandler.get_response
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
407 BaseHandler.get_response = sentry_patched_get_response
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
412 patch_get_response_async(BaseHandler, _before_get_response)
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
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
433 with capture_internal_exceptions():
434 DjangoRequestExtractor(request).extract_into_event(event)
436 if _should_send_default_pii():
437 with capture_internal_exceptions():
438 _set_user_info(request, event)
440 return event
442 return event_processor
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:
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)
455 # If an integration is there, a client has to be there.
456 client = hub.client # type: Any
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)
466class DjangoRequestExtractor(RequestExtractor):
467 def env(self):
468 # type: () -> Dict[str, str]
469 return self.request.META
471 def cookies(self):
472 # type: () -> Dict[str, str]
473 return self.request.COOKIES
475 def raw_data(self):
476 # type: () -> bytes
477 return self.request.body
479 def form(self):
480 # type: () -> QueryDict
481 return self.request.POST
483 def files(self):
484 # type: () -> MultiValueDict
485 return self.request.FILES
487 def size_of_file(self, file):
488 # type: (Any) -> int
489 return file.size
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)
499def _set_user_info(request, event):
500 # type: (WSGIRequest, Dict[str, Any]) -> None
501 user_info = event.setdefault("user", {})
503 user = getattr(request, "user", None)
505 if user is None or not is_authenticated(user):
506 return
508 try:
509 user_info.setdefault("id", str(user.pk))
510 except Exception:
511 pass
513 try:
514 user_info.setdefault("email", user.email)
515 except Exception:
516 pass
518 try:
519 user_info.setdefault("username", user.get_username())
520 except Exception:
521 pass
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
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
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
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)
553 with record_sql_queries(
554 hub, self.cursor, sql, params, paramstyle="format", executemany=False
555 ):
556 return real_execute(self, sql, params)
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)
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)
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)
575 with capture_internal_exceptions():
576 hub.add_breadcrumb(message="connect", category="query")
578 with hub.start_span(op="db", description="connect"):
579 return real_connect(self)
581 CursorWrapper.execute = execute
582 CursorWrapper.executemany = executemany
583 BaseDatabaseWrapper.connect = connect
584 ignore_logger("django.db.backends")