graphql 2.4.10 → 2.4.12

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/visitor.rb +35 -40
  3. data/lib/graphql/analysis.rb +12 -9
  4. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
  5. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
  6. data/lib/graphql/dashboard/statics/dashboard.css +3 -0
  7. data/lib/graphql/dashboard/statics/dashboard.js +78 -0
  8. data/lib/graphql/dashboard/statics/header-icon.png +0 -0
  9. data/lib/graphql/dashboard/statics/icon.png +0 -0
  10. data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
  11. data/lib/graphql/dashboard/views/graphql/dashboard/traces/index.html.erb +63 -0
  12. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +60 -0
  13. data/lib/graphql/dashboard.rb +142 -0
  14. data/lib/graphql/execution/interpreter/runtime.rb +1 -1
  15. data/lib/graphql/invalid_name_error.rb +1 -1
  16. data/lib/graphql/invalid_null_error.rb +5 -15
  17. data/lib/graphql/language/lexer.rb +7 -3
  18. data/lib/graphql/language/parser.rb +1 -1
  19. data/lib/graphql/schema/build_from_definition.rb +0 -1
  20. data/lib/graphql/schema/enum.rb +16 -1
  21. data/lib/graphql/schema/input_object.rb +1 -1
  22. data/lib/graphql/schema/loader.rb +0 -1
  23. data/lib/graphql/schema/member/has_dataloader.rb +4 -0
  24. data/lib/graphql/schema/resolver.rb +5 -1
  25. data/lib/graphql/schema.rb +51 -9
  26. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -1
  27. data/lib/graphql/tracing/active_support_notifications_trace.rb +6 -2
  28. data/lib/graphql/tracing/appoptics_trace.rb +2 -0
  29. data/lib/graphql/tracing/appsignal_trace.rb +6 -0
  30. data/lib/graphql/tracing/data_dog_trace.rb +5 -0
  31. data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
  32. data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
  33. data/lib/graphql/tracing/detailed_trace.rb +93 -0
  34. data/lib/graphql/tracing/new_relic_trace.rb +47 -23
  35. data/lib/graphql/tracing/perfetto_trace.rb +13 -2
  36. data/lib/graphql/tracing/prometheus_trace.rb +22 -0
  37. data/lib/graphql/tracing/scout_trace.rb +6 -0
  38. data/lib/graphql/tracing/sentry_trace.rb +5 -0
  39. data/lib/graphql/tracing/statsd_trace.rb +9 -0
  40. data/lib/graphql/tracing.rb +1 -0
  41. data/lib/graphql/version.rb +1 -1
  42. data/lib/graphql.rb +3 -0
  43. metadata +15 -2
@@ -2,33 +2,23 @@
2
2
  module GraphQL
3
3
  # Raised automatically when a field's resolve function returns `nil`
4
4
  # for a non-null field.
5
- class InvalidNullError < GraphQL::RuntimeTypeError
5
+ class InvalidNullError < GraphQL::Error
6
6
  # @return [GraphQL::BaseType] The owner of {#field}
7
7
  attr_reader :parent_type
8
8
 
9
9
  # @return [GraphQL::Field] The field which failed to return a value
10
10
  attr_reader :field
11
11
 
12
- # @return [nil, GraphQL::ExecutionError] The invalid value for this field
13
- attr_reader :value
12
+ # @return [GraphQL::Language::Nodes::Field] the field where the error occurred
13
+ attr_reader :ast_node
14
14
 
15
- def initialize(parent_type, field, value)
15
+ def initialize(parent_type, field, ast_node)
16
16
  @parent_type = parent_type
17
17
  @field = field
18
- @value = value
18
+ @ast_node = ast_node
19
19
  super("Cannot return null for non-nullable field #{@parent_type.graphql_name}.#{@field.graphql_name}")
20
20
  end
21
21
 
22
- # @return [Hash] An entry for the response's "errors" key
23
- def to_h
24
- { "message" => message }
25
- end
26
-
27
- # @deprecated always false
28
- def parent_error?
29
- false
30
- end
31
-
32
22
  class << self
33
23
  attr_accessor :parent_class
34
24
 
@@ -13,17 +13,21 @@ module GraphQL
13
13
  @pos = nil
14
14
  @max_tokens = max_tokens || Float::INFINITY
15
15
  @tokens_count = 0
16
+ @finished = false
16
17
  end
17
18
 
18
- def eos?
19
- @scanner.eos?
19
+ def finished?
20
+ @finished
20
21
  end
21
22
 
22
23
  attr_reader :pos, :tokens_count
23
24
 
24
25
  def advance
25
26
  @scanner.skip(IGNORE_REGEXP)
26
- return false if @scanner.eos?
27
+ if @scanner.eos?
28
+ @finished = true
29
+ return false
30
+ end
27
31
  @tokens_count += 1
28
32
  if @tokens_count > @max_tokens
29
33
  raise_parse_error("This query is too large to execute.")
@@ -110,7 +110,7 @@ module GraphQL
110
110
  # Only ignored characters is not a valid document
111
111
  raise GraphQL::ParseError.new("Unexpected end of document", nil, nil, @graphql_str)
112
112
  end
113
- while !@lexer.eos?
113
+ while !@lexer.finished?
114
114
  defns << definition
115
115
  end
116
116
  Document.new(pos: 0, definitions: defns, filename: @filename, source: self)
@@ -298,7 +298,6 @@ module GraphQL
298
298
  description: enum_value_definition.description,
299
299
  directives: builder.prepare_directives(enum_value_definition, type_resolver),
300
300
  ast_node: enum_value_definition,
301
- value_method: GraphQL::Schema::Enum.respond_to?(enum_value_definition.name.downcase) ? false : nil,
302
301
  )
303
302
  end
304
303
  end
@@ -70,7 +70,9 @@ module GraphQL
70
70
  kwargs[:owner] = self
71
71
  value = enum_value_class.new(*args, **kwargs, &block)
72
72
 
73
- generate_value_method(value, value_method)
73
+ if value_method || (value_methods && value_method != false)
74
+ generate_value_method(value, value_method)
75
+ end
74
76
 
75
77
  key = value.graphql_name
76
78
  prev_value = own_values[key]
@@ -159,6 +161,18 @@ module GraphQL
159
161
  end
160
162
  end
161
163
 
164
+ def value_methods(new_value = NOT_CONFIGURED)
165
+ if NOT_CONFIGURED.equal?(new_value)
166
+ if @value_methods != nil
167
+ @value_methods
168
+ else
169
+ find_inherited_value(:value_methods, false)
170
+ end
171
+ else
172
+ @value_methods = new_value
173
+ end
174
+ end
175
+
162
176
  def kind
163
177
  GraphQL::TypeKinds::ENUM
164
178
  end
@@ -220,6 +234,7 @@ module GraphQL
220
234
  # because they would end up with names like `#<Class0x1234>::UnresolvedValueError` which messes up bug trackers
221
235
  child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError))
222
236
  end
237
+ child_class.class_eval { @value_methods = nil }
223
238
  super
224
239
  end
225
240
 
@@ -156,7 +156,7 @@ module GraphQL
156
156
  def #{method_name}
157
157
  self[#{method_name.inspect}]
158
158
  end
159
- alias_method :#{method_name}, :#{method_name}
159
+ alias_method #{method_name.inspect}, #{method_name.inspect}
160
160
  RUBY
161
161
  end
162
162
  argument_defn
@@ -108,7 +108,6 @@ module GraphQL
108
108
  enum_value["name"],
109
109
  description: enum_value["description"],
110
110
  deprecation_reason: enum_value["deprecationReason"],
111
- value_method: respond_to?(enum_value["name"].downcase) ? false : nil
112
111
  )
113
112
  end
114
113
  end
@@ -23,6 +23,10 @@ module GraphQL
23
23
  # @param find_by_value [Object] Usually an `id`, might be another value if `find_by:` is also provided
24
24
  # @param find_by [Symbol, String] A column name to look the record up by. (Defaults to the model's primary key.)
25
25
  # @return [ActiveRecord::Base, nil]
26
+ # @example Finding a record by ID
27
+ # dataload_record(Post, 5) # Like `Post.find(5)`, but dataloaded
28
+ # @example Finding a record by another attribute
29
+ # dataload_record(User, "matz", find_by: :handle) # Like `User.find_by(handle: "matz")`, but dataloaded
26
30
  def dataload_record(model, find_by_value, find_by: nil)
27
31
  source = if find_by
28
32
  dataloader.with(Dataloader::ActiveRecordSource, model, find_by: find_by)
@@ -22,7 +22,6 @@ module GraphQL
22
22
  include Schema::Member::GraphQLTypeNames
23
23
  # Really we only need description & comment from here, but:
24
24
  extend Schema::Member::BaseDSLMethods
25
- extend Member::BaseDSLMethods::ConfigurationExtension
26
25
  extend GraphQL::Schema::Member::HasArguments
27
26
  extend GraphQL::Schema::Member::HasValidators
28
27
  include Schema::Member::HasPath
@@ -404,6 +403,11 @@ module GraphQL
404
403
  end
405
404
  end
406
405
 
406
+ def inherited(child_class)
407
+ child_class.description(description)
408
+ super
409
+ end
410
+
407
411
  private
408
412
 
409
413
  attr_reader :own_extensions
@@ -821,13 +821,13 @@ module GraphQL
821
821
 
822
822
  attr_writer :validate_timeout
823
823
 
824
- def validate_timeout(new_validate_timeout = nil)
825
- if new_validate_timeout
824
+ def validate_timeout(new_validate_timeout = NOT_CONFIGURED)
825
+ if !NOT_CONFIGURED.equal?(new_validate_timeout)
826
826
  @validate_timeout = new_validate_timeout
827
827
  elsif defined?(@validate_timeout)
828
828
  @validate_timeout
829
829
  else
830
- find_inherited_value(:validate_timeout)
830
+ find_inherited_value(:validate_timeout) || 3
831
831
  end
832
832
  end
833
833
 
@@ -1298,7 +1298,10 @@ module GraphQL
1298
1298
  def type_error(type_error, ctx)
1299
1299
  case type_error
1300
1300
  when GraphQL::InvalidNullError
1301
- ctx.errors << type_error
1301
+ execution_error = GraphQL::ExecutionError.new(type_error.message, ast_node: type_error.ast_node)
1302
+ execution_error.path = ctx[:current_path]
1303
+
1304
+ ctx.errors << execution_error
1302
1305
  when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
1303
1306
  raise type_error
1304
1307
  when GraphQL::IntegerDecodingError
@@ -1366,6 +1369,16 @@ module GraphQL
1366
1369
  }.freeze
1367
1370
  end
1368
1371
 
1372
+ # @return [GraphQL::Tracing::DetailedTrace] if it has been configured for this schema
1373
+ attr_accessor :detailed_trace
1374
+
1375
+ # @param query [GraphQL::Query, GraphQL::Execution::Multiplex] Called with a multiplex when multiple queries are executed at once (with {.multiplex})
1376
+ # @return [Boolean] When `true`, save a detailed trace for this query.
1377
+ # @see Tracing::DetailedTrace DetailedTrace saves traces when this method returns true
1378
+ def detailed_trace?(query)
1379
+ raise "#{self} must implement `def.detailed_trace?(query)` to use DetailedTrace. Implement this method in your schema definition."
1380
+ end
1381
+
1369
1382
  def tracer(new_tracer, silence_deprecation_warning: false)
1370
1383
  if !silence_deprecation_warning
1371
1384
  warn("`Schema.tracer(#{new_tracer.inspect})` is deprecated; use module-based `trace_with` instead. See: https://graphql-ruby.org/queries/tracing.html")
@@ -1383,14 +1396,22 @@ module GraphQL
1383
1396
  find_inherited_value(:tracers, EMPTY_ARRAY) + own_tracers
1384
1397
  end
1385
1398
 
1386
- # Mix `trace_mod` into this schema's `Trace` class so that its methods
1387
- # will be called at runtime.
1399
+ # Mix `trace_mod` into this schema's `Trace` class so that its methods will be called at runtime.
1400
+ #
1401
+ # You can attach a module to run in only _some_ circumstances by using `mode:`. When a module is added with `mode:`,
1402
+ # it will only run for queries with a matching `context[:trace_mode]`.
1403
+ #
1404
+ # Any custom trace modes _also_ include the default `trace_with ...` modules (that is, those added _without_ any particular `mode: ...` configuration).
1405
+ #
1406
+ # @example Adding a trace in a special mode
1407
+ # # only runs when `query.context[:trace_mode]` is `:special`
1408
+ # trace_with SpecialTrace, mode: :special
1388
1409
  #
1389
1410
  # @param trace_mod [Module] A module that implements tracing methods
1390
1411
  # @param mode [Symbol] Trace module will only be used for this trade mode
1391
1412
  # @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
1392
1413
  # @return [void]
1393
- # @see GraphQL::Tracing::Trace for available tracing methods
1414
+ # @see GraphQL::Tracing::Trace Tracing::Trace for available tracing methods
1394
1415
  def trace_with(trace_mod, mode: :default, **options)
1395
1416
  if mode.is_a?(Array)
1396
1417
  mode.each { |m| trace_with(trace_mod, mode: m, **options) }
@@ -1440,12 +1461,33 @@ module GraphQL
1440
1461
  #
1441
1462
  # If no `mode:` is given, then {default_trace_mode} will be used.
1442
1463
  #
1464
+ # If this schema is using {Tracing::DetailedTrace} and {.detailed_trace?} returns `true`, then
1465
+ # DetailedTrace's mode will override the passed-in `mode`.
1466
+ #
1443
1467
  # @param mode [Symbol] Trace modules for this trade mode will be included
1444
1468
  # @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
1445
1469
  # @return [Tracing::Trace]
1446
1470
  def new_trace(mode: nil, **options)
1447
- target = options[:query] || options[:multiplex]
1448
- mode ||= target && target.context[:trace_mode]
1471
+ should_sample = if detailed_trace
1472
+ if (query = options[:query])
1473
+ detailed_trace?(query)
1474
+ elsif (multiplex = options[:multiplex])
1475
+ if multiplex.queries.length == 1
1476
+ detailed_trace?(multiplex.queries.first)
1477
+ else
1478
+ detailed_trace?(multiplex)
1479
+ end
1480
+ end
1481
+ else
1482
+ false
1483
+ end
1484
+
1485
+ if should_sample
1486
+ mode = detailed_trace.trace_mode
1487
+ else
1488
+ target = options[:query] || options[:multiplex]
1489
+ mode ||= target && target.context[:trace_mode]
1490
+ end
1449
1491
 
1450
1492
  trace_mode = mode || default_trace_mode
1451
1493
  base_trace_options = trace_options_for(trace_mode)
@@ -345,7 +345,7 @@ module GraphQL
345
345
  fields << Field.new(node, definition, owner_type, parents)
346
346
  when GraphQL::Language::Nodes::InlineFragment
347
347
  fragment_type = node.type ? @types.type(node.type.name) : owner_type
348
- find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: owner_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
348
+ find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: fragment_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
349
349
  when GraphQL::Language::Nodes::FragmentSpread
350
350
  fragment_spreads << FragmentSpread.new(node.name, parents)
351
351
  end
@@ -4,8 +4,12 @@ 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)
@@ -22,10 +22,12 @@ module GraphQL
22
22
  # These GraphQL events will show up as 'graphql.execute' spans
23
23
  EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze
24
24
 
25
+
25
26
  # During auto-instrumentation this version of AppOpticsTracing is compared
26
27
  # with the version provided in the appoptics_apm gem, so that the newer
27
28
  # version of the class can be used
28
29
 
30
+
29
31
  def self.version
30
32
  Gem::Version.new('1.0.0')
31
33
  end
@@ -4,6 +4,12 @@ require "graphql/tracing/platform_trace"
4
4
 
5
5
  module GraphQL
6
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
7
13
  module AppsignalTrace
8
14
  include PlatformTrace
9
15
 
@@ -4,6 +4,11 @@ require "graphql/tracing/platform_trace"
4
4
 
5
5
  module GraphQL
6
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
7
12
  module DataDogTrace
8
13
  # @param tracer [#trace] Deprecated
9
14
  # @param analytics_enabled [Boolean] Deprecated
@@ -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