449 lines
14 KiB
Python
449 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import absolute_import
|
|
|
|
import sys
|
|
import threading
|
|
import weakref
|
|
|
|
from django import VERSION as DJANGO_VERSION # type: ignore
|
|
from django.core import signals # type: ignore
|
|
|
|
from sentry_sdk._types import MYPY
|
|
from sentry_sdk.utils import HAS_REAL_CONTEXTVARS
|
|
|
|
if MYPY:
|
|
from typing import Any
|
|
from typing import Callable
|
|
from typing import Dict
|
|
from typing import Optional
|
|
from typing import Union
|
|
|
|
from django.core.handlers.wsgi import WSGIRequest # type: ignore
|
|
from django.http.response import HttpResponse # type: ignore
|
|
from django.http.request import QueryDict # type: ignore
|
|
from django.utils.datastructures import MultiValueDict # type: ignore
|
|
|
|
from sentry_sdk.integrations.wsgi import _ScopedResponse
|
|
from sentry_sdk._types import Event, Hint
|
|
|
|
|
|
try:
|
|
from django.urls import resolve # type: ignore
|
|
except ImportError:
|
|
from django.core.urlresolvers import resolve # type: ignore
|
|
|
|
from sentry_sdk import Hub
|
|
from sentry_sdk.hub import _should_send_default_pii
|
|
from sentry_sdk.scope import add_global_event_processor
|
|
from sentry_sdk.serializer import add_global_repr_processor
|
|
from sentry_sdk.tracing import record_sql_queries
|
|
from sentry_sdk.utils import (
|
|
capture_internal_exceptions,
|
|
event_from_exception,
|
|
transaction_from_function,
|
|
walk_exception_chain,
|
|
)
|
|
from sentry_sdk.integrations import Integration
|
|
from sentry_sdk.integrations.logging import ignore_logger
|
|
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
|
|
from sentry_sdk.integrations._wsgi_common import RequestExtractor
|
|
from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
|
|
from sentry_sdk.integrations.django.templates import get_template_frame_from_exception
|
|
from sentry_sdk.integrations.django.middleware import patch_django_middlewares
|
|
|
|
|
|
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
|
|
|
|
|
|
class DjangoIntegration(Integration):
|
|
identifier = "django"
|
|
|
|
transaction_style = None
|
|
middleware_spans = None
|
|
|
|
def __init__(self, transaction_style="url", middleware_spans=True):
|
|
# type: (str, bool) -> None
|
|
TRANSACTION_STYLE_VALUES = ("function_name", "url")
|
|
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
|
|
|
|
@staticmethod
|
|
def setup_once():
|
|
# type: () -> None
|
|
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__
|
|
|
|
def sentry_patched_wsgi_handler(self, environ, start_response):
|
|
# type: (Any, Dict[str, str], Callable) -> _ScopedResponse
|
|
if Hub.current.get_integration(DjangoIntegration) is None:
|
|
return old_app(self, environ, start_response)
|
|
|
|
bound_old_app = old_app.__get__(self, WSGIHandler)
|
|
|
|
return SentryWsgiMiddleware(bound_old_app)(environ, start_response)
|
|
|
|
WSGIHandler.__call__ = sentry_patched_wsgi_handler
|
|
|
|
# patch get_response, because at that point we have the Django request
|
|
# object
|
|
from django.core.handlers.base import BaseHandler # type: ignore
|
|
|
|
old_get_response = BaseHandler.get_response
|
|
|
|
def sentry_patched_get_response(self, request):
|
|
# type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException]
|
|
hub = Hub.current
|
|
integration = hub.get_integration(DjangoIntegration)
|
|
if integration is not None:
|
|
_patch_drf()
|
|
|
|
with hub.configure_scope() as scope:
|
|
# Rely on WSGI middleware to start a trace
|
|
try:
|
|
if integration.transaction_style == "function_name":
|
|
scope.transaction = transaction_from_function(
|
|
resolve(request.path).func
|
|
)
|
|
elif integration.transaction_style == "url":
|
|
scope.transaction = LEGACY_RESOLVER.resolve(request.path)
|
|
except Exception:
|
|
pass
|
|
|
|
scope.add_event_processor(
|
|
_make_event_processor(weakref.ref(request), integration)
|
|
)
|
|
return old_get_response(self, request)
|
|
|
|
BaseHandler.get_response = sentry_patched_get_response
|
|
|
|
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 ("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):
|
|
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 # type: ignore
|
|
except Exception:
|
|
return NotImplemented
|
|
|
|
if not isinstance(value, QuerySet) or value._result_cache:
|
|
return NotImplemented
|
|
|
|
# Do not call Hub.get_integration here. It is intentional that
|
|
# running under a new hub does not suddenly start executing
|
|
# querysets. This might be surprising to the user but it's likely
|
|
# less annoying.
|
|
|
|
return u"<%s from %s at 0x%x>" % (
|
|
value.__class__.__name__,
|
|
value.__module__,
|
|
id(value),
|
|
)
|
|
|
|
_patch_channels()
|
|
patch_django_middlewares()
|
|
|
|
|
|
_DRF_PATCHED = False
|
|
_DRF_PATCH_LOCK = threading.Lock()
|
|
|
|
|
|
def _patch_drf():
|
|
"""
|
|
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):
|
|
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():
|
|
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.
|
|
raise RuntimeError(
|
|
"We detected that you are using Django channels 2.0. To get proper "
|
|
"instrumentation for ASGI requests, the Sentry SDK requires "
|
|
"Python 3.7+ or the aiocontextvars package from PyPI."
|
|
)
|
|
|
|
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
|
|
|
old_app = AsgiHandler.__call__
|
|
|
|
def sentry_patched_asgi_handler(self, receive, send):
|
|
if Hub.current.get_integration(DjangoIntegration) is None:
|
|
return old_app(receive, send)
|
|
|
|
middleware = SentryAsgiMiddleware(
|
|
lambda _scope: old_app.__get__(self, AsgiHandler)
|
|
)
|
|
|
|
return middleware(self.scope)(receive, send)
|
|
|
|
AsgiHandler.__call__ = sentry_patched_asgi_handler
|
|
|
|
|
|
def _make_event_processor(weak_request, integration):
|
|
# type: (Callable[[], WSGIRequest], DjangoIntegration) -> Callable
|
|
def event_processor(event, hint):
|
|
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
|
|
# 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
|
|
|
|
try:
|
|
drf_request = request._sentry_drf_request_backref()
|
|
if drf_request is not None:
|
|
request = drf_request
|
|
except AttributeError:
|
|
pass
|
|
|
|
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 event_processor
|
|
|
|
|
|
def _got_request_exception(request=None, **kwargs):
|
|
# type: (WSGIRequest, **Any) -> None
|
|
hub = Hub.current
|
|
integration = hub.get_integration(DjangoIntegration)
|
|
if integration is not None:
|
|
|
|
# If an integration is there, a client has to be there.
|
|
client = hub.client # type: Any
|
|
|
|
event, hint = event_from_exception(
|
|
sys.exc_info(),
|
|
client_options=client.options,
|
|
mechanism={"type": "django", "handled": False},
|
|
)
|
|
hub.capture_event(event, hint=hint)
|
|
|
|
|
|
class DjangoRequestExtractor(RequestExtractor):
|
|
def env(self):
|
|
# type: () -> Dict[str, str]
|
|
return self.request.META
|
|
|
|
def cookies(self):
|
|
# type: () -> Dict[str, str]
|
|
return self.request.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):
|
|
return file.size
|
|
|
|
def parsed_body(self):
|
|
try:
|
|
return self.request.data
|
|
except AttributeError:
|
|
return RequestExtractor.parsed_body(self)
|
|
|
|
|
|
def _set_user_info(request, event):
|
|
# type: (WSGIRequest, Dict[str, Any]) -> None
|
|
user_info = event.setdefault("user", {})
|
|
|
|
user = getattr(request, "user", None)
|
|
|
|
if user is None or not is_authenticated(user):
|
|
return
|
|
|
|
try:
|
|
user_info["id"] = str(user.pk)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
user_info["email"] = user.email
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
user_info["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 # type: ignore
|
|
except ImportError:
|
|
from django.db.backends.util import CursorWrapper # type: ignore
|
|
|
|
try:
|
|
real_execute = CursorWrapper.execute
|
|
real_executemany = CursorWrapper.executemany
|
|
except AttributeError:
|
|
# This won't work on Django versions < 1.6
|
|
return
|
|
|
|
def execute(self, sql, params=None):
|
|
hub = Hub.current
|
|
if hub.get_integration(DjangoIntegration) is None:
|
|
return real_execute(self, sql, params)
|
|
|
|
with record_sql_queries(
|
|
hub, self.cursor, sql, params, paramstyle="format", executemany=False
|
|
):
|
|
return real_execute(self, sql, params)
|
|
|
|
def executemany(self, sql, param_list):
|
|
hub = Hub.current
|
|
if hub.get_integration(DjangoIntegration) is None:
|
|
return real_executemany(self, sql, param_list)
|
|
|
|
with record_sql_queries(
|
|
hub, self.cursor, sql, param_list, paramstyle="format", executemany=True
|
|
):
|
|
return real_executemany(self, sql, param_list)
|
|
|
|
CursorWrapper.execute = execute
|
|
CursorWrapper.executemany = executemany
|
|
ignore_logger("django.db.backends")
|