Skip to content

enhancement: DjangoDebugContext captures exceptions #1122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/debug.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Django Debug Middleware
You can debug your GraphQL queries in a similar way to
`django-debug-toolbar <https://django-debug-toolbar.readthedocs.org/>`__,
but outputting in the results in GraphQL response as fields, instead of
the graphical HTML interface.
the graphical HTML interface. Exceptions with their stack traces are also exposed.

For that, you will need to add the plugin in your graphene schema.

Expand Down Expand Up @@ -63,6 +63,10 @@ the GraphQL request, like:
sql {
rawSql
}
exceptions {
message
stack
}
}
}

Expand Down
Empty file.
17 changes: 17 additions & 0 deletions graphene_django/debug/exception/formating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import traceback

from django.utils.encoding import force_str

from .types import DjangoDebugException


def wrap_exception(exception):
return DjangoDebugException(
message=force_str(exception),
exc_type=force_str(type(exception)),
stack="".join(
traceback.format_exception(
etype=type(exception), value=exception, tb=exception.__traceback__
)
),
)
10 changes: 10 additions & 0 deletions graphene_django/debug/exception/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from graphene import ObjectType, String


class DjangoDebugException(ObjectType):
class Meta:
description = "Represents a single exception raised."

exc_type = String(required=True, description="The class of the exception")
message = String(required=True, description="The message of the exception")
stack = String(required=True, description="The stack trace")
13 changes: 11 additions & 2 deletions graphene_django/debug/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@
from promise import Promise

from .sql.tracking import unwrap_cursor, wrap_cursor
from .exception.formating import wrap_exception
from .types import DjangoDebug


class DjangoDebugContext(object):
def __init__(self):
self.debug_promise = None
self.promises = []
self.object = DjangoDebug(sql=[], exceptions=[])
self.enable_instrumentation()
self.object = DjangoDebug(sql=[])

def get_debug_promise(self):
if not self.debug_promise:
self.debug_promise = Promise.all(self.promises)
self.promises = []
return self.debug_promise.then(self.on_resolve_all_promises).get()

def on_resolve_error(self, value):
if hasattr(self, "object"):
self.object.exceptions.append(wrap_exception(value))
return Promise.reject(value)

def on_resolve_all_promises(self, values):
if self.promises:
self.debug_promise = None
Expand Down Expand Up @@ -57,6 +63,9 @@ def resolve(self, next, root, info, **args):
)
if info.schema.get_type("DjangoDebug") == info.return_type:
return context.django_debug.get_debug_promise()
promise = next(root, info, **args)
try:
promise = next(root, info, **args)
except Exception as e:
return context.django_debug.on_resolve_error(e)
context.django_debug.add_promise(promise)
return promise
39 changes: 39 additions & 0 deletions graphene_django/debug/tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,42 @@ def resolve_all_reporters(self, info, **args):
assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"]
query = str(Reporter.objects.all()[:1].query)
assert result.data["_debug"]["sql"][1]["rawSql"] == query


def test_should_query_stack_trace():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
fields = "__all__"

class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
debug = graphene.Field(DjangoDebug, name="_debug")

def resolve_reporter(self, info, **args):
raise Exception("caught stack trace")

query = """
query ReporterQuery {
reporter {
lastName
}
_debug {
exceptions {
message
stack
}
}
}
"""
schema = graphene.Schema(query=Query)
result = schema.execute(
query, context_value=context(), middleware=[DjangoDebugMiddleware()]
)
assert result.errors
assert len(result.data["_debug"]["exceptions"])
debug_exception = result.data["_debug"]["exceptions"][0]
assert debug_exception["stack"].count("\n") > 1
assert "test_query.py" in debug_exception["stack"]
assert debug_exception["message"] == "caught stack trace"
4 changes: 4 additions & 0 deletions graphene_django/debug/types.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from graphene import List, ObjectType

from .sql.types import DjangoDebugSQL
from .exception.types import DjangoDebugException


class DjangoDebug(ObjectType):
class Meta:
description = "Debugging information for the current query."

sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.")
exceptions = List(
DjangoDebugException, description="Raise exceptions for this API query."
)