bugsnag 6.19.0 → 6.26.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/CHANGELOG.md +176 -0
  4. data/VERSION +1 -1
  5. data/bugsnag.gemspec +18 -1
  6. data/lib/bugsnag/breadcrumb_type.rb +14 -0
  7. data/lib/bugsnag/breadcrumbs/breadcrumb.rb +34 -1
  8. data/lib/bugsnag/breadcrumbs/breadcrumbs.rb +1 -0
  9. data/lib/bugsnag/breadcrumbs/on_breadcrumb_callback_list.rb +50 -0
  10. data/lib/bugsnag/cleaner.rb +31 -18
  11. data/lib/bugsnag/configuration.rb +243 -25
  12. data/lib/bugsnag/delivery/synchronous.rb +2 -2
  13. data/lib/bugsnag/delivery/thread_queue.rb +2 -2
  14. data/lib/bugsnag/endpoint_configuration.rb +11 -0
  15. data/lib/bugsnag/endpoint_validator.rb +80 -0
  16. data/lib/bugsnag/error.rb +25 -0
  17. data/lib/bugsnag/event.rb +7 -0
  18. data/lib/bugsnag/feature_flag.rb +74 -0
  19. data/lib/bugsnag/integrations/mongo.rb +5 -3
  20. data/lib/bugsnag/integrations/rack.rb +3 -3
  21. data/lib/bugsnag/integrations/rails/active_job.rb +102 -0
  22. data/lib/bugsnag/integrations/rails/rails_breadcrumbs.rb +2 -0
  23. data/lib/bugsnag/integrations/railtie.rb +70 -27
  24. data/lib/bugsnag/integrations/resque.rb +17 -3
  25. data/lib/bugsnag/integrations/sidekiq.rb +1 -0
  26. data/lib/bugsnag/middleware/active_job.rb +18 -0
  27. data/lib/bugsnag/middleware/classify_error.rb +1 -0
  28. data/lib/bugsnag/middleware/delayed_job.rb +21 -2
  29. data/lib/bugsnag/middleware/exception_meta_data.rb +2 -0
  30. data/lib/bugsnag/middleware/rack_request.rb +84 -19
  31. data/lib/bugsnag/middleware/rails3_request.rb +2 -2
  32. data/lib/bugsnag/middleware/rake.rb +1 -1
  33. data/lib/bugsnag/middleware/session_data.rb +3 -1
  34. data/lib/bugsnag/middleware/sidekiq.rb +1 -1
  35. data/lib/bugsnag/middleware/suggestion_data.rb +9 -7
  36. data/lib/bugsnag/middleware_stack.rb +6 -6
  37. data/lib/bugsnag/report.rb +204 -8
  38. data/lib/bugsnag/session_tracker.rb +52 -12
  39. data/lib/bugsnag/stacktrace.rb +13 -2
  40. data/lib/bugsnag/tasks/bugsnag.rake +1 -1
  41. data/lib/bugsnag/utility/duplicator.rb +124 -0
  42. data/lib/bugsnag/utility/feature_data_store.rb +41 -0
  43. data/lib/bugsnag/utility/feature_flag_delegate.rb +89 -0
  44. data/lib/bugsnag/utility/metadata_delegate.rb +102 -0
  45. data/lib/bugsnag.rb +156 -8
  46. metadata +24 -7
@@ -1,9 +1,13 @@
1
1
  require "json"
2
2
  require "pathname"
3
+ require "bugsnag/error"
3
4
  require "bugsnag/stacktrace"
4
5
 
5
6
  module Bugsnag
7
+ # rubocop:todo Metrics/ClassLength
6
8
  class Report
9
+ include Utility::FeatureDataStore
10
+
7
11
  NOTIFIER_NAME = "Ruby Bugsnag Notifier"
8
12
  NOTIFIER_VERSION = Bugsnag::VERSION
9
13
  NOTIFIER_URL = "https://www.bugsnag.com"
@@ -45,16 +49,13 @@ module Bugsnag
45
49
  # @return [Configuration]
46
50
  attr_accessor :configuration
47
51
 
48
- # Additional context for this report
49
- # @return [String, nil]
50
- attr_accessor :context
51
-
52
52
  # The delivery method that will be used for this report
53
53
  # @see Configuration#delivery_method
54
54
  # @return [Symbol]
55
55
  attr_accessor :delivery_method
56
56
 
57
57
  # The list of exceptions in this report
58
+ # @deprecated Use {#errors} instead
58
59
  # @return [Array<Hash>]
59
60
  attr_accessor :exceptions
60
61
 
@@ -72,10 +73,12 @@ module Bugsnag
72
73
  attr_accessor :grouping_hash
73
74
 
74
75
  # Arbitrary metadata attached to this report
76
+ # @deprecated Use {#metadata} instead
75
77
  # @return [Hash]
76
78
  attr_accessor :meta_data
77
79
 
78
80
  # The raw Exception instances for this report
81
+ # @deprecated Use {#original_error} instead
79
82
  # @see #exceptions
80
83
  # @return [Array<Exception>]
81
84
  attr_accessor :raw_exceptions
@@ -102,31 +105,68 @@ module Bugsnag
102
105
  # @return [Hash]
103
106
  attr_accessor :user
104
107
 
108
+ # A list of errors in this report
109
+ # @return [Array<Error>]
110
+ attr_reader :errors
111
+
112
+ # The Exception instance this report was created for
113
+ # @return [Exception]
114
+ attr_reader :original_error
115
+
105
116
  ##
106
117
  # Initializes a new report from an exception.
107
118
  def initialize(exception, passed_configuration, auto_notify=false)
119
+ # store the creation time for use as device.time
120
+ @created_at = Time.now.utc.iso8601(3)
121
+
108
122
  @should_ignore = false
109
123
  @unhandled = auto_notify
124
+ @initial_unhandled = @unhandled
110
125
 
111
126
  self.configuration = passed_configuration
112
127
 
128
+ @original_error = exception
113
129
  self.raw_exceptions = generate_raw_exceptions(exception)
114
130
  self.exceptions = generate_exception_list
131
+ @errors = generate_error_list
115
132
 
116
133
  self.api_key = configuration.api_key
117
134
  self.app_type = configuration.app_type
118
135
  self.app_version = configuration.app_version
119
136
  self.breadcrumbs = []
137
+ self.context = configuration.context if configuration.context_set?
120
138
  self.delivery_method = configuration.delivery_method
121
139
  self.hostname = configuration.hostname
122
140
  self.runtime_versions = configuration.runtime_versions.dup
123
- self.meta_data = {}
141
+ self.meta_data = Utility::Duplicator.duplicate(configuration.metadata)
124
142
  self.release_stage = configuration.release_stage
125
143
  self.severity = auto_notify ? "error" : "warning"
126
144
  self.severity_reason = auto_notify ? {:type => UNHANDLED_EXCEPTION} : {:type => HANDLED_EXCEPTION}
127
145
  self.user = {}
146
+
147
+ @metadata_delegate = Utility::MetadataDelegate.new
148
+ @feature_flag_delegate = Bugsnag.feature_flag_delegate.dup
128
149
  end
129
150
 
151
+ ##
152
+ # Additional context for this report
153
+ # @!attribute context
154
+ # @return [String, nil]
155
+ def context
156
+ return @context if defined?(@context)
157
+
158
+ @automatic_context
159
+ end
160
+
161
+ attr_writer :context
162
+
163
+ ##
164
+ # Context set automatically by Bugsnag uses this attribute, which prevents
165
+ # it from overwriting the user-supplied context
166
+ # @api private
167
+ # @return [String, nil]
168
+ attr_accessor :automatic_context
169
+
130
170
  ##
131
171
  # Add a new metadata tab to this notification.
132
172
  #
@@ -135,6 +175,8 @@ module Bugsnag
135
175
  # exists, this will be merged with the existing values. If a Hash is not
136
176
  # given, the value will be placed into the 'custom' tab
137
177
  # @return [void]
178
+ #
179
+ # @deprecated Use {#add_metadata} instead
138
180
  def add_tab(name, value)
139
181
  return if name.nil?
140
182
 
@@ -153,6 +195,8 @@ module Bugsnag
153
195
  #
154
196
  # @param name [String]
155
197
  # @return [void]
198
+ #
199
+ # @deprecated Use {#clear_metadata} instead
156
200
  def remove_tab(name)
157
201
  return if name.nil?
158
202
 
@@ -175,9 +219,11 @@ module Bugsnag
175
219
  context: context,
176
220
  device: {
177
221
  hostname: hostname,
178
- runtimeVersions: runtime_versions
222
+ runtimeVersions: runtime_versions,
223
+ time: @created_at
179
224
  },
180
225
  exceptions: exceptions,
226
+ featureFlags: @feature_flag_delegate.as_json,
181
227
  groupingHash: grouping_hash,
182
228
  metaData: meta_data,
183
229
  session: session,
@@ -197,6 +243,7 @@ module Bugsnag
197
243
  :version => NOTIFIER_VERSION,
198
244
  :url => NOTIFIER_URL
199
245
  },
246
+ :payloadVersion => CURRENT_PAYLOAD_VERSION,
200
247
  :events => [payload_event]
201
248
  }
202
249
  end
@@ -257,24 +304,172 @@ module Bugsnag
257
304
  end
258
305
  end
259
306
 
307
+ # A Hash containing arbitrary metadata
308
+ # @!attribute metadata
309
+ # @return [Hash]
310
+ def metadata
311
+ @meta_data
312
+ end
313
+
314
+ # @param metadata [Hash]
315
+ # @return [void]
316
+ def metadata=(metadata)
317
+ @meta_data = metadata
318
+ end
319
+
320
+ ##
321
+ # Data from the current HTTP request. May be nil if no data has been recorded
322
+ #
323
+ # @return [Hash, nil]
324
+ def request
325
+ @meta_data[:request]
326
+ end
327
+
328
+ ##
329
+ # Add values to metadata
330
+ #
331
+ # @overload add_metadata(section, data)
332
+ # Merges data into the given section of metadata
333
+ # @param section [String, Symbol]
334
+ # @param data [Hash]
335
+ #
336
+ # @overload add_metadata(section, key, value)
337
+ # Sets key to value in the given section of metadata. If the value is nil
338
+ # the key will be deleted
339
+ # @param section [String, Symbol]
340
+ # @param key [String, Symbol]
341
+ # @param value
342
+ #
343
+ # @return [void]
344
+ def add_metadata(section, key_or_data, *args)
345
+ @metadata_delegate.add_metadata(@meta_data, section, key_or_data, *args)
346
+ end
347
+
348
+ ##
349
+ # Clear values from metadata
350
+ #
351
+ # @overload clear_metadata(section)
352
+ # Clears the given section of metadata
353
+ # @param section [String, Symbol]
354
+ #
355
+ # @overload clear_metadata(section, key)
356
+ # Clears the key in the given section of metadata
357
+ # @param section [String, Symbol]
358
+ # @param key [String, Symbol]
359
+ #
360
+ # @return [void]
361
+ def clear_metadata(section, *args)
362
+ @metadata_delegate.clear_metadata(@meta_data, section, *args)
363
+ end
364
+
365
+ # Get the array of stored feature flags
366
+ #
367
+ # @return [Array<Bugsnag::FeatureFlag>]
368
+ def feature_flags
369
+ @feature_flag_delegate.to_a
370
+ end
371
+
372
+ ##
373
+ # Set information about the current user
374
+ #
375
+ # Additional user fields can be added as metadata in a "user" section
376
+ #
377
+ # Setting a field to 'nil' will remove it from the user data
378
+ #
379
+ # @param id [String, nil]
380
+ # @param email [String, nil]
381
+ # @param name [String, nil]
382
+ # @return [void]
383
+ def set_user(id = nil, email = nil, name = nil)
384
+ new_user = { id: id, email: email, name: name }
385
+ new_user.reject! { |key, value| value.nil? }
386
+
387
+ @user = new_user
388
+ end
389
+
390
+ def unhandled=(new_unhandled)
391
+ # fix the handled/unhandled counts in the current session
392
+ update_handled_counts(new_unhandled, @unhandled)
393
+
394
+ @unhandled = new_unhandled
395
+ end
396
+
397
+ ##
398
+ # Returns true if the unhandled flag has been changed from its initial value
399
+ #
400
+ # @api private
401
+ # @return [Boolean]
402
+ def unhandled_overridden?
403
+ @unhandled != @initial_unhandled
404
+ end
405
+
260
406
  private
261
407
 
408
+ attr_reader :feature_flag_delegate
409
+
410
+ def update_handled_counts(is_unhandled, was_unhandled)
411
+ # do nothing if there is no session to update
412
+ return if @session.nil?
413
+
414
+ # increment the counts for the current unhandled value
415
+ if is_unhandled
416
+ @session[:events][:unhandled] += 1
417
+ else
418
+ @session[:events][:handled] += 1
419
+ end
420
+
421
+ # decrement the counts for the previous unhandled value
422
+ if was_unhandled
423
+ @session[:events][:unhandled] -= 1
424
+ else
425
+ @session[:events][:handled] -= 1
426
+ end
427
+ end
428
+
262
429
  def generate_exception_list
263
430
  raw_exceptions.map do |exception|
431
+ class_name = error_class(exception)
432
+
264
433
  {
265
- errorClass: error_class(exception),
266
- message: exception.message,
434
+ errorClass: class_name,
435
+ message: error_message(exception, class_name),
267
436
  stacktrace: Stacktrace.process(exception.backtrace, configuration)
268
437
  }
269
438
  end
270
439
  end
271
440
 
441
+ def generate_error_list
442
+ exceptions.map do |exception|
443
+ Error.new(exception[:errorClass], exception[:message], exception[:stacktrace])
444
+ end
445
+ end
446
+
272
447
  def error_class(exception)
273
448
  # The "Class" check is for some strange exceptions like Timeout::Error
274
449
  # which throw the error class instead of an instance
275
450
  (exception.is_a? Class) ? exception.name : exception.class.name
276
451
  end
277
452
 
453
+ def error_message(exception, class_name)
454
+ # Ruby 3.2 added Exception#detailed_message for Gems like "Did you mean"
455
+ # to annotate an exception's message
456
+ return exception.message unless exception.respond_to?(:detailed_message)
457
+
458
+ # the "highlight" argument may add terminal escape codes to the output,
459
+ # which we don't want to include
460
+ # it _should_ always be present but it's possible to forget to add it or
461
+ # to have implemented this method before Ruby 3.2
462
+ message =
463
+ begin
464
+ exception.detailed_message(highlight: false)
465
+ rescue ArgumentError
466
+ exception.detailed_message
467
+ end
468
+
469
+ # remove the class name to be consistent with Exception#message
470
+ message.sub(" (#{class_name})", '')
471
+ end
472
+
278
473
  def generate_raw_exceptions(exception)
279
474
  exceptions = []
280
475
 
@@ -311,4 +506,5 @@ module Bugsnag
311
506
  exceptions
312
507
  end
313
508
  end
509
+ # rubocop:enable Metrics/ClassLength
314
510
  end
@@ -34,16 +34,20 @@ module Bugsnag
34
34
  # Starts a new session, storing it on the current thread.
35
35
  #
36
36
  # This allows Bugsnag to track error rates for a release.
37
+ #
38
+ # @return [void]
37
39
  def start_session
38
- return unless Bugsnag.configuration.enable_sessions
40
+ return unless Bugsnag.configuration.enable_sessions && Bugsnag.configuration.should_notify_release_stage?
41
+
39
42
  start_delivery_thread
40
43
  start_time = Time.now().utc().strftime('%Y-%m-%dT%H:%M:00')
41
44
  new_session = {
42
- :id => SecureRandom.uuid,
43
- :startedAt => start_time,
44
- :events => {
45
- :handled => 0,
46
- :unhandled => 0
45
+ id: SecureRandom.uuid,
46
+ startedAt: start_time,
47
+ paused?: false,
48
+ events: {
49
+ handled: 0,
50
+ unhandled: 0
47
51
  }
48
52
  }
49
53
  SessionTracker.set_current_session(new_session)
@@ -52,6 +56,47 @@ module Bugsnag
52
56
 
53
57
  alias_method :create_session, :start_session
54
58
 
59
+ ##
60
+ # Stop any events being attributed to the current session until it is
61
+ # resumed or a new session is started
62
+ #
63
+ # @see resume_session
64
+ #
65
+ # @return [void]
66
+ def pause_session
67
+ current_session = SessionTracker.get_current_session
68
+
69
+ return unless current_session
70
+
71
+ current_session[:paused?] = true
72
+ end
73
+
74
+ ##
75
+ # Resume the current session if it was previously paused. If there is no
76
+ # current session, a new session will be started
77
+ #
78
+ # @see pause_session
79
+ #
80
+ # @return [Boolean] true if a paused session was resumed
81
+ def resume_session
82
+ current_session = SessionTracker.get_current_session
83
+
84
+ if current_session
85
+ # if the session is paused then resume it, otherwise we don't need to
86
+ # do anything
87
+ if current_session[:paused?]
88
+ current_session[:paused?] = false
89
+
90
+ return true
91
+ end
92
+ else
93
+ # if there's no current session, start a new one
94
+ start_session
95
+ end
96
+
97
+ false
98
+ end
99
+
55
100
  ##
56
101
  # Delivers the current session_counts lists to the session endpoint.
57
102
  def send_sessions
@@ -83,7 +128,7 @@ module Bugsnag
83
128
  end
84
129
  end
85
130
  end
86
- @delivery_thread = Concurrent::TimerTask.execute(execution_interval: 30) do
131
+ @delivery_thread = Concurrent::TimerTask.execute(execution_interval: 10) do
87
132
  if @session_counts.size > 0
88
133
  send_sessions
89
134
  end
@@ -106,11 +151,6 @@ module Bugsnag
106
151
  return
107
152
  end
108
153
 
109
- if !Bugsnag.configuration.should_notify_release_stage?
110
- Bugsnag.configuration.debug("Not delivering sessions due to notify_release_stages :#{Bugsnag.configuration.notify_release_stages.inspect}")
111
- return
112
- end
113
-
114
154
  body = {
115
155
  :notifier => {
116
156
  :name => Bugsnag::Report::NOTIFIER_NAME,
@@ -43,12 +43,14 @@ module Bugsnag
43
43
  if defined?(configuration.project_root) && configuration.project_root.to_s != ''
44
44
  trace_hash[:inProject] = true if file.start_with?(configuration.project_root.to_s)
45
45
  file.sub!(/#{configuration.project_root}\//, "")
46
- trace_hash.delete(:inProject) if file.match(configuration.vendor_path)
46
+ trace_hash.delete(:inProject) if vendor_path?(configuration, file)
47
47
  end
48
48
 
49
49
  # Strip common gem path prefixes
50
50
  if defined?(Gem)
51
- file = Gem.path.inject(file) {|line, path| line.sub(/#{path}\//, "") }
51
+ Gem.path.each do |path|
52
+ file.sub!("#{path}/", "")
53
+ end
52
54
  end
53
55
 
54
56
  trace_hash[:file] = file
@@ -67,5 +69,14 @@ module Bugsnag
67
69
 
68
70
  processed_backtrace
69
71
  end
72
+
73
+ # @api private
74
+ def self.vendor_path?(configuration, file_path)
75
+ return true if configuration.vendor_path && file_path.match(configuration.vendor_path)
76
+
77
+ configuration.vendor_paths.any? do |vendor_path|
78
+ file_path.start_with?("#{vendor_path.sub(/\/$/, '')}/")
79
+ end
80
+ end
70
81
  end
71
82
  end
@@ -7,7 +7,7 @@ namespace :bugsnag do
7
7
  raise RuntimeError.new("Bugsnag test exception")
8
8
  rescue => e
9
9
  Bugsnag.notify(e) do |report|
10
- report.context = "rake#test_exception"
10
+ report.automatic_context = "rake#test_exception"
11
11
  end
12
12
  end
13
13
  end
@@ -0,0 +1,124 @@
1
+ module Bugsnag::Utility
2
+ # @api private
3
+ class Duplicator
4
+ class << self
5
+ ##
6
+ # Duplicate (deep clone) the given object
7
+ #
8
+ # @param object [Object]
9
+ # @param seen_objects [Hash<String, Object>]
10
+ # @return [Object]
11
+ def duplicate(object, seen_objects = {})
12
+ case object
13
+ # return immutable & non-duplicatable objects as-is
14
+ when Symbol, Numeric, Method, TrueClass, FalseClass, NilClass
15
+ object
16
+ when Array
17
+ duplicate_array(object, seen_objects)
18
+ when Hash
19
+ duplicate_hash(object, seen_objects)
20
+ when Range
21
+ duplicate_range(object, seen_objects)
22
+ when Struct
23
+ duplicate_struct(object, seen_objects)
24
+ else
25
+ duplicate_generic_object(object, seen_objects)
26
+ end
27
+ rescue StandardError
28
+ object
29
+ end
30
+
31
+ private
32
+
33
+ def duplicate_array(array, seen_objects)
34
+ id = array.object_id
35
+
36
+ return seen_objects[id] if seen_objects.key?(id)
37
+
38
+ copy = array.dup
39
+ seen_objects[id] = copy
40
+
41
+ copy.map! do |value|
42
+ duplicate(value, seen_objects)
43
+ end
44
+
45
+ copy
46
+ end
47
+
48
+ def duplicate_hash(hash, seen_objects)
49
+ id = hash.object_id
50
+
51
+ return seen_objects[id] if seen_objects.key?(id)
52
+
53
+ copy = {}
54
+ seen_objects[id] = copy
55
+
56
+ hash.each do |key, value|
57
+ copy[duplicate(key, seen_objects)] = duplicate(value, seen_objects)
58
+ end
59
+
60
+ copy
61
+ end
62
+
63
+ ##
64
+ # Ranges are immutable but the values they contain may not be
65
+ #
66
+ # For example, a range of "a".."z" can be mutated: range.first.upcase!
67
+ def duplicate_range(range, seen_objects)
68
+ id = range.object_id
69
+
70
+ return seen_objects[id] if seen_objects.key?(id)
71
+
72
+ begin
73
+ copy = range.class.new(
74
+ duplicate(range.first, seen_objects),
75
+ duplicate(range.last, seen_objects),
76
+ range.exclude_end?
77
+ )
78
+ rescue StandardError
79
+ copy = range.dup
80
+ end
81
+
82
+ seen_objects[id] = copy
83
+ end
84
+
85
+ def duplicate_struct(struct, seen_objects)
86
+ id = struct.object_id
87
+
88
+ return seen_objects[id] if seen_objects.key?(id)
89
+
90
+ copy = struct.dup
91
+ seen_objects[id] = copy
92
+
93
+ struct.each_pair do |attribute, value|
94
+ begin
95
+ copy.send("#{attribute}=", duplicate(value, seen_objects))
96
+ rescue StandardError # rubocop:todo Lint/SuppressedException
97
+ end
98
+ end
99
+
100
+ copy
101
+ end
102
+
103
+ def duplicate_generic_object(object, seen_objects)
104
+ id = object.object_id
105
+
106
+ return seen_objects[id] if seen_objects.key?(id)
107
+
108
+ copy = object.dup
109
+ seen_objects[id] = copy
110
+
111
+ begin
112
+ copy.instance_variables.each do |variable|
113
+ value = copy.instance_variable_get(variable)
114
+
115
+ copy.instance_variable_set(variable, duplicate(value, seen_objects))
116
+ end
117
+ rescue StandardError # rubocop:todo Lint/SuppressedException
118
+ end
119
+
120
+ copy
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,41 @@
1
+ module Bugsnag::Utility
2
+ # @abstract Requires a #feature_flag_delegate method returning a
3
+ # {Bugsnag::Utility::FeatureFlagDelegate}
4
+ module FeatureDataStore
5
+ # Add a feature flag with the given name & variant
6
+ #
7
+ # @param name [String]
8
+ # @param variant [String, nil]
9
+ # @return [void]
10
+ def add_feature_flag(name, variant = nil)
11
+ feature_flag_delegate.add(name, variant)
12
+ end
13
+
14
+ # Merge the given array of FeatureFlag instances into the stored feature
15
+ # flags
16
+ #
17
+ # New flags will be appended to the array. Flags with the same name will be
18
+ # overwritten, but their position in the array will not change
19
+ #
20
+ # @param feature_flags [Array<Bugsnag::FeatureFlag>]
21
+ # @return [void]
22
+ def add_feature_flags(feature_flags)
23
+ feature_flag_delegate.merge(feature_flags)
24
+ end
25
+
26
+ # Remove the stored flag with the given name
27
+ #
28
+ # @param name [String]
29
+ # @return [void]
30
+ def clear_feature_flag(name)
31
+ feature_flag_delegate.remove(name)
32
+ end
33
+
34
+ # Remove all the stored flags
35
+ #
36
+ # @return [void]
37
+ def clear_feature_flags
38
+ feature_flag_delegate.clear
39
+ end
40
+ end
41
+ end