airbrake-ruby 4.13.3 → 5.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) 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/git_last_checkout_filter.rb +2 -1
  9. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  10. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  11. data/lib/airbrake-ruby/filters/keys_filter.rb +5 -5
  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 +128 -0
  15. data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
  16. data/lib/airbrake-ruby/sync_sender.rb +1 -1
  17. data/lib/airbrake-ruby/thread_pool.rb +1 -0
  18. data/lib/airbrake-ruby/version.rb +1 -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/git_last_checkout_filter_spec.rb +20 -3
  25. data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +10 -10
  26. data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +10 -10
  27. data/spec/filters/sql_filter_spec.rb +3 -3
  28. data/spec/notice_notifier/options_spec.rb +4 -4
  29. data/spec/performance_notifier_spec.rb +2 -2
  30. data/spec/remote_settings/settings_data_spec.rb +327 -0
  31. data/spec/remote_settings_spec.rb +212 -0
  32. data/spec/sync_sender_spec.rb +3 -1
  33. data/spec/thread_pool_spec.rb +25 -5
  34. metadata +20 -12
@@ -1,4 +1,4 @@
1
- RSpec.describe Airbrake::Filters::KeysBlacklist do
1
+ RSpec.describe Airbrake::Filters::KeysBlocklist do
2
2
  subject { described_class.new(patterns) }
3
3
 
4
4
  let(:notice) { Airbrake::Notice.new(AirbrakeTestError.new) }
@@ -91,10 +91,10 @@ RSpec.describe Airbrake::Filters::KeysBlacklist do
91
91
 
92
92
  it "logs an error" do
93
93
  expect(Airbrake::Loggable.instance).to receive(:error).with(
94
- /KeysBlacklist is invalid.+patterns: \[#<Object:.+>\]/,
94
+ /KeysBlocklist is invalid.+patterns: \[#<Object:.+>\]/,
95
95
  )
96
- keys_blacklist = described_class.new(patterns)
97
- keys_blacklist.call(notice)
96
+ keys_blocklist = described_class.new(patterns)
97
+ keys_blocklist.call(notice)
98
98
  end
99
99
  end
100
100
 
@@ -104,10 +104,10 @@ RSpec.describe Airbrake::Filters::KeysBlacklist do
104
104
  context "and when the filter is called once" do
105
105
  it "logs an error" do
106
106
  expect(Airbrake::Loggable.instance).to receive(:error).with(
107
- /KeysBlacklist is invalid.+patterns: \[#<Proc:.+>\]/,
107
+ /KeysBlocklist is invalid.+patterns: \[#<Proc:.+>\]/,
108
108
  )
109
- keys_blacklist = described_class.new(patterns)
110
- keys_blacklist.call(notice)
109
+ keys_blocklist = described_class.new(patterns)
110
+ keys_blocklist.call(notice)
111
111
  end
112
112
  end
113
113
 
@@ -133,10 +133,10 @@ RSpec.describe Airbrake::Filters::KeysBlacklist do
133
133
 
134
134
  it "logs an error" do
135
135
  expect(Airbrake::Loggable.instance).to receive(:error).with(
136
- /KeysBlacklist is invalid.+patterns: \[#<Object:.+>\]/,
136
+ /KeysBlocklist is invalid.+patterns: \[#<Object:.+>\]/,
137
137
  )
138
- keys_blacklist = described_class.new(patterns)
139
- keys_blacklist.call(notice)
138
+ keys_blocklist = described_class.new(patterns)
139
+ keys_blocklist.call(notice)
140
140
  end
141
141
  end
142
142
 
@@ -10,7 +10,7 @@ RSpec.describe Airbrake::Filters::SqlFilter do
10
10
  end
11
11
  end
12
12
 
13
- shared_examples "query blacklisting" do |query, opts|
13
+ shared_examples "query blocklisting" do |query, opts|
14
14
  it "ignores '#{query}'" do
15
15
  filter = described_class.new('postgres')
16
16
  q = Airbrake::Query.new(query: query, method: 'GET', route: '/', timing: 1)
@@ -263,12 +263,12 @@ RSpec.describe Airbrake::Filters::SqlFilter do
263
263
 
264
264
  'SELECT t.oid, t.typname FROM pg_type as t WHERE t.typname IN (?)',
265
265
  ].each do |query|
266
- include_examples 'query blacklisting', query, should_ignore: true
266
+ include_examples 'query blocklisting', query, should_ignore: true
267
267
  end
268
268
 
269
269
  [
270
270
  'UPDATE "users" SET "last_sign_in_at" = ? WHERE "users"."id" = ?',
271
271
  ].each do |query|
272
- include_examples 'query blacklisting', query, should_ignore: false
272
+ include_examples 'query blocklisting', query, should_ignore: false
273
273
  end
274
274
  end
@@ -234,14 +234,14 @@ RSpec.describe Airbrake::NoticeNotifier do
234
234
  end
235
235
  end
236
236
 
237
- describe ":blacklist_keys" do
237
+ describe ":blocklist_keys" do
238
238
  # Fixes https://github.com/airbrake/airbrake-ruby/issues/276
239
- context "when specified along with :whitelist_keys" do
239
+ context "when specified along with :allowlist_keys" do
240
240
  context "and when context payload is present" do
241
241
  before do
242
242
  Airbrake::Config.instance.merge(
243
- blacklist_keys: %i[password password_confirmation],
244
- whitelist_keys: [:email, /user/i, 'account_id'],
243
+ blocklist_keys: %i[password password_confirmation],
244
+ allowlist_keys: [:email, /user/i, 'account_id'],
245
245
  )
246
246
  end
247
247
 
@@ -389,7 +389,7 @@ RSpec.describe Airbrake::PerformanceNotifier do
389
389
  subject.close
390
390
 
391
391
  expect(promise).to be_an(Airbrake::Promise)
392
- expect(promise.value).to eq('' => nil)
392
+ expect(promise.value).to eq('' => '')
393
393
  end
394
394
 
395
395
  it "checks performance stat configuration" do
@@ -601,7 +601,7 @@ RSpec.describe Airbrake::PerformanceNotifier do
601
601
  body: %r|\A{"queries":\[{"method":"POST","route":"/foo"|,
602
602
  ),
603
603
  ).to have_been_made
604
- expect(retval).to eq('' => nil)
604
+ expect(retval).to eq('' => '')
605
605
  end
606
606
  end
607
607
 
@@ -0,0 +1,327 @@
1
+ RSpec.describe Airbrake::RemoteSettings::SettingsData do
2
+ let(:project_id) { 123 }
3
+
4
+ describe "#merge!" do
5
+ it "returns self" do
6
+ settings_data = described_class.new(project_id, {})
7
+ expect(settings_data.merge!({})).to eql(settings_data)
8
+ end
9
+
10
+ it "merges the given hash with the data" do
11
+ settings_data = described_class.new(project_id, {})
12
+ # rubocop:disable Performance/RedundantMerge
13
+ settings_data.merge!('poll_sec' => 123, 'config_route' => 'abc')
14
+ # rubocop:enable Performance/RedundantMerge
15
+
16
+ expect(settings_data.interval).to eq(123)
17
+ expect(settings_data.config_route).to eq('abc')
18
+ end
19
+ end
20
+
21
+ describe "#interval" do
22
+ context "when given data has zero interval" do
23
+ let(:data) do
24
+ { 'poll_sec' => 0 }
25
+ end
26
+
27
+ it "returns the default interval" do
28
+ expect(described_class.new(project_id, data).interval).to eq(600)
29
+ end
30
+ end
31
+
32
+ context "when given data has negative interval" do
33
+ let(:data) do
34
+ { 'poll_sec' => -1 }
35
+ end
36
+
37
+ it "returns the default interval" do
38
+ expect(described_class.new(project_id, data).interval).to eq(600)
39
+ end
40
+ end
41
+
42
+ context "when given data has nil interval" do
43
+ let(:data) do
44
+ { 'poll_sec' => nil }
45
+ end
46
+
47
+ it "returns the default interval" do
48
+ expect(described_class.new(project_id, data).interval).to eq(600)
49
+ end
50
+ end
51
+
52
+ context "when given data has a positive interval" do
53
+ let(:data) do
54
+ { 'poll_sec' => 123 }
55
+ end
56
+
57
+ it "returns the interval from data" do
58
+ expect(described_class.new(project_id, data).interval).to eq(123)
59
+ end
60
+ end
61
+ end
62
+
63
+ describe "#config_route" do
64
+ context "when given a pathname" do
65
+ let(:data) do
66
+ { 'config_route' => 'http://example.com' }
67
+ end
68
+
69
+ it "returns the given pathname" do
70
+ expect(described_class.new(project_id, data).config_route)
71
+ .to eq('http://example.com')
72
+ end
73
+ end
74
+
75
+ context "when the pathname is nil" do
76
+ let(:data) do
77
+ { 'config_route' => nil }
78
+ end
79
+
80
+ it "returns the default pathname" do
81
+ expect(described_class.new(project_id, data).config_route).to eq(
82
+ 'https://staging-notifier-configs.s3.amazonaws.com/' \
83
+ "2020-06-18/config/#{project_id}/config.json",
84
+ )
85
+ end
86
+ end
87
+ end
88
+
89
+ describe "#error_notifications?" do
90
+ context "when the 'errors' setting is present" do
91
+ context "and when it is enabled" do
92
+ let(:data) do
93
+ {
94
+ 'settings' => [
95
+ {
96
+ 'name' => 'errors',
97
+ 'enabled' => true,
98
+ },
99
+ ],
100
+ }
101
+ end
102
+
103
+ it "returns true" do
104
+ expect(described_class.new(project_id, data).error_notifications?)
105
+ .to eq(true)
106
+ end
107
+ end
108
+
109
+ context "and when it is disabled" do
110
+ let(:data) do
111
+ {
112
+ 'settings' => [
113
+ {
114
+ 'name' => 'errors',
115
+ 'enabled' => false,
116
+ },
117
+ ],
118
+ }
119
+ end
120
+
121
+ it "returns false" do
122
+ expect(described_class.new(project_id, data).error_notifications?)
123
+ .to eq(false)
124
+ end
125
+ end
126
+ end
127
+
128
+ context "when the 'errors' setting is missing" do
129
+ let(:data) do
130
+ { 'settings' => [] }
131
+ end
132
+
133
+ it "returns true" do
134
+ expect(described_class.new(project_id, data).error_notifications?)
135
+ .to eq(true)
136
+ end
137
+ end
138
+ end
139
+
140
+ describe "#performance_stats?" do
141
+ context "when the 'apm' setting is present" do
142
+ context "and when it is enabled" do
143
+ let(:data) do
144
+ {
145
+ 'settings' => [
146
+ {
147
+ 'name' => 'apm',
148
+ 'enabled' => true,
149
+ },
150
+ ],
151
+ }
152
+ end
153
+
154
+ it "returns true" do
155
+ expect(described_class.new(project_id, data).performance_stats?)
156
+ .to eq(true)
157
+ end
158
+ end
159
+
160
+ context "and when it is disabled" do
161
+ let(:data) do
162
+ {
163
+ 'settings' => [
164
+ {
165
+ 'name' => 'apm',
166
+ 'enabled' => false,
167
+ },
168
+ ],
169
+ }
170
+ end
171
+
172
+ it "returns false" do
173
+ expect(described_class.new(project_id, data).performance_stats?)
174
+ .to eq(false)
175
+ end
176
+ end
177
+ end
178
+
179
+ context "when the 'apm' setting is missing" do
180
+ let(:data) do
181
+ { 'settings' => [] }
182
+ end
183
+
184
+ it "returns true" do
185
+ expect(described_class.new(project_id, data).performance_stats?)
186
+ .to eq(true)
187
+ end
188
+ end
189
+ end
190
+
191
+ describe "#error_host" do
192
+ context "when the 'errors' setting is present" do
193
+ context "and when 'endpoint' is specified" do
194
+ let(:endpoint) { 'https://api.example.com/' }
195
+
196
+ let(:data) do
197
+ {
198
+ 'settings' => [
199
+ {
200
+ 'name' => 'errors',
201
+ 'enabled' => true,
202
+ 'endpoint' => endpoint,
203
+ },
204
+ ],
205
+ }
206
+ end
207
+
208
+ it "returns the endpoint" do
209
+ expect(described_class.new(project_id, data).error_host).to eq(endpoint)
210
+ end
211
+ end
212
+
213
+ context "and when an endpoint is NOT specified" do
214
+ let(:data) do
215
+ {
216
+ 'settings' => [
217
+ {
218
+ 'name' => 'errors',
219
+ 'enabled' => true,
220
+ },
221
+ ],
222
+ }
223
+ end
224
+
225
+ it "returns nil" do
226
+ expect(described_class.new(project_id, data).error_host).to be_nil
227
+ end
228
+ end
229
+ end
230
+
231
+ context "when the 'errors' setting is missing" do
232
+ let(:data) do
233
+ { 'settings' => [] }
234
+ end
235
+
236
+ it "returns nil" do
237
+ expect(described_class.new(project_id, data).error_host).to be_nil
238
+ end
239
+ end
240
+ end
241
+
242
+ describe "#apm_host" do
243
+ context "when the 'apm' setting is present" do
244
+ context "and when 'endpoint' is specified" do
245
+ let(:endpoint) { 'https://api.example.com/' }
246
+
247
+ let(:data) do
248
+ {
249
+ 'settings' => [
250
+ {
251
+ 'name' => 'apm',
252
+ 'enabled' => true,
253
+ 'endpoint' => endpoint,
254
+ },
255
+ ],
256
+ }
257
+ end
258
+
259
+ it "returns the endpoint" do
260
+ expect(described_class.new(project_id, data).apm_host).to eq(endpoint)
261
+ end
262
+ end
263
+
264
+ context "and when an endpoint is NOT specified" do
265
+ let(:data) do
266
+ {
267
+ 'settings' => [
268
+ {
269
+ 'name' => 'apm',
270
+ 'enabled' => true,
271
+ },
272
+ ],
273
+ }
274
+ end
275
+
276
+ it "returns nil" do
277
+ expect(described_class.new(project_id, data).apm_host).to be_nil
278
+ end
279
+ end
280
+ end
281
+
282
+ context "when the 'apm' setting is missing" do
283
+ let(:data) do
284
+ { 'settings' => [] }
285
+ end
286
+
287
+ it "returns nil" do
288
+ expect(described_class.new(project_id, data).apm_host).to be_nil
289
+ end
290
+ end
291
+ end
292
+
293
+ describe "#to_h" do
294
+ let(:data) do
295
+ {
296
+ 'poll_sec' => 123,
297
+ 'settings' => [
298
+ {
299
+ 'name' => 'apm',
300
+ 'enabled' => false,
301
+ },
302
+ ],
303
+ }
304
+ end
305
+
306
+ subject { described_class.new(project_id, data) }
307
+
308
+ it "returns a hash representation of settings" do
309
+ expect(described_class.new(project_id, data).to_h).to eq(data)
310
+ end
311
+
312
+ it "doesn't allow mutation of the original data object" do
313
+ hash = subject.to_h
314
+ hash['poll_sec'] = 0
315
+
316
+ expect(subject.to_h).to eq(
317
+ 'poll_sec' => 123,
318
+ 'settings' => [
319
+ {
320
+ 'name' => 'apm',
321
+ 'enabled' => false,
322
+ },
323
+ ],
324
+ )
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,212 @@
1
+ RSpec.describe Airbrake::RemoteSettings do
2
+ let(:project_id) { 123 }
3
+
4
+ let(:endpoint) do
5
+ "https://staging-notifier-configs.s3.amazonaws.com/2020-06-18/config/" \
6
+ "#{project_id}/config.json"
7
+ end
8
+
9
+ let(:body) do
10
+ {
11
+ 'poll_sec' => 1,
12
+ 'settings' => [
13
+ {
14
+ 'name' => 'apm',
15
+ 'enabled' => false,
16
+ },
17
+ {
18
+ 'name' => 'errors',
19
+ 'enabled' => true,
20
+ },
21
+ ],
22
+ }
23
+ end
24
+
25
+ let(:config_path) { described_class::CONFIG_DUMP_PATH }
26
+ let(:config_dir) { File.dirname(config_path) }
27
+
28
+ before do
29
+ stub_request(:get, endpoint).to_return(status: 200, body: body.to_json)
30
+
31
+ # Do not create config dumps on disk.
32
+ allow(Dir).to receive(:mkdir).with(config_dir)
33
+ allow(File).to receive(:write).with(config_path, anything)
34
+ end
35
+
36
+ describe ".poll" do
37
+ describe "config loading" do
38
+ let(:settings_data) { described_class::SettingsData.new(project_id, body) }
39
+
40
+ before do
41
+ allow(File).to receive(:exist?).with(config_path).and_return(true)
42
+ allow(File).to receive(:read).with(config_path).and_return(body.to_json)
43
+
44
+ allow(described_class::SettingsData).to receive(:new).and_return(settings_data)
45
+ end
46
+
47
+ it "loads the config from disk" do
48
+ expect(File).to receive(:read).with(config_path)
49
+ expect(settings_data).to receive(:merge!).with(body).twice
50
+
51
+ remote_settings = described_class.poll(project_id) {}
52
+ sleep(0.2)
53
+ remote_settings.stop_polling
54
+
55
+ expect(a_request(:get, endpoint)).to have_been_made.once
56
+ end
57
+
58
+ it "yields the config to the block twice" do
59
+ block = proc {}
60
+ expect(block).to receive(:call).twice
61
+
62
+ remote_settings = described_class.poll(project_id, &block)
63
+ sleep(0.2)
64
+ remote_settings.stop_polling
65
+
66
+ expect(a_request(:get, endpoint)).to have_been_made.once
67
+ end
68
+
69
+ context "when config loading fails" do
70
+ it "logs an error" do
71
+ expect(File).to receive(:read).and_raise(StandardError)
72
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
73
+ '**Airbrake: config loading failed: StandardError',
74
+ )
75
+
76
+ remote_settings = described_class.poll(project_id) {}
77
+ sleep(0.2)
78
+ remote_settings.stop_polling
79
+
80
+ expect(a_request(:get, endpoint)).to have_been_made.once
81
+ end
82
+ end
83
+ end
84
+
85
+ context "when no errors are raised" do
86
+ it "makes a request to AWS S3" do
87
+ remote_settings = described_class.poll(project_id) {}
88
+ sleep(0.1)
89
+ remote_settings.stop_polling
90
+
91
+ expect(a_request(:get, endpoint)).to have_been_made.at_least_once
92
+ end
93
+
94
+ it "fetches remote settings" do
95
+ settings = nil
96
+ remote_settings = described_class.poll(project_id) do |data|
97
+ settings = data
98
+ end
99
+ sleep(0.1)
100
+ remote_settings.stop_polling
101
+
102
+ expect(settings.error_notifications?).to eq(true)
103
+ expect(settings.performance_stats?).to eq(false)
104
+ expect(settings.interval).to eq(1)
105
+ end
106
+ end
107
+
108
+ context "when an error is raised while making a HTTP request" do
109
+ before do
110
+ allow(Net::HTTP).to receive(:get).and_raise(StandardError)
111
+ end
112
+
113
+ it "doesn't fetch remote settings" do
114
+ settings = nil
115
+ remote_settings = described_class.poll(project_id) do |data|
116
+ settings = data
117
+ end
118
+ sleep(0.1)
119
+ remote_settings.stop_polling
120
+
121
+ expect(a_request(:get, endpoint)).not_to have_been_made
122
+ expect(settings.interval).to eq(600)
123
+ end
124
+ end
125
+
126
+ context "when an error is raised while parsing returned JSON" do
127
+ before do
128
+ allow(JSON).to receive(:parse).and_raise(JSON::ParserError)
129
+ end
130
+
131
+ it "doesn't update settings data" do
132
+ settings = nil
133
+ remote_settings = described_class.poll(project_id) do |data|
134
+ settings = data
135
+ end
136
+ sleep(0.1)
137
+ remote_settings.stop_polling
138
+
139
+ expect(a_request(:get, endpoint)).to have_been_made.once
140
+ expect(settings.interval).to eq(600)
141
+ end
142
+ end
143
+
144
+ context "when API returns an XML response" do
145
+ before do
146
+ stub_request(:get, endpoint).to_return(status: 200, body: '<?xml ...')
147
+ end
148
+
149
+ it "doesn't update settings data" do
150
+ settings = nil
151
+ remote_settings = described_class.poll(project_id) do |data|
152
+ settings = data
153
+ end
154
+ sleep(0.1)
155
+ remote_settings.stop_polling
156
+
157
+ expect(a_request(:get, endpoint)).to have_been_made.once
158
+ expect(settings.interval).to eq(600)
159
+ end
160
+ end
161
+
162
+ context "when a config route is specified in the returned data" do
163
+ let(:new_endpoint) { 'http://example.com' }
164
+
165
+ let(:body) do
166
+ { 'config_route' => new_endpoint, 'poll_sec' => 0.1 }
167
+ end
168
+
169
+ before do
170
+ stub_request(:get, new_endpoint).to_return(status: 200, body: body.to_json)
171
+ end
172
+
173
+ it "makes the next request to the specified config route" do
174
+ remote_settings = described_class.poll(project_id) {}
175
+ sleep(0.2)
176
+
177
+ remote_settings.stop_polling
178
+
179
+ expect(a_request(:get, endpoint)).to have_been_made.once
180
+ expect(a_request(:get, new_endpoint)).to have_been_made.once
181
+ end
182
+ end
183
+ end
184
+
185
+ describe "#stop_polling" do
186
+ it "dumps config data to disk" do
187
+ expect(Dir).to receive(:mkdir).with(config_dir)
188
+ expect(File).to receive(:write).with(config_path, body.to_json)
189
+
190
+ remote_settings = described_class.poll(project_id) {}
191
+ sleep(0.2)
192
+ remote_settings.stop_polling
193
+
194
+ expect(a_request(:get, endpoint)).to have_been_made.once
195
+ end
196
+
197
+ context "when config dumping fails" do
198
+ it "logs an error" do
199
+ expect(File).to receive(:write).and_raise(StandardError)
200
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
201
+ '**Airbrake: config dumping failed: StandardError',
202
+ )
203
+
204
+ remote_settings = described_class.poll(project_id) {}
205
+ sleep(0.2)
206
+ remote_settings.stop_polling
207
+
208
+ expect(a_request(:get, endpoint)).to have_been_made.once
209
+ end
210
+ end
211
+ end
212
+ end