airbrake-ruby 4.13.4-java → 5.0.0.rc.2-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +23 -32
  3. data/lib/airbrake-ruby/async_sender.rb +1 -1
  4. data/lib/airbrake-ruby/config.rb +65 -13
  5. data/lib/airbrake-ruby/config/processor.rb +80 -0
  6. data/lib/airbrake-ruby/config/validator.rb +4 -0
  7. data/lib/airbrake-ruby/filter_chain.rb +15 -1
  8. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  9. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  10. data/lib/airbrake-ruby/filters/keys_filter.rb +5 -5
  11. data/lib/airbrake-ruby/notice.rb +1 -8
  12. data/lib/airbrake-ruby/notice_notifier.rb +6 -0
  13. data/lib/airbrake-ruby/performance_notifier.rb +1 -1
  14. data/lib/airbrake-ruby/remote_settings.rb +143 -0
  15. data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
  16. data/lib/airbrake-ruby/sync_sender.rb +2 -2
  17. data/lib/airbrake-ruby/thread_pool.rb +1 -0
  18. data/lib/airbrake-ruby/version.rb +11 -1
  19. data/spec/airbrake_spec.rb +71 -36
  20. data/spec/config/processor_spec.rb +223 -0
  21. data/spec/config/validator_spec.rb +18 -1
  22. data/spec/config_spec.rb +38 -6
  23. data/spec/filter_chain_spec.rb +27 -0
  24. data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +10 -10
  25. data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +10 -10
  26. data/spec/filters/sql_filter_spec.rb +3 -3
  27. data/spec/notice_notifier/options_spec.rb +4 -4
  28. data/spec/performance_notifier_spec.rb +2 -2
  29. data/spec/remote_settings/settings_data_spec.rb +327 -0
  30. data/spec/remote_settings_spec.rb +230 -0
  31. data/spec/sync_sender_spec.rb +3 -1
  32. data/spec/thread_pool_spec.rb +25 -5
  33. metadata +22 -13
@@ -4,19 +4,12 @@ module Airbrake
4
4
  #
5
5
  # @since v1.0.0
6
6
  class Notice
7
- # @return [Hash{Symbol=>String}] the information about the notifier library
8
- NOTIFIER = {
9
- name: 'airbrake-ruby'.freeze,
10
- version: Airbrake::AIRBRAKE_RUBY_VERSION,
11
- url: 'https://github.com/airbrake/airbrake-ruby'.freeze,
12
- }.freeze
13
-
14
7
  # @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
15
8
  # Context tab in the dashboard
16
9
  CONTEXT = {
17
10
  os: RUBY_PLATFORM,
18
11
  language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
19
- notifier: NOTIFIER,
12
+ notifier: Airbrake::NOTIFIER_INFO,
20
13
  }.freeze
21
14
 
22
15
  # @return [Integer] the maxium size of the JSON payload in bytes
@@ -82,6 +82,12 @@ module Airbrake
82
82
  @context.merge!(context)
83
83
  end
84
84
 
85
+ # @return [Boolean]
86
+ # @since v4.14.0
87
+ def has_filter?(filter_class) # rubocop:disable Naming/PredicateName
88
+ @filter_chain.includes?(filter_class)
89
+ end
90
+
85
91
  private
86
92
 
87
93
  def convert_to_exception(ex)
@@ -144,7 +144,7 @@ module Airbrake
144
144
 
145
145
  with_grouped_payload(payload) do |resource_hash, destination|
146
146
  url = URI.join(
147
- @config.host,
147
+ @config.apm_host,
148
148
  "api/v5/projects/#{@config.project_id}/#{destination}",
149
149
  )
150
150
 
@@ -0,0 +1,143 @@
1
+ module Airbrake
2
+ # RemoteSettings polls the remote config of the passed project at fixed
3
+ # intervals. The fetched config is yielded as a callback parameter so that the
4
+ # invoker can define read config values.
5
+ #
6
+ # @example Disable/enable error notifications based on the remote value
7
+ # RemoteSettings.poll do |data|
8
+ # config.error_notifications = data.error_notifications?
9
+ # end
10
+ #
11
+ # When {#poll} is called, it will try to load remote settings from disk, so
12
+ # that it doesn't wait on the result from the API call.
13
+ #
14
+ # When {#stop_polling} is called, the current config will be dumped to disk.
15
+ #
16
+ # @since ?.?.?
17
+ # @api private
18
+ class RemoteSettings
19
+ include Airbrake::Loggable
20
+
21
+ # @return [String] the path to the persistent config
22
+ CONFIG_DUMP_PATH = File.join(
23
+ File.expand_path(__dir__),
24
+ '../../config/config.json',
25
+ ).freeze
26
+
27
+ # @return [Hash{Symbol=>String}] metadata to be attached to every GET
28
+ # request
29
+ QUERY_PARAMS = URI.encode_www_form(
30
+ notifier_name: Airbrake::NOTIFIER_INFO[:name],
31
+ notifier_version: Airbrake::NOTIFIER_INFO[:version],
32
+ os: RUBY_PLATFORM,
33
+ language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
34
+ ).freeze
35
+
36
+ # Polls remote config of the given project.
37
+ #
38
+ # @param [Integer] project_id
39
+ # @yield [data]
40
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
41
+ # @return [Airbrake::RemoteSettings]
42
+ def self.poll(project_id, &block)
43
+ new(project_id, &block).poll
44
+ end
45
+
46
+ # @param [Integer] project_id
47
+ # @yield [data]
48
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
49
+ def initialize(project_id, &block)
50
+ @data = SettingsData.new(project_id, {})
51
+ @block = block
52
+ @poll = nil
53
+ end
54
+
55
+ # Polls remote config of the given project in background. Loads local config
56
+ # first (if exists).
57
+ #
58
+ # @return [self]
59
+ def poll
60
+ @poll ||= Thread.new do
61
+ begin
62
+ load_config
63
+ rescue StandardError => ex
64
+ logger.error("#{LOG_LABEL} config loading failed: #{ex}")
65
+ end
66
+
67
+ @block.call(@data)
68
+
69
+ loop do
70
+ @block.call(@data.merge!(fetch_config))
71
+ sleep(@data.interval)
72
+ end
73
+ end
74
+
75
+ self
76
+ end
77
+
78
+ # Stops the background poller thread. Dumps current config to disk.
79
+ #
80
+ # @return [void]
81
+ def stop_polling
82
+ @poll.kill if @poll
83
+
84
+ begin
85
+ dump_config
86
+ rescue StandardError => ex
87
+ logger.error("#{LOG_LABEL} config dumping failed: #{ex}")
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def fetch_config
94
+ response = nil
95
+ begin
96
+ response = Net::HTTP.get(build_config_uri)
97
+ rescue StandardError => ex
98
+ logger.error(ex)
99
+ return {}
100
+ end
101
+
102
+ # AWS S3 API returns XML when request is not valid. In this case we just
103
+ # print the returned body and exit the method.
104
+ if response.start_with?('<?xml ')
105
+ logger.error(response)
106
+ return {}
107
+ end
108
+
109
+ json = nil
110
+ begin
111
+ json = JSON.parse(response)
112
+ rescue JSON::ParserError => ex
113
+ logger.error(ex)
114
+ return {}
115
+ end
116
+
117
+ json
118
+ end
119
+
120
+ def build_config_uri
121
+ uri = URI(@data.config_route)
122
+ uri.query = QUERY_PARAMS
123
+ uri
124
+ end
125
+
126
+ def load_config
127
+ config_dir = File.dirname(CONFIG_DUMP_PATH)
128
+ Dir.mkdir(config_dir) unless File.directory?(config_dir)
129
+
130
+ return unless File.exist?(CONFIG_DUMP_PATH)
131
+
132
+ config = File.read(CONFIG_DUMP_PATH)
133
+ @data.merge!(JSON.parse(config))
134
+ end
135
+
136
+ def dump_config
137
+ config_dir = File.dirname(CONFIG_DUMP_PATH)
138
+ Dir.mkdir(config_dir) unless File.directory?(config_dir)
139
+
140
+ File.write(CONFIG_DUMP_PATH, JSON.dump(@data.to_h))
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,116 @@
1
+ module Airbrake
2
+ class RemoteSettings
3
+ # SettingsData is a container, which wraps JSON payload returned by the
4
+ # remote settings API. It exposes the payload via convenient methods and
5
+ # also ensures that in case some data from the payload is missing, a default
6
+ # value would be returned instead.
7
+ #
8
+ # @example
9
+ # # Create the object and pass initial data (empty hash).
10
+ # settings_data = SettingsData.new({})
11
+ #
12
+ # settings_data.interval #=> 600
13
+ #
14
+ # @since ?.?.?
15
+ # @api private
16
+ class SettingsData
17
+ # @return [Integer] how frequently we should poll the config API
18
+ DEFAULT_INTERVAL = 600
19
+
20
+ # @return [String] API version of the S3 API to poll
21
+ API_VER = '2020-06-18'.freeze
22
+
23
+ # @return [String] what URL to poll
24
+ CONFIG_ROUTE_PATTERN =
25
+ 'https://v1-%<bucket>s.s3.amazonaws.com/' \
26
+ "#{API_VER}/config/%<project_id>s/config.json".freeze
27
+
28
+ # @return [Hash{Symbol=>String}] the hash of all supported settings where
29
+ # the value is the name of the setting returned by the API
30
+ SETTINGS = {
31
+ errors: 'errors',
32
+ apm: 'apm',
33
+ }.freeze
34
+
35
+ # @param [Integer] project_id
36
+ # @param [Hash{String=>Object}] data
37
+ def initialize(project_id, data)
38
+ @project_id = project_id
39
+ @data = data
40
+ end
41
+
42
+ # Merges the given +hash+ with internal data.
43
+ #
44
+ # @param [Hash{String=>Object}] hash
45
+ # @return [self]
46
+ def merge!(hash)
47
+ @data.merge!(hash)
48
+
49
+ self
50
+ end
51
+
52
+ # @return [Integer] how frequently we should poll for the config
53
+ def interval
54
+ return DEFAULT_INTERVAL if !@data.key?('poll_sec') || !@data['poll_sec']
55
+
56
+ @data['poll_sec'] > 0 ? @data['poll_sec'] : DEFAULT_INTERVAL
57
+ end
58
+
59
+ # @return [String] where the config is stored on S3.
60
+ def config_route
61
+ if !@data.key?('config_route') || !@data['config_route']
62
+ return format(
63
+ CONFIG_ROUTE_PATTERN,
64
+ bucket: 'staging-notifier-configs',
65
+ project_id: @project_id,
66
+ )
67
+ end
68
+
69
+ @data['config_route']
70
+ end
71
+
72
+ # @return [Boolean] whether error notifications are enabled
73
+ def error_notifications?
74
+ return true unless (s = find_setting(SETTINGS[:errors]))
75
+
76
+ s['enabled']
77
+ end
78
+
79
+ # @return [Boolean] whether APM is enabled
80
+ def performance_stats?
81
+ return true unless (s = find_setting(SETTINGS[:apm]))
82
+
83
+ s['enabled']
84
+ end
85
+
86
+ # @return [String, nil] the host, which provides the API endpoint to which
87
+ # exceptions should be sent
88
+ def error_host
89
+ return unless (s = find_setting(SETTINGS[:errors]))
90
+
91
+ s['endpoint']
92
+ end
93
+
94
+ # @return [String, nil] the host, which provides the API endpoint to which
95
+ # APM data should be sent
96
+ def apm_host
97
+ return unless (s = find_setting(SETTINGS[:apm]))
98
+
99
+ s['endpoint']
100
+ end
101
+
102
+ # @return [Hash{String=>Object}] raw representation of JSON payload
103
+ def to_h
104
+ @data.dup
105
+ end
106
+
107
+ private
108
+
109
+ def find_setting(name)
110
+ return unless @data.key?('settings')
111
+
112
+ @data['settings'].find { |s| s['name'] == name }
113
+ end
114
+ end
115
+ end
116
+ end
@@ -23,7 +23,7 @@ module Airbrake
23
23
  # @param [#to_json] data
24
24
  # @param [URI::HTTPS] endpoint
25
25
  # @return [Hash{String=>String}] the parsed HTTP response
26
- def send(data, promise, endpoint = @config.endpoint)
26
+ def send(data, promise, endpoint = @config.error_endpoint)
27
27
  return promise if rate_limited_ip?(promise)
28
28
 
29
29
  response = nil
@@ -79,7 +79,7 @@ module Airbrake
79
79
  req['Authorization'] = "Bearer #{@config.project_key}"
80
80
  req['Content-Type'] = CONTENT_TYPE
81
81
  req['User-Agent'] =
82
- "#{Airbrake::Notice::NOTIFIER[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION}" \
82
+ "#{Airbrake::NOTIFIER_INFO[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION}" \
83
83
  " Ruby/#{RUBY_VERSION}"
84
84
 
85
85
  req
@@ -83,6 +83,7 @@ module Airbrake
83
83
 
84
84
  if @pid != Process.pid && @workers.list.empty?
85
85
  @pid = Process.pid
86
+ @workers = ThreadGroup.new
86
87
  spawn_workers
87
88
  end
88
89
 
@@ -2,5 +2,15 @@
2
2
  # More information: http://semver.org/
3
3
  module Airbrake
4
4
  # @return [String] the library version
5
- AIRBRAKE_RUBY_VERSION = '4.13.4'.freeze
5
+ # @api public
6
+ AIRBRAKE_RUBY_VERSION = '5.0.0.rc.2'.freeze
7
+
8
+ # @return [Hash{Symbol=>String}] the information about the notifier library
9
+ # @since ?.?.?
10
+ # @api public
11
+ NOTIFIER_INFO = {
12
+ name: 'airbrake-ruby'.freeze,
13
+ version: Airbrake::AIRBRAKE_RUBY_VERSION,
14
+ url: 'https://github.com/airbrake/airbrake-ruby'.freeze,
15
+ }.freeze
6
16
  end
@@ -1,4 +1,13 @@
1
1
  RSpec.describe Airbrake do
2
+ let(:remote_settings) { instance_double(Airbrake::RemoteSettings) }
3
+
4
+ before do
5
+ allow(Airbrake::RemoteSettings).to receive(:poll).and_return(remote_settings)
6
+ allow(remote_settings).to receive(:stop_polling)
7
+ end
8
+
9
+ after { described_class.instance_variable_set(:@remote_settings, nil) }
10
+
2
11
  it "gets initialized with a performance notifier" do
3
12
  expect(described_class.performance_notifier).not_to be_nil
4
13
  end
@@ -85,43 +94,55 @@ RSpec.describe Airbrake do
85
94
  expect(described_class.notice_notifier).not_to receive(:add_filter)
86
95
  10.times { described_class.configure {} }
87
96
  end
97
+
98
+ it "appends some default filters" do
99
+ allow(described_class.notice_notifier).to receive(:add_filter)
100
+ expect(described_class.notice_notifier).to receive(:add_filter).with(
101
+ an_instance_of(Airbrake::Filters::RootDirectoryFilter),
102
+ )
103
+
104
+ described_class.configure do |c|
105
+ c.project_id = 1
106
+ c.project_key = '2'
107
+ end
108
+ end
88
109
  end
89
110
 
90
- context "when blacklist_keys gets configured" do
91
- before { allow(Airbrake.notice_notifier).to receive(:add_filter) }
111
+ context "when blocklist_keys gets configured" do
112
+ before { allow(described_class.notice_notifier).to receive(:add_filter) }
92
113
 
93
- it "adds blacklist filter" do
94
- expect(Airbrake.notice_notifier).to receive(:add_filter)
95
- .with(an_instance_of(Airbrake::Filters::KeysBlacklist))
96
- described_class.configure { |c| c.blacklist_keys = %w[password] }
114
+ it "adds blocklist filter" do
115
+ expect(described_class.notice_notifier).to receive(:add_filter)
116
+ .with(an_instance_of(Airbrake::Filters::KeysBlocklist))
117
+ described_class.configure { |c| c.blocklist_keys = %w[password] }
97
118
  end
98
119
 
99
- it "initializes blacklist with specified parameters" do
100
- expect(Airbrake::Filters::KeysBlacklist).to receive(:new).with(%w[password])
101
- described_class.configure { |c| c.blacklist_keys = %w[password] }
120
+ it "initializes blocklist with specified parameters" do
121
+ expect(Airbrake::Filters::KeysBlocklist).to receive(:new).with(%w[password])
122
+ described_class.configure { |c| c.blocklist_keys = %w[password] }
102
123
  end
103
124
  end
104
125
 
105
- context "when whitelist_keys gets configured" do
106
- before { allow(Airbrake.notice_notifier).to receive(:add_filter) }
126
+ context "when allowlist_keys gets configured" do
127
+ before { allow(described_class.notice_notifier).to receive(:add_filter) }
107
128
 
108
- it "adds whitelist filter" do
109
- expect(Airbrake.notice_notifier).to receive(:add_filter)
110
- .with(an_instance_of(Airbrake::Filters::KeysWhitelist))
111
- described_class.configure { |c| c.whitelist_keys = %w[banana] }
129
+ it "adds allowlist filter" do
130
+ expect(described_class.notice_notifier).to receive(:add_filter)
131
+ .with(an_instance_of(Airbrake::Filters::KeysAllowlist))
132
+ described_class.configure { |c| c.allowlist_keys = %w[banana] }
112
133
  end
113
134
 
114
- it "initializes whitelist with specified parameters" do
115
- expect(Airbrake::Filters::KeysWhitelist).to receive(:new).with(%w[banana])
116
- described_class.configure { |c| c.whitelist_keys = %w[banana] }
135
+ it "initializes allowlist with specified parameters" do
136
+ expect(Airbrake::Filters::KeysAllowlist).to receive(:new).with(%w[banana])
137
+ described_class.configure { |c| c.allowlist_keys = %w[banana] }
117
138
  end
118
139
  end
119
140
 
120
141
  context "when root_directory gets configured" do
121
- before { allow(Airbrake.notice_notifier).to receive(:add_filter) }
142
+ before { allow(described_class.notice_notifier).to receive(:add_filter) }
122
143
 
123
144
  it "adds root directory filter" do
124
- expect(Airbrake.notice_notifier).to receive(:add_filter)
145
+ expect(described_class.notice_notifier).to receive(:add_filter)
125
146
  .with(an_instance_of(Airbrake::Filters::RootDirectoryFilter))
126
147
  described_class.configure { |c| c.root_directory = '/my/path' }
127
148
  end
@@ -133,7 +154,7 @@ RSpec.describe Airbrake do
133
154
  end
134
155
 
135
156
  it "adds git revision filter" do
136
- expect(Airbrake.notice_notifier).to receive(:add_filter)
157
+ expect(described_class.notice_notifier).to receive(:add_filter)
137
158
  .with(an_instance_of(Airbrake::Filters::GitRevisionFilter))
138
159
  described_class.configure { |c| c.root_directory = '/my/path' }
139
160
  end
@@ -145,7 +166,7 @@ RSpec.describe Airbrake do
145
166
  end
146
167
 
147
168
  it "adds git repository filter" do
148
- expect(Airbrake.notice_notifier).to receive(:add_filter)
169
+ expect(described_class.notice_notifier).to receive(:add_filter)
149
170
  .with(an_instance_of(Airbrake::Filters::GitRepositoryFilter))
150
171
  described_class.configure { |c| c.root_directory = '/my/path' }
151
172
  end
@@ -157,7 +178,7 @@ RSpec.describe Airbrake do
157
178
  end
158
179
 
159
180
  it "adds git last checkout filter" do
160
- expect(Airbrake.notice_notifier).to receive(:add_filter)
181
+ expect(described_class.notice_notifier).to receive(:add_filter)
161
182
  .with(an_instance_of(Airbrake::Filters::GitLastCheckoutFilter))
162
183
  described_class.configure { |c| c.root_directory = '/my/path' }
163
184
  end
@@ -170,7 +191,7 @@ RSpec.describe Airbrake do
170
191
  end
171
192
  end
172
193
 
173
- describe "#notify_request" do
194
+ describe ".notify_request" do
174
195
  context "when :stash key is not provided" do
175
196
  it "doesn't add anything to the stash of the request" do
176
197
  expect(described_class.performance_notifier).to receive(:notify) do |request|
@@ -205,7 +226,7 @@ RSpec.describe Airbrake do
205
226
  end
206
227
  end
207
228
 
208
- describe "#notify_request_sync" do
229
+ describe ".notify_request_sync" do
209
230
  it "notifies request synchronously" do
210
231
  expect(described_class.performance_notifier).to receive(:notify_sync)
211
232
 
@@ -221,7 +242,7 @@ RSpec.describe Airbrake do
221
242
  end
222
243
  end
223
244
 
224
- describe "#notify_query" do
245
+ describe ".notify_query" do
225
246
  context "when :stash key is not provided" do
226
247
  it "doesn't add anything to the stash of the query" do
227
248
  expect(described_class.performance_notifier).to receive(:notify) do |query|
@@ -256,7 +277,7 @@ RSpec.describe Airbrake do
256
277
  end
257
278
  end
258
279
 
259
- describe "#notify_query_sync" do
280
+ describe ".notify_query_sync" do
260
281
  it "notifies query synchronously" do
261
282
  expect(described_class.performance_notifier).to receive(:notify_sync)
262
283
 
@@ -272,7 +293,7 @@ RSpec.describe Airbrake do
272
293
  end
273
294
  end
274
295
 
275
- describe "#notify_performance_breakdown" do
296
+ describe ".notify_performance_breakdown" do
276
297
  context "when :stash key is not provided" do
277
298
  it "doesn't add anything to the stash of the performance breakdown" do
278
299
  expect(described_class.performance_notifier).to receive(:notify) do |query|
@@ -310,7 +331,7 @@ RSpec.describe Airbrake do
310
331
  end
311
332
  end
312
333
 
313
- describe "#notify_performance_breakdown_sync" do
334
+ describe ".notify_performance_breakdown_sync" do
314
335
  it "notifies performance breakdown synchronously" do
315
336
  expect(described_class.performance_notifier).to receive(:notify_sync)
316
337
 
@@ -327,7 +348,7 @@ RSpec.describe Airbrake do
327
348
  end
328
349
  end
329
350
 
330
- describe "#notify_queue" do
351
+ describe ".notify_queue" do
331
352
  context "when :stash key is not provided" do
332
353
  it "doesn't add anything to the stash of the queue" do
333
354
  expect(described_class.performance_notifier).to receive(:notify) do |queue|
@@ -358,7 +379,7 @@ RSpec.describe Airbrake do
358
379
  end
359
380
  end
360
381
 
361
- describe "#notify_queue_sync" do
382
+ describe ".notify_queue_sync" do
362
383
  it "notifies queue synchronously" do
363
384
  expect(described_class.performance_notifier).to receive(:notify_sync)
364
385
 
@@ -392,33 +413,47 @@ RSpec.describe Airbrake do
392
413
  end
393
414
 
394
415
  describe ".close" do
395
- after { Airbrake.reset }
416
+ after { described_class.reset }
396
417
 
397
418
  context "when notice_notifier is defined" do
398
419
  it "gets closed" do
399
- expect(Airbrake.notice_notifier).to receive(:close)
420
+ expect(described_class.notice_notifier).to receive(:close)
400
421
  end
401
422
  end
402
423
 
403
424
  context "when notice_notifier is undefined" do
404
425
  it "doesn't get closed (because it wasn't initialized)" do
405
- Airbrake.instance_variable_set(:@notice_notifier, nil)
426
+ described_class.instance_variable_set(:@notice_notifier, nil)
406
427
  expect_any_instance_of(Airbrake::NoticeNotifier).not_to receive(:close)
407
428
  end
408
429
  end
409
430
 
410
431
  context "when performance_notifier is defined" do
411
432
  it "gets closed" do
412
- expect(Airbrake.performance_notifier).to receive(:close)
433
+ expect(described_class.performance_notifier).to receive(:close)
413
434
  end
414
435
  end
415
436
 
416
437
  context "when perforance_notifier is undefined" do
417
438
  it "doesn't get closed (because it wasn't initialized)" do
418
- Airbrake.instance_variable_set(:@performance_notifier, nil)
439
+ described_class.instance_variable_set(:@performance_notifier, nil)
419
440
  expect_any_instance_of(Airbrake::PerformanceNotifier)
420
441
  .not_to receive(:close)
421
442
  end
422
443
  end
444
+
445
+ context "when remote settings are defined" do
446
+ it "stops polling" do
447
+ described_class.instance_variable_set(:@remote_settings, remote_settings)
448
+ expect(remote_settings).to receive(:stop_polling)
449
+ end
450
+ end
451
+
452
+ context "when remote settings are undefined" do
453
+ it "doesn't stop polling (because they weren't initialized)" do
454
+ described_class.instance_variable_set(:@remote_settings, nil)
455
+ expect(remote_settings).not_to receive(:stop_polling)
456
+ end
457
+ end
423
458
  end
424
459
  end