airbrake-ruby 4.13.4 → 5.0.0.rc.2

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
@@ -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://v1-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,230 @@
1
+ RSpec.describe Airbrake::RemoteSettings do
2
+ let(:project_id) { 123 }
3
+
4
+ let(:endpoint) do
5
+ "https://v1-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
+ let!(:stub) do
29
+ stub_request(:get, Regexp.new(endpoint))
30
+ .to_return(status: 200, body: body.to_json)
31
+ end
32
+
33
+ before do
34
+ # Do not create config dumps on disk.
35
+ allow(Dir).to receive(:mkdir).with(config_dir)
36
+ allow(File).to receive(:write).with(config_path, anything)
37
+ end
38
+
39
+ describe ".poll" do
40
+ describe "config loading" do
41
+ let(:settings_data) { described_class::SettingsData.new(project_id, body) }
42
+
43
+ before do
44
+ allow(File).to receive(:exist?).with(config_path).and_return(true)
45
+ allow(File).to receive(:read).with(config_path).and_return(body.to_json)
46
+
47
+ allow(described_class::SettingsData).to receive(:new).and_return(settings_data)
48
+ end
49
+
50
+ it "loads the config from disk" do
51
+ expect(File).to receive(:read).with(config_path)
52
+ expect(settings_data).to receive(:merge!).with(body).twice
53
+
54
+ remote_settings = described_class.poll(project_id) {}
55
+ sleep(0.2)
56
+ remote_settings.stop_polling
57
+
58
+ expect(stub).to have_been_requested.once
59
+ end
60
+
61
+ it "yields the config to the block twice" do
62
+ block = proc {}
63
+ expect(block).to receive(:call).twice
64
+
65
+ remote_settings = described_class.poll(project_id, &block)
66
+ sleep(0.2)
67
+ remote_settings.stop_polling
68
+
69
+ expect(stub).to have_been_requested.once
70
+ end
71
+
72
+ context "when config loading fails" do
73
+ it "logs an error" do
74
+ expect(File).to receive(:read).and_raise(StandardError)
75
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
76
+ '**Airbrake: config loading failed: StandardError',
77
+ )
78
+
79
+ remote_settings = described_class.poll(project_id) {}
80
+ sleep(0.2)
81
+ remote_settings.stop_polling
82
+
83
+ expect(stub).to have_been_requested.once
84
+ end
85
+ end
86
+ end
87
+
88
+ context "when no errors are raised" do
89
+ it "makes a request to AWS S3" do
90
+ remote_settings = described_class.poll(project_id) {}
91
+ sleep(0.1)
92
+ remote_settings.stop_polling
93
+
94
+ expect(stub).to have_been_requested.at_least_once
95
+ end
96
+
97
+ it "sends params about the environment with the request" do
98
+ remote_settings = described_class.poll(project_id) {}
99
+ sleep(0.1)
100
+ remote_settings.stop_polling
101
+
102
+ stub_with_query_params = stub.with(
103
+ query: URI.decode_www_form(described_class::QUERY_PARAMS).to_h,
104
+ )
105
+ expect(stub_with_query_params).to have_been_requested.at_least_once
106
+ end
107
+
108
+ it "fetches remote settings" do
109
+ settings = nil
110
+ remote_settings = described_class.poll(project_id) do |data|
111
+ settings = data
112
+ end
113
+ sleep(0.1)
114
+ remote_settings.stop_polling
115
+
116
+ expect(settings.error_notifications?).to eq(true)
117
+ expect(settings.performance_stats?).to eq(false)
118
+ expect(settings.interval).to eq(1)
119
+ end
120
+ end
121
+
122
+ context "when an error is raised while making a HTTP request" do
123
+ before do
124
+ allow(Net::HTTP).to receive(:get).and_raise(StandardError)
125
+ end
126
+
127
+ it "doesn't fetch remote settings" do
128
+ settings = nil
129
+ remote_settings = described_class.poll(project_id) do |data|
130
+ settings = data
131
+ end
132
+ sleep(0.1)
133
+ remote_settings.stop_polling
134
+
135
+ expect(stub).not_to have_been_requested
136
+ expect(settings.interval).to eq(600)
137
+ end
138
+ end
139
+
140
+ context "when an error is raised while parsing returned JSON" do
141
+ before do
142
+ allow(JSON).to receive(:parse).and_raise(JSON::ParserError)
143
+ end
144
+
145
+ it "doesn't update settings data" do
146
+ settings = nil
147
+ remote_settings = described_class.poll(project_id) do |data|
148
+ settings = data
149
+ end
150
+ sleep(0.1)
151
+ remote_settings.stop_polling
152
+
153
+ expect(stub).to have_been_requested.once
154
+ expect(settings.interval).to eq(600)
155
+ end
156
+ end
157
+
158
+ context "when API returns an XML response" do
159
+ let!(:stub) do
160
+ stub_request(:get, Regexp.new(endpoint))
161
+ .to_return(status: 200, body: '<?xml ...')
162
+ end
163
+
164
+ it "doesn't update settings data" do
165
+ settings = nil
166
+ remote_settings = described_class.poll(project_id) do |data|
167
+ settings = data
168
+ end
169
+ sleep(0.1)
170
+ remote_settings.stop_polling
171
+
172
+ expect(stub).to have_been_requested.once
173
+ expect(settings.interval).to eq(600)
174
+ end
175
+ end
176
+
177
+ context "when a config route is specified in the returned data" do
178
+ let(:new_endpoint) do
179
+ "http://example.com"
180
+ end
181
+
182
+ let(:body) do
183
+ { 'config_route' => new_endpoint, 'poll_sec' => 0.1 }
184
+ end
185
+
186
+ let!(:new_stub) do
187
+ stub_request(:get, Regexp.new(new_endpoint))
188
+ .to_return(status: 200, body: body.to_json)
189
+ end
190
+
191
+ it "makes the next request to the specified config route" do
192
+ remote_settings = described_class.poll(project_id) {}
193
+ sleep(0.2)
194
+
195
+ remote_settings.stop_polling
196
+
197
+ expect(stub).to have_been_requested.once
198
+ expect(new_stub).to have_been_requested.once
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "#stop_polling" do
204
+ it "dumps config data to disk" do
205
+ expect(Dir).to receive(:mkdir).with(config_dir)
206
+ expect(File).to receive(:write).with(config_path, body.to_json)
207
+
208
+ remote_settings = described_class.poll(project_id) {}
209
+ sleep(0.2)
210
+ remote_settings.stop_polling
211
+
212
+ expect(stub).to have_been_requested.once
213
+ end
214
+
215
+ context "when config dumping fails" do
216
+ it "logs an error" do
217
+ expect(File).to receive(:write).and_raise(StandardError)
218
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
219
+ '**Airbrake: config dumping failed: StandardError',
220
+ )
221
+
222
+ remote_settings = described_class.poll(project_id) {}
223
+ sleep(0.2)
224
+ remote_settings.stop_polling
225
+
226
+ expect(stub).to have_been_requested.once
227
+ end
228
+ end
229
+ end
230
+ end