2025-07-01
This commit is contained in:
+747
@@ -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, TRANSACTION_SOURCE_URL
|
||||
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 = TRANSACTION_SOURCE_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 AttributeError:
|
||||
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)
|
||||
@@ -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
|
||||
+191
@@ -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
|
||||
+187
@@ -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
|
||||
+91
@@ -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
|
||||
+188
@@ -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,
|
||||
}
|
||||
+159
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user