aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django')
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/__init__.py747
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/asgi.py245
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/caching.py191
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/middleware.py187
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/signals_handlers.py91
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/templates.py188
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/transactions.py159
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/views.py96
8 files changed, 1904 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/__init__.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/__init__.py
new file mode 100644
index 00000000..ff67b3e3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/__init__.py
@@ -0,0 +1,747 @@
+import inspect
+import sys
+import threading
+import weakref
+from importlib import import_module
+
+import sentry_sdk
+from sentry_sdk.consts import OP, SPANDATA
+from sentry_sdk.scope import add_global_event_processor, should_send_default_pii
+from sentry_sdk.serializer import add_global_repr_processor
+from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
+from sentry_sdk.tracing_utils import add_query_source, record_sql_queries
+from sentry_sdk.utils import (
+ AnnotatedValue,
+ HAS_REAL_CONTEXTVARS,
+ CONTEXTVARS_ERROR_MESSAGE,
+ SENSITIVE_DATA_SUBSTITUTE,
+ logger,
+ capture_internal_exceptions,
+ ensure_integration_enabled,
+ event_from_exception,
+ transaction_from_function,
+ walk_exception_chain,
+)
+from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
+from sentry_sdk.integrations.logging import ignore_logger
+from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
+from sentry_sdk.integrations._wsgi_common import (
+ DEFAULT_HTTP_METHODS_TO_CAPTURE,
+ RequestExtractor,
+)
+
+try:
+ from django import VERSION as DJANGO_VERSION
+ from django.conf import settings as django_settings
+ from django.core import signals
+ from django.conf import settings
+
+ try:
+ from django.urls import resolve
+ except ImportError:
+ from django.core.urlresolvers import resolve
+
+ try:
+ from django.urls import Resolver404
+ except ImportError:
+ from django.core.urlresolvers import Resolver404
+
+ # Only available in Django 3.0+
+ try:
+ from django.core.handlers.asgi import ASGIRequest
+ except Exception:
+ ASGIRequest = None
+
+except ImportError:
+ raise DidNotEnable("Django not installed")
+
+from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
+from sentry_sdk.integrations.django.templates import (
+ get_template_frame_from_exception,
+ patch_templates,
+)
+from sentry_sdk.integrations.django.middleware import patch_django_middlewares
+from sentry_sdk.integrations.django.signals_handlers import patch_signals
+from sentry_sdk.integrations.django.views import patch_views
+
+if DJANGO_VERSION[:2] > (1, 8):
+ from sentry_sdk.integrations.django.caching import patch_caching
+else:
+ patch_caching = None # type: ignore
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any
+ from typing import Callable
+ from typing import Dict
+ from typing import Optional
+ from typing import Union
+ from typing import List
+
+ from django.core.handlers.wsgi import WSGIRequest
+ from django.http.response import HttpResponse
+ from django.http.request import QueryDict
+ from django.utils.datastructures import MultiValueDict
+
+ from sentry_sdk.tracing import Span
+ from sentry_sdk.integrations.wsgi import _ScopedResponse
+ from sentry_sdk._types import Event, Hint, EventProcessor, NotImplementedType
+
+
+if DJANGO_VERSION < (1, 10):
+
+ def is_authenticated(request_user):
+ # type: (Any) -> bool
+ return request_user.is_authenticated()
+
+else:
+
+ def is_authenticated(request_user):
+ # type: (Any) -> bool
+ return request_user.is_authenticated
+
+
+TRANSACTION_STYLE_VALUES = ("function_name", "url")
+
+
+class DjangoIntegration(Integration):
+ """
+ Auto instrument a Django application.
+
+ :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`.
+ :param middleware_spans: Whether to create spans for middleware. Defaults to `True`.
+ :param signals_spans: Whether to create spans for signals. Defaults to `True`.
+ :param signals_denylist: A list of signals to ignore when creating spans.
+ :param cache_spans: Whether to create spans for cache operations. Defaults to `False`.
+ """
+
+ identifier = "django"
+ origin = f"auto.http.{identifier}"
+ origin_db = f"auto.db.{identifier}"
+
+ transaction_style = ""
+ middleware_spans = None
+ signals_spans = None
+ cache_spans = None
+ signals_denylist = [] # type: list[signals.Signal]
+
+ def __init__(
+ self,
+ transaction_style="url", # type: str
+ middleware_spans=True, # type: bool
+ signals_spans=True, # type: bool
+ cache_spans=False, # type: bool
+ signals_denylist=None, # type: Optional[list[signals.Signal]]
+ http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
+ ):
+ # type: (...) -> None
+ if transaction_style not in TRANSACTION_STYLE_VALUES:
+ raise ValueError(
+ "Invalid value for transaction_style: %s (must be in %s)"
+ % (transaction_style, TRANSACTION_STYLE_VALUES)
+ )
+ self.transaction_style = transaction_style
+ self.middleware_spans = middleware_spans
+
+ self.signals_spans = signals_spans
+ self.signals_denylist = signals_denylist or []
+
+ self.cache_spans = cache_spans
+
+ self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ _check_minimum_version(DjangoIntegration, DJANGO_VERSION)
+
+ install_sql_hook()
+ # Patch in our custom middleware.
+
+ # logs an error for every 500
+ ignore_logger("django.server")
+ ignore_logger("django.request")
+
+ from django.core.handlers.wsgi import WSGIHandler
+
+ old_app = WSGIHandler.__call__
+
+ @ensure_integration_enabled(DjangoIntegration, old_app)
+ def sentry_patched_wsgi_handler(self, environ, start_response):
+ # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse
+ bound_old_app = old_app.__get__(self, WSGIHandler)
+
+ from django.conf import settings
+
+ use_x_forwarded_for = settings.USE_X_FORWARDED_HOST
+
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
+
+ middleware = SentryWsgiMiddleware(
+ bound_old_app,
+ use_x_forwarded_for,
+ span_origin=DjangoIntegration.origin,
+ http_methods_to_capture=(
+ integration.http_methods_to_capture
+ if integration
+ else DEFAULT_HTTP_METHODS_TO_CAPTURE
+ ),
+ )
+ return middleware(environ, start_response)
+
+ WSGIHandler.__call__ = sentry_patched_wsgi_handler
+
+ _patch_get_response()
+
+ _patch_django_asgi_handler()
+
+ signals.got_request_exception.connect(_got_request_exception)
+
+ @add_global_event_processor
+ def process_django_templates(event, hint):
+ # type: (Event, Optional[Hint]) -> Optional[Event]
+ if hint is None:
+ return event
+
+ exc_info = hint.get("exc_info", None)
+
+ if exc_info is None:
+ return event
+
+ exception = event.get("exception", None)
+
+ if exception is None:
+ return event
+
+ values = exception.get("values", None)
+
+ if values is None:
+ return event
+
+ for exception, (_, exc_value, _) in zip(
+ reversed(values), walk_exception_chain(exc_info)
+ ):
+ frame = get_template_frame_from_exception(exc_value)
+ if frame is not None:
+ frames = exception.get("stacktrace", {}).get("frames", [])
+
+ for i in reversed(range(len(frames))):
+ f = frames[i]
+ if (
+ f.get("function") in ("Parser.parse", "parse", "render")
+ and f.get("module") == "django.template.base"
+ ):
+ i += 1
+ break
+ else:
+ i = len(frames)
+
+ frames.insert(i, frame)
+
+ return event
+
+ @add_global_repr_processor
+ def _django_queryset_repr(value, hint):
+ # type: (Any, Dict[str, Any]) -> Union[NotImplementedType, str]
+ try:
+ # Django 1.6 can fail to import `QuerySet` when Django settings
+ # have not yet been initialized.
+ #
+ # If we fail to import, return `NotImplemented`. It's at least
+ # unlikely that we have a query set in `value` when importing
+ # `QuerySet` fails.
+ from django.db.models.query import QuerySet
+ except Exception:
+ return NotImplemented
+
+ if not isinstance(value, QuerySet) or value._result_cache:
+ return NotImplemented
+
+ return "<%s from %s at 0x%x>" % (
+ value.__class__.__name__,
+ value.__module__,
+ id(value),
+ )
+
+ _patch_channels()
+ patch_django_middlewares()
+ patch_views()
+ patch_templates()
+ patch_signals()
+
+ if patch_caching is not None:
+ patch_caching()
+
+
+_DRF_PATCHED = False
+_DRF_PATCH_LOCK = threading.Lock()
+
+
+def _patch_drf():
+ # type: () -> None
+ """
+ Patch Django Rest Framework for more/better request data. DRF's request
+ type is a wrapper around Django's request type. The attribute we're
+ interested in is `request.data`, which is a cached property containing a
+ parsed request body. Reading a request body from that property is more
+ reliable than reading from any of Django's own properties, as those don't
+ hold payloads in memory and therefore can only be accessed once.
+
+ We patch the Django request object to include a weak backreference to the
+ DRF request object, such that we can later use either in
+ `DjangoRequestExtractor`.
+
+ This function is not called directly on SDK setup, because importing almost
+ any part of Django Rest Framework will try to access Django settings (where
+ `sentry_sdk.init()` might be called from in the first place). Instead we
+ run this function on every request and do the patching on the first
+ request.
+ """
+
+ global _DRF_PATCHED
+
+ if _DRF_PATCHED:
+ # Double-checked locking
+ return
+
+ with _DRF_PATCH_LOCK:
+ if _DRF_PATCHED:
+ return
+
+ # We set this regardless of whether the code below succeeds or fails.
+ # There is no point in trying to patch again on the next request.
+ _DRF_PATCHED = True
+
+ with capture_internal_exceptions():
+ try:
+ from rest_framework.views import APIView # type: ignore
+ except ImportError:
+ pass
+ else:
+ old_drf_initial = APIView.initial
+
+ def sentry_patched_drf_initial(self, request, *args, **kwargs):
+ # type: (APIView, Any, *Any, **Any) -> Any
+ with capture_internal_exceptions():
+ request._request._sentry_drf_request_backref = weakref.ref(
+ request
+ )
+ pass
+ return old_drf_initial(self, request, *args, **kwargs)
+
+ APIView.initial = sentry_patched_drf_initial
+
+
+def _patch_channels():
+ # type: () -> None
+ try:
+ from channels.http import AsgiHandler # type: ignore
+ except ImportError:
+ return
+
+ if not HAS_REAL_CONTEXTVARS:
+ # We better have contextvars or we're going to leak state between
+ # requests.
+ #
+ # We cannot hard-raise here because channels may not be used at all in
+ # the current process. That is the case when running traditional WSGI
+ # workers in gunicorn+gevent and the websocket stuff in a separate
+ # process.
+ logger.warning(
+ "We detected that you are using Django channels 2.0."
+ + CONTEXTVARS_ERROR_MESSAGE
+ )
+
+ from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl
+
+ patch_channels_asgi_handler_impl(AsgiHandler)
+
+
+def _patch_django_asgi_handler():
+ # type: () -> None
+ try:
+ from django.core.handlers.asgi import ASGIHandler
+ except ImportError:
+ return
+
+ if not HAS_REAL_CONTEXTVARS:
+ # We better have contextvars or we're going to leak state between
+ # requests.
+ #
+ # We cannot hard-raise here because Django's ASGI stuff may not be used
+ # at all.
+ logger.warning(
+ "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE
+ )
+
+ from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl
+
+ patch_django_asgi_handler_impl(ASGIHandler)
+
+
+def _set_transaction_name_and_source(scope, transaction_style, request):
+ # type: (sentry_sdk.Scope, str, WSGIRequest) -> None
+ try:
+ transaction_name = None
+ if transaction_style == "function_name":
+ fn = resolve(request.path).func
+ transaction_name = transaction_from_function(getattr(fn, "view_class", fn))
+
+ elif transaction_style == "url":
+ if hasattr(request, "urlconf"):
+ transaction_name = LEGACY_RESOLVER.resolve(
+ request.path_info, urlconf=request.urlconf
+ )
+ else:
+ transaction_name = LEGACY_RESOLVER.resolve(request.path_info)
+
+ if transaction_name is None:
+ transaction_name = request.path_info
+ source = TransactionSource.URL
+ else:
+ source = SOURCE_FOR_STYLE[transaction_style]
+
+ scope.set_transaction_name(
+ transaction_name,
+ source=source,
+ )
+ except Resolver404:
+ urlconf = import_module(settings.ROOT_URLCONF)
+ # This exception only gets thrown when transaction_style is `function_name`
+ # So we don't check here what style is configured
+ if hasattr(urlconf, "handler404"):
+ handler = urlconf.handler404
+ if isinstance(handler, str):
+ scope.transaction = handler
+ else:
+ scope.transaction = transaction_from_function(
+ getattr(handler, "view_class", handler)
+ )
+ except Exception:
+ pass
+
+
+def _before_get_response(request):
+ # type: (WSGIRequest) -> None
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
+ if integration is None:
+ return
+
+ _patch_drf()
+
+ scope = sentry_sdk.get_current_scope()
+ # Rely on WSGI middleware to start a trace
+ _set_transaction_name_and_source(scope, integration.transaction_style, request)
+
+ scope.add_event_processor(
+ _make_wsgi_request_event_processor(weakref.ref(request), integration)
+ )
+
+
+def _attempt_resolve_again(request, scope, transaction_style):
+ # type: (WSGIRequest, sentry_sdk.Scope, str) -> None
+ """
+ Some django middlewares overwrite request.urlconf
+ so we need to respect that contract,
+ so we try to resolve the url again.
+ """
+ if not hasattr(request, "urlconf"):
+ return
+
+ _set_transaction_name_and_source(scope, transaction_style, request)
+
+
+def _after_get_response(request):
+ # type: (WSGIRequest) -> None
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
+ if integration is None or integration.transaction_style != "url":
+ return
+
+ scope = sentry_sdk.get_current_scope()
+ _attempt_resolve_again(request, scope, integration.transaction_style)
+
+
+def _patch_get_response():
+ # type: () -> None
+ """
+ patch get_response, because at that point we have the Django request object
+ """
+ from django.core.handlers.base import BaseHandler
+
+ old_get_response = BaseHandler.get_response
+
+ def sentry_patched_get_response(self, request):
+ # type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException]
+ _before_get_response(request)
+ rv = old_get_response(self, request)
+ _after_get_response(request)
+ return rv
+
+ BaseHandler.get_response = sentry_patched_get_response
+
+ if hasattr(BaseHandler, "get_response_async"):
+ from sentry_sdk.integrations.django.asgi import patch_get_response_async
+
+ patch_get_response_async(BaseHandler, _before_get_response)
+
+
+def _make_wsgi_request_event_processor(weak_request, integration):
+ # type: (Callable[[], WSGIRequest], DjangoIntegration) -> EventProcessor
+ def wsgi_request_event_processor(event, hint):
+ # type: (Event, dict[str, Any]) -> Event
+ # if the request is gone we are fine not logging the data from
+ # it. This might happen if the processor is pushed away to
+ # another thread.
+ request = weak_request()
+ if request is None:
+ return event
+
+ django_3 = ASGIRequest is not None
+ if django_3 and type(request) == ASGIRequest:
+ # We have a `asgi_request_event_processor` for this.
+ return event
+
+ with capture_internal_exceptions():
+ DjangoRequestExtractor(request).extract_into_event(event)
+
+ if should_send_default_pii():
+ with capture_internal_exceptions():
+ _set_user_info(request, event)
+
+ return event
+
+ return wsgi_request_event_processor
+
+
+def _got_request_exception(request=None, **kwargs):
+ # type: (WSGIRequest, **Any) -> None
+ client = sentry_sdk.get_client()
+ integration = client.get_integration(DjangoIntegration)
+ if integration is None:
+ return
+
+ if request is not None and integration.transaction_style == "url":
+ scope = sentry_sdk.get_current_scope()
+ _attempt_resolve_again(request, scope, integration.transaction_style)
+
+ event, hint = event_from_exception(
+ sys.exc_info(),
+ client_options=client.options,
+ mechanism={"type": "django", "handled": False},
+ )
+ sentry_sdk.capture_event(event, hint=hint)
+
+
+class DjangoRequestExtractor(RequestExtractor):
+ def __init__(self, request):
+ # type: (Union[WSGIRequest, ASGIRequest]) -> None
+ try:
+ drf_request = request._sentry_drf_request_backref()
+ if drf_request is not None:
+ request = drf_request
+ except AttributeError:
+ pass
+ self.request = request
+
+ def env(self):
+ # type: () -> Dict[str, str]
+ return self.request.META
+
+ def cookies(self):
+ # type: () -> Dict[str, Union[str, AnnotatedValue]]
+ privacy_cookies = [
+ django_settings.CSRF_COOKIE_NAME,
+ django_settings.SESSION_COOKIE_NAME,
+ ]
+
+ clean_cookies = {} # type: Dict[str, Union[str, AnnotatedValue]]
+ for key, val in self.request.COOKIES.items():
+ if key in privacy_cookies:
+ clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE
+ else:
+ clean_cookies[key] = val
+
+ return clean_cookies
+
+ def raw_data(self):
+ # type: () -> bytes
+ return self.request.body
+
+ def form(self):
+ # type: () -> QueryDict
+ return self.request.POST
+
+ def files(self):
+ # type: () -> MultiValueDict
+ return self.request.FILES
+
+ def size_of_file(self, file):
+ # type: (Any) -> int
+ return file.size
+
+ def parsed_body(self):
+ # type: () -> Optional[Dict[str, Any]]
+ try:
+ return self.request.data
+ except Exception:
+ return RequestExtractor.parsed_body(self)
+
+
+def _set_user_info(request, event):
+ # type: (WSGIRequest, Event) -> None
+ user_info = event.setdefault("user", {})
+
+ user = getattr(request, "user", None)
+
+ if user is None or not is_authenticated(user):
+ return
+
+ try:
+ user_info.setdefault("id", str(user.pk))
+ except Exception:
+ pass
+
+ try:
+ user_info.setdefault("email", user.email)
+ except Exception:
+ pass
+
+ try:
+ user_info.setdefault("username", user.get_username())
+ except Exception:
+ pass
+
+
+def install_sql_hook():
+ # type: () -> None
+ """If installed this causes Django's queries to be captured."""
+ try:
+ from django.db.backends.utils import CursorWrapper
+ except ImportError:
+ from django.db.backends.util import CursorWrapper
+
+ try:
+ # django 1.6 and 1.7 compatability
+ from django.db.backends import BaseDatabaseWrapper
+ except ImportError:
+ # django 1.8 or later
+ from django.db.backends.base.base import BaseDatabaseWrapper
+
+ try:
+ real_execute = CursorWrapper.execute
+ real_executemany = CursorWrapper.executemany
+ real_connect = BaseDatabaseWrapper.connect
+ except AttributeError:
+ # This won't work on Django versions < 1.6
+ return
+
+ @ensure_integration_enabled(DjangoIntegration, real_execute)
+ def execute(self, sql, params=None):
+ # type: (CursorWrapper, Any, Optional[Any]) -> Any
+ with record_sql_queries(
+ cursor=self.cursor,
+ query=sql,
+ params_list=params,
+ paramstyle="format",
+ executemany=False,
+ span_origin=DjangoIntegration.origin_db,
+ ) as span:
+ _set_db_data(span, self)
+ result = real_execute(self, sql, params)
+
+ with capture_internal_exceptions():
+ add_query_source(span)
+
+ return result
+
+ @ensure_integration_enabled(DjangoIntegration, real_executemany)
+ def executemany(self, sql, param_list):
+ # type: (CursorWrapper, Any, List[Any]) -> Any
+ with record_sql_queries(
+ cursor=self.cursor,
+ query=sql,
+ params_list=param_list,
+ paramstyle="format",
+ executemany=True,
+ span_origin=DjangoIntegration.origin_db,
+ ) as span:
+ _set_db_data(span, self)
+
+ result = real_executemany(self, sql, param_list)
+
+ with capture_internal_exceptions():
+ add_query_source(span)
+
+ return result
+
+ @ensure_integration_enabled(DjangoIntegration, real_connect)
+ def connect(self):
+ # type: (BaseDatabaseWrapper) -> None
+ with capture_internal_exceptions():
+ sentry_sdk.add_breadcrumb(message="connect", category="query")
+
+ with sentry_sdk.start_span(
+ op=OP.DB,
+ name="connect",
+ origin=DjangoIntegration.origin_db,
+ ) as span:
+ _set_db_data(span, self)
+ return real_connect(self)
+
+ CursorWrapper.execute = execute
+ CursorWrapper.executemany = executemany
+ BaseDatabaseWrapper.connect = connect
+ ignore_logger("django.db.backends")
+
+
+def _set_db_data(span, cursor_or_db):
+ # type: (Span, Any) -> None
+ db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db
+ vendor = db.vendor
+ span.set_data(SPANDATA.DB_SYSTEM, vendor)
+
+ # Some custom backends override `__getattr__`, making it look like `cursor_or_db`
+ # actually has a `connection` and the `connection` has a `get_dsn_parameters`
+ # attribute, only to throw an error once you actually want to call it.
+ # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable
+ # function.
+ is_psycopg2 = (
+ hasattr(cursor_or_db, "connection")
+ and hasattr(cursor_or_db.connection, "get_dsn_parameters")
+ and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters)
+ )
+ if is_psycopg2:
+ connection_params = cursor_or_db.connection.get_dsn_parameters()
+ else:
+ try:
+ # psycopg3, only extract needed params as get_parameters
+ # can be slow because of the additional logic to filter out default
+ # values
+ connection_params = {
+ "dbname": cursor_or_db.connection.info.dbname,
+ "port": cursor_or_db.connection.info.port,
+ }
+ # PGhost returns host or base dir of UNIX socket as an absolute path
+ # starting with /, use it only when it contains host
+ pg_host = cursor_or_db.connection.info.host
+ if pg_host and not pg_host.startswith("/"):
+ connection_params["host"] = pg_host
+ except Exception:
+ connection_params = db.get_connection_params()
+
+ db_name = connection_params.get("dbname") or connection_params.get("database")
+ if db_name is not None:
+ span.set_data(SPANDATA.DB_NAME, db_name)
+
+ server_address = connection_params.get("host")
+ if server_address is not None:
+ span.set_data(SPANDATA.SERVER_ADDRESS, server_address)
+
+ server_port = connection_params.get("port")
+ if server_port is not None:
+ span.set_data(SPANDATA.SERVER_PORT, str(server_port))
+
+ server_socket_address = connection_params.get("unix_socket")
+ if server_socket_address is not None:
+ span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address)
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/asgi.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/asgi.py
new file mode 100644
index 00000000..73a25acc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/asgi.py
@@ -0,0 +1,245 @@
+"""
+Instrumentation for Django 3.0
+
+Since this file contains `async def` it is conditionally imported in
+`sentry_sdk.integrations.django` (depending on the existence of
+`django.core.handlers.asgi`.
+"""
+
+import asyncio
+import functools
+import inspect
+
+from django.core.handlers.wsgi import WSGIRequest
+
+import sentry_sdk
+from sentry_sdk.consts import OP
+
+from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
+from sentry_sdk.scope import should_send_default_pii
+from sentry_sdk.utils import (
+ capture_internal_exceptions,
+ ensure_integration_enabled,
+)
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any, Callable, Union, TypeVar
+
+ from django.core.handlers.asgi import ASGIRequest
+ from django.http.response import HttpResponse
+
+ from sentry_sdk._types import Event, EventProcessor
+
+ _F = TypeVar("_F", bound=Callable[..., Any])
+
+
+# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for
+# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker.
+# The latter is replaced with the inspect.markcoroutinefunction decorator.
+# Until 3.12 is the minimum supported Python version, provide a shim.
+# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py
+if hasattr(inspect, "markcoroutinefunction"):
+ iscoroutinefunction = inspect.iscoroutinefunction
+ markcoroutinefunction = inspect.markcoroutinefunction
+else:
+ iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment]
+
+ def markcoroutinefunction(func: "_F") -> "_F":
+ func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore
+ return func
+
+
+def _make_asgi_request_event_processor(request):
+ # type: (ASGIRequest) -> EventProcessor
+ def asgi_request_event_processor(event, hint):
+ # type: (Event, dict[str, Any]) -> Event
+ # if the request is gone we are fine not logging the data from
+ # it. This might happen if the processor is pushed away to
+ # another thread.
+ from sentry_sdk.integrations.django import (
+ DjangoRequestExtractor,
+ _set_user_info,
+ )
+
+ if request is None:
+ return event
+
+ if type(request) == WSGIRequest:
+ return event
+
+ with capture_internal_exceptions():
+ DjangoRequestExtractor(request).extract_into_event(event)
+
+ if should_send_default_pii():
+ with capture_internal_exceptions():
+ _set_user_info(request, event)
+
+ return event
+
+ return asgi_request_event_processor
+
+
+def patch_django_asgi_handler_impl(cls):
+ # type: (Any) -> None
+
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ old_app = cls.__call__
+
+ async def sentry_patched_asgi_handler(self, scope, receive, send):
+ # type: (Any, Any, Any, Any) -> Any
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
+ if integration is None:
+ return await old_app(self, scope, receive, send)
+
+ middleware = SentryAsgiMiddleware(
+ old_app.__get__(self, cls),
+ unsafe_context_data=True,
+ span_origin=DjangoIntegration.origin,
+ http_methods_to_capture=integration.http_methods_to_capture,
+ )._run_asgi3
+
+ return await middleware(scope, receive, send)
+
+ cls.__call__ = sentry_patched_asgi_handler
+
+ modern_django_asgi_support = hasattr(cls, "create_request")
+ if modern_django_asgi_support:
+ old_create_request = cls.create_request
+
+ @ensure_integration_enabled(DjangoIntegration, old_create_request)
+ def sentry_patched_create_request(self, *args, **kwargs):
+ # type: (Any, *Any, **Any) -> Any
+ request, error_response = old_create_request(self, *args, **kwargs)
+ scope = sentry_sdk.get_isolation_scope()
+ scope.add_event_processor(_make_asgi_request_event_processor(request))
+
+ return request, error_response
+
+ cls.create_request = sentry_patched_create_request
+
+
+def patch_get_response_async(cls, _before_get_response):
+ # type: (Any, Any) -> None
+ old_get_response_async = cls.get_response_async
+
+ async def sentry_patched_get_response_async(self, request):
+ # type: (Any, Any) -> Union[HttpResponse, BaseException]
+ _before_get_response(request)
+ return await old_get_response_async(self, request)
+
+ cls.get_response_async = sentry_patched_get_response_async
+
+
+def patch_channels_asgi_handler_impl(cls):
+ # type: (Any) -> None
+ import channels # type: ignore
+
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ if channels.__version__ < "3.0.0":
+ old_app = cls.__call__
+
+ async def sentry_patched_asgi_handler(self, receive, send):
+ # type: (Any, Any, Any) -> Any
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
+ if integration is None:
+ return await old_app(self, receive, send)
+
+ middleware = SentryAsgiMiddleware(
+ lambda _scope: old_app.__get__(self, cls),
+ unsafe_context_data=True,
+ span_origin=DjangoIntegration.origin,
+ http_methods_to_capture=integration.http_methods_to_capture,
+ )
+
+ return await middleware(self.scope)(receive, send)
+
+ cls.__call__ = sentry_patched_asgi_handler
+
+ else:
+ # The ASGI handler in Channels >= 3 has the same signature as
+ # the Django handler.
+ patch_django_asgi_handler_impl(cls)
+
+
+def wrap_async_view(callback):
+ # type: (Any) -> Any
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ @functools.wraps(callback)
+ async def sentry_wrapped_callback(request, *args, **kwargs):
+ # type: (Any, *Any, **Any) -> Any
+ current_scope = sentry_sdk.get_current_scope()
+ if current_scope.transaction is not None:
+ current_scope.transaction.update_active_thread()
+
+ sentry_scope = sentry_sdk.get_isolation_scope()
+ if sentry_scope.profile is not None:
+ sentry_scope.profile.update_active_thread_id()
+
+ with sentry_sdk.start_span(
+ op=OP.VIEW_RENDER,
+ name=request.resolver_match.view_name,
+ origin=DjangoIntegration.origin,
+ ):
+ return await callback(request, *args, **kwargs)
+
+ return sentry_wrapped_callback
+
+
+def _asgi_middleware_mixin_factory(_check_middleware_span):
+ # type: (Callable[..., Any]) -> Any
+ """
+ Mixin class factory that generates a middleware mixin for handling requests
+ in async mode.
+ """
+
+ class SentryASGIMixin:
+ if TYPE_CHECKING:
+ _inner = None
+
+ def __init__(self, get_response):
+ # type: (Callable[..., Any]) -> None
+ self.get_response = get_response
+ self._acall_method = None
+ self._async_check()
+
+ def _async_check(self):
+ # type: () -> None
+ """
+ If get_response is a coroutine function, turns us into async mode so
+ a thread is not consumed during a whole request.
+ Taken from django.utils.deprecation::MiddlewareMixin._async_check
+ """
+ if iscoroutinefunction(self.get_response):
+ markcoroutinefunction(self)
+
+ def async_route_check(self):
+ # type: () -> bool
+ """
+ Function that checks if we are in async mode,
+ and if we are forwards the handling of requests to __acall__
+ """
+ return iscoroutinefunction(self.get_response)
+
+ async def __acall__(self, *args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ f = self._acall_method
+ if f is None:
+ if hasattr(self._inner, "__acall__"):
+ self._acall_method = f = self._inner.__acall__ # type: ignore
+ else:
+ self._acall_method = f = self._inner
+
+ middleware_span = _check_middleware_span(old_method=f)
+
+ if middleware_span is None:
+ return await f(*args, **kwargs)
+
+ with middleware_span:
+ return await f(*args, **kwargs)
+
+ return SentryASGIMixin
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/caching.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/caching.py
new file mode 100644
index 00000000..79856117
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/caching.py
@@ -0,0 +1,191 @@
+import functools
+from typing import TYPE_CHECKING
+from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string
+from urllib3.util import parse_url as urlparse
+
+from django import VERSION as DJANGO_VERSION
+from django.core.cache import CacheHandler
+
+import sentry_sdk
+from sentry_sdk.consts import OP, SPANDATA
+from sentry_sdk.utils import (
+ capture_internal_exceptions,
+ ensure_integration_enabled,
+)
+
+
+if TYPE_CHECKING:
+ from typing import Any
+ from typing import Callable
+ from typing import Optional
+
+
+METHODS_TO_INSTRUMENT = [
+ "set",
+ "set_many",
+ "get",
+ "get_many",
+]
+
+
+def _get_span_description(method_name, args, kwargs):
+ # type: (str, tuple[Any], dict[str, Any]) -> str
+ return _key_as_string(_get_safe_key(method_name, args, kwargs))
+
+
+def _patch_cache_method(cache, method_name, address, port):
+ # type: (CacheHandler, str, Optional[str], Optional[int]) -> None
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ original_method = getattr(cache, method_name)
+
+ @ensure_integration_enabled(DjangoIntegration, original_method)
+ def _instrument_call(
+ cache, method_name, original_method, args, kwargs, address, port
+ ):
+ # type: (CacheHandler, str, Callable[..., Any], tuple[Any, ...], dict[str, Any], Optional[str], Optional[int]) -> Any
+ is_set_operation = method_name.startswith("set")
+ is_get_operation = not is_set_operation
+
+ op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET
+ description = _get_span_description(method_name, args, kwargs)
+
+ with sentry_sdk.start_span(
+ op=op,
+ name=description,
+ origin=DjangoIntegration.origin,
+ ) as span:
+ value = original_method(*args, **kwargs)
+
+ with capture_internal_exceptions():
+ if address is not None:
+ span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address)
+
+ if port is not None:
+ span.set_data(SPANDATA.NETWORK_PEER_PORT, port)
+
+ key = _get_safe_key(method_name, args, kwargs)
+ if key is not None:
+ span.set_data(SPANDATA.CACHE_KEY, key)
+
+ item_size = None
+ if is_get_operation:
+ if value:
+ item_size = len(str(value))
+ span.set_data(SPANDATA.CACHE_HIT, True)
+ else:
+ span.set_data(SPANDATA.CACHE_HIT, False)
+ else: # TODO: We don't handle `get_or_set` which we should
+ arg_count = len(args)
+ if arg_count >= 2:
+ # 'set' command
+ item_size = len(str(args[1]))
+ elif arg_count == 1:
+ # 'set_many' command
+ item_size = len(str(args[0]))
+
+ if item_size is not None:
+ span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size)
+
+ return value
+
+ @functools.wraps(original_method)
+ def sentry_method(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ return _instrument_call(
+ cache, method_name, original_method, args, kwargs, address, port
+ )
+
+ setattr(cache, method_name, sentry_method)
+
+
+def _patch_cache(cache, address=None, port=None):
+ # type: (CacheHandler, Optional[str], Optional[int]) -> None
+ if not hasattr(cache, "_sentry_patched"):
+ for method_name in METHODS_TO_INSTRUMENT:
+ _patch_cache_method(cache, method_name, address, port)
+ cache._sentry_patched = True
+
+
+def _get_address_port(settings):
+ # type: (dict[str, Any]) -> tuple[Optional[str], Optional[int]]
+ location = settings.get("LOCATION")
+
+ # TODO: location can also be an array of locations
+ # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis
+ # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062
+ if not isinstance(location, str):
+ return None, None
+
+ if "://" in location:
+ parsed_url = urlparse(location)
+ # remove the username and password from URL to not leak sensitive data.
+ address = "{}://{}{}".format(
+ parsed_url.scheme or "",
+ parsed_url.hostname or "",
+ parsed_url.path or "",
+ )
+ port = parsed_url.port
+ else:
+ address = location
+ port = None
+
+ return address, int(port) if port is not None else None
+
+
+def should_enable_cache_spans():
+ # type: () -> bool
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ client = sentry_sdk.get_client()
+ integration = client.get_integration(DjangoIntegration)
+ from django.conf import settings
+
+ return integration is not None and (
+ (client.spotlight is not None and settings.DEBUG is True)
+ or integration.cache_spans is True
+ )
+
+
+def patch_caching():
+ # type: () -> None
+ if not hasattr(CacheHandler, "_sentry_patched"):
+ if DJANGO_VERSION < (3, 2):
+ original_get_item = CacheHandler.__getitem__
+
+ @functools.wraps(original_get_item)
+ def sentry_get_item(self, alias):
+ # type: (CacheHandler, str) -> Any
+ cache = original_get_item(self, alias)
+
+ if should_enable_cache_spans():
+ from django.conf import settings
+
+ address, port = _get_address_port(
+ settings.CACHES[alias or "default"]
+ )
+
+ _patch_cache(cache, address, port)
+
+ return cache
+
+ CacheHandler.__getitem__ = sentry_get_item
+ CacheHandler._sentry_patched = True
+
+ else:
+ original_create_connection = CacheHandler.create_connection
+
+ @functools.wraps(original_create_connection)
+ def sentry_create_connection(self, alias):
+ # type: (CacheHandler, str) -> Any
+ cache = original_create_connection(self, alias)
+
+ if should_enable_cache_spans():
+ address, port = _get_address_port(self.settings[alias or "default"])
+
+ _patch_cache(cache, address, port)
+
+ return cache
+
+ CacheHandler.create_connection = sentry_create_connection
+ CacheHandler._sentry_patched = True
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/middleware.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/middleware.py
new file mode 100644
index 00000000..24527656
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/middleware.py
@@ -0,0 +1,187 @@
+"""
+Create spans from Django middleware invocations
+"""
+
+from functools import wraps
+
+from django import VERSION as DJANGO_VERSION
+
+import sentry_sdk
+from sentry_sdk.consts import OP
+from sentry_sdk.utils import (
+ ContextVar,
+ transaction_from_function,
+ capture_internal_exceptions,
+)
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any
+ from typing import Callable
+ from typing import Optional
+ from typing import TypeVar
+
+ from sentry_sdk.tracing import Span
+
+ F = TypeVar("F", bound=Callable[..., Any])
+
+_import_string_should_wrap_middleware = ContextVar(
+ "import_string_should_wrap_middleware"
+)
+
+DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1)
+
+if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE:
+ _asgi_middleware_mixin_factory = lambda _: object
+else:
+ from .asgi import _asgi_middleware_mixin_factory
+
+
+def patch_django_middlewares():
+ # type: () -> None
+ from django.core.handlers import base
+
+ old_import_string = base.import_string
+
+ def sentry_patched_import_string(dotted_path):
+ # type: (str) -> Any
+ rv = old_import_string(dotted_path)
+
+ if _import_string_should_wrap_middleware.get(None):
+ rv = _wrap_middleware(rv, dotted_path)
+
+ return rv
+
+ base.import_string = sentry_patched_import_string
+
+ old_load_middleware = base.BaseHandler.load_middleware
+
+ def sentry_patched_load_middleware(*args, **kwargs):
+ # type: (Any, Any) -> Any
+ _import_string_should_wrap_middleware.set(True)
+ try:
+ return old_load_middleware(*args, **kwargs)
+ finally:
+ _import_string_should_wrap_middleware.set(False)
+
+ base.BaseHandler.load_middleware = sentry_patched_load_middleware
+
+
+def _wrap_middleware(middleware, middleware_name):
+ # type: (Any, str) -> Any
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ def _check_middleware_span(old_method):
+ # type: (Callable[..., Any]) -> Optional[Span]
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
+ if integration is None or not integration.middleware_spans:
+ return None
+
+ function_name = transaction_from_function(old_method)
+
+ description = middleware_name
+ function_basename = getattr(old_method, "__name__", None)
+ if function_basename:
+ description = "{}.{}".format(description, function_basename)
+
+ middleware_span = sentry_sdk.start_span(
+ op=OP.MIDDLEWARE_DJANGO,
+ name=description,
+ origin=DjangoIntegration.origin,
+ )
+ middleware_span.set_tag("django.function_name", function_name)
+ middleware_span.set_tag("django.middleware_name", middleware_name)
+
+ return middleware_span
+
+ def _get_wrapped_method(old_method):
+ # type: (F) -> F
+ with capture_internal_exceptions():
+
+ def sentry_wrapped_method(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ middleware_span = _check_middleware_span(old_method)
+
+ if middleware_span is None:
+ return old_method(*args, **kwargs)
+
+ with middleware_span:
+ return old_method(*args, **kwargs)
+
+ try:
+ # fails for __call__ of function on Python 2 (see py2.7-django-1.11)
+ sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method)
+
+ # Necessary for Django 3.1
+ sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore
+ except Exception:
+ pass
+
+ return sentry_wrapped_method # type: ignore
+
+ return old_method
+
+ class SentryWrappingMiddleware(
+ _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore
+ ):
+ sync_capable = getattr(middleware, "sync_capable", True)
+ async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr(
+ middleware, "async_capable", False
+ )
+
+ def __init__(self, get_response=None, *args, **kwargs):
+ # type: (Optional[Callable[..., Any]], *Any, **Any) -> None
+ if get_response:
+ self._inner = middleware(get_response, *args, **kwargs)
+ else:
+ self._inner = middleware(*args, **kwargs)
+ self.get_response = get_response
+ self._call_method = None
+ if self.async_capable:
+ super().__init__(get_response)
+
+ # We need correct behavior for `hasattr()`, which we can only determine
+ # when we have an instance of the middleware we're wrapping.
+ def __getattr__(self, method_name):
+ # type: (str) -> Any
+ if method_name not in (
+ "process_request",
+ "process_view",
+ "process_template_response",
+ "process_response",
+ "process_exception",
+ ):
+ raise AttributeError()
+
+ old_method = getattr(self._inner, method_name)
+ rv = _get_wrapped_method(old_method)
+ self.__dict__[method_name] = rv
+ return rv
+
+ def __call__(self, *args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ if hasattr(self, "async_route_check") and self.async_route_check():
+ return self.__acall__(*args, **kwargs)
+
+ f = self._call_method
+ if f is None:
+ self._call_method = f = self._inner.__call__
+
+ middleware_span = _check_middleware_span(old_method=f)
+
+ if middleware_span is None:
+ return f(*args, **kwargs)
+
+ with middleware_span:
+ return f(*args, **kwargs)
+
+ for attr in (
+ "__name__",
+ "__module__",
+ "__qualname__",
+ ):
+ if hasattr(middleware, attr):
+ setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr))
+
+ return SentryWrappingMiddleware
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/signals_handlers.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/signals_handlers.py
new file mode 100644
index 00000000..cb0f8b9d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/signals_handlers.py
@@ -0,0 +1,91 @@
+from functools import wraps
+
+from django.dispatch import Signal
+
+import sentry_sdk
+from sentry_sdk.consts import OP
+from sentry_sdk.integrations.django import DJANGO_VERSION
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import Any, Union
+
+
+def _get_receiver_name(receiver):
+ # type: (Callable[..., Any]) -> str
+ name = ""
+
+ if hasattr(receiver, "__qualname__"):
+ name = receiver.__qualname__
+ elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__
+ name = receiver.__name__
+ elif hasattr(
+ receiver, "func"
+ ): # certain functions (like partials) dont have a name
+ if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"):
+ name = "partial(<function " + receiver.func.__name__ + ">)"
+
+ if (
+ name == ""
+ ): # In case nothing was found, return the string representation (this is the slowest case)
+ return str(receiver)
+
+ if hasattr(receiver, "__module__"): # prepend with module, if there is one
+ name = receiver.__module__ + "." + name
+
+ return name
+
+
+def patch_signals():
+ # type: () -> None
+ """
+ Patch django signal receivers to create a span.
+
+ This only wraps sync receivers. Django>=5.0 introduced async receivers, but
+ since we don't create transactions for ASGI Django, we don't wrap them.
+ """
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ old_live_receivers = Signal._live_receivers
+
+ def _sentry_live_receivers(self, sender):
+ # type: (Signal, Any) -> Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]
+ if DJANGO_VERSION >= (5, 0):
+ sync_receivers, async_receivers = old_live_receivers(self, sender)
+ else:
+ sync_receivers = old_live_receivers(self, sender)
+ async_receivers = []
+
+ def sentry_sync_receiver_wrapper(receiver):
+ # type: (Callable[..., Any]) -> Callable[..., Any]
+ @wraps(receiver)
+ def wrapper(*args, **kwargs):
+ # type: (Any, Any) -> Any
+ signal_name = _get_receiver_name(receiver)
+ with sentry_sdk.start_span(
+ op=OP.EVENT_DJANGO,
+ name=signal_name,
+ origin=DjangoIntegration.origin,
+ ) as span:
+ span.set_data("signal", signal_name)
+ return receiver(*args, **kwargs)
+
+ return wrapper
+
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
+ if (
+ integration
+ and integration.signals_spans
+ and self not in integration.signals_denylist
+ ):
+ for idx, receiver in enumerate(sync_receivers):
+ sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver)
+
+ if DJANGO_VERSION >= (5, 0):
+ return sync_receivers, async_receivers
+ else:
+ return sync_receivers
+
+ Signal._live_receivers = _sentry_live_receivers
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/templates.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/templates.py
new file mode 100644
index 00000000..10e8a924
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/templates.py
@@ -0,0 +1,188 @@
+import functools
+
+from django.template import TemplateSyntaxError
+from django.utils.safestring import mark_safe
+from django import VERSION as DJANGO_VERSION
+
+import sentry_sdk
+from sentry_sdk.consts import OP
+from sentry_sdk.utils import ensure_integration_enabled
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any
+ from typing import Dict
+ from typing import Optional
+ from typing import Iterator
+ from typing import Tuple
+
+try:
+ # support Django 1.9
+ from django.template.base import Origin
+except ImportError:
+ # backward compatibility
+ from django.template.loader import LoaderOrigin as Origin
+
+
+def get_template_frame_from_exception(exc_value):
+ # type: (Optional[BaseException]) -> Optional[Dict[str, Any]]
+
+ # As of Django 1.9 or so the new template debug thing showed up.
+ if hasattr(exc_value, "template_debug"):
+ return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore
+
+ # As of r16833 (Django) all exceptions may contain a
+ # ``django_template_source`` attribute (rather than the legacy
+ # ``TemplateSyntaxError.source`` check)
+ if hasattr(exc_value, "django_template_source"):
+ return _get_template_frame_from_source(
+ exc_value.django_template_source # type: ignore
+ )
+
+ if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"):
+ source = exc_value.source
+ if isinstance(source, (tuple, list)) and isinstance(source[0], Origin):
+ return _get_template_frame_from_source(source) # type: ignore
+
+ return None
+
+
+def _get_template_name_description(template_name):
+ # type: (str) -> str
+ if isinstance(template_name, (list, tuple)):
+ if template_name:
+ return "[{}, ...]".format(template_name[0])
+ else:
+ return template_name
+
+
+def patch_templates():
+ # type: () -> None
+ from django.template.response import SimpleTemplateResponse
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ real_rendered_content = SimpleTemplateResponse.rendered_content
+
+ @property # type: ignore
+ @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget)
+ def rendered_content(self):
+ # type: (SimpleTemplateResponse) -> str
+ with sentry_sdk.start_span(
+ op=OP.TEMPLATE_RENDER,
+ name=_get_template_name_description(self.template_name),
+ origin=DjangoIntegration.origin,
+ ) as span:
+ span.set_data("context", self.context_data)
+ return real_rendered_content.fget(self)
+
+ SimpleTemplateResponse.rendered_content = rendered_content
+
+ if DJANGO_VERSION < (1, 7):
+ return
+ import django.shortcuts
+
+ real_render = django.shortcuts.render
+
+ @functools.wraps(real_render)
+ @ensure_integration_enabled(DjangoIntegration, real_render)
+ def render(request, template_name, context=None, *args, **kwargs):
+ # type: (django.http.HttpRequest, str, Optional[Dict[str, Any]], *Any, **Any) -> django.http.HttpResponse
+
+ # Inject trace meta tags into template context
+ context = context or {}
+ if "sentry_trace_meta" not in context:
+ context["sentry_trace_meta"] = mark_safe(
+ sentry_sdk.get_current_scope().trace_propagation_meta()
+ )
+
+ with sentry_sdk.start_span(
+ op=OP.TEMPLATE_RENDER,
+ name=_get_template_name_description(template_name),
+ origin=DjangoIntegration.origin,
+ ) as span:
+ span.set_data("context", context)
+ return real_render(request, template_name, context, *args, **kwargs)
+
+ django.shortcuts.render = render
+
+
+def _get_template_frame_from_debug(debug):
+ # type: (Dict[str, Any]) -> Dict[str, Any]
+ if debug is None:
+ return None
+
+ lineno = debug["line"]
+ filename = debug["name"]
+ if filename is None:
+ filename = "<django template>"
+
+ pre_context = []
+ post_context = []
+ context_line = None
+
+ for i, line in debug["source_lines"]:
+ if i < lineno:
+ pre_context.append(line)
+ elif i > lineno:
+ post_context.append(line)
+ else:
+ context_line = line
+
+ return {
+ "filename": filename,
+ "lineno": lineno,
+ "pre_context": pre_context[-5:],
+ "post_context": post_context[:5],
+ "context_line": context_line,
+ "in_app": True,
+ }
+
+
+def _linebreak_iter(template_source):
+ # type: (str) -> Iterator[int]
+ yield 0
+ p = template_source.find("\n")
+ while p >= 0:
+ yield p + 1
+ p = template_source.find("\n", p + 1)
+
+
+def _get_template_frame_from_source(source):
+ # type: (Tuple[Origin, Tuple[int, int]]) -> Optional[Dict[str, Any]]
+ if not source:
+ return None
+
+ origin, (start, end) = source
+ filename = getattr(origin, "loadname", None)
+ if filename is None:
+ filename = "<django template>"
+ template_source = origin.reload()
+ lineno = None
+ upto = 0
+ pre_context = []
+ post_context = []
+ context_line = None
+
+ for num, next in enumerate(_linebreak_iter(template_source)):
+ line = template_source[upto:next]
+ if start >= upto and end <= next:
+ lineno = num
+ context_line = line
+ elif lineno is None:
+ pre_context.append(line)
+ else:
+ post_context.append(line)
+
+ upto = next
+
+ if context_line is None or lineno is None:
+ return None
+
+ return {
+ "filename": filename,
+ "lineno": lineno,
+ "pre_context": pre_context[-5:],
+ "post_context": post_context[:5],
+ "context_line": context_line,
+ }
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/transactions.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/transactions.py
new file mode 100644
index 00000000..5a7d69f3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/transactions.py
@@ -0,0 +1,159 @@
+"""
+Copied from raven-python.
+
+Despite being called "legacy" in some places this resolver is very much still
+in use.
+"""
+
+import re
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from django.urls.resolvers import URLResolver
+ from typing import Dict
+ from typing import List
+ from typing import Optional
+ from django.urls.resolvers import URLPattern
+ from typing import Tuple
+ from typing import Union
+ from re import Pattern
+
+from django import VERSION as DJANGO_VERSION
+
+if DJANGO_VERSION >= (2, 0):
+ from django.urls.resolvers import RoutePattern
+else:
+ RoutePattern = None
+
+try:
+ from django.urls import get_resolver
+except ImportError:
+ from django.core.urlresolvers import get_resolver
+
+
+def get_regex(resolver_or_pattern):
+ # type: (Union[URLPattern, URLResolver]) -> Pattern[str]
+ """Utility method for django's deprecated resolver.regex"""
+ try:
+ regex = resolver_or_pattern.regex
+ except AttributeError:
+ regex = resolver_or_pattern.pattern.regex
+ return regex
+
+
+class RavenResolver:
+ _new_style_group_matcher = re.compile(
+ r"<(?:([^>:]+):)?([^>]+)>"
+ ) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247
+ _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)")
+ _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+")
+ _non_named_group_matcher = re.compile(r"\([^\)]+\)")
+ # [foo|bar|baz]
+ _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]")
+ _camel_re = re.compile(r"([A-Z]+)([a-z])")
+
+ _cache = {} # type: Dict[URLPattern, str]
+
+ def _simplify(self, pattern):
+ # type: (Union[URLPattern, URLResolver]) -> str
+ r"""
+ Clean up urlpattern regexes into something readable by humans:
+
+ From:
+ > "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$"
+
+ To:
+ > "{sport_slug}/athletes/{athlete_slug}/"
+ """
+ # "new-style" path patterns can be parsed directly without turning them
+ # into regexes first
+ if (
+ RoutePattern is not None
+ and hasattr(pattern, "pattern")
+ and isinstance(pattern.pattern, RoutePattern)
+ ):
+ return self._new_style_group_matcher.sub(
+ lambda m: "{%s}" % m.group(2), str(pattern.pattern._route)
+ )
+
+ result = get_regex(pattern).pattern
+
+ # remove optional params
+ # TODO(dcramer): it'd be nice to change these into [%s] but it currently
+ # conflicts with the other rules because we're doing regexp matches
+ # rather than parsing tokens
+ result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result)
+
+ # handle named groups first
+ result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result)
+
+ # handle non-named groups
+ result = self._non_named_group_matcher.sub("{var}", result)
+
+ # handle optional params
+ result = self._either_option_matcher.sub(lambda m: m.group(1), result)
+
+ # clean up any outstanding regex-y characters.
+ result = (
+ result.replace("^", "")
+ .replace("$", "")
+ .replace("?", "")
+ .replace("\\A", "")
+ .replace("\\Z", "")
+ .replace("//", "/")
+ .replace("\\", "")
+ )
+
+ return result
+
+ def _resolve(self, resolver, path, parents=None):
+ # type: (URLResolver, str, Optional[List[URLResolver]]) -> Optional[str]
+
+ match = get_regex(resolver).search(path) # Django < 2.0
+
+ if not match:
+ return None
+
+ if parents is None:
+ parents = [resolver]
+ elif resolver not in parents:
+ parents = parents + [resolver]
+
+ new_path = path[match.end() :]
+ for pattern in resolver.url_patterns:
+ # this is an include()
+ if not pattern.callback:
+ match_ = self._resolve(pattern, new_path, parents)
+ if match_:
+ return match_
+ continue
+ elif not get_regex(pattern).search(new_path):
+ continue
+
+ try:
+ return self._cache[pattern]
+ except KeyError:
+ pass
+
+ prefix = "".join(self._simplify(p) for p in parents)
+ result = prefix + self._simplify(pattern)
+ if not result.startswith("/"):
+ result = "/" + result
+ self._cache[pattern] = result
+ return result
+
+ return None
+
+ def resolve(
+ self,
+ path, # type: str
+ urlconf=None, # type: Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]
+ ):
+ # type: (...) -> Optional[str]
+ resolver = get_resolver(urlconf)
+ match = self._resolve(resolver, path)
+ return match
+
+
+LEGACY_RESOLVER = RavenResolver()
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/views.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/views.py
new file mode 100644
index 00000000..0a9861a6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django/views.py
@@ -0,0 +1,96 @@
+import functools
+
+import sentry_sdk
+from sentry_sdk.consts import OP
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any
+
+
+try:
+ from asyncio import iscoroutinefunction
+except ImportError:
+ iscoroutinefunction = None # type: ignore
+
+
+try:
+ from sentry_sdk.integrations.django.asgi import wrap_async_view
+except (ImportError, SyntaxError):
+ wrap_async_view = None # type: ignore
+
+
+def patch_views():
+ # type: () -> None
+
+ from django.core.handlers.base import BaseHandler
+ from django.template.response import SimpleTemplateResponse
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ old_make_view_atomic = BaseHandler.make_view_atomic
+ old_render = SimpleTemplateResponse.render
+
+ def sentry_patched_render(self):
+ # type: (SimpleTemplateResponse) -> Any
+ with sentry_sdk.start_span(
+ op=OP.VIEW_RESPONSE_RENDER,
+ name="serialize response",
+ origin=DjangoIntegration.origin,
+ ):
+ return old_render(self)
+
+ @functools.wraps(old_make_view_atomic)
+ def sentry_patched_make_view_atomic(self, *args, **kwargs):
+ # type: (Any, *Any, **Any) -> Any
+ callback = old_make_view_atomic(self, *args, **kwargs)
+
+ # XXX: The wrapper function is created for every request. Find more
+ # efficient way to wrap views (or build a cache?)
+
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
+ if integration is not None and integration.middleware_spans:
+ is_async_view = (
+ iscoroutinefunction is not None
+ and wrap_async_view is not None
+ and iscoroutinefunction(callback)
+ )
+ if is_async_view:
+ sentry_wrapped_callback = wrap_async_view(callback)
+ else:
+ sentry_wrapped_callback = _wrap_sync_view(callback)
+
+ else:
+ sentry_wrapped_callback = callback
+
+ return sentry_wrapped_callback
+
+ SimpleTemplateResponse.render = sentry_patched_render
+ BaseHandler.make_view_atomic = sentry_patched_make_view_atomic
+
+
+def _wrap_sync_view(callback):
+ # type: (Any) -> Any
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ @functools.wraps(callback)
+ def sentry_wrapped_callback(request, *args, **kwargs):
+ # type: (Any, *Any, **Any) -> Any
+ current_scope = sentry_sdk.get_current_scope()
+ if current_scope.transaction is not None:
+ current_scope.transaction.update_active_thread()
+
+ sentry_scope = sentry_sdk.get_isolation_scope()
+ # set the active thread id to the handler thread for sync views
+ # this isn't necessary for async views since that runs on main
+ if sentry_scope.profile is not None:
+ sentry_scope.profile.update_active_thread_id()
+
+ with sentry_sdk.start_span(
+ op=OP.VIEW_RENDER,
+ name=request.resolver_match.view_name,
+ origin=DjangoIntegration.origin,
+ ):
+ return callback(request, *args, **kwargs)
+
+ return sentry_wrapped_callback