graphql 2.0.17.2 → 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.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 +254 -211
- data/lib/graphql/execution/interpreter.rb +9 -14
- data/lib/graphql/execution/lazy.rb +2 -4
- data/lib/graphql/execution/multiplex.rb +2 -1
- data/lib/graphql/filter.rb +7 -2
- 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 +27 -9
- data/lib/graphql/language/parser.rb +509 -491
- data/lib/graphql/language/parser.y +43 -38
- 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 -9
- data/lib/graphql/schema/build_from_definition.rb +15 -3
- data/lib/graphql/schema/enum_value.rb +2 -5
- data/lib/graphql/schema/field.rb +44 -31
- data/lib/graphql/schema/field_extension.rb +1 -4
- data/lib/graphql/schema/find_inherited_value.rb +2 -7
- data/lib/graphql/schema/member/base_dsl_methods.rb +13 -11
- data/lib/graphql/schema/member/has_arguments.rb +1 -1
- 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 +87 -37
- data/lib/graphql/schema/member/has_validators.rb +2 -2
- data/lib/graphql/schema/member/relay_shortcuts.rb +19 -0
- data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
- 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 +11 -2
- data/lib/graphql/schema.rb +72 -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/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 -2
- data/lib/graphql/types/relay/edge_behaviors.rb +16 -2
- 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/version.rb +1 -1
- data/lib/graphql.rb +16 -9
- metadata +33 -8
- data/lib/graphql/language/lexer.rl +0 -280
- data/lib/graphql/types/relay/default_relay.rb +0 -27
@@ -89,16 +89,16 @@ module GraphQL
|
|
89
89
|
def generate_payload_type
|
90
90
|
resolver_name = graphql_name
|
91
91
|
resolver_fields = all_field_definitions
|
92
|
-
Class.new(object_class)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
end
|
92
|
+
pt = Class.new(object_class)
|
93
|
+
pt.graphql_name("#{resolver_name}Payload")
|
94
|
+
pt.description("Autogenerated return type of #{resolver_name}.")
|
95
|
+
resolver_fields.each do |f|
|
96
|
+
# Reattach the already-defined field here
|
97
|
+
# (The field's `.owner` will still point to the mutation, not the object type, I think)
|
98
|
+
# Don't re-warn about a method conflict. Since this type is generated, it should be fixed in the resolver instead.
|
99
|
+
pt.add_field(f, method_conflict_warning: false)
|
101
100
|
end
|
101
|
+
pt
|
102
102
|
end
|
103
103
|
end
|
104
104
|
end
|
@@ -311,8 +311,8 @@ module GraphQL
|
|
311
311
|
# (`nil` means "unlimited max page size".)
|
312
312
|
# @param max_page_size [Integer, nil] Set a new value
|
313
313
|
# @return [Integer, nil] The `max_page_size` assigned to fields that use this resolver
|
314
|
-
def max_page_size(new_max_page_size =
|
315
|
-
if new_max_page_size !=
|
314
|
+
def max_page_size(new_max_page_size = NOT_CONFIGURED)
|
315
|
+
if new_max_page_size != NOT_CONFIGURED
|
316
316
|
@max_page_size = new_max_page_size
|
317
317
|
elsif defined?(@max_page_size)
|
318
318
|
@max_page_size
|
@@ -332,8 +332,8 @@ module GraphQL
|
|
332
332
|
# (`nil` means "unlimited default page size".)
|
333
333
|
# @param default_page_size [Integer, nil] Set a new value
|
334
334
|
# @return [Integer, nil] The `default_page_size` assigned to fields that use this resolver
|
335
|
-
def default_page_size(new_default_page_size =
|
336
|
-
if new_default_page_size !=
|
335
|
+
def default_page_size(new_default_page_size = NOT_CONFIGURED)
|
336
|
+
if new_default_page_size != NOT_CONFIGURED
|
337
337
|
@default_page_size = new_default_page_size
|
338
338
|
elsif defined?(@default_page_size)
|
339
339
|
@default_page_size
|
@@ -33,60 +33,56 @@ module GraphQL
|
|
33
33
|
# end
|
34
34
|
#
|
35
35
|
class Timeout
|
36
|
-
def self.use(schema,
|
37
|
-
|
38
|
-
schema.
|
36
|
+
def self.use(schema, max_seconds: nil)
|
37
|
+
timeout = self.new(max_seconds: max_seconds)
|
38
|
+
schema.trace_with(self::Trace, timeout: timeout)
|
39
39
|
end
|
40
40
|
|
41
|
-
# @param max_seconds [Numeric] how many seconds the query should be allowed to resolve new fields
|
42
41
|
def initialize(max_seconds:)
|
43
42
|
@max_seconds = max_seconds
|
44
43
|
end
|
45
44
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
45
|
+
module Trace
|
46
|
+
# @param max_seconds [Numeric] how many seconds the query should be allowed to resolve new fields
|
47
|
+
def initialize(timeout:, **rest)
|
48
|
+
@timeout = timeout
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def execute_multiplex(multiplex:)
|
53
|
+
multiplex.queries.each do |query|
|
54
|
+
timeout_duration_s = @timeout.max_seconds(query)
|
51
55
|
timeout_state = if timeout_duration_s == false
|
52
56
|
# if the method returns `false`, don't apply a timeout
|
53
57
|
false
|
54
58
|
else
|
55
59
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
56
|
-
timeout_at = now + (
|
60
|
+
timeout_at = now + (timeout_duration_s * 1000)
|
57
61
|
{
|
58
62
|
timeout_at: timeout_at,
|
59
63
|
timed_out: false
|
60
64
|
}
|
61
65
|
end
|
62
|
-
query.context.namespace(
|
66
|
+
query.context.namespace(@timeout)[:state] = timeout_state
|
63
67
|
end
|
68
|
+
super
|
69
|
+
end
|
64
70
|
|
65
|
-
|
66
|
-
|
67
|
-
query_context = data[:context] || data[:query].context
|
68
|
-
timeout_state = query_context.namespace(self.class).fetch(:state)
|
71
|
+
def execute_field(query:, field:, **_rest)
|
72
|
+
timeout_state = query.context.namespace(@timeout).fetch(:state)
|
69
73
|
# If the `:state` is `false`, then `max_seconds(query)` opted out of timeout for this query.
|
70
74
|
if timeout_state != false && Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) > timeout_state.fetch(:timeout_at)
|
71
|
-
error =
|
72
|
-
GraphQL::Schema::Timeout::TimeoutError.new(query_context.parent_type, query_context.field)
|
73
|
-
else
|
74
|
-
field = data.fetch(:field)
|
75
|
-
GraphQL::Schema::Timeout::TimeoutError.new(field.owner, field)
|
76
|
-
end
|
77
|
-
|
75
|
+
error = GraphQL::Schema::Timeout::TimeoutError.new(field)
|
78
76
|
# Only invoke the timeout callback for the first timeout
|
79
77
|
if !timeout_state[:timed_out]
|
80
78
|
timeout_state[:timed_out] = true
|
81
|
-
handle_timeout(error,
|
79
|
+
@timeout.handle_timeout(error, query)
|
82
80
|
end
|
83
81
|
|
84
82
|
error
|
85
83
|
else
|
86
|
-
|
84
|
+
super
|
87
85
|
end
|
88
|
-
else
|
89
|
-
yield
|
90
86
|
end
|
91
87
|
end
|
92
88
|
|
@@ -114,8 +110,8 @@ module GraphQL
|
|
114
110
|
# to take this error and raise a new one which _doesn't_ descend from {GraphQL::ExecutionError},
|
115
111
|
# such as `RuntimeError`.
|
116
112
|
class TimeoutError < GraphQL::ExecutionError
|
117
|
-
def initialize(
|
118
|
-
super("Timeout on #{
|
113
|
+
def initialize(field)
|
114
|
+
super("Timeout on #{field.path}")
|
119
115
|
end
|
120
116
|
end
|
121
117
|
end
|
@@ -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
|
@@ -96,6 +98,13 @@ module GraphQL
|
|
96
98
|
@subscription = @schema.subscription
|
97
99
|
@context = context
|
98
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
|
99
108
|
end
|
100
109
|
|
101
110
|
# @return [Hash<String, GraphQL::BaseType>] Visible types in the schema
|
@@ -347,7 +356,7 @@ module GraphQL
|
|
347
356
|
end
|
348
357
|
|
349
358
|
def reachable_type_set
|
350
|
-
return @reachable_type_set if
|
359
|
+
return @reachable_type_set if @reachable_type_set
|
351
360
|
|
352
361
|
@reachable_type_set = Set.new
|
353
362
|
rt_hash = {}
|
data/lib/graphql/schema.rb
CHANGED
@@ -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)
|
@@ -922,6 +959,12 @@ module GraphQL
|
|
922
959
|
end
|
923
960
|
|
924
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
|
+
|
925
968
|
own_tracers << new_tracer
|
926
969
|
end
|
927
970
|
|
@@ -929,6 +972,34 @@ module GraphQL
|
|
929
972
|
find_inherited_value(:tracers, EMPTY_ARRAY) + own_tracers
|
930
973
|
end
|
931
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
|
+
|
932
1003
|
def query_analyzer(new_analyzer)
|
933
1004
|
own_query_analyzers << new_analyzer
|
934
1005
|
end
|
@@ -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
|
@@ -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
|