Skip to content

Commit e9f25ec

Browse files
authored
enhancement: DjangoDebugContext captures exceptions and allows captured stack traces to be queried (#1122)
1 parent 6046a71 commit e9f25ec

File tree

7 files changed

+86
-3
lines changed

7 files changed

+86
-3
lines changed

docs/debug.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Django Debug Middleware
44
You can debug your GraphQL queries in a similar way to
55
`django-debug-toolbar <https://django-debug-toolbar.readthedocs.org/>`__,
66
but outputting in the results in GraphQL response as fields, instead of
7-
the graphical HTML interface.
7+
the graphical HTML interface. Exceptions with their stack traces are also exposed.
88

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

@@ -63,6 +63,10 @@ the GraphQL request, like:
6363
sql {
6464
rawSql
6565
}
66+
exceptions {
67+
message
68+
stack
69+
}
6670
}
6771
}
6872

graphene_django/debug/exception/__init__.py

Whitespace-only changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import traceback
2+
3+
from django.utils.encoding import force_str
4+
5+
from .types import DjangoDebugException
6+
7+
8+
def wrap_exception(exception):
9+
return DjangoDebugException(
10+
message=force_str(exception),
11+
exc_type=force_str(type(exception)),
12+
stack="".join(
13+
traceback.format_exception(
14+
etype=type(exception), value=exception, tb=exception.__traceback__
15+
)
16+
),
17+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from graphene import ObjectType, String
2+
3+
4+
class DjangoDebugException(ObjectType):
5+
class Meta:
6+
description = "Represents a single exception raised."
7+
8+
exc_type = String(required=True, description="The class of the exception")
9+
message = String(required=True, description="The message of the exception")
10+
stack = String(required=True, description="The stack trace")

graphene_django/debug/middleware.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@
33
from promise import Promise
44

55
from .sql.tracking import unwrap_cursor, wrap_cursor
6+
from .exception.formating import wrap_exception
67
from .types import DjangoDebug
78

89

910
class DjangoDebugContext(object):
1011
def __init__(self):
1112
self.debug_promise = None
1213
self.promises = []
14+
self.object = DjangoDebug(sql=[], exceptions=[])
1315
self.enable_instrumentation()
14-
self.object = DjangoDebug(sql=[])
1516

1617
def get_debug_promise(self):
1718
if not self.debug_promise:
1819
self.debug_promise = Promise.all(self.promises)
1920
self.promises = []
2021
return self.debug_promise.then(self.on_resolve_all_promises).get()
2122

23+
def on_resolve_error(self, value):
24+
if hasattr(self, "object"):
25+
self.object.exceptions.append(wrap_exception(value))
26+
return Promise.reject(value)
27+
2228
def on_resolve_all_promises(self, values):
2329
if self.promises:
2430
self.debug_promise = None
@@ -57,6 +63,9 @@ def resolve(self, next, root, info, **args):
5763
)
5864
if info.schema.get_type("DjangoDebug") == info.return_type:
5965
return context.django_debug.get_debug_promise()
60-
promise = next(root, info, **args)
66+
try:
67+
promise = next(root, info, **args)
68+
except Exception as e:
69+
return context.django_debug.on_resolve_error(e)
6170
context.django_debug.add_promise(promise)
6271
return promise

graphene_django/debug/tests/test_query.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,42 @@ def resolve_all_reporters(self, info, **args):
272272
assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"]
273273
query = str(Reporter.objects.all()[:1].query)
274274
assert result.data["_debug"]["sql"][1]["rawSql"] == query
275+
276+
277+
def test_should_query_stack_trace():
278+
class ReporterType(DjangoObjectType):
279+
class Meta:
280+
model = Reporter
281+
interfaces = (Node,)
282+
fields = "__all__"
283+
284+
class Query(graphene.ObjectType):
285+
reporter = graphene.Field(ReporterType)
286+
debug = graphene.Field(DjangoDebug, name="_debug")
287+
288+
def resolve_reporter(self, info, **args):
289+
raise Exception("caught stack trace")
290+
291+
query = """
292+
query ReporterQuery {
293+
reporter {
294+
lastName
295+
}
296+
_debug {
297+
exceptions {
298+
message
299+
stack
300+
}
301+
}
302+
}
303+
"""
304+
schema = graphene.Schema(query=Query)
305+
result = schema.execute(
306+
query, context_value=context(), middleware=[DjangoDebugMiddleware()]
307+
)
308+
assert result.errors
309+
assert len(result.data["_debug"]["exceptions"])
310+
debug_exception = result.data["_debug"]["exceptions"][0]
311+
assert debug_exception["stack"].count("\n") > 1
312+
assert "test_query.py" in debug_exception["stack"]
313+
assert debug_exception["message"] == "caught stack trace"

graphene_django/debug/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from graphene import List, ObjectType
22

33
from .sql.types import DjangoDebugSQL
4+
from .exception.types import DjangoDebugException
45

56

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

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

0 commit comments

Comments
 (0)