Changed code to support older Python versions
This commit is contained in:
parent
eb92d2d36f
commit
582458cdd0
5027 changed files with 794942 additions and 4 deletions
|
|
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue