graphql 2.4.3 → 2.4.13

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.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/analyzer.rb +2 -1
  3. data/lib/graphql/analysis/visitor.rb +38 -41
  4. data/lib/graphql/analysis.rb +15 -12
  5. data/lib/graphql/autoload.rb +38 -0
  6. data/lib/graphql/backtrace/table.rb +95 -55
  7. data/lib/graphql/backtrace.rb +1 -19
  8. data/lib/graphql/current.rb +6 -1
  9. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
  10. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
  11. data/lib/graphql/dashboard/statics/dashboard.css +3 -0
  12. data/lib/graphql/dashboard/statics/dashboard.js +78 -0
  13. data/lib/graphql/dashboard/statics/header-icon.png +0 -0
  14. data/lib/graphql/dashboard/statics/icon.png +0 -0
  15. data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
  16. data/lib/graphql/dashboard/views/graphql/dashboard/traces/index.html.erb +63 -0
  17. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +60 -0
  18. data/lib/graphql/dashboard.rb +142 -0
  19. data/lib/graphql/dataloader/active_record_association_source.rb +64 -0
  20. data/lib/graphql/dataloader/active_record_source.rb +26 -0
  21. data/lib/graphql/dataloader/async_dataloader.rb +21 -9
  22. data/lib/graphql/dataloader/null_dataloader.rb +1 -1
  23. data/lib/graphql/dataloader/source.rb +3 -3
  24. data/lib/graphql/dataloader.rb +43 -14
  25. data/lib/graphql/execution/interpreter/resolve.rb +3 -3
  26. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +11 -4
  27. data/lib/graphql/execution/interpreter/runtime.rb +67 -40
  28. data/lib/graphql/execution/interpreter.rb +16 -6
  29. data/lib/graphql/execution/multiplex.rb +0 -4
  30. data/lib/graphql/introspection/directive_location_enum.rb +1 -1
  31. data/lib/graphql/invalid_name_error.rb +1 -1
  32. data/lib/graphql/invalid_null_error.rb +5 -15
  33. data/lib/graphql/language/cache.rb +13 -0
  34. data/lib/graphql/language/document_from_schema_definition.rb +8 -7
  35. data/lib/graphql/language/lexer.rb +11 -4
  36. data/lib/graphql/language/nodes.rb +3 -0
  37. data/lib/graphql/language/parser.rb +2 -2
  38. data/lib/graphql/language/printer.rb +8 -8
  39. data/lib/graphql/language/static_visitor.rb +37 -33
  40. data/lib/graphql/language/visitor.rb +59 -55
  41. data/lib/graphql/pagination/connection.rb +1 -1
  42. data/lib/graphql/query/context/scoped_context.rb +1 -1
  43. data/lib/graphql/query/context.rb +6 -5
  44. data/lib/graphql/query/variable_validation_error.rb +1 -1
  45. data/lib/graphql/query.rb +20 -22
  46. data/lib/graphql/railtie.rb +7 -0
  47. data/lib/graphql/schema/addition.rb +1 -1
  48. data/lib/graphql/schema/argument.rb +3 -5
  49. data/lib/graphql/schema/build_from_definition.rb +8 -7
  50. data/lib/graphql/schema/directive/flagged.rb +1 -1
  51. data/lib/graphql/schema/directive.rb +2 -2
  52. data/lib/graphql/schema/enum.rb +36 -1
  53. data/lib/graphql/schema/enum_value.rb +1 -1
  54. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  55. data/lib/graphql/schema/field.rb +12 -12
  56. data/lib/graphql/schema/field_extension.rb +1 -1
  57. data/lib/graphql/schema/has_single_input_argument.rb +3 -1
  58. data/lib/graphql/schema/input_object.rb +70 -34
  59. data/lib/graphql/schema/interface.rb +3 -2
  60. data/lib/graphql/schema/loader.rb +1 -1
  61. data/lib/graphql/schema/member/has_arguments.rb +25 -17
  62. data/lib/graphql/schema/member/has_dataloader.rb +60 -0
  63. data/lib/graphql/schema/member/has_directives.rb +4 -4
  64. data/lib/graphql/schema/member/has_fields.rb +19 -1
  65. data/lib/graphql/schema/member/has_interfaces.rb +5 -5
  66. data/lib/graphql/schema/member/has_validators.rb +1 -1
  67. data/lib/graphql/schema/member/scoped.rb +1 -1
  68. data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
  69. data/lib/graphql/schema/member.rb +1 -0
  70. data/lib/graphql/schema/object.rb +25 -8
  71. data/lib/graphql/schema/relay_classic_mutation.rb +0 -1
  72. data/lib/graphql/schema/resolver.rb +11 -10
  73. data/lib/graphql/schema/subscription.rb +52 -6
  74. data/lib/graphql/schema/union.rb +1 -1
  75. data/lib/graphql/schema/validator/required_validator.rb +23 -6
  76. data/lib/graphql/schema/validator.rb +1 -1
  77. data/lib/graphql/schema/visibility/migration.rb +1 -0
  78. data/lib/graphql/schema/visibility/profile.rb +69 -237
  79. data/lib/graphql/schema/visibility/visit.rb +190 -0
  80. data/lib/graphql/schema/visibility.rb +169 -28
  81. data/lib/graphql/schema/warden.rb +18 -5
  82. data/lib/graphql/schema.rb +90 -43
  83. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +1 -1
  84. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +1 -1
  85. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -1
  86. data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
  87. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
  88. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +1 -1
  89. data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
  90. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
  91. data/lib/graphql/static_validation/validation_context.rb +1 -0
  92. data/lib/graphql/static_validation/validator.rb +6 -1
  93. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -1
  94. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
  95. data/lib/graphql/subscriptions/event.rb +12 -1
  96. data/lib/graphql/subscriptions/serialize.rb +1 -1
  97. data/lib/graphql/subscriptions.rb +1 -1
  98. data/lib/graphql/testing/helpers.rb +2 -2
  99. data/lib/graphql/tracing/active_support_notifications_trace.rb +7 -3
  100. data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
  101. data/lib/graphql/tracing/appoptics_trace.rb +9 -1
  102. data/lib/graphql/tracing/appoptics_tracing.rb +2 -0
  103. data/lib/graphql/tracing/appsignal_trace.rb +12 -0
  104. data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
  105. data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
  106. data/lib/graphql/tracing/data_dog_trace.rb +11 -0
  107. data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
  108. data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
  109. data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
  110. data/lib/graphql/tracing/detailed_trace.rb +93 -0
  111. data/lib/graphql/tracing/legacy_hooks_trace.rb +1 -0
  112. data/lib/graphql/tracing/legacy_trace.rb +4 -61
  113. data/lib/graphql/tracing/new_relic_trace.rb +164 -41
  114. data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
  115. data/lib/graphql/tracing/notifications_trace.rb +4 -0
  116. data/lib/graphql/tracing/notifications_tracing.rb +2 -0
  117. data/lib/graphql/tracing/null_trace.rb +9 -0
  118. data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
  119. data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
  120. data/lib/graphql/tracing/perfetto_trace.rb +737 -0
  121. data/lib/graphql/tracing/platform_trace.rb +5 -0
  122. data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
  123. data/lib/graphql/tracing/prometheus_trace.rb +31 -0
  124. data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
  125. data/lib/graphql/tracing/scout_trace.rb +11 -0
  126. data/lib/graphql/tracing/scout_tracing.rb +2 -0
  127. data/lib/graphql/tracing/sentry_trace.rb +11 -0
  128. data/lib/graphql/tracing/statsd_trace.rb +15 -0
  129. data/lib/graphql/tracing/statsd_tracing.rb +2 -0
  130. data/lib/graphql/tracing/trace.rb +128 -1
  131. data/lib/graphql/tracing.rb +30 -30
  132. data/lib/graphql/types/relay/connection_behaviors.rb +3 -3
  133. data/lib/graphql/types/relay/edge_behaviors.rb +2 -2
  134. data/lib/graphql/types.rb +18 -11
  135. data/lib/graphql/version.rb +1 -1
  136. data/lib/graphql.rb +55 -47
  137. metadata +152 -10
  138. data/lib/graphql/backtrace/inspect_result.rb +0 -38
  139. data/lib/graphql/backtrace/trace.rb +0 -93
  140. data/lib/graphql/backtrace/tracer.rb +0 -80
  141. data/lib/graphql/schema/null_mask.rb +0 -11
@@ -24,7 +24,7 @@ module GraphQL
24
24
  .map!(&:name)
25
25
 
26
26
  missing_names = required_argument_names - present_argument_names
27
- if missing_names.any?
27
+ if !missing_names.empty?
28
28
  add_error(GraphQL::StaticValidation::RequiredArgumentsArePresentError.new(
29
29
  "#{ast_node.class.name.split("::").last} '#{ast_node.name}' is missing required arguments: #{missing_names.join(", ")}",
30
30
  nodes: ast_node,
@@ -21,7 +21,7 @@ module GraphQL
21
21
 
22
22
  DIRECTIVE_NODE_HOOKS.each do |method_name|
23
23
  define_method(method_name) do |node, parent|
24
- if node.directives.any?
24
+ if !node.directives.empty?
25
25
  validate_directive_location(node)
26
26
  end
27
27
  super(node, parent)
@@ -4,7 +4,7 @@ module GraphQL
4
4
  module VariableNamesAreUnique
5
5
  def on_operation_definition(node, parent)
6
6
  var_defns = node.variables
7
- if var_defns.any?
7
+ if !var_defns.empty?
8
8
  vars_by_name = Hash.new { |h, k| h[k] = [] }
9
9
  var_defns.each { |v| vars_by_name[v.name] << v }
10
10
  vars_by_name.each do |name, defns|
@@ -21,7 +21,7 @@ module GraphQL
21
21
  end
22
22
  node_values = node_values.select { |value| value.is_a? GraphQL::Language::Nodes::VariableIdentifier }
23
23
 
24
- if node_values.any?
24
+ if !node_values.empty?
25
25
  argument_owner = case parent
26
26
  when GraphQL::Language::Nodes::Field
27
27
  context.field_definition
@@ -29,6 +29,7 @@ module GraphQL
29
29
  @visitor = visitor_class.new(document, self)
30
30
  end
31
31
 
32
+ # TODO stop using def_delegators because of Array allocations
32
33
  def_delegators :@visitor,
33
34
  :path, :type_definition, :field_definition, :argument_definition,
34
35
  :parent_type_definition, :directive_definition, :object_types, :dependencies
@@ -27,6 +27,8 @@ 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
+ errors = nil
31
+ query.current_trace.begin_validate(query, validate)
30
32
  query.current_trace.validate(validate: validate, query: query) do
31
33
  begin_t = Time.now
32
34
  errors = if validate == false
@@ -58,10 +60,13 @@ module GraphQL
58
60
  }
59
61
  end
60
62
  rescue GraphQL::ExecutionError => e
63
+ errors = [e]
61
64
  {
62
65
  remaining_timeout: nil,
63
- errors: [e],
66
+ errors: errors,
64
67
  }
68
+ ensure
69
+ query.current_trace.end_validate(query, validate, errors)
65
70
  end
66
71
 
67
72
  # Invoked when static validation times out.
@@ -171,7 +171,7 @@ module GraphQL
171
171
  events_by_fingerprint = @events[topic]
172
172
  object = nil
173
173
  events_by_fingerprint.each do |_fingerprint, events|
174
- if events.any? && events.first == initial_event
174
+ if !events.empty? && events.first == initial_event
175
175
  # The fingerprint has told us that this response should be shared by all subscribers,
176
176
  # so just run it once, then deliver the result to every subscriber
177
177
  first_event = events.first
@@ -20,12 +20,22 @@ module GraphQL
20
20
  def after_resolve(value:, context:, object:, arguments:, **rest)
21
21
  if value.is_a?(GraphQL::ExecutionError)
22
22
  value
23
+ elsif @field.resolver&.method_defined?(:subscription_written?) &&
24
+ (subscription_namespace = context.namespace(:subscriptions)) &&
25
+ (subscriptions_by_path = subscription_namespace[:subscriptions])
26
+ (subscription_instance = subscriptions_by_path[context.current_path])
27
+ # If it was already written, don't append this event to be written later
28
+ if !subscription_instance.subscription_written?
29
+ events = context.namespace(:subscriptions)[:events]
30
+ events << subscription_instance.event
31
+ end
32
+ value
23
33
  elsif (events = context.namespace(:subscriptions)[:events])
24
34
  # This is the first execution, so gather an Event
25
35
  # for the backend to register:
26
36
  event = Subscriptions::Event.new(
27
37
  name: field.name,
28
- arguments: arguments_without_field_extras(arguments: arguments),
38
+ arguments: arguments,
29
39
  context: context,
30
40
  field: field,
31
41
  )
@@ -33,7 +43,7 @@ module GraphQL
33
43
  value
34
44
  elsif context.query.subscription_topic == Subscriptions::Event.serialize(
35
45
  field.name,
36
- arguments_without_field_extras(arguments: arguments),
46
+ arguments,
37
47
  field,
38
48
  scope: (field.subscription_scope ? context[field.subscription_scope] : nil),
39
49
  )
@@ -45,14 +55,6 @@ module GraphQL
45
55
  context.skip
46
56
  end
47
57
  end
48
-
49
- private
50
-
51
- def arguments_without_field_extras(arguments:)
52
- arguments.dup.tap do |event_args|
53
- field.extras.each { |k| event_args.delete(k) }
54
- end
55
- end
56
58
  end
57
59
  end
58
60
  end
@@ -20,7 +20,7 @@ module GraphQL
20
20
 
21
21
  def initialize(name:, arguments:, field: nil, context: nil, scope: nil)
22
22
  @name = name
23
- @arguments = arguments
23
+ @arguments = self.class.arguments_without_field_extras(arguments: arguments, field: field)
24
24
  @context = context
25
25
  field ||= context.field
26
26
  scope_key = field.subscription_scope
@@ -39,6 +39,7 @@ module GraphQL
39
39
  # @return [String] an identifier for this unit of subscription
40
40
  def self.serialize(_name, arguments, field, scope:, context: GraphQL::Query::NullContext.instance)
41
41
  subscription = field.resolver || GraphQL::Schema::Subscription
42
+ arguments = arguments_without_field_extras(field: field, arguments: arguments)
42
43
  normalized_args = stringify_args(field, arguments.to_h, context)
43
44
  subscription.topic_for(arguments: normalized_args, field: field, scope: scope)
44
45
  end
@@ -60,6 +61,16 @@ module GraphQL
60
61
  end
61
62
 
62
63
  class << self
64
+ def arguments_without_field_extras(arguments:, field:)
65
+ if !field.extras.empty?
66
+ arguments = arguments.dup
67
+ field.extras.each do |extra_key|
68
+ arguments.delete(extra_key)
69
+ end
70
+ end
71
+ arguments
72
+ end
73
+
63
74
  private
64
75
 
65
76
  # This method does not support cyclic references in the Hash,
@@ -146,7 +146,7 @@ module GraphQL
146
146
  elsif obj.is_a?(Date) || obj.is_a?(Time)
147
147
  # DateTime extends Date; for TimeWithZone, call `.utc` first.
148
148
  { TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] }
149
- elsif obj.is_a?(OpenStruct)
149
+ elsif defined?(OpenStruct) && obj.is_a?(OpenStruct)
150
150
  { OPEN_STRUCT_KEY => dump_value(obj.to_h) }
151
151
  elsif defined?(ActiveRecord::Relation) && obj.is_a?(ActiveRecord::Relation)
152
152
  dump_value(obj.to_a)
@@ -291,7 +291,7 @@ module GraphQL
291
291
  end
292
292
  end
293
293
 
294
- if missing_arg_names.any?
294
+ if !missing_arg_names.empty?
295
295
  arg_owner_name = if arg_owner.is_a?(GraphQL::Schema::Field)
296
296
  arg_owner.path
297
297
  elsif arg_owner.is_a?(Class)
@@ -58,7 +58,7 @@ module GraphQL
58
58
  query_context[:current_field] = visible_field
59
59
  field_args = visible_field.coerce_arguments(graphql_result, arguments, query_context)
60
60
  field_args = schema.sync_lazy(field_args)
61
- if visible_field.extras.any?
61
+ if !visible_field.extras.empty?
62
62
  extra_args = {}
63
63
  visible_field.extras.each do |extra|
64
64
  extra_args[extra] = case extra
@@ -92,7 +92,7 @@ module GraphQL
92
92
  end
93
93
  graphql_result
94
94
  else
95
- unfiltered_type = Schema::Visibility::Profile.null_profile(schema: schema, context: context).type(type_name)
95
+ unfiltered_type = schema.use_visibility_profile? ? schema.visibility.get_type(type_name) : schema.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop
96
96
  if unfiltered_type
97
97
  raise TypeNotVisibleError.new(type_name: type_name)
98
98
  else
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'graphql/tracing/notifications_trace'
3
+ require "graphql/tracing/notifications_trace"
4
4
 
5
5
  module GraphQL
6
6
  module Tracing
7
- # This implementation forwards events to ActiveSupport::Notifications
8
- # with a `graphql` suffix.
7
+ # This implementation forwards events to ActiveSupport::Notifications with a `graphql` suffix.
8
+ #
9
+ # @example Sending execution events to ActiveSupport::Notifications
10
+ # class MySchema < GraphQL::Schema
11
+ # trace_with(GraphQL::Tracing::ActiveSupportNotificationsTrace)
12
+ # end
9
13
  module ActiveSupportNotificationsTrace
10
14
  include NotificationsTrace
11
15
  def initialize(engine: ActiveSupport::Notifications, **rest)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'graphql/tracing/notifications_tracing'
3
+ require "graphql/tracing/notifications_tracing"
4
4
 
5
5
  module GraphQL
6
6
  module Tracing
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "graphql/tracing/platform_trace"
4
+
3
5
  module GraphQL
4
6
  module Tracing
5
7
 
@@ -20,14 +22,18 @@ module GraphQL
20
22
  # These GraphQL events will show up as 'graphql.execute' spans
21
23
  EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze
22
24
 
25
+
23
26
  # During auto-instrumentation this version of AppOpticsTracing is compared
24
27
  # with the version provided in the appoptics_apm gem, so that the newer
25
28
  # version of the class can be used
26
29
 
30
+
27
31
  def self.version
28
32
  Gem::Version.new('1.0.0')
29
33
  end
30
34
 
35
+ # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
36
+
31
37
  [
32
38
  'lex',
33
39
  'parse',
@@ -55,6 +61,8 @@ module GraphQL
55
61
  RUBY
56
62
  end
57
63
 
64
+ # rubocop:enable Development/NoEvalCop
65
+
58
66
  def execute_field(query:, field:, ast_node:, arguments:, object:)
59
67
  return_type = field.type.unwrap
60
68
  trace_field = if return_type.kind.scalar? || return_type.kind.enum?
@@ -81,7 +89,7 @@ module GraphQL
81
89
  end
82
90
  end
83
91
 
84
- def execute_field_lazy(query:, field:, ast_node:, arguments:, object:)
92
+ def execute_field_lazy(query:, field:, ast_node:, arguments:, object:) # rubocop:disable Development/TraceCallsSuperCop
85
93
  execute_field(query: query, field: field, ast_node: ast_node, arguments: arguments, object: object)
86
94
  end
87
95
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "graphql/tracing/platform_tracing"
4
+
3
5
  module GraphQL
4
6
  module Tracing
5
7
 
@@ -1,7 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "graphql/tracing/platform_trace"
4
+
3
5
  module GraphQL
4
6
  module Tracing
7
+ # Instrumentation for reporting GraphQL-Ruby times to Appsignal.
8
+ #
9
+ # @example Installing the tracer
10
+ # class MySchema < GraphQL::Schema
11
+ # trace_with GraphQL::Tracing::AppsignalTrace
12
+ # end
5
13
  module AppsignalTrace
6
14
  include PlatformTrace
7
15
 
@@ -13,6 +21,8 @@ module GraphQL
13
21
  super
14
22
  end
15
23
 
24
+ # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
25
+
16
26
  {
17
27
  "lex" => "lex.graphql",
18
28
  "parse" => "parse.graphql",
@@ -43,6 +53,8 @@ module GraphQL
43
53
  RUBY
44
54
  end
45
55
 
56
+ # rubocop:enable Development/NoEvalCop
57
+
46
58
  def platform_execute_field(platform_key)
47
59
  Appsignal.instrument(platform_key) do
48
60
  yield
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "graphql/tracing/platform_tracing"
4
+
3
5
  module GraphQL
4
6
  module Tracing
5
7
  class AppsignalTracing < PlatformTracing
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+ # This trace class calls legacy-style tracer with payload hashes.
6
+ # New-style `trace_with` modules significantly reduce the overhead of tracing,
7
+ # but that advantage is lost when legacy-style tracers are also used (since the payload hashes are still constructed).
8
+ module CallLegacyTracers
9
+ def lex(query_string:)
10
+ (@multiplex || @query).trace("lex", { query_string: query_string }) { super }
11
+ end
12
+
13
+ def parse(query_string:)
14
+ (@multiplex || @query).trace("parse", { query_string: query_string }) { super }
15
+ end
16
+
17
+ def validate(query:, validate:)
18
+ query.trace("validate", { validate: validate, query: query }) { super }
19
+ end
20
+
21
+ def analyze_multiplex(multiplex:)
22
+ multiplex.trace("analyze_multiplex", { multiplex: multiplex }) { super }
23
+ end
24
+
25
+ def analyze_query(query:)
26
+ query.trace("analyze_query", { query: query }) { super }
27
+ end
28
+
29
+ def execute_multiplex(multiplex:)
30
+ multiplex.trace("execute_multiplex", { multiplex: multiplex }) { super }
31
+ end
32
+
33
+ def execute_query(query:)
34
+ query.trace("execute_query", { query: query }) { super }
35
+ end
36
+
37
+ def execute_query_lazy(query:, multiplex:)
38
+ multiplex.trace("execute_query_lazy", { multiplex: multiplex, query: query }) { super }
39
+ end
40
+
41
+ def execute_field(field:, query:, ast_node:, arguments:, object:)
42
+ query.trace("execute_field", { field: field, query: query, ast_node: ast_node, arguments: arguments, object: object, owner: field.owner, path: query.context[:current_path] }) { super }
43
+ end
44
+
45
+ def execute_field_lazy(field:, query:, ast_node:, arguments:, object:)
46
+ 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] }) { super }
47
+ end
48
+
49
+ def authorized(query:, type:, object:)
50
+ query.trace("authorized", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
51
+ end
52
+
53
+ def authorized_lazy(query:, type:, object:)
54
+ query.trace("authorized_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
55
+ end
56
+
57
+ def resolve_type(query:, type:, object:)
58
+ query.trace("resolve_type", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
59
+ end
60
+
61
+ def resolve_type_lazy(query:, type:, object:)
62
+ query.trace("resolve_type_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "graphql/tracing/platform_trace"
4
+
3
5
  module GraphQL
4
6
  module Tracing
7
+ # A tracer for reporting to DataDog
8
+ # @example Adding this tracer to your schema
9
+ # class MySchema < GraphQL::Schema
10
+ # trace_with GraphQL::Tracing::DataDogTrace
11
+ # end
5
12
  module DataDogTrace
6
13
  # @param tracer [#trace] Deprecated
7
14
  # @param analytics_enabled [Boolean] Deprecated
@@ -20,6 +27,8 @@ module GraphQL
20
27
  super
21
28
  end
22
29
 
30
+ # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
31
+
23
32
  {
24
33
  'lex' => 'lex.graphql',
25
34
  'parse' => 'parse.graphql',
@@ -69,6 +78,8 @@ module GraphQL
69
78
  RUBY
70
79
  end
71
80
 
81
+ # rubocop:enable Development/NoEvalCop
82
+
72
83
  def execute_field_span(span_key, query, field, ast_node, arguments, object)
73
84
  return_type = field.type.unwrap
74
85
  trace_field = if return_type.kind.scalar? || return_type.kind.enum?
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "graphql/tracing/platform_tracing"
4
+
3
5
  module GraphQL
4
6
  module Tracing
5
7
  class DataDogTracing < PlatformTracing
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+ class DetailedTrace
6
+ # An in-memory trace storage backend. Suitable for testing and development only.
7
+ # It won't work for multi-process deployments and everything is erased when the app is restarted.
8
+ class MemoryBackend
9
+ def initialize(limit: nil)
10
+ @limit = limit
11
+ @traces = {}
12
+ @next_id = 0
13
+ end
14
+
15
+ def traces(last:, before:)
16
+ page = []
17
+ @traces.values.reverse_each do |trace|
18
+ if page.size == last
19
+ break
20
+ elsif before.nil? || trace.begin_ms < before
21
+ page << trace
22
+ end
23
+ end
24
+ page
25
+ end
26
+
27
+ def find_trace(id)
28
+ @traces[id]
29
+ end
30
+
31
+ def delete_trace(id)
32
+ @traces.delete(id.to_i)
33
+ nil
34
+ end
35
+
36
+ def delete_all_traces
37
+ @traces.clear
38
+ nil
39
+ end
40
+
41
+ def save_trace(operation_name, duration, begin_ms, trace_data)
42
+ id = @next_id
43
+ @next_id += 1
44
+ @traces[id] = DetailedTrace::StoredTrace.new(
45
+ id: id,
46
+ operation_name: operation_name,
47
+ duration_ms: duration,
48
+ begin_ms: begin_ms,
49
+ trace_data: trace_data
50
+ )
51
+ if @limit && @traces.size > @limit
52
+ del_keys = @traces.keys[0...-@limit]
53
+ del_keys.each { |k| @traces.delete(k) }
54
+ end
55
+ id
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+ class DetailedTrace
6
+ class RedisBackend
7
+ KEY_PREFIX = "gql:trace:"
8
+ def initialize(redis:, limit: nil)
9
+ @redis = redis
10
+ @key = KEY_PREFIX + "traces"
11
+ @remrangebyrank_limit = limit ? -limit - 1 : nil
12
+ end
13
+
14
+ def traces(last:, before:)
15
+ before = case before
16
+ when Numeric
17
+ "(#{before}"
18
+ when nil
19
+ "+inf"
20
+ end
21
+ str_pairs = @redis.zrange(@key, before, 0, byscore: true, rev: true, limit: [0, last || 100], withscores: true)
22
+ str_pairs.map do |(str_data, score)|
23
+ entry_to_trace(score, str_data)
24
+ end
25
+ end
26
+
27
+ def delete_trace(id)
28
+ @redis.zremrangebyscore(@key, id, id)
29
+ nil
30
+ end
31
+
32
+ def delete_all_traces
33
+ @redis.del(@key)
34
+ end
35
+
36
+ def find_trace(id)
37
+ str_data = @redis.zrange(@key, id, id, byscore: true).first
38
+ if str_data.nil?
39
+ nil
40
+ else
41
+ entry_to_trace(id, str_data)
42
+ end
43
+ end
44
+
45
+ def save_trace(operation_name, duration_ms, begin_ms, trace_data)
46
+ id = begin_ms
47
+ data = JSON.dump({ "o" => operation_name, "d" => duration_ms, "b" => begin_ms, "t" => Base64.encode64(trace_data) })
48
+ @redis.pipelined do |pipeline|
49
+ pipeline.zadd(@key, id, data)
50
+ if @remrangebyrank_limit
51
+ pipeline.zremrangebyrank(@key, 0, @remrangebyrank_limit)
52
+ end
53
+ end
54
+ id
55
+ end
56
+
57
+ private
58
+
59
+ def entry_to_trace(id, json_str)
60
+ data = JSON.parse(json_str)
61
+ StoredTrace.new(
62
+ id: id,
63
+ operation_name: data["o"],
64
+ duration_ms: data["d"].to_f,
65
+ begin_ms: data["b"].to_i,
66
+ trace_data: Base64.decode64(data["t"]),
67
+ )
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+ require "graphql/tracing/detailed_trace/memory_backend"
3
+ require "graphql/tracing/detailed_trace/redis_backend"
4
+
5
+ module GraphQL
6
+ module Tracing
7
+ # `DetailedTrace` can make detailed profiles for a subset of production traffic.
8
+ #
9
+ # When `MySchema.detailed_trace?(query)` returns `true`, a profiler-specific `trace_mode: ...` will be used for the query,
10
+ # overriding the one in `context[:trace_mode]`.
11
+ #
12
+ # __Redis__: The sampler stores its results in a provided Redis database. Depending on your needs,
13
+ # You can configure this database to retail all data (persistent) or to expire data according to your rules.
14
+ # If you need to save traces indefinitely, you can download them from Perfetto after opening them there.
15
+ #
16
+ # @example Adding the sampler to your schema
17
+ # class MySchema < GraphQL::Schema
18
+ # # Add the sampler:
19
+ # use GraphQL::Tracing::DetailedTrace, redis: Redis.new(...), limit: 100
20
+ #
21
+ # # And implement this hook to tell it when to take a sample:
22
+ # def self.detailed_trace?(query)
23
+ # # Could use `query.context`, `query.selected_operation_name`, `query.query_string` here
24
+ # # Could call out to Flipper, etc
25
+ # rand <= 0.000_1 # one in ten thousand
26
+ # end
27
+ # end
28
+ #
29
+ # @see Graphql::Dashboard GraphQL::Dashboard for viewing stored results
30
+ class DetailedTrace
31
+ # @param redis [Redis] If provided, profiles will be stored in Redis for later review
32
+ # @param limit [Integer] A maximum number of profiles to store
33
+ def self.use(schema, trace_mode: :profile_sample, memory: false, redis: nil, limit: nil)
34
+ storage = if redis
35
+ RedisBackend.new(redis: redis, limit: limit)
36
+ elsif memory
37
+ MemoryBackend.new(limit: limit)
38
+ else
39
+ raise ArgumentError, "Pass `redis: ...` to store traces in Redis for later review"
40
+ end
41
+ schema.detailed_trace = self.new(storage: storage, trace_mode: trace_mode)
42
+ schema.trace_with(PerfettoTrace, mode: trace_mode, save_profile: true)
43
+ end
44
+
45
+ def initialize(storage:, trace_mode:)
46
+ @storage = storage
47
+ @trace_mode = trace_mode
48
+ end
49
+
50
+ # @return [Symbol] The trace mode to use when {Schema.detailed_trace?} returns `true`
51
+ attr_reader :trace_mode
52
+
53
+ # @return [String] ID of saved trace
54
+ def save_trace(operation_name, duration_ms, begin_ms, trace_data)
55
+ @storage.save_trace(operation_name, duration_ms, begin_ms, trace_data)
56
+ end
57
+
58
+ # @param last [Integer]
59
+ # @param before [Integer] Timestamp in milliseconds since epoch
60
+ # @return [Enumerable<StoredTrace>]
61
+ def traces(last: nil, before: nil)
62
+ @storage.traces(last: last, before: before)
63
+ end
64
+
65
+ # @return [StoredTrace, nil]
66
+ def find_trace(id)
67
+ @storage.find_trace(id)
68
+ end
69
+
70
+ # @return [void]
71
+ def delete_trace(id)
72
+ @storage.delete_trace(id)
73
+ end
74
+
75
+ # @return [void]
76
+ def delete_all_traces
77
+ @storage.delete_all_traces
78
+ end
79
+
80
+ class StoredTrace
81
+ def initialize(id:, operation_name:, duration_ms:, begin_ms:, trace_data:)
82
+ @id = id
83
+ @operation_name = operation_name
84
+ @duration_ms = duration_ms
85
+ @begin_ms = begin_ms
86
+ @trace_data = trace_data
87
+ end
88
+
89
+ attr_reader :id, :operation_name, :duration_ms, :begin_ms, :trace_data
90
+ end
91
+ end
92
+ end
93
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Tracing
4
5
  module LegacyHooksTrace