# -*- coding: utf-8 -*-
from __future__ import absolute_import
import sys
import weakref
from django import VERSION as DJANGO_VERSION # type: ignore
from django.db.models.query import QuerySet # type: ignore
from django.core import signals # type: ignore
if False:
from typing import Any
from typing import Dict
from typing import Tuple
from typing import Union
from sentry_sdk.integrations.wsgi import _ScopedResponse
from typing import Callable
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 typing import List
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.utils import (
add_global_repr_processor,
capture_internal_exceptions,
event_from_exception,
safe_repr,
format_and_strip,
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
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
def __init__(self, transaction_style="url"):
# type: (str) -> 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
@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)
return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))(
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:
with hub.configure_scope() as scope:
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: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
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(
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):
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),
)
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:
if integration.transaction_style == "function_name":
event["transaction"] = transaction_from_function(
resolve(request.path).func
)
elif integration.transaction_style == "url":
event["transaction"] = LEGACY_RESOLVER.resolve(request.path)
except Exception:
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:
event, hint = event_from_exception(
sys.exc_info(),
client_options=hub.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
class _FormatConverter(object):
def __init__(self, param_mapping):
# type: (Dict[str, int]) -> None
self.param_mapping = param_mapping
self.params = [] # type: List[Any]
def __getitem__(self, val):
# type: (str) -> str
self.params.append(self.param_mapping.get(val))
return "%s"
def format_sql(sql, params):
# type: (Any, Any) -> Tuple[str, List[str]]
rv = []
if isinstance(params, dict):
# convert sql with named parameters to sql with unnamed parameters
conv = _FormatConverter(params)
if params:
sql = sql % conv
params = conv.params
else:
params = ()
for param in params or ():
if param is None:
rv.append("NULL")
param = safe_repr(param)
rv.append(param)
return sql, rv
def record_sql(sql, params, cursor=None):
# type: (Any, Any, Any) -> None
hub = Hub.current
if hub.get_integration(DjangoIntegration) is None:
return
real_sql = None
real_params = None
try:
# Prefer our own SQL formatting logic because it's the only one that
# has proper value trimming.
real_sql, real_params = format_sql(sql, params)
if real_sql:
real_sql = format_and_strip(real_sql, real_params)
except Exception:
pass
if not real_sql and cursor and hasattr(cursor, "mogrify"):
# If formatting failed and we're using psycopg2, it could be that we're
# looking at a query that uses Composed objects. Use psycopg2's mogrify
# function to format the query. We lose per-parameter trimming but gain
# accuracy in formatting.
#
# This is intentionally the second choice because we assume Composed
# queries are not widely used, while per-parameter trimming is
# generally highly desirable.
try:
if cursor and hasattr(cursor, "mogrify"):
real_sql = cursor.mogrify(sql, params)
if isinstance(real_sql, bytes):
real_sql = real_sql.decode(cursor.connection.encoding)
except Exception:
pass
if real_sql:
with capture_internal_exceptions():
hub.add_breadcrumb(message=real_sql, category="query")
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 record_many_sql(sql, param_list, cursor):
for params in param_list:
record_sql(sql, params, cursor)
def execute(self, sql, params=None):
try:
return real_execute(self, sql, params)
finally:
record_sql(sql, params, self.cursor)
def executemany(self, sql, param_list):
try:
return real_executemany(self, sql, param_list)
finally:
record_many_sql(sql, param_list, self.cursor)
CursorWrapper.execute = execute
CursorWrapper.executemany = executemany
ignore_logger("django.db.backends")