bugsnag 6.19.0 → 6.26.0

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 (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