From 1f7b785f95f43e72516dc74a127e0cf02fb40a86 Mon Sep 17 00:00:00 2001 From: Jason Kraus Date: Tue, 16 Feb 2021 14:51:06 -0800 Subject: [PATCH] enhancement: DjangoDebugContext captures exceptions and allows captured stack traces to be queried --- docs/debug.rst | 6 ++- graphene_django/debug/exception/__init__.py | 0 graphene_django/debug/exception/formating.py | 17 +++++++++ graphene_django/debug/exception/types.py | 10 +++++ graphene_django/debug/middleware.py | 13 ++++++- graphene_django/debug/tests/test_query.py | 39 ++++++++++++++++++++ graphene_django/debug/types.py | 4 ++ 7 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 graphene_django/debug/exception/__init__.py create mode 100644 graphene_django/debug/exception/formating.py create mode 100644 graphene_django/debug/exception/types.py diff --git a/docs/debug.rst b/docs/debug.rst index 22865190f..1de52f189 100644 --- a/docs/debug.rst +++ b/docs/debug.rst @@ -4,7 +4,7 @@ Django Debug Middleware You can debug your GraphQL queries in a similar way to `django-debug-toolbar `__, 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. @@ -63,6 +63,10 @@ the GraphQL request, like: sql { rawSql } + exceptions { + message + stack + } } } diff --git a/graphene_django/debug/exception/__init__.py b/graphene_django/debug/exception/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/graphene_django/debug/exception/formating.py b/graphene_django/debug/exception/formating.py new file mode 100644 index 000000000..ed7ebabf3 --- /dev/null +++ b/graphene_django/debug/exception/formating.py @@ -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__ + ) + ), + ) diff --git a/graphene_django/debug/exception/types.py b/graphene_django/debug/exception/types.py new file mode 100644 index 000000000..3484ccb49 --- /dev/null +++ b/graphene_django/debug/exception/types.py @@ -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") diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 8621b55bc..804e7c838 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -3,6 +3,7 @@ from promise import Promise from .sql.tracking import unwrap_cursor, wrap_cursor +from .exception.formating import wrap_exception from .types import DjangoDebug @@ -10,8 +11,8 @@ 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: @@ -19,6 +20,11 @@ def get_debug_promise(self): 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 @@ -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 diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index d963b9cd3..eae94dcd4 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -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" diff --git a/graphene_django/debug/types.py b/graphene_django/debug/types.py index 1cd816d4d..a523b4fe1 100644 --- a/graphene_django/debug/types.py +++ b/graphene_django/debug/types.py @@ -1,6 +1,7 @@ from graphene import List, ObjectType from .sql.types import DjangoDebugSQL +from .exception.types import DjangoDebugException class DjangoDebug(ObjectType): @@ -8,3 +9,6 @@ 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." + )