Skip to content
13 changes: 13 additions & 0 deletions elasticapm/utils/encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
import uuid
from decimal import Decimal

try:
from django.db.models import QuerySet as DjangoQuerySet
except ImportError:
DjangoQuerySet = None

from elasticapm.conf.constants import KEYWORD_MAX_LENGTH, LABEL_RE, LABEL_TYPES, LONG_FIELD_MAX_LENGTH

PROTECTED_TYPES = (int, type(None), float, Decimal, datetime.datetime, datetime.date, datetime.time)
Expand Down Expand Up @@ -144,6 +149,14 @@ class value_type(list):
ret = float(value)
elif isinstance(value, int):
ret = int(value)
elif (
DjangoQuerySet is not None
and isinstance(value, DjangoQuerySet)
and getattr(value, "_result_cache", True) is None
):
# if we have a Django QuerySet a None result cache it may mean that the underlying query failed
# so represent it as unevaluated instead of retrying the query again
ret = "<%s `unevaluated`>" % (value.__class__.__name__)
elif value is not None:
try:
ret = transform(repr(value))
Expand Down
17 changes: 17 additions & 0 deletions tests/contrib/django/django_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,23 @@ def test_capture_post_errors_dict(client, django_elasticapm_client):
assert error["context"]["request"]["body"] == "[REDACTED]"


@pytest.mark.parametrize(
"django_elasticapm_client",
[{"capture_body": "errors"}, {"capture_body": "transactions"}, {"capture_body": "all"}, {"capture_body": "off"}],
indirect=True,
)
def test_capture_django_orm_timeout_error(client, django_elasticapm_client):
with pytest.raises(DatabaseError):
client.get(reverse("elasticapm-django-orm-exc"))

errors = django_elasticapm_client.events[ERROR]
if django_elasticapm_client.config.capture_body in (constants.ERROR, "all"):
stacktrace = errors[0]["exception"]["stacktrace"]
frames = [frame for frame in stacktrace if frame["function"] == "django_queryset_error"]
qs_var = frames[0]["vars"]["qs"]
assert qs_var == "<CustomQuerySet `unevaluated`>"


def test_capture_body_config_is_dynamic_for_errors(client, django_elasticapm_client):
django_elasticapm_client.config.update(version="1", capture_body="all")
with pytest.raises(MyException):
Expand Down
1 change: 1 addition & 0 deletions tests/contrib/django/testapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def handler500(request):
re_path(r"^trigger-500-ioerror$", views.raise_ioerror, name="elasticapm-raise-ioerror"),
re_path(r"^trigger-500-decorated$", views.decorated_raise_exc, name="elasticapm-raise-exc-decor"),
re_path(r"^trigger-500-django$", views.django_exc, name="elasticapm-django-exc"),
re_path(r"^trigger-500-django-orm-exc$", views.django_queryset_error, name="elasticapm-django-orm-exc"),
re_path(r"^trigger-500-template$", views.template_exc, name="elasticapm-template-exc"),
re_path(r"^trigger-500-log-request$", views.logging_request_exc, name="elasticapm-log-request-exc"),
re_path(r"^streaming$", views.streaming_view, name="elasticapm-streaming-view"),
Expand Down
16 changes: 16 additions & 0 deletions tests/contrib/django/testapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import time

from django.contrib.auth.models import User
from django.db import DatabaseError
from django.db.models import QuerySet
from django.http import HttpResponse, StreamingHttpResponse
from django.shortcuts import get_object_or_404, render
from django.views import View
Expand Down Expand Up @@ -70,6 +72,20 @@ def django_exc(request):
return get_object_or_404(MyException, pk=1)


def django_queryset_error(request):
"""Simulation of django ORM timeout"""

class CustomQuerySet(QuerySet):
def all(self):
raise DatabaseError()

def __repr__(self) -> str:
return str(self._result_cache)

qs = CustomQuerySet()
list(qs.all())


def raise_exc(request):
raise MyException(request.GET.get("message", "view exception"))

Expand Down
Loading