Changed code to support older Python versions

This commit is contained in:
Malasaur 2025-12-01 23:27:09 +01:00
parent eb92d2d36f
commit 582458cdd0
5027 changed files with 794942 additions and 4 deletions

View file

@ -0,0 +1,758 @@
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, add_repr_sequence_type
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()
add_template_context_repr_sequence()
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)
def add_template_context_repr_sequence():
# type: () -> None
try:
from django.template.context import BaseContext
add_repr_sequence_type(BaseContext)
except Exception:
pass

View file

@ -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) # type: ignore
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) # type: ignore
with middleware_span:
return await f(*args, **kwargs) # type: ignore
return SentryASGIMixin

View file

@ -0,0 +1,204 @@
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_method = method_name == "get"
is_get_many_method = method_name == "get_many"
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_many_method:
if value != {}:
item_size = len(str(value))
span.set_data(SPANDATA.CACHE_HIT, True)
else:
span.set_data(SPANDATA.CACHE_HIT, False)
elif is_get_method:
default_value = None
if len(args) >= 2:
default_value = args[1]
elif "default" in kwargs:
default_value = kwargs["default"]
if value != default_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

View file

@ -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

View file

@ -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

View file

@ -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,
}

View file

@ -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()

View file

@ -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