graphql 2.5.18 → 2.5.19

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3512540a3ee54e940cc015c0bd3931895b376d8c01550e8c1aedc2244937dd54
4
- data.tar.gz: d988a0eeca9a38d914a0def3409e8dc89025420de492a632bc5a63a8203d89d2
3
+ metadata.gz: 3a94d75c592000805bf9fd18507343c0b3428f1cf1d9a1828edc921828fd2735
4
+ data.tar.gz: 2984583eb702965d43e45f58f1ac29b6a782b8ecf02b4ee1953b5c5bbb6ec872
5
5
  SHA512:
6
- metadata.gz: 4ab540f193797624203186b0008a05fbfcea52975b6cf9cd078a4a1f275c076526b2ac76fedcd281164fe199a1dee82800636f23ce2f998a8d1a61d623c1c6c0
7
- data.tar.gz: fd5a869314ee4a8cf3d6b0019bc1f91908a03425f5c8a6d1618e8a6f95570782a59d014a048a4f526a7e15539d5179db2c5e78c1fd0a79aef7f2104d5532db23
6
+ metadata.gz: 743bd603f22bb38bdff984c75b37eea32d460aeafd50617fc52dc9c4b93fe1d3265df196b728432a7b2d6494a6ba20c47b658ee84b387377037ea173844c654b
7
+ data.tar.gz: 12354210f6208443a5ec0dc931606440de29bfe0bdb7c6468e7faf9bc013a67671eab62fcc3bd88405d1ffb840a5c0e04be337d9a6707d511a2046d315722f63
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ require 'rails/generators/active_record'
3
+
4
+ module Graphql
5
+ module Generators
6
+ class DetailedTraceGenerator < ::Rails::Generators::Base
7
+ include ::Rails::Generators::Migration
8
+ desc "Install GraphQL::Tracing::DetailedTrace for your schema"
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ class_option :redis,
12
+ type: :boolean,
13
+ default: false,
14
+ desc: "Use Redis for persistence instead of ActiveRecord"
15
+
16
+ def self.next_migration_number(dirname)
17
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
18
+ end
19
+
20
+ def install_detailed_traces
21
+
22
+ schema_glob = File.expand_path("app/graphql/*_schema.rb", destination_root)
23
+ schema_file = Dir.glob(schema_glob).first
24
+ if !schema_file
25
+ raise ArgumentError, "Failed to find schema definition file (checked: #{schema_glob.inspect})"
26
+ end
27
+ schema_file_match = /( *)class ([A-Za-z:]+) < GraphQL::Schema/.match(File.read(schema_file))
28
+ schema_name = schema_file_match[2]
29
+ indent = schema_file_match[1] + " "
30
+
31
+ if !options.redis?
32
+ migration_template 'create_graphql_detailed_traces.erb', 'db/migrate/create_graphql_detailed_traces.rb'
33
+ end
34
+
35
+ log :add_detailed_traces_plugin
36
+ sentinel = /< GraphQL::Schema\s*\n/m
37
+ code = <<-RUBY
38
+ #{indent}use GraphQL::Tracing::DetailedTrace#{options.redis? ? ", redis: raise(\"TODO: pass a connection to a persistent redis database\")" : ""}, limit: 50
39
+
40
+ #{indent}# When this returns true, DetailedTrace will trace the query
41
+ #{indent}# Could use `query.context`, `query.selected_operation_name`, `query.query_string` here
42
+ #{indent}# Could call out to Flipper, etc
43
+ #{indent}def self.detailed_trace?(query)
44
+ #{indent} rand <= 0.000_1 # one in ten thousand
45
+ #{indent}end
46
+
47
+ RUBY
48
+
49
+ in_root do
50
+ inject_into_file schema_file, code, after: sentinel, force: false
51
+ end
52
+
53
+ routes_source = File.read(File.expand_path("config/routes.rb", destination_root))
54
+ already_has_dashboard = routes_source.include?("GraphQL::Dashboard") ||
55
+ routes_source.include?("Schema.dashboard") ||
56
+ routes_source.include?("GraphQL::Pro::Routes::Lazy")
57
+
58
+ if (!already_has_dashboard || behavior == :revoke)
59
+ log :route, "GraphQL::Dashboard"
60
+ shell.mute do
61
+ route <<~RUBY
62
+ # TODO: add authorization to this route and expose it in production
63
+ # See https://graphql-ruby.org/pro/dashboard.html#authorizing-the-dashboard
64
+ if Rails.env.development?
65
+ mount GraphQL::Dashboard, at: "/graphql/dashboard", schema: #{schema_name.inspect}
66
+ end
67
+
68
+ RUBY
69
+ end
70
+
71
+ gem("google-protobuf")
72
+
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,10 @@
1
+ class CreateGraphqlDetailedTraces < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :graphql_detailed_traces, force: true do |t|
4
+ t.bigint :begin_ms, null: false
5
+ t.float :duration_ms, null: false
6
+ t.binary :trace_data, null: false
7
+ t.string :operation_name, null: false
8
+ end
9
+ end
10
+ end
@@ -120,7 +120,7 @@ module Graphql
120
120
  end
121
121
 
122
122
  class StaticsController < ApplicationController
123
- skip_after_action :verify_same_origin_request
123
+ skip_forgery_protection
124
124
  # Use an explicit list of files to avoid any chance of reading other files from disk
125
125
  STATICS = {}
126
126
 
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+ class DetailedTrace
6
+ class ActiveRecordBackend
7
+ class GraphqlDetailedTrace < ActiveRecord::Base
8
+ end
9
+
10
+ def initialize(limit: nil, model_class: nil)
11
+ @limit = limit
12
+ @model_class = model_class || GraphqlDetailedTrace
13
+ end
14
+
15
+ def traces(last:, before:)
16
+ gdts = @model_class.all.order("begin_ms DESC")
17
+ if before
18
+ gdts = gdts.where("begin_ms < ?", before)
19
+ end
20
+ if last
21
+ gdts = gdts.limit(last)
22
+ end
23
+ gdts.map { |gdt| record_to_stored_trace(gdt) }
24
+ end
25
+
26
+ def delete_trace(id)
27
+ @model_class.where(id: id).destroy_all
28
+ nil
29
+ end
30
+
31
+ def delete_all_traces
32
+ @model_class.all.destroy_all
33
+ end
34
+
35
+ def find_trace(id)
36
+ gdt = @model_class.find_by(id: id)
37
+ if gdt
38
+ record_to_stored_trace(gdt)
39
+ else
40
+ nil
41
+ end
42
+ end
43
+
44
+ def save_trace(operation_name, duration_ms, begin_ms, trace_data)
45
+ gdt = @model_class.create!(
46
+ begin_ms: begin_ms,
47
+ operation_name: operation_name,
48
+ duration_ms: duration_ms,
49
+ trace_data: trace_data,
50
+ )
51
+ if @limit
52
+ @model_class
53
+ .where("id NOT IN(SELECT id FROM graphql_detailed_traces ORDER BY begin_ms DESC LIMIT ?)", @limit)
54
+ .delete_all
55
+ end
56
+ gdt.id
57
+ end
58
+
59
+ private
60
+
61
+ def record_to_stored_trace(gdt)
62
+ StoredTrace.new(
63
+ id: gdt.id,
64
+ begin_ms: gdt.begin_ms,
65
+ operation_name: gdt.operation_name,
66
+ duration_ms: gdt.duration_ms,
67
+ trace_data: gdt.trace_data
68
+ )
69
+
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
+ if defined?(ActiveRecord)
3
+ require "graphql/tracing/detailed_trace/active_record_backend"
4
+ end
2
5
  require "graphql/tracing/detailed_trace/memory_backend"
3
6
  require "graphql/tracing/detailed_trace/redis_backend"
4
7
 
5
8
  module GraphQL
6
9
  module Tracing
7
- # `DetailedTrace` can make detailed profiles for a subset of production traffic.
10
+ # `DetailedTrace` can make detailed profiles for a subset of production traffic. Install it in Rails with `rails generate graphql:detailed_trace`.
8
11
  #
9
12
  # When `MySchema.detailed_trace?(query)` returns `true`, a profiler-specific `trace_mode: ...` will be used for the query,
10
13
  # overriding the one in `context[:trace_mode]`.
@@ -13,10 +16,19 @@ module GraphQL
13
16
  # this behavior by extending {DetailedTrace} and overriding {#inspect_object}. You can opt out of debug annotations
14
17
  # entirely with `use ..., debug: false` or for a single query with `context: { detailed_trace_debug: false }`.
15
18
  #
16
- # __Redis__: The sampler stores its results in a provided Redis database. Depending on your needs,
17
- # You can configure this database to retain all data (persistent) or to expire data according to your rules.
19
+ # You can store saved traces in two ways:
20
+ #
21
+ # - __ActiveRecord__: With `rails generate graphql:detailed_trace`, a new migration will be added to your app.
22
+ # That table will be used to store trace data.
23
+ #
24
+ # - __Redis__: Pass `redis: ...` to save trace data to a Redis database. Depending on your needs,
25
+ # you can configure this database to retain all data (persistent) or to expire data according to your rules.
26
+ #
18
27
  # If you need to save traces indefinitely, you can download them from Perfetto after opening them there.
19
28
  #
29
+ # @example Installing with Rails
30
+ # rails generate graphql:detailed_trace # optional: --redis
31
+ #
20
32
  # @example Adding the sampler to your schema
21
33
  # class MySchema < GraphQL::Schema
22
34
  # # Add the sampler:
@@ -53,13 +65,16 @@ module GraphQL
53
65
  # @param redis [Redis] If provided, profiles will be stored in Redis for later review
54
66
  # @param limit [Integer] A maximum number of profiles to store
55
67
  # @param debug [Boolean] if `false`, it won't create `debug` annotations in Perfetto traces (reduces overhead)
56
- def self.use(schema, trace_mode: :profile_sample, memory: false, debug: debug?, redis: nil, limit: nil)
68
+ # @param model_class [Class<ActiveRecord::Base>] Overrides {ActiveRecordBackend::GraphqlDetailedTrace} if present
69
+ def self.use(schema, trace_mode: :profile_sample, memory: false, debug: debug?, redis: nil, limit: nil, model_class: nil)
57
70
  storage = if redis
58
71
  RedisBackend.new(redis: redis, limit: limit)
59
72
  elsif memory
60
73
  MemoryBackend.new(limit: limit)
74
+ elsif defined?(ActiveRecord)
75
+ ActiveRecordBackend.new(limit: limit, model_class: model_class)
61
76
  else
62
- raise ArgumentError, "Pass `redis: ...` to store traces in Redis for later review"
77
+ raise ArgumentError, "To store traces, install ActiveRecord or provide `redis: ...`"
63
78
  end
64
79
  detailed_trace = self.new(storage: storage, trace_mode: trace_mode, debug: debug)
65
80
  schema.detailed_trace = detailed_trace
@@ -91,6 +91,21 @@ module GraphQL
91
91
  ctx_debug
92
92
  end
93
93
 
94
+ @arguments_filter = if (ctx = query&.context) && (dtf = ctx[:detailed_trace_filter])
95
+ dtf
96
+ elsif defined?(ActiveSupport::ParameterFilter)
97
+ fp = if defined?(Rails) && Rails.application && (app_config = Rails.application.config.filter_parameters).present? && !app_config.empty?
98
+ app_config
99
+ elsif ActiveSupport.respond_to?(:filter_parameters)
100
+ ActiveSupport.filter_parameters
101
+ else
102
+ EmptyObjects::EMPTY_ARRAY
103
+ end
104
+ ActiveSupport::ParameterFilter.new(fp, mask: ArgumentsFilter::FILTERED)
105
+ else
106
+ ArgumentsFilter.new
107
+ end
108
+
94
109
  Fiber[:graphql_flow_stack] = nil
95
110
  @sequence_id = object_id
96
111
  @pid = Process.pid
@@ -590,6 +605,22 @@ module GraphQL
590
605
  Fiber.current.object_id
591
606
  end
592
607
 
608
+ class ArgumentsFilter
609
+ # From Rails defaults
610
+ # https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt#L6-L8
611
+ SENSITIVE_KEY = /passw|token|crypt|email|_key|salt|certificate|secret|ssn|cvv|cvc|otp/i
612
+ FILTERED = "[FILTERED]"
613
+
614
+ def filter_param(key, value)
615
+ if (key.is_a?(String) && SENSITIVE_KEY.match?(key)) ||
616
+ (key.is_a?(Symbol) && SENSITIVE_KEY.match?(key.name))
617
+ FILTERED
618
+ else
619
+ value
620
+ end
621
+ end
622
+ end
623
+
593
624
  def debug_annotation(iid, value_key, value)
594
625
  if iid
595
626
  DebugAnnotation.new(name_iid: iid, value_key => value)
@@ -634,7 +665,22 @@ module GraphQL
634
665
  when Array
635
666
  debug_annotation(iid, :array_values, v.each_with_index.map { |v2, idx| payload_to_debug((k ? "#{k}.#{idx}" : String(idx)), v2, intern_value: intern_value) }.compact)
636
667
  when Hash
637
- debug_annotation(iid, :dict_entries, v.map { |k2, v2| payload_to_debug(k2, v2, intern_value: intern_value) }.compact)
668
+ debug_v = v.map { |k2, v2|
669
+ debug_k = case k2
670
+ when String
671
+ k2
672
+ when Symbol
673
+ k2.name
674
+ else
675
+ String(k2)
676
+ end
677
+ filtered_v2 = @arguments_filter.filter_param(debug_k, v2)
678
+ payload_to_debug(debug_k, filtered_v2, intern_value: intern_value)
679
+ }
680
+ debug_v.compact!
681
+ debug_annotation(iid, :dict_entries, debug_v)
682
+ when GraphQL::Schema::InputObject
683
+ payload_to_debug(k, v.to_h, iid: iid, intern_value: intern_value)
638
684
  else
639
685
  class_name_iid = @interned_da_string_values[v.class.name]
640
686
  da = [
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "2.5.18"
3
+ VERSION = "2.5.19"
4
4
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.18
4
+ version: 2.5.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-01-22 00:00:00.000000000 Z
10
+ date: 2026-02-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -383,6 +383,7 @@ files:
383
383
  - ".yardopts"
384
384
  - MIT-LICENSE
385
385
  - lib/generators/graphql/core.rb
386
+ - lib/generators/graphql/detailed_trace_generator.rb
386
387
  - lib/generators/graphql/enum_generator.rb
387
388
  - lib/generators/graphql/field_extractor.rb
388
389
  - lib/generators/graphql/input_generator.rb
@@ -412,6 +413,7 @@ files:
412
413
  - lib/generators/graphql/templates/base_resolver.erb
413
414
  - lib/generators/graphql/templates/base_scalar.erb
414
415
  - lib/generators/graphql/templates/base_union.erb
416
+ - lib/generators/graphql/templates/create_graphql_detailed_traces.erb
415
417
  - lib/generators/graphql/templates/enum.erb
416
418
  - lib/generators/graphql/templates/graphql_controller.erb
417
419
  - lib/generators/graphql/templates/input.erb
@@ -741,6 +743,7 @@ files:
741
743
  - lib/graphql/tracing/data_dog_trace.rb
742
744
  - lib/graphql/tracing/data_dog_tracing.rb
743
745
  - lib/graphql/tracing/detailed_trace.rb
746
+ - lib/graphql/tracing/detailed_trace/active_record_backend.rb
744
747
  - lib/graphql/tracing/detailed_trace/memory_backend.rb
745
748
  - lib/graphql/tracing/detailed_trace/redis_backend.rb
746
749
  - lib/graphql/tracing/legacy_hooks_trace.rb