graphql 2.0.16 → 2.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of graphql might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/graphql/analysis/ast/visitor.rb +42 -35
- data/lib/graphql/analysis/ast.rb +2 -2
- data/lib/graphql/backtrace/trace.rb +96 -0
- data/lib/graphql/backtrace/tracer.rb +1 -1
- data/lib/graphql/backtrace.rb +6 -1
- data/lib/graphql/execution/interpreter/arguments.rb +1 -1
- data/lib/graphql/execution/interpreter/arguments_cache.rb +2 -3
- data/lib/graphql/execution/interpreter/resolve.rb +19 -0
- data/lib/graphql/execution/interpreter/runtime.rb +264 -211
- data/lib/graphql/execution/interpreter.rb +15 -10
- data/lib/graphql/execution/lazy.rb +6 -12
- data/lib/graphql/execution/multiplex.rb +2 -1
- data/lib/graphql/filter.rb +7 -2
- data/lib/graphql/introspection/directive_type.rb +2 -2
- data/lib/graphql/introspection/field_type.rb +1 -1
- data/lib/graphql/introspection/schema_type.rb +2 -2
- data/lib/graphql/introspection/type_type.rb +5 -5
- data/lib/graphql/language/document_from_schema_definition.rb +25 -9
- data/lib/graphql/language/lexer.rb +216 -1505
- data/lib/graphql/language/nodes.rb +66 -40
- data/lib/graphql/language/parser.rb +509 -491
- data/lib/graphql/language/parser.y +43 -38
- data/lib/graphql/language/visitor.rb +191 -83
- data/lib/graphql/pagination/active_record_relation_connection.rb +0 -8
- data/lib/graphql/pagination/connection.rb +5 -5
- data/lib/graphql/query/context.rb +62 -31
- data/lib/graphql/query/null_context.rb +1 -1
- data/lib/graphql/query.rb +22 -5
- data/lib/graphql/schema/argument.rb +7 -13
- data/lib/graphql/schema/build_from_definition.rb +15 -3
- data/lib/graphql/schema/directive.rb +12 -2
- data/lib/graphql/schema/enum.rb +24 -17
- data/lib/graphql/schema/enum_value.rb +2 -3
- data/lib/graphql/schema/field.rb +68 -57
- data/lib/graphql/schema/field_extension.rb +1 -4
- data/lib/graphql/schema/find_inherited_value.rb +2 -7
- data/lib/graphql/schema/interface.rb +0 -10
- data/lib/graphql/schema/late_bound_type.rb +2 -0
- data/lib/graphql/schema/member/base_dsl_methods.rb +17 -14
- data/lib/graphql/schema/member/has_arguments.rb +105 -58
- data/lib/graphql/schema/member/has_ast_node.rb +12 -0
- data/lib/graphql/schema/member/has_deprecation_reason.rb +3 -4
- data/lib/graphql/schema/member/has_directives.rb +15 -10
- data/lib/graphql/schema/member/has_fields.rb +95 -38
- data/lib/graphql/schema/member/has_interfaces.rb +49 -8
- data/lib/graphql/schema/member/has_validators.rb +32 -6
- data/lib/graphql/schema/member/relay_shortcuts.rb +19 -0
- data/lib/graphql/schema/member/type_system_helpers.rb +17 -0
- data/lib/graphql/schema/object.rb +2 -4
- data/lib/graphql/schema/resolver/has_payload_type.rb +9 -9
- data/lib/graphql/schema/resolver.rb +4 -4
- data/lib/graphql/schema/timeout.rb +24 -28
- data/lib/graphql/schema/validator.rb +1 -1
- data/lib/graphql/schema/warden.rb +29 -5
- data/lib/graphql/schema.rb +76 -25
- data/lib/graphql/static_validation/literal_validator.rb +15 -1
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +12 -4
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +2 -2
- data/lib/graphql/static_validation/validator.rb +1 -1
- data/lib/graphql/subscriptions/event.rb +2 -7
- data/lib/graphql/tracing/active_support_notifications_trace.rb +16 -0
- data/lib/graphql/tracing/appoptics_trace.rb +231 -0
- data/lib/graphql/tracing/appsignal_trace.rb +77 -0
- data/lib/graphql/tracing/data_dog_trace.rb +148 -0
- data/lib/graphql/tracing/legacy_trace.rb +65 -0
- data/lib/graphql/tracing/new_relic_trace.rb +75 -0
- data/lib/graphql/tracing/notifications_trace.rb +42 -0
- data/lib/graphql/tracing/platform_trace.rb +109 -0
- data/lib/graphql/tracing/platform_tracing.rb +15 -3
- data/lib/graphql/tracing/prometheus_trace.rb +89 -0
- data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +1 -1
- data/lib/graphql/tracing/prometheus_tracing.rb +3 -3
- data/lib/graphql/tracing/scout_trace.rb +72 -0
- data/lib/graphql/tracing/statsd_trace.rb +56 -0
- data/lib/graphql/tracing/trace.rb +75 -0
- data/lib/graphql/tracing.rb +16 -39
- data/lib/graphql/type_kinds.rb +6 -3
- data/lib/graphql/types/relay/base_connection.rb +1 -1
- data/lib/graphql/types/relay/connection_behaviors.rb +24 -6
- data/lib/graphql/types/relay/edge_behaviors.rb +16 -6
- data/lib/graphql/types/relay/node_behaviors.rb +7 -1
- data/lib/graphql/types/relay/page_info_behaviors.rb +7 -2
- data/lib/graphql/types/relay.rb +0 -1
- data/lib/graphql/types/string.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +16 -9
- metadata +34 -9
- data/lib/graphql/language/lexer.rl +0 -280
- data/lib/graphql/types/relay/default_relay.rb +0 -27
@@ -38,7 +38,9 @@ module GraphQL
|
|
38
38
|
# @api private
|
39
39
|
class Warden
|
40
40
|
def self.from_context(context)
|
41
|
-
|
41
|
+
context.warden # this might be a hash which won't respond to this
|
42
|
+
rescue
|
43
|
+
PassThruWarden
|
42
44
|
end
|
43
45
|
|
44
46
|
# @param visibility_method [Symbol] a Warden method to call for this entry
|
@@ -80,6 +82,8 @@ module GraphQL
|
|
80
82
|
def visible_type?(type, ctx); type.visible?(ctx); end
|
81
83
|
def visible_enum_value?(ev, ctx); ev.visible?(ctx); end
|
82
84
|
def visible_type_membership?(tm, ctx); tm.visible?(ctx); end
|
85
|
+
def interface_type_memberships(obj_t, ctx); obj_t.interface_type_memberships; end
|
86
|
+
def arguments(owner, ctx); owner.arguments(ctx); end
|
83
87
|
end
|
84
88
|
end
|
85
89
|
|
@@ -94,6 +98,13 @@ module GraphQL
|
|
94
98
|
@subscription = @schema.subscription
|
95
99
|
@context = context
|
96
100
|
@visibility_cache = read_through { |m| filter.call(m, context) }
|
101
|
+
# Initialize all ivars to improve object shape consistency:
|
102
|
+
@types = @visible_types = @reachable_types = @visible_parent_fields =
|
103
|
+
@visible_possible_types = @visible_fields = @visible_arguments = @visible_enum_arrays =
|
104
|
+
@visible_enum_values = @visible_interfaces = @type_visibility = @type_memberships =
|
105
|
+
@visible_and_reachable_type = @unions = @unfiltered_interfaces = @references_to =
|
106
|
+
@reachable_type_set =
|
107
|
+
nil
|
97
108
|
end
|
98
109
|
|
99
110
|
# @return [Hash<String, GraphQL::BaseType>] Visible types in the schema
|
@@ -174,14 +185,20 @@ module GraphQL
|
|
174
185
|
|
175
186
|
# @param argument_owner [GraphQL::Field, GraphQL::InputObjectType]
|
176
187
|
# @return [Array<GraphQL::Argument>] Visible arguments on `argument_owner`
|
177
|
-
def arguments(argument_owner)
|
178
|
-
@visible_arguments ||= read_through { |o| o.arguments(@context).each_value.select { |a| visible_argument?(a) } }
|
188
|
+
def arguments(argument_owner, ctx = nil)
|
189
|
+
@visible_arguments ||= read_through { |o| o.arguments(@context).each_value.select { |a| visible_argument?(a, @context) } }
|
179
190
|
@visible_arguments[argument_owner]
|
180
191
|
end
|
181
192
|
|
182
193
|
# @return [Array<GraphQL::EnumType::EnumValue>] Visible members of `enum_defn`
|
183
194
|
def enum_values(enum_defn)
|
184
|
-
@visible_enum_arrays ||= read_through { |e|
|
195
|
+
@visible_enum_arrays ||= read_through { |e|
|
196
|
+
values = e.enum_values(@context)
|
197
|
+
if values.size == 0
|
198
|
+
raise GraphQL::Schema::Enum::MissingValuesError.new(e)
|
199
|
+
end
|
200
|
+
values
|
201
|
+
}
|
185
202
|
@visible_enum_arrays[enum_defn]
|
186
203
|
end
|
187
204
|
|
@@ -233,6 +250,13 @@ module GraphQL
|
|
233
250
|
visible?(type_membership)
|
234
251
|
end
|
235
252
|
|
253
|
+
def interface_type_memberships(obj_type, _ctx = nil)
|
254
|
+
@type_memberships ||= read_through do |obj_t|
|
255
|
+
obj_t.interface_type_memberships
|
256
|
+
end
|
257
|
+
@type_memberships[obj_type]
|
258
|
+
end
|
259
|
+
|
236
260
|
private
|
237
261
|
|
238
262
|
def visible_and_reachable_type?(type_defn)
|
@@ -332,7 +356,7 @@ module GraphQL
|
|
332
356
|
end
|
333
357
|
|
334
358
|
def reachable_type_set
|
335
|
-
return @reachable_type_set if
|
359
|
+
return @reachable_type_set if @reachable_type_set
|
336
360
|
|
337
361
|
@reachable_type_set = Set.new
|
338
362
|
rt_hash = {}
|
data/lib/graphql/schema.rb
CHANGED
@@ -62,7 +62,7 @@ module GraphQL
|
|
62
62
|
# Schemas can specify how queries should be executed against them.
|
63
63
|
# `query_execution_strategy`, `mutation_execution_strategy` and `subscription_execution_strategy`
|
64
64
|
# each apply to corresponding root types.
|
65
|
-
#
|
65
|
+
#
|
66
66
|
# @example defining a schema
|
67
67
|
# class MySchema < GraphQL::Schema
|
68
68
|
# query QueryType
|
@@ -143,6 +143,43 @@ module GraphQL
|
|
143
143
|
@subscriptions = new_implementation
|
144
144
|
end
|
145
145
|
|
146
|
+
def trace_class(new_class = nil)
|
147
|
+
if new_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
|
164
|
+
else
|
165
|
+
GraphQL::Tracing::Trace
|
166
|
+
end
|
167
|
+
Class.new(base_class)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
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
|
+
|
146
183
|
# Returns the JSON response of {Introspection::INTROSPECTION_QUERY}.
|
147
184
|
# @see {#as_json}
|
148
185
|
# @return [String]
|
@@ -207,7 +244,7 @@ module GraphQL
|
|
207
244
|
end
|
208
245
|
|
209
246
|
def default_filter
|
210
|
-
GraphQL::Filter.new(except: default_mask)
|
247
|
+
GraphQL::Filter.new(except: default_mask, silence_deprecation_warning: true)
|
211
248
|
end
|
212
249
|
|
213
250
|
def default_mask(new_mask = nil)
|
@@ -755,7 +792,7 @@ module GraphQL
|
|
755
792
|
if handler
|
756
793
|
obj = context[:current_object]
|
757
794
|
args = context[:current_arguments]
|
758
|
-
args = args && args.keyword_arguments
|
795
|
+
args = args && args.respond_to?(:keyword_arguments) ? args.keyword_arguments : nil
|
759
796
|
field = context[:current_field]
|
760
797
|
if obj.is_a?(GraphQL::Schema::Object)
|
761
798
|
obj = obj.object
|
@@ -785,11 +822,7 @@ module GraphQL
|
|
785
822
|
end
|
786
823
|
|
787
824
|
if resolved_type.nil? || (resolved_type.is_a?(Module) && resolved_type.respond_to?(:kind))
|
788
|
-
|
789
|
-
[resolved_type, resolved_value]
|
790
|
-
else
|
791
|
-
resolved_type
|
792
|
-
end
|
825
|
+
[resolved_type, resolved_value]
|
793
826
|
else
|
794
827
|
raise ".resolve_type should return a type definition, but got #{resolved_type.inspect} (#{resolved_type.class}) from `resolve_type(#{type}, #{obj}, #{ctx})`"
|
795
828
|
end
|
@@ -826,10 +859,6 @@ module GraphQL
|
|
826
859
|
member.visible?(ctx)
|
827
860
|
end
|
828
861
|
|
829
|
-
def accessible?(member, ctx)
|
830
|
-
member.accessible?(ctx)
|
831
|
-
end
|
832
|
-
|
833
862
|
def schema_directive(dir_class, **options)
|
834
863
|
@own_schema_directives ||= []
|
835
864
|
Member::HasDirectives.add_directive(self, @own_schema_directives, dir_class, options)
|
@@ -839,18 +868,6 @@ module GraphQL
|
|
839
868
|
Member::HasDirectives.get_directives(self, @own_schema_directives, :schema_directives)
|
840
869
|
end
|
841
870
|
|
842
|
-
# This hook is called when a client tries to access one or more
|
843
|
-
# fields that fail the `accessible?` check.
|
844
|
-
#
|
845
|
-
# By default, an error is added to the response. Override this hook to
|
846
|
-
# track metrics or return a different error to the client.
|
847
|
-
#
|
848
|
-
# @param error [InaccessibleFieldsError] The analysis error for this check
|
849
|
-
# @return [AnalysisError, nil] Return an error to skip the query
|
850
|
-
def inaccessible_fields(error)
|
851
|
-
error
|
852
|
-
end
|
853
|
-
|
854
871
|
# This hook is called when an object fails an `authorized?` check.
|
855
872
|
# You might report to your bug tracker here, so you can correct
|
856
873
|
# the field resolvers not to return unauthorized objects.
|
@@ -900,7 +917,7 @@ module GraphQL
|
|
900
917
|
# A function to call when {#execute} receives an invalid query string
|
901
918
|
#
|
902
919
|
# The default is to add the error to `context.errors`
|
903
|
-
# @param
|
920
|
+
# @param parse_err [GraphQL::ParseError] The error encountered during parsing
|
904
921
|
# @param ctx [GraphQL::Query::Context] The context for the query where the error occurred
|
905
922
|
# @return void
|
906
923
|
def parse_error(parse_err, ctx)
|
@@ -942,6 +959,12 @@ module GraphQL
|
|
942
959
|
end
|
943
960
|
|
944
961
|
def tracer(new_tracer)
|
962
|
+
if defined?(@trace_modes) && !(trace_class_for(:default) < GraphQL::Tracing::LegacyTrace)
|
963
|
+
raise ArgumentError, "Can't add tracer after configuring a `trace_class`, use GraphQL::Tracing::LegacyTrace to merge legacy tracers into a trace class instead."
|
964
|
+
else
|
965
|
+
trace_mode(:default, Class.new(GraphQL::Tracing::LegacyTrace))
|
966
|
+
end
|
967
|
+
|
945
968
|
own_tracers << new_tracer
|
946
969
|
end
|
947
970
|
|
@@ -949,6 +972,34 @@ module GraphQL
|
|
949
972
|
find_inherited_value(:tracers, EMPTY_ARRAY) + own_tracers
|
950
973
|
end
|
951
974
|
|
975
|
+
# Mix `trace_mod` into this schema's `Trace` class so that its methods
|
976
|
+
# will be called at runtime.
|
977
|
+
#
|
978
|
+
# @param trace_mod [Module] A module that implements tracing methods
|
979
|
+
# @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
|
980
|
+
# @return [void]
|
981
|
+
def trace_with(trace_mod, **options)
|
982
|
+
trace_options.merge!(options)
|
983
|
+
trace_class.include(trace_mod)
|
984
|
+
end
|
985
|
+
|
986
|
+
def trace_options
|
987
|
+
@trace_options ||= superclass.respond_to?(:trace_options) ? superclass.trace_options.dup : {}
|
988
|
+
end
|
989
|
+
|
990
|
+
def new_trace(**options)
|
991
|
+
if defined?(@trace_options)
|
992
|
+
options = trace_options.merge(options)
|
993
|
+
end
|
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
|
1001
|
+
end
|
1002
|
+
|
952
1003
|
def query_analyzer(new_analyzer)
|
953
1004
|
own_query_analyzers << new_analyzer
|
954
1005
|
end
|
@@ -18,6 +18,19 @@ module GraphQL
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
+
def replace_nulls_in(ast_value)
|
22
|
+
case ast_value
|
23
|
+
when Array
|
24
|
+
ast_value.map { |v| replace_nulls_in(v) }
|
25
|
+
when GraphQL::Language::Nodes::InputObject
|
26
|
+
ast_value.to_h
|
27
|
+
when GraphQL::Language::Nodes::NullValue
|
28
|
+
nil
|
29
|
+
else
|
30
|
+
ast_value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
21
34
|
def recursively_validate(ast_value, type)
|
22
35
|
if type.nil?
|
23
36
|
# this means we're an undefined argument, see #present_input_field_values_are_valid
|
@@ -42,7 +55,8 @@ module GraphQL
|
|
42
55
|
@valid_response
|
43
56
|
elsif type.kind.scalar? && constant_scalar?(ast_value)
|
44
57
|
maybe_raise_if_invalid(ast_value) do
|
45
|
-
|
58
|
+
ruby_value = replace_nulls_in(ast_value)
|
59
|
+
type.validate_input(ruby_value, @context)
|
46
60
|
end
|
47
61
|
elsif type.kind.enum?
|
48
62
|
maybe_raise_if_invalid(ast_value) do
|
@@ -26,11 +26,19 @@ module GraphQL
|
|
26
26
|
msg = if resolved_type.nil?
|
27
27
|
nil
|
28
28
|
elsif resolved_type.kind.scalar? && ast_node.selections.any?
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
selection_strs = ast_node.selections.map do |n|
|
30
|
+
case n
|
31
|
+
when GraphQL::Language::Nodes::InlineFragment
|
32
|
+
"\"... on #{n.type.name} { ... }\""
|
33
|
+
when GraphQL::Language::Nodes::Field
|
34
|
+
"\"#{n.name}\""
|
35
|
+
when GraphQL::Language::Nodes::FragmentSpread
|
36
|
+
"\"#{n.name}\""
|
37
|
+
else
|
38
|
+
raise "Invariant: unexpected selection node: #{n}"
|
39
|
+
end
|
33
40
|
end
|
41
|
+
"Selections can't be made on scalars (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])"
|
34
42
|
elsif resolved_type.kind.fields? && ast_node.selections.empty?
|
35
43
|
"Field must have selections (%{node_name} returns #{resolved_type.graphql_name} but has no selections. Did you mean '#{ast_node.name} { ... }'?)"
|
36
44
|
else
|
@@ -9,7 +9,7 @@ module GraphQL
|
|
9
9
|
# without ambiguity.
|
10
10
|
#
|
11
11
|
# Original Algorithm: https://github.com/graphql/graphql-js/blob/master/src/validation/rules/OverlappingFieldsCanBeMerged.js
|
12
|
-
NO_ARGS =
|
12
|
+
NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
|
13
13
|
|
14
14
|
Field = Struct.new(:node, :definition, :owner_type, :parents)
|
15
15
|
FragmentSpread = Struct.new(:name, :parents)
|
@@ -323,7 +323,7 @@ module GraphQL
|
|
323
323
|
end
|
324
324
|
end
|
325
325
|
|
326
|
-
NO_SELECTIONS = [
|
326
|
+
NO_SELECTIONS = [GraphQL::EmptyObjects::EMPTY_HASH, GraphQL::EmptyObjects::EMPTY_ARRAY].freeze
|
327
327
|
|
328
328
|
def fields_and_fragments_from_selection(node, owner_type:, parents:)
|
329
329
|
if node.selections.empty?
|
@@ -27,7 +27,7 @@ module GraphQL
|
|
27
27
|
# @param max_errors [Integer] Maximum number of errors before aborting validation. Any positive number will limit the number of errors. Defaults to nil for no limit.
|
28
28
|
# @return [Array<Hash>]
|
29
29
|
def validate(query, validate: true, timeout: nil, max_errors: nil)
|
30
|
-
query.
|
30
|
+
query.current_trace.validate(validate: validate, query: query) do
|
31
31
|
errors = if validate == false
|
32
32
|
[]
|
33
33
|
else
|
@@ -100,13 +100,8 @@ module GraphQL
|
|
100
100
|
arg_name = k.to_s
|
101
101
|
camelized_arg_name = GraphQL::Schema::Member::BuildType.camelize(arg_name)
|
102
102
|
arg_defn = get_arg_definition(arg_owner, camelized_arg_name, context)
|
103
|
-
|
104
|
-
|
105
|
-
normalized_arg_name = camelized_arg_name
|
106
|
-
else
|
107
|
-
normalized_arg_name = arg_name
|
108
|
-
arg_defn = get_arg_definition(arg_owner, normalized_arg_name, context)
|
109
|
-
end
|
103
|
+
arg_defn ||= get_arg_definition(arg_owner, arg_name, context)
|
104
|
+
normalized_arg_name = arg_defn.graphql_name
|
110
105
|
arg_base_type = arg_defn.type.unwrap
|
111
106
|
# In the case where the value being emitted is seen as a "JSON"
|
112
107
|
# type, treat the value as one atomic unit of serialization
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql/tracing/notifications_trace'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Tracing
|
7
|
+
# This implementation forwards events to ActiveSupport::Notifications
|
8
|
+
# with a `graphql` suffix.
|
9
|
+
module ActiveSupportNotificationsTrace
|
10
|
+
include NotificationsTrace
|
11
|
+
def initialize(engine: ActiveSupport::Notifications, **rest)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Tracing
|
5
|
+
|
6
|
+
# This class uses the AppopticsAPM SDK from the appoptics_apm gem to create
|
7
|
+
# traces for GraphQL.
|
8
|
+
#
|
9
|
+
# There are 4 configurations available. They can be set in the
|
10
|
+
# appoptics_apm config file or in code. Please see:
|
11
|
+
# {https://docs.appoptics.com/kb/apm_tracing/ruby/configure}
|
12
|
+
#
|
13
|
+
# AppOpticsAPM::Config[:graphql][:enabled] = true|false
|
14
|
+
# AppOpticsAPM::Config[:graphql][:transaction_name] = true|false
|
15
|
+
# AppOpticsAPM::Config[:graphql][:sanitize_query] = true|false
|
16
|
+
# AppOpticsAPM::Config[:graphql][:remove_comments] = true|false
|
17
|
+
module AppOpticsTrace
|
18
|
+
# These GraphQL events will show up as 'graphql.prep' spans
|
19
|
+
PREP_KEYS = ['lex', 'parse', 'validate', 'analyze_query', 'analyze_multiplex'].freeze
|
20
|
+
# These GraphQL events will show up as 'graphql.execute' spans
|
21
|
+
EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze
|
22
|
+
|
23
|
+
# During auto-instrumentation this version of AppOpticsTracing is compared
|
24
|
+
# with the version provided in the appoptics_apm gem, so that the newer
|
25
|
+
# version of the class can be used
|
26
|
+
|
27
|
+
def self.version
|
28
|
+
Gem::Version.new('1.0.0')
|
29
|
+
end
|
30
|
+
|
31
|
+
[
|
32
|
+
'lex',
|
33
|
+
'parse',
|
34
|
+
'validate',
|
35
|
+
'analyze_query',
|
36
|
+
'analyze_multiplex',
|
37
|
+
'execute_multiplex',
|
38
|
+
'execute_query',
|
39
|
+
'execute_query_lazy',
|
40
|
+
].each do |trace_method|
|
41
|
+
module_eval <<-RUBY, __FILE__, __LINE__
|
42
|
+
def #{trace_method}(**data)
|
43
|
+
return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false
|
44
|
+
layer = span_name("#{trace_method}")
|
45
|
+
kvs = metadata(data, layer)
|
46
|
+
kvs[:Key] = "#{trace_method}" if (PREP_KEYS + EXEC_KEYS).include?("#{trace_method}")
|
47
|
+
|
48
|
+
transaction_name(kvs[:InboundQuery]) if kvs[:InboundQuery] && layer == 'graphql.execute'
|
49
|
+
|
50
|
+
::AppOpticsAPM::SDK.trace(layer, kvs) do
|
51
|
+
kvs.clear # we don't have to send them twice
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
RUBY
|
56
|
+
end
|
57
|
+
|
58
|
+
def platform_execute_field(platform_key, data)
|
59
|
+
return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false
|
60
|
+
layer = platform_key
|
61
|
+
kvs = metadata(data, layer)
|
62
|
+
|
63
|
+
::AppOpticsAPM::SDK.trace(layer, kvs) do
|
64
|
+
kvs.clear # we don't have to send them twice
|
65
|
+
yield
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def authorized(**data)
|
70
|
+
return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false
|
71
|
+
layer = @platform_authorized_key_cache[data[:type]]
|
72
|
+
kvs = metadata(data, layer)
|
73
|
+
|
74
|
+
::AppOpticsAPM::SDK.trace(layer, kvs) do
|
75
|
+
kvs.clear # we don't have to send them twice
|
76
|
+
super
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def authorized_lazy(**data)
|
81
|
+
return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false
|
82
|
+
layer = @platform_authorized_key_cache[data[:type]]
|
83
|
+
kvs = metadata(data, layer)
|
84
|
+
|
85
|
+
::AppOpticsAPM::SDK.trace(layer, kvs) do
|
86
|
+
kvs.clear # we don't have to send them twice
|
87
|
+
super
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def resolve_type(**data)
|
92
|
+
return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false
|
93
|
+
layer = @platform_resolve_type_key_cache[data[:type]]
|
94
|
+
kvs = metadata(data, layer)
|
95
|
+
|
96
|
+
::AppOpticsAPM::SDK.trace(layer, kvs) do
|
97
|
+
kvs.clear # we don't have to send them twice
|
98
|
+
super
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def resolve_type_lazy(**data)
|
103
|
+
return super if !defined?(AppOpticsAPM) || gql_config[:enabled] == false
|
104
|
+
layer = @platform_resolve_type_key_cache[data[:type]]
|
105
|
+
kvs = metadata(data, layer)
|
106
|
+
|
107
|
+
::AppOpticsAPM::SDK.trace(layer, kvs) do
|
108
|
+
kvs.clear # we don't have to send them twice
|
109
|
+
super
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
include PlatformTrace
|
114
|
+
|
115
|
+
def platform_field_key(field)
|
116
|
+
"graphql.#{field.owner.graphql_name}.#{field.graphql_name}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def platform_authorized_key(type)
|
120
|
+
"graphql.authorized.#{type.graphql_name}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def platform_resolve_type_key(type)
|
124
|
+
"graphql.resolve_type.#{type.graphql_name}"
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def gql_config
|
130
|
+
::AppOpticsAPM::Config[:graphql] ||= {}
|
131
|
+
end
|
132
|
+
|
133
|
+
def transaction_name(query)
|
134
|
+
return if gql_config[:transaction_name] == false ||
|
135
|
+
::AppOpticsAPM::SDK.get_transaction_name
|
136
|
+
|
137
|
+
split_query = query.strip.split(/\W+/, 3)
|
138
|
+
split_query[0] = 'query' if split_query[0].empty?
|
139
|
+
name = "graphql.#{split_query[0..1].join('.')}"
|
140
|
+
|
141
|
+
::AppOpticsAPM::SDK.set_transaction_name(name)
|
142
|
+
end
|
143
|
+
|
144
|
+
def multiplex_transaction_name(names)
|
145
|
+
return if gql_config[:transaction_name] == false ||
|
146
|
+
::AppOpticsAPM::SDK.get_transaction_name
|
147
|
+
|
148
|
+
name = "graphql.multiplex.#{names.join('.')}"
|
149
|
+
name = "#{name[0..251]}..." if name.length > 254
|
150
|
+
|
151
|
+
::AppOpticsAPM::SDK.set_transaction_name(name)
|
152
|
+
end
|
153
|
+
|
154
|
+
def span_name(key)
|
155
|
+
return 'graphql.prep' if PREP_KEYS.include?(key)
|
156
|
+
return 'graphql.execute' if EXEC_KEYS.include?(key)
|
157
|
+
|
158
|
+
key[/^graphql\./] ? key : "graphql.#{key}"
|
159
|
+
end
|
160
|
+
|
161
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
162
|
+
def metadata(data, layer)
|
163
|
+
data.keys.map do |key|
|
164
|
+
case key
|
165
|
+
when :context
|
166
|
+
graphql_context(data[key], layer)
|
167
|
+
when :query
|
168
|
+
graphql_query(data[key])
|
169
|
+
when :query_string
|
170
|
+
graphql_query_string(data[key])
|
171
|
+
when :multiplex
|
172
|
+
graphql_multiplex(data[key])
|
173
|
+
when :path
|
174
|
+
[key, data[key].join(".")]
|
175
|
+
else
|
176
|
+
[key, data[key]]
|
177
|
+
end
|
178
|
+
end.flatten(2).each_slice(2).to_h.merge(Spec: 'graphql')
|
179
|
+
end
|
180
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
181
|
+
|
182
|
+
def graphql_context(context, layer)
|
183
|
+
context.errors && context.errors.each do |err|
|
184
|
+
AppOpticsAPM::API.log_exception(layer, err)
|
185
|
+
end
|
186
|
+
|
187
|
+
[[:Path, context.path.join('.')]]
|
188
|
+
end
|
189
|
+
|
190
|
+
def graphql_query(query)
|
191
|
+
return [] unless query
|
192
|
+
|
193
|
+
query_string = query.query_string
|
194
|
+
query_string = remove_comments(query_string) if gql_config[:remove_comments] != false
|
195
|
+
query_string = sanitize(query_string) if gql_config[:sanitize_query] != false
|
196
|
+
|
197
|
+
[[:InboundQuery, query_string],
|
198
|
+
[:Operation, query.selected_operation_name]]
|
199
|
+
end
|
200
|
+
|
201
|
+
def graphql_query_string(query_string)
|
202
|
+
query_string = remove_comments(query_string) if gql_config[:remove_comments] != false
|
203
|
+
query_string = sanitize(query_string) if gql_config[:sanitize_query] != false
|
204
|
+
|
205
|
+
[:InboundQuery, query_string]
|
206
|
+
end
|
207
|
+
|
208
|
+
def graphql_multiplex(data)
|
209
|
+
names = data.queries.map(&:operations).map(&:keys).flatten.compact
|
210
|
+
multiplex_transaction_name(names) if names.size > 1
|
211
|
+
|
212
|
+
[:Operations, names.join(', ')]
|
213
|
+
end
|
214
|
+
|
215
|
+
def sanitize(query)
|
216
|
+
return unless query
|
217
|
+
|
218
|
+
# remove arguments
|
219
|
+
query.gsub(/"[^"]*"/, '"?"') # strings
|
220
|
+
.gsub(/-?[0-9]*\.?[0-9]+e?[0-9]*/, '?') # ints + floats
|
221
|
+
.gsub(/\[[^\]]*\]/, '[?]') # arrays
|
222
|
+
end
|
223
|
+
|
224
|
+
def remove_comments(query)
|
225
|
+
return unless query
|
226
|
+
|
227
|
+
query.gsub(/#[^\n\r]*/, '')
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Tracing
|
5
|
+
module AppsignalTrace
|
6
|
+
include PlatformTrace
|
7
|
+
|
8
|
+
# @param set_action_name [Boolean] If true, the GraphQL operation name will be used as the transaction name.
|
9
|
+
# This is not advised if you run more than one query per HTTP request, for example, with `graphql-client` or multiplexing.
|
10
|
+
# It can also be specified per-query with `context[:set_appsignal_action_name]`.
|
11
|
+
def initialize(set_action_name: false, **rest)
|
12
|
+
@set_action_name = set_action_name
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
{
|
17
|
+
"lex" => "lex.graphql",
|
18
|
+
"parse" => "parse.graphql",
|
19
|
+
"validate" => "validate.graphql",
|
20
|
+
"analyze_query" => "analyze.graphql",
|
21
|
+
"analyze_multiplex" => "analyze.graphql",
|
22
|
+
"execute_multiplex" => "execute.graphql",
|
23
|
+
"execute_query" => "execute.graphql",
|
24
|
+
"execute_query_lazy" => "execute.graphql",
|
25
|
+
}.each do |trace_method, platform_key|
|
26
|
+
module_eval <<-RUBY, __FILE__, __LINE__
|
27
|
+
def #{trace_method}(**data)
|
28
|
+
#{
|
29
|
+
if trace_method == "execute_query"
|
30
|
+
<<-RUBY
|
31
|
+
set_this_txn_name = data[:query].context[:set_appsignal_action_name]
|
32
|
+
if set_this_txn_name == true || (set_this_txn_name.nil? && @set_action_name)
|
33
|
+
Appsignal::Transaction.current.set_action(transaction_name(data[:query]))
|
34
|
+
end
|
35
|
+
RUBY
|
36
|
+
end
|
37
|
+
}
|
38
|
+
|
39
|
+
Appsignal.instrument("#{platform_key}") do
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
RUBY
|
44
|
+
end
|
45
|
+
|
46
|
+
def platform_execute_field(platform_key)
|
47
|
+
Appsignal.instrument(platform_key) do
|
48
|
+
yield
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def platform_authorized(platform_key)
|
53
|
+
Appsignal.instrument(platform_key) do
|
54
|
+
yield
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def platform_resolve_type(platform_key)
|
59
|
+
Appsignal.instrument(platform_key) do
|
60
|
+
yield
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def platform_field_key(field)
|
65
|
+
"#{field.owner.graphql_name}.#{field.graphql_name}.graphql"
|
66
|
+
end
|
67
|
+
|
68
|
+
def platform_authorized_key(type)
|
69
|
+
"#{type.graphql_name}.authorized.graphql"
|
70
|
+
end
|
71
|
+
|
72
|
+
def platform_resolve_type_key(type)
|
73
|
+
"#{type.graphql_name}.resolve_type.graphql"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|