diff options
author | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
---|---|---|
committer | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
commit | 4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch) | |
tree | ee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django | |
parent | cc961e04ba734dd72309fb548a2f97d67d578813 (diff) | |
download | gn-ai-master.tar.gz |
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/integrations/django')
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 |