Skip to content

Commit fc79dfe

Browse files
authored
Merge pull request #4437 from rmosolgo/backtrace-trace
Make GraphQL::Backtrace work with module traces
2 parents d314505 + a0eb401 commit fc79dfe

File tree

10 files changed

+297
-141
lines changed

10 files changed

+297
-141
lines changed

lib/graphql/backtrace.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "graphql/backtrace/table"
44
require "graphql/backtrace/traced_error"
55
require "graphql/backtrace/tracer"
6+
require "graphql/backtrace/trace"
67
module GraphQL
78
# Wrap unhandled errors with {TracedError}.
89
#
@@ -23,7 +24,7 @@ class Backtrace
2324
def_delegators :to_a, :each, :[]
2425

2526
def self.use(schema_defn)
26-
schema_defn.tracer(self::Tracer)
27+
schema_defn.trace_with(self::Trace)
2728
end
2829

2930
def initialize(context, value: nil)
@@ -54,5 +55,9 @@ def initialize(path:, query:, ast_node:, object:, field:, arguments:, parent_fra
5455
@parent_frame = parent_frame
5556
end
5657
end
58+
59+
class DefaultBacktraceTrace < GraphQL::Tracing::Trace
60+
include GraphQL::Backtrace::Trace
61+
end
5762
end
5863
end

lib/graphql/backtrace/trace.rb

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# frozen_string_literal: true
2+
module GraphQL
3+
class Backtrace
4+
module Trace
5+
def validate(query:, validate:)
6+
if query.multiplex
7+
push_query_backtrace_context(query)
8+
end
9+
super
10+
end
11+
12+
def analyze_query(query:)
13+
if query.multiplex # missing for stand-alone static validation
14+
push_query_backtrace_context(query)
15+
end
16+
super
17+
end
18+
19+
def execute_query(query:)
20+
push_query_backtrace_context(query)
21+
super
22+
end
23+
24+
def execute_query_lazy(query:, multiplex:)
25+
query ||= multiplex.queries.first
26+
push_query_backtrace_context(query)
27+
super
28+
end
29+
30+
def execute_field(field:, query:, ast_node:, arguments:, object:)
31+
push_field_backtrace_context(field, query, ast_node, arguments, object)
32+
super
33+
end
34+
35+
def execute_field_lazy(field:, query:, ast_node:, arguments:, object:)
36+
push_field_backtrace_context(field, query, ast_node, arguments, object)
37+
super
38+
end
39+
40+
def execute_multiplex(multiplex:)
41+
super
42+
rescue StandardError => err
43+
# This is an unhandled error from execution,
44+
# Re-raise it with a GraphQL trace.
45+
multiplex_context = multiplex.context
46+
potential_context = multiplex_context[:last_graphql_backtrace_context]
47+
48+
if potential_context.is_a?(GraphQL::Query::Context) ||
49+
potential_context.is_a?(Backtrace::Frame)
50+
raise TracedError.new(err, potential_context)
51+
else
52+
raise
53+
end
54+
ensure
55+
multiplex_context = multiplex.context
56+
multiplex_context.delete(:graphql_backtrace_contexts)
57+
multiplex_context.delete(:last_graphql_backtrace_context)
58+
end
59+
60+
private
61+
62+
def push_query_backtrace_context(query)
63+
push_data = query
64+
multiplex = query.multiplex
65+
push_key = []
66+
push_storage = multiplex.context[:graphql_backtrace_contexts] ||= {}
67+
push_storage[push_key] = push_data
68+
multiplex.context[:last_graphql_backtrace_context] = push_data
69+
end
70+
71+
def push_field_backtrace_context(field, query, ast_node, arguments, object)
72+
multiplex = query.multiplex
73+
push_key = query.context[:current_path]
74+
push_storage = multiplex.context[:graphql_backtrace_contexts]
75+
parent_frame = push_storage[push_key[0..-2]]
76+
77+
if parent_frame.is_a?(GraphQL::Query)
78+
parent_frame = parent_frame.context
79+
end
80+
81+
push_data = Frame.new(
82+
query: query,
83+
path: push_key,
84+
ast_node: ast_node,
85+
field: field,
86+
object: object,
87+
arguments: arguments,
88+
parent_frame: parent_frame,
89+
)
90+
91+
push_storage[push_key] = push_data
92+
multiplex.context[:last_graphql_backtrace_context] = push_data
93+
end
94+
end
95+
end
96+
end

lib/graphql/query.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,16 @@ def initialize(schema, query_string = nil, query: nil, document: nil, context: n
100100

101101
# Support `ctx[:backtrace] = true` for wrapping backtraces
102102
if context && context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
103-
context_tracers += [GraphQL::Backtrace::Tracer]
104-
@tracers << GraphQL::Backtrace::Tracer
103+
if schema.trace_class <= GraphQL::Tracing::LegacyTrace
104+
context_tracers += [GraphQL::Backtrace::Tracer]
105+
@tracers << GraphQL::Backtrace::Tracer
106+
elsif !(current_trace.class <= GraphQL::Backtrace::Trace)
107+
raise "Invariant: `backtrace: true` should have provided a trace class with Backtrace mixed in, but it didnt. (Found: #{current_trace.class.ancestors}). This is a bug in GraphQL-Ruby, please report it on GitHub."
108+
end
105109
end
106110

107111
if context_tracers.any? && !(schema.trace_class <= GraphQL::Tracing::LegacyTrace)
108-
raise ArgumentError, "context[:tracers] and context[:backtrace] are not supported without `trace_class(GraphQL::Tracing::LegacyTrace)` in the schema configuration, please add it."
112+
raise ArgumentError, "context[:tracers] are not supported without `trace_class(GraphQL::Tracing::LegacyTrace)` in the schema configuration, please add it."
109113
end
110114

111115

lib/graphql/schema.rb

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,41 @@ def subscriptions=(new_implementation)
145145

146146
def trace_class(new_class = nil)
147147
if new_class
148-
@trace_class = new_class
149-
elsif !defined?(@trace_class)
150-
parent_trace_class = if superclass.respond_to?(:trace_class)
151-
superclass.trace_class
148+
trace_mode(:default, new_class)
149+
backtrace_class = Class.new(new_class)
150+
backtrace_class.include(GraphQL::Backtrace::Trace)
151+
trace_mode(:default_backtrace, backtrace_class)
152+
end
153+
trace_class_for(:default)
154+
end
155+
156+
# @return [Class] Return the trace class to use for this mode, looking one up on the superclass if this Schema doesn't have one defined.
157+
def trace_class_for(mode)
158+
@trace_modes ||= {}
159+
@trace_modes[mode] ||= begin
160+
base_class = if superclass.respond_to?(:trace_class_for)
161+
superclass.trace_class_for(mode)
162+
elsif mode == :default_backtrace
163+
GraphQL::Backtrace::DefaultBacktraceTrace
152164
else
153165
GraphQL::Tracing::Trace
154166
end
155-
@trace_class = Class.new(parent_trace_class)
167+
Class.new(base_class)
156168
end
157-
@trace_class
158169
end
159170

171+
# Configure `trace_class` to be used whenever `context: { trace_mode: mode_name }` is requested.
172+
# `:default` is used when no `trace_mode: ...` is requested.
173+
# @param mode_name [Symbol]
174+
# @param trace_class [Class] subclass of GraphQL::Tracing::Trace
175+
# @return void
176+
def trace_mode(mode_name, trace_class)
177+
@trace_modes ||= {}
178+
@trace_modes[mode_name] = trace_class
179+
nil
180+
end
181+
182+
160183
# Returns the JSON response of {Introspection::INTROSPECTION_QUERY}.
161184
# @see {#as_json}
162185
# @return [String]
@@ -936,10 +959,10 @@ def default_directives
936959
end
937960

938961
def tracer(new_tracer)
939-
if defined?(@trace_class) && !(@trace_class < GraphQL::Tracing::LegacyTrace)
962+
if defined?(@trace_modes) && !(trace_class_for(:default) < GraphQL::Tracing::LegacyTrace)
940963
raise ArgumentError, "Can't add tracer after configuring a `trace_class`, use GraphQL::Tracing::LegacyTrace to merge legacy tracers into a trace class instead."
941-
elsif !defined?(@trace_class)
942-
@trace_class = Class.new(GraphQL::Tracing::LegacyTrace)
964+
else
965+
trace_mode(:default, Class.new(GraphQL::Tracing::LegacyTrace))
943966
end
944967

945968
own_tracers << new_tracer
@@ -968,7 +991,13 @@ def new_trace(**options)
968991
if defined?(@trace_options)
969992
options = trace_options.merge(options)
970993
end
971-
trace_class.new(**options)
994+
trace_mode = if (target = options[:query] || options[:multiplex]) && target.context[:backtrace]
995+
:default_backtrace
996+
else
997+
:default
998+
end
999+
trace = trace_class_for(trace_mode).new(**options)
1000+
trace
9721001
end
9731002

9741003
def query_analyzer(new_analyzer)

lib/graphql/tracing.rb

Lines changed: 3 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# frozen_string_literal: true
2+
require "graphql/tracing/trace"
3+
require "graphql/tracing/legacy_trace"
4+
25
# Legacy tracing:
36
require "graphql/tracing/active_support_notifications_tracing"
47
require "graphql/tracing/platform_tracing"
@@ -26,131 +29,8 @@
2629

2730
module GraphQL
2831
module Tracing
29-
class Trace
30-
# @param multiplex [GraphQL::Execution::Multiplex, nil]
31-
# @param query [GraphQL::Query, nil]
32-
def initialize(multiplex: nil, query: nil, **_options)
33-
@multiplex = multiplex
34-
@query = query
35-
end
36-
37-
def lex(query_string:)
38-
yield
39-
end
40-
41-
def parse(query_string:)
42-
yield
43-
end
44-
45-
def validate(query:, validate:)
46-
yield
47-
end
48-
49-
def analyze_multiplex(multiplex:)
50-
yield
51-
end
52-
53-
def analyze_query(query:)
54-
yield
55-
end
56-
57-
def execute_multiplex(multiplex:)
58-
yield
59-
end
60-
61-
def execute_query(query:)
62-
yield
63-
end
64-
65-
def execute_query_lazy(query:, multiplex:)
66-
yield
67-
end
68-
69-
def execute_field(field:, query:, ast_node:, arguments:, object:)
70-
yield
71-
end
72-
73-
def execute_field_lazy(field:, query:, ast_node:, arguments:, object:)
74-
yield
75-
end
76-
77-
def authorized(query:, type:, object:)
78-
yield
79-
end
80-
81-
def authorized_lazy(query:, type:, object:)
82-
yield
83-
end
84-
85-
def resolve_type(query:, type:, object:)
86-
yield
87-
end
88-
89-
def resolve_type_lazy(query:, type:, object:)
90-
yield
91-
end
92-
end
93-
9432
NullTrace = Trace.new
9533

96-
class LegacyTrace < Trace
97-
def lex(query_string:, &block)
98-
(@multiplex || @query).trace("lex", { query_string: query_string }, &block)
99-
end
100-
101-
def parse(query_string:, &block)
102-
(@multiplex || @query).trace("parse", { query_string: query_string }, &block)
103-
end
104-
105-
def validate(query:, validate:, &block)
106-
query.trace("validate", { validate: validate, query: query }, &block)
107-
end
108-
109-
def analyze_multiplex(multiplex:, &block)
110-
multiplex.trace("analyze_multiplex", { multiplex: multiplex }, &block)
111-
end
112-
113-
def analyze_query(query:, &block)
114-
query.trace("analyze_query", { query: query }, &block)
115-
end
116-
117-
def execute_multiplex(multiplex:, &block)
118-
multiplex.trace("execute_multiplex", { multiplex: multiplex }, &block)
119-
end
120-
121-
def execute_query(query:, &block)
122-
query.trace("execute_query", { query: query }, &block)
123-
end
124-
125-
def execute_query_lazy(query:, multiplex:, &block)
126-
multiplex.trace("execute_query_lazy", { multiplex: multiplex, query: query }, &block)
127-
end
128-
129-
def execute_field(field:, query:, ast_node:, arguments:, object:, &block)
130-
query.trace("execute_field", { field: field, query: query, ast_node: ast_node, arguments: arguments, object: object, owner: field.owner, path: query.context[:current_path] }, &block)
131-
end
132-
133-
def execute_field_lazy(field:, query:, ast_node:, arguments:, object:, &block)
134-
query.trace("execute_field_lazy", { field: field, query: query, ast_node: ast_node, arguments: arguments, object: object, owner: field.owner, path: query.context[:current_path] }, &block)
135-
end
136-
137-
def authorized(query:, type:, object:, &block)
138-
query.trace("authorized", { context: query.context, type: type, object: object, path: query.context[:current_path] }, &block)
139-
end
140-
141-
def authorized_lazy(query:, type:, object:, &block)
142-
query.trace("authorized_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }, &block)
143-
end
144-
145-
def resolve_type(query:, type:, object:, &block)
146-
query.trace("resolve_type", { context: query.context, type: type, object: object, path: query.context[:current_path] }, &block)
147-
end
148-
149-
def resolve_type_lazy(query:, type:, object:, &block)
150-
query.trace("resolve_type_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }, &block)
151-
end
152-
end
153-
15434
# Objects may include traceable to gain a `.trace(...)` method.
15535
# The object must have a `@tracers` ivar of type `Array<<#trace(k, d, &b)>>`.
15636
# @api private

0 commit comments

Comments
 (0)