about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
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