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.

Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/ast/visitor.rb +42 -35
  3. data/lib/graphql/analysis/ast.rb +2 -2
  4. data/lib/graphql/backtrace/trace.rb +96 -0
  5. data/lib/graphql/backtrace/tracer.rb +1 -1
  6. data/lib/graphql/backtrace.rb +6 -1
  7. data/lib/graphql/execution/interpreter/arguments.rb +1 -1
  8. data/lib/graphql/execution/interpreter/arguments_cache.rb +2 -3
  9. data/lib/graphql/execution/interpreter/resolve.rb +19 -0
  10. data/lib/graphql/execution/interpreter/runtime.rb +264 -211
  11. data/lib/graphql/execution/interpreter.rb +15 -10
  12. data/lib/graphql/execution/lazy.rb +6 -12
  13. data/lib/graphql/execution/multiplex.rb +2 -1
  14. data/lib/graphql/filter.rb +7 -2
  15. data/lib/graphql/introspection/directive_type.rb +2 -2
  16. data/lib/graphql/introspection/field_type.rb +1 -1
  17. data/lib/graphql/introspection/schema_type.rb +2 -2
  18. data/lib/graphql/introspection/type_type.rb +5 -5
  19. data/lib/graphql/language/document_from_schema_definition.rb +25 -9
  20. data/lib/graphql/language/lexer.rb +216 -1505
  21. data/lib/graphql/language/nodes.rb +66 -40
  22. data/lib/graphql/language/parser.rb +509 -491
  23. data/lib/graphql/language/parser.y +43 -38
  24. data/lib/graphql/language/visitor.rb +191 -83
  25. data/lib/graphql/pagination/active_record_relation_connection.rb +0 -8
  26. data/lib/graphql/pagination/connection.rb +5 -5
  27. data/lib/graphql/query/context.rb +62 -31
  28. data/lib/graphql/query/null_context.rb +1 -1
  29. data/lib/graphql/query.rb +22 -5
  30. data/lib/graphql/schema/argument.rb +7 -13
  31. data/lib/graphql/schema/build_from_definition.rb +15 -3
  32. data/lib/graphql/schema/directive.rb +12 -2
  33. data/lib/graphql/schema/enum.rb +24 -17
  34. data/lib/graphql/schema/enum_value.rb +2 -3
  35. data/lib/graphql/schema/field.rb +68 -57
  36. data/lib/graphql/schema/field_extension.rb +1 -4
  37. data/lib/graphql/schema/find_inherited_value.rb +2 -7
  38. data/lib/graphql/schema/interface.rb +0 -10
  39. data/lib/graphql/schema/late_bound_type.rb +2 -0
  40. data/lib/graphql/schema/member/base_dsl_methods.rb +17 -14
  41. data/lib/graphql/schema/member/has_arguments.rb +105 -58
  42. data/lib/graphql/schema/member/has_ast_node.rb +12 -0
  43. data/lib/graphql/schema/member/has_deprecation_reason.rb +3 -4
  44. data/lib/graphql/schema/member/has_directives.rb +15 -10
  45. data/lib/graphql/schema/member/has_fields.rb +95 -38
  46. data/lib/graphql/schema/member/has_interfaces.rb +49 -8
  47. data/lib/graphql/schema/member/has_validators.rb +32 -6
  48. data/lib/graphql/schema/member/relay_shortcuts.rb +19 -0
  49. data/lib/graphql/schema/member/type_system_helpers.rb +17 -0
  50. data/lib/graphql/schema/object.rb +2 -4
  51. data/lib/graphql/schema/resolver/has_payload_type.rb +9 -9
  52. data/lib/graphql/schema/resolver.rb +4 -4
  53. data/lib/graphql/schema/timeout.rb +24 -28
  54. data/lib/graphql/schema/validator.rb +1 -1
  55. data/lib/graphql/schema/warden.rb +29 -5
  56. data/lib/graphql/schema.rb +76 -25
  57. data/lib/graphql/static_validation/literal_validator.rb +15 -1
  58. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +12 -4
  59. data/lib/graphql/static_validation/rules/fields_will_merge.rb +2 -2
  60. data/lib/graphql/static_validation/validator.rb +1 -1
  61. data/lib/graphql/subscriptions/event.rb +2 -7
  62. data/lib/graphql/tracing/active_support_notifications_trace.rb +16 -0
  63. data/lib/graphql/tracing/appoptics_trace.rb +231 -0
  64. data/lib/graphql/tracing/appsignal_trace.rb +77 -0
  65. data/lib/graphql/tracing/data_dog_trace.rb +148 -0
  66. data/lib/graphql/tracing/legacy_trace.rb +65 -0
  67. data/lib/graphql/tracing/new_relic_trace.rb +75 -0
  68. data/lib/graphql/tracing/notifications_trace.rb +42 -0
  69. data/lib/graphql/tracing/platform_trace.rb +109 -0
  70. data/lib/graphql/tracing/platform_tracing.rb +15 -3
  71. data/lib/graphql/tracing/prometheus_trace.rb +89 -0
  72. data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +1 -1
  73. data/lib/graphql/tracing/prometheus_tracing.rb +3 -3
  74. data/lib/graphql/tracing/scout_trace.rb +72 -0
  75. data/lib/graphql/tracing/statsd_trace.rb +56 -0
  76. data/lib/graphql/tracing/trace.rb +75 -0
  77. data/lib/graphql/tracing.rb +16 -39
  78. data/lib/graphql/type_kinds.rb +6 -3
  79. data/lib/graphql/types/relay/base_connection.rb +1 -1
  80. data/lib/graphql/types/relay/connection_behaviors.rb +24 -6
  81. data/lib/graphql/types/relay/edge_behaviors.rb +16 -6
  82. data/lib/graphql/types/relay/node_behaviors.rb +7 -1
  83. data/lib/graphql/types/relay/page_info_behaviors.rb +7 -2
  84. data/lib/graphql/types/relay.rb +0 -1
  85. data/lib/graphql/types/string.rb +1 -1
  86. data/lib/graphql/version.rb +1 -1
  87. data/lib/graphql.rb +16 -9
  88. metadata +34 -9
  89. data/lib/graphql/language/lexer.rl +0 -280
  90. 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
- (context.respond_to?(:warden) && context.warden) || PassThruWarden
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| e.enum_values(@context) }
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 defined?(@reachable_type_set)
359
+ return @reachable_type_set if @reachable_type_set
336
360
 
337
361
  @reachable_type_set = Set.new
338
362
  rt_hash = {}
@@ -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
- if resolved_value
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 err [GraphQL::ParseError] The error encountered during parsing
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
- type.validate_input(ast_value, @context)
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
- if ast_node.selections.first.is_a?(GraphQL::Language::Nodes::InlineFragment)
30
- "Selections can't be made on scalars (%{node_name} returns #{resolved_type.graphql_name} but has inline fragments [#{ast_node.selections.map(&:type).map(&:name).join(", ")}])"
31
- else
32
- "Selections can't be made on scalars (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{ast_node.selections.map(&:name).join(", ")}])"
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 = {}.freeze
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 = [{}.freeze, [].freeze].freeze
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.trace("validate", { validate: validate, query: query }) do
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
- if arg_defn
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