enhancement: DjangoDebugContext captures exceptions and allows captured stack traces to be queried (#1122)

This commit is contained in:
Jason Kraus 2021-03-02 10:45:46 -08:00 committed by GitHub
parent 6046a710c8
commit e9f25ecf2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 3 deletions

View File

@ -4,7 +4,7 @@ Django Debug Middleware
You can debug your GraphQL queries in a similar way to You can debug your GraphQL queries in a similar way to
`django-debug-toolbar <https://django-debug-toolbar.readthedocs.org/>`__, `django-debug-toolbar <https://django-debug-toolbar.readthedocs.org/>`__,
but outputting in the results in GraphQL response as fields, instead of 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. For that, you will need to add the plugin in your graphene schema.
@ -63,6 +63,10 @@ the GraphQL request, like:
sql { sql {
rawSql rawSql
} }
exceptions {
message
stack
}
} }
} }

View File

@ -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__
)
),
)

View File

@ -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")

View File

@ -3,6 +3,7 @@ from django.db import connections
from promise import Promise from promise import Promise
from .sql.tracking import unwrap_cursor, wrap_cursor from .sql.tracking import unwrap_cursor, wrap_cursor
from .exception.formating import wrap_exception
from .types import DjangoDebug from .types import DjangoDebug
@ -10,8 +11,8 @@ class DjangoDebugContext(object):
def __init__(self): def __init__(self):
self.debug_promise = None self.debug_promise = None
self.promises = [] self.promises = []
self.object = DjangoDebug(sql=[], exceptions=[])
self.enable_instrumentation() self.enable_instrumentation()
self.object = DjangoDebug(sql=[])
def get_debug_promise(self): def get_debug_promise(self):
if not self.debug_promise: if not self.debug_promise:
@ -19,6 +20,11 @@ class DjangoDebugContext(object):
self.promises = [] self.promises = []
return self.debug_promise.then(self.on_resolve_all_promises).get() 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): def on_resolve_all_promises(self, values):
if self.promises: if self.promises:
self.debug_promise = None self.debug_promise = None
@ -57,6 +63,9 @@ class DjangoDebugMiddleware(object):
) )
if info.schema.get_type("DjangoDebug") == info.return_type: if info.schema.get_type("DjangoDebug") == info.return_type:
return context.django_debug.get_debug_promise() return context.django_debug.get_debug_promise()
try:
promise = next(root, info, **args) promise = next(root, info, **args)
except Exception as e:
return context.django_debug.on_resolve_error(e)
context.django_debug.add_promise(promise) context.django_debug.add_promise(promise)
return promise return promise

View File

@ -272,3 +272,42 @@ def test_should_query_connectionfilter(graphene_settings, max_limit):
assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"] assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"]
query = str(Reporter.objects.all()[:1].query) query = str(Reporter.objects.all()[:1].query)
assert result.data["_debug"]["sql"][1]["rawSql"] == 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"

View File

@ -1,6 +1,7 @@
from graphene import List, ObjectType from graphene import List, ObjectType
from .sql.types import DjangoDebugSQL from .sql.types import DjangoDebugSQL
from .exception.types import DjangoDebugException
class DjangoDebug(ObjectType): class DjangoDebug(ObjectType):
@ -8,3 +9,6 @@ class DjangoDebug(ObjectType):
description = "Debugging information for the current query." description = "Debugging information for the current query."
sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.") sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.")
exceptions = List(
DjangoDebugException, description="Raise exceptions for this API query."
)