exception_handling 2.17.0.pre.tstarck.1 → 3.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -3
  3. data/.ruby-version +1 -1
  4. data/Gemfile +16 -12
  5. data/Gemfile.lock +138 -153
  6. data/README.md +21 -90
  7. data/Rakefile +11 -8
  8. data/exception_handling.gemspec +10 -14
  9. data/lib/exception_handling/exception_info.rb +11 -15
  10. data/lib/exception_handling/honeybadger_callbacks.rb +59 -0
  11. data/lib/exception_handling/log_stub_error.rb +1 -2
  12. data/lib/exception_handling/methods.rb +53 -6
  13. data/lib/exception_handling/testing.rb +10 -20
  14. data/lib/exception_handling/version.rb +1 -1
  15. data/lib/exception_handling.rb +34 -135
  16. data/semaphore_ci/setup.sh +3 -0
  17. data/{spec → test}/helpers/exception_helpers.rb +2 -2
  18. data/{spec/spec_helper.rb → test/test_helper.rb} +45 -75
  19. data/test/unit/exception_handling/exception_catalog_test.rb +85 -0
  20. data/test/unit/exception_handling/exception_description_test.rb +82 -0
  21. data/{spec/unit/exception_handling/exception_info_spec.rb → test/unit/exception_handling/exception_info_test.rb} +114 -170
  22. data/test/unit/exception_handling/honeybadger_callbacks_test.rb +122 -0
  23. data/{spec/unit/exception_handling/log_error_stub_spec.rb → test/unit/exception_handling/log_error_stub_test.rb} +22 -38
  24. data/{spec/unit/exception_handling/mailer_spec.rb → test/unit/exception_handling/mailer_test.rb} +18 -17
  25. data/test/unit/exception_handling/methods_test.rb +84 -0
  26. data/test/unit/exception_handling/sensu_test.rb +52 -0
  27. data/test/unit/exception_handling_test.rb +1109 -0
  28. metadata +59 -99
  29. data/.github/CODEOWNERS +0 -1
  30. data/.github/workflows/pipeline.yml +0 -36
  31. data/.rspec +0 -3
  32. data/.tool-versions +0 -1
  33. data/Appraisals +0 -19
  34. data/CHANGELOG.md +0 -149
  35. data/gemfiles/rails_5.gemfile +0 -18
  36. data/gemfiles/rails_6.gemfile +0 -18
  37. data/gemfiles/rails_7.gemfile +0 -18
  38. data/lib/exception_handling/escalate_callback.rb +0 -19
  39. data/lib/exception_handling/logging_methods.rb +0 -27
  40. data/spec/rake_test_warning_false.rb +0 -20
  41. data/spec/unit/exception_handling/escalate_callback_spec.rb +0 -81
  42. data/spec/unit/exception_handling/exception_catalog_spec.rb +0 -85
  43. data/spec/unit/exception_handling/exception_description_spec.rb +0 -82
  44. data/spec/unit/exception_handling/logging_methods_spec.rb +0 -38
  45. data/spec/unit/exception_handling/methods_spec.rb +0 -105
  46. data/spec/unit/exception_handling/sensu_spec.rb +0 -51
  47. data/spec/unit/exception_handling_spec.rb +0 -1465
  48. /data/{spec → test}/helpers/controller_helpers.rb +0 -0
@@ -1,1465 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require File.expand_path('../spec_helper', __dir__)
4
- require_test_helper 'controller_helpers'
5
- require_test_helper 'exception_helpers'
6
-
7
- describe ExceptionHandling do
8
- include ControllerHelpers
9
- include ExceptionHelpers
10
-
11
- before do
12
- @fail_count = 0
13
- end
14
-
15
- def dont_stub_log_error
16
- true
17
- end
18
-
19
- def append_organization_info_config(data)
20
- data[:user_details] = {}
21
- data[:user_details][:username] = "CaryP"
22
- data[:user_details][:organization] = "Invoca Engineering Dept."
23
- rescue StandardError
24
- # don't let these out!
25
- end
26
-
27
- def custom_data_callback_returns_nil_message_exception(_data)
28
- raise_exception_with_nil_message
29
- end
30
-
31
- def log_error_callback(_data, _ex, _treat_like_warning, _honeybadger_status)
32
- @fail_count += 1
33
- end
34
-
35
- def log_error_callback_config(data, _ex, treat_like_warning, honeybadger_status)
36
- @callback_data = data
37
- @treat_like_warning = treat_like_warning
38
- @fail_count += 1
39
- @honeybadger_status = honeybadger_status
40
- end
41
-
42
- def log_error_callback_with_failure(_data, _ex)
43
- raise "this should be rescued"
44
- end
45
-
46
- def log_error_callback_returns_nil_message_exception(_data, _ex)
47
- raise_exception_with_nil_message
48
- end
49
-
50
- module EventMachineStub
51
- class << self
52
- attr_accessor :block
53
-
54
- def schedule(&block)
55
- @block = block
56
- end
57
- end
58
- end
59
-
60
- class DNSResolvStub
61
- class << self
62
- attr_accessor :callback_block
63
- attr_accessor :errback_block
64
-
65
- def resolve(_hostname)
66
- self
67
- end
68
-
69
- def callback(&block)
70
- @callback_block = block
71
- end
72
-
73
- def errback(&block)
74
- @errback_block = block
75
- end
76
- end
77
- end
78
-
79
- class SmtpClientStub
80
- class << self
81
- attr_reader :block
82
- attr_reader :last_method
83
-
84
- def errback(&block)
85
- @block = block
86
- end
87
-
88
- def send_hash
89
- @send_hash ||= {}
90
- end
91
-
92
- def send(hash)
93
- @last_method = :send
94
- send_hash.clear
95
- send_hash.merge!(hash)
96
- self
97
- end
98
-
99
- def asend(hash)
100
- send(hash)
101
- @last_method = :asend
102
- self
103
- end
104
- end
105
- end
106
-
107
- class SmtpClientErrbackStub < SmtpClientStub
108
- end
109
-
110
- before(:each) do
111
- # Reset this for every test since they are applied to the class
112
- ExceptionHandling.honeybadger_auto_tagger = nil
113
- ExceptionHandling.clear_honeybadger_tags_from_log_context
114
- end
115
-
116
- context "with warn and honeybadger notify stubbed" do
117
- before do
118
- allow(ExceptionHandling).to receive(:warn).with(any_args)
119
- allow(Honeybadger).to receive(:notify).with(any_args)
120
- end
121
-
122
- context "with logger stashed" do
123
- before { @original_logger = ExceptionHandling.logger }
124
- after { ExceptionHandling.logger = @original_logger }
125
-
126
- it "stores logger as-is if it has ContextualLogger::Mixin" do
127
- logger = Logger.new('/dev/null')
128
- logger.extend(ContextualLogger::LoggerMixin)
129
- ancestors = logger.singleton_class.ancestors.*.name
130
-
131
- ExceptionHandling.logger = logger
132
- expect(ExceptionHandling.logger.singleton_class.ancestors.*.name).to eq(ancestors)
133
- end
134
-
135
- it "allows logger = nil (no deprecation warning)" do
136
- expect(STDERR).to receive(:puts).with(/DEPRECATION WARNING/).never
137
- ExceptionHandling.logger = nil
138
- end
139
-
140
- it "[deprecated] mixes in ContextualLogger::Mixin if not there" do
141
- expect(STDERR).to receive(:puts).with(/DEPRECATION WARNING: implicit extend with ContextualLogger::LoggerMixin is deprecated and will be removed from exception_handling 3\.0/)
142
- logger = Logger.new('/dev/null')
143
- ancestors = logger.singleton_class.ancestors.*.name
144
-
145
- ExceptionHandling.logger = logger
146
- expect(ExceptionHandling.logger).to be_kind_of(ContextualLogger::LoggerMixin)
147
- expect(ExceptionHandling.logger.singleton_class.ancestors.*.name).to_not eq(ancestors)
148
- end
149
-
150
- context "#log_error" do
151
- it "takes in additional logging context hash and pass it to the logger" do
152
- ExceptionHandling.log_error('This is an Error', 'This is the prefix context', service_name: 'exception_handling')
153
- expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Error/)
154
- expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
155
- expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
156
- end
157
-
158
- it "passes :honeybadger_tags in log context to honeybadger" do
159
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: " , awesome , totallytubular, " }))
160
- ExceptionHandling.log_error('This is an Error', 'This is the prefix context', honeybadger_tags: ' , awesome , totallytubular, ')
161
-
162
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: " cool neat" }))
163
- ExceptionHandling.log_error('This is an Error', 'This is the prefix context', honeybadger_tags: [" ", " cool ", "neat"])
164
- end
165
-
166
- it "logs with Severity::FATAL" do
167
- ExceptionHandling.log_error('This is a Warning', service_name: 'exception_handling')
168
- expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
169
- end
170
- end
171
- end
172
-
173
- context "#log_warning" do
174
- it "have empty array as a backtrace" do
175
- expect(ExceptionHandling).to receive(:log_error) do |error|
176
- expect(error.class).to eq(ExceptionHandling::Warning)
177
- expect(error.backtrace).to eq([])
178
- end
179
- ExceptionHandling.log_warning('Now with empty array as a backtrace!')
180
- end
181
-
182
- it "take in additional key word args as logging context and pass them to the logger" do
183
- ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
184
- expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Warning/)
185
- expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
186
- expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
187
- end
188
-
189
- it "log with Severity::WARN" do
190
- ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
191
- expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
192
- end
193
- end
194
-
195
- context "#log_info" do
196
- it "take in additional key word args as logging context and pass them to the logger" do
197
- ExceptionHandling.log_info('This is an Info', service_name: 'exception_handling')
198
- expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Info/)
199
- expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
200
- expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
201
- end
202
-
203
- it "log with Severity::INFO" do
204
- ExceptionHandling.log_info('This is a Warning', service_name: 'exception_handling')
205
- expect('INFO').to eq(logged_excluding_reload_filter.last[:severity])
206
- end
207
- end
208
-
209
- context "#log_debug" do
210
- it "take in additional key word args as logging context and pass them to the logger" do
211
- ExceptionHandling.log_debug('This is a Debug', service_name: 'exception_handling')
212
- expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Debug/)
213
- expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
214
- expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
215
- end
216
-
217
- it "log with Severity::DEBUG" do
218
- ExceptionHandling.log_debug('This is a Warning', service_name: 'exception_handling')
219
- expect('DEBUG').to eq(logged_excluding_reload_filter.last[:severity])
220
- end
221
- end
222
-
223
- context "#write_exception_to_log" do
224
- it "log warnings with Severity::WARN" do
225
- warning = ExceptionHandling::Warning.new('This is a Warning')
226
- ExceptionHandling.write_exception_to_log(warning, '', Time.now.to_i, service_name: 'exception_handling')
227
- expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
228
- end
229
-
230
- it "log everything else with Severity::FATAL" do
231
- error = RuntimeError.new('This is a runtime error')
232
- ExceptionHandling.write_exception_to_log(error, '', Time.now.to_i, service_name: 'exception_handling')
233
- expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
234
- end
235
- end
236
-
237
- context "configuration with custom_data_hook or post_log_error_hook" do
238
- after do
239
- ExceptionHandling.custom_data_hook = nil
240
- ExceptionHandling.post_log_error_hook = nil
241
- end
242
-
243
- it "support a custom_data_hook" do
244
- capture_notifications
245
-
246
- ExceptionHandling.custom_data_hook = method(:append_organization_info_config)
247
- ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
248
-
249
- expect(sent_notifications.last.enhanced_data['user_details'].to_s).to match(/Invoca Engineering Dept./)
250
- end
251
-
252
- it "support a log_error hook, and pass exception_data, treat_like_warning, and logged_to_honeybadger to it" do
253
- @honeybadger_status = nil
254
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
255
-
256
- notify_args = []
257
- expect(Honeybadger).to receive(:notify).with(any_args) { |info| notify_args << info; '06220c5a-b471-41e5-baeb-de247da45a56' }
258
- ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
259
- expect(@fail_count).to eq(1)
260
- expect(@treat_like_warning).to eq(false)
261
- expect(@honeybadger_status).to eq(:success)
262
-
263
- expect(@callback_data["notes"]).to eq("this is used by a test")
264
- expect(notify_args.size).to eq(1), notify_args.inspect
265
- expect(notify_args.last[:context].to_s).to match(/this is used by a test/)
266
- end
267
-
268
- it "plumb treat_like_warning and logged_to_honeybadger to log error hook" do
269
- @honeybadger_status = nil
270
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
271
- ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true)
272
- expect(@fail_count).to eq(1)
273
- expect(@treat_like_warning).to eq(true)
274
- expect(@honeybadger_status).to eq(:skipped)
275
- end
276
-
277
- it "include logging context in the exception data" do
278
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
279
- ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true, log_context_test: "contextual_logging")
280
-
281
- expected_log_context = {
282
- "log_context_test" => "contextual_logging"
283
- }
284
- expect(@callback_data[:log_context]).to eq(expected_log_context)
285
- end
286
-
287
- it "support rescue exceptions from a log_error hook" do
288
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_with_failure)
289
- log_info_messages = []
290
- allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
291
- log_info_messages << message
292
- end
293
- expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
294
- expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
295
- end
296
-
297
- it "handle nil message exceptions resulting from the log_error hook" do
298
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_returns_nil_message_exception)
299
- log_info_messages = []
300
- allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
301
- log_info_messages << message
302
- end
303
- expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
304
- expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
305
- end
306
-
307
- it "handle nil message exceptions resulting from the custom data hook" do
308
- ExceptionHandling.custom_data_hook = method(:custom_data_callback_returns_nil_message_exception)
309
- log_info_messages = []
310
- allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
311
- log_info_messages << message
312
- end
313
- expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.not_to raise_error
314
- expect(log_info_messages.find { |message| message =~ /Unable to execute custom custom_data_hook callback/ }).to be_truthy
315
- end
316
- end
317
-
318
- context "Exception Handling" do
319
- context "default_metric_name" do
320
- context "with include_prefix true" do
321
- it "logs a deprecation warning" do
322
- expect { ExceptionHandling.default_metric_name({}, StandardError.new('this is an exception'), false) }
323
- .to output(/DEPRECATION WARNING: the 'expection_handling\.' prefix in ExceptionHandling::default_metric_name is deprecated/).to_stderr
324
- end
325
-
326
- context "when metric_name is present in exception_data" do
327
- it "include metric_name in resulting metric name" do
328
- exception = StandardError.new('this is an exception')
329
- metric = ExceptionHandling.default_metric_name({ 'metric_name' => 'special_metric' }, exception, true, include_prefix: true)
330
- expect(metric).to eq('exception_handling.special_metric')
331
- end
332
- end
333
-
334
- context "when metric_name is not present in exception_data" do
335
- it "return exception_handling.warning when using log warning" do
336
- warning = ExceptionHandling::Warning.new('this is a warning')
337
- metric = ExceptionHandling.default_metric_name({}, warning, false, include_prefix: true)
338
- expect(metric).to eq('exception_handling.warning')
339
- end
340
-
341
- it "return exception_handling.exception when using log error" do
342
- exception = StandardError.new('this is an exception')
343
- metric = ExceptionHandling.default_metric_name({}, exception, false, include_prefix: true)
344
- expect(metric).to eq('exception_handling.exception')
345
- end
346
-
347
- context "when using log error with treat_like_warning" do
348
- it "return exception_handling.unforwarded_exception when exception not present" do
349
- metric = ExceptionHandling.default_metric_name({}, nil, true, include_prefix: true)
350
- expect(metric).to eq('exception_handling.unforwarded_exception')
351
- end
352
-
353
- it "return exception_handling.unforwarded_exception with exception classname when exception is present" do
354
- module SomeModule
355
- class SomeException < StandardError
356
- end
357
- end
358
-
359
- exception = SomeModule::SomeException.new('this is an exception')
360
- metric = ExceptionHandling.default_metric_name({}, exception, true, include_prefix: true)
361
- expect(metric).to eq('exception_handling.unforwarded_exception_SomeException')
362
- end
363
- end
364
- end
365
- end
366
-
367
- context "with include_prefix false" do
368
- context "when metric_name is present in exception_data" do
369
- it "include metric_name in resulting metric name" do
370
- exception = StandardError.new('this is an exception')
371
- metric = ExceptionHandling.default_metric_name({ 'metric_name' => 'special_metric' }, exception, true, include_prefix: false)
372
- expect(metric).to eq('special_metric')
373
- end
374
- end
375
-
376
- context "when metric_name is not present in exception_data" do
377
- it "return exception_handling.warning when using log warning" do
378
- warning = ExceptionHandling::Warning.new('this is a warning')
379
- metric = ExceptionHandling.default_metric_name({}, warning, false, include_prefix: false)
380
- expect(metric).to eq('warning')
381
- end
382
-
383
- it "return exception_handling.exception when using log error" do
384
- exception = StandardError.new('this is an exception')
385
- metric = ExceptionHandling.default_metric_name({}, exception, false, include_prefix: false)
386
- expect(metric).to eq('exception')
387
- end
388
-
389
- context "when using log error with treat_like_warning" do
390
- it "return exception_handling.unforwarded_exception when exception not present" do
391
- metric = ExceptionHandling.default_metric_name({}, nil, true, include_prefix: false)
392
- expect(metric).to eq('unforwarded_exception')
393
- end
394
-
395
- it "return exception_handling.unforwarded_exception with exception classname when exception is present" do
396
- module SomeModule
397
- class SomeException < StandardError
398
- end
399
- end
400
-
401
- exception = SomeModule::SomeException.new('this is an exception')
402
- metric = ExceptionHandling.default_metric_name({}, exception, true, include_prefix: false)
403
- expect(metric).to eq('unforwarded_exception_SomeException')
404
- end
405
- end
406
- end
407
- end
408
- end
409
-
410
- context "default_honeybadger_metric_name" do
411
- it "return exception_handling.honeybadger.success when status is :success" do
412
- metric = ExceptionHandling.default_honeybadger_metric_name(:success)
413
- expect(metric).to eq('exception_handling.honeybadger.success')
414
- end
415
-
416
- it "return exception_handling.honeybadger.failure when status is :failure" do
417
- metric = ExceptionHandling.default_honeybadger_metric_name(:failure)
418
- expect(metric).to eq('exception_handling.honeybadger.failure')
419
- end
420
-
421
- it "return exception_handling.honeybadger.skipped when status is :skipped" do
422
- metric = ExceptionHandling.default_honeybadger_metric_name(:skipped)
423
- expect(metric).to eq('exception_handling.honeybadger.skipped')
424
- end
425
-
426
- it "return exception_handling.honeybadger.unknown_status when status is not recognized" do
427
- metric = ExceptionHandling.default_honeybadger_metric_name(nil)
428
- expect(metric).to eq('exception_handling.honeybadger.unknown_status')
429
- end
430
- end
431
-
432
- context "ExceptionHandling.ensure_safe" do
433
- it "log an exception with call stack if an exception is raised." do
434
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
435
- ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
436
- end
437
-
438
- if ActionView::VERSION::MAJOR >= 5
439
- it "log an exception with call stack if an ActionView template exception is raised." do
440
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:\d+\) \nActionView::Template::Error: \(blah\):\n /, any_args)
441
- ExceptionHandling.ensure_safe do
442
- begin
443
- # Rails 5 made the switch from ActionView::TemplateError taking in the original exception
444
- # as an argument to using the $! global to extract the original exception
445
- raise ArgumentError, "blah"
446
- rescue
447
- raise ActionView::TemplateError.new({})
448
- end
449
- end
450
- end
451
- else
452
- it "log an exception with call stack if an ActionView template exception is raised." do
453
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:\d+\) \nActionView::Template::Error: \(blah\):\n /, any_args)
454
- ExceptionHandling.ensure_safe { raise ActionView::TemplateError.new({}, ArgumentError.new("blah")) }
455
- end
456
- end
457
-
458
- it "should not log an exception if an exception is not raised." do
459
- expect(ExceptionHandling.logger).to_not receive(:fatal)
460
- ExceptionHandling.ensure_safe { ; }
461
- end
462
-
463
- it "return its value if used during an assignment" do
464
- expect(ExceptionHandling.logger).to_not receive(:fatal)
465
- b = ExceptionHandling.ensure_safe { 5 }
466
- expect(b).to eq(5)
467
- end
468
-
469
- it "return nil if an exception is raised during an assignment" do
470
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
471
- b = ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
472
- expect(b).to be_nil
473
- end
474
-
475
- it "allow a message to be appended to the error when logged." do
476
- expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
477
- b = ExceptionHandling.ensure_safe("mooo") { raise ArgumentError, "blah" }
478
- expect(b).to be_nil
479
- end
480
-
481
- it "only rescue StandardError and descendents" do
482
- expect { ExceptionHandling.ensure_safe("mooo") { raise Exception } }.to raise_exception(Exception)
483
-
484
- expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nStandardError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
485
-
486
- b = ExceptionHandling.ensure_safe("mooo") { raise StandardError, "blah" }
487
- expect(b).to be_nil
488
- end
489
- end
490
-
491
- context "ExceptionHandling.ensure_completely_safe" do
492
- it "log an exception if an exception is raised." do
493
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
494
- ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
495
- end
496
-
497
- it "should not log an exception if an exception is not raised." do
498
- expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
499
- ExceptionHandling.ensure_completely_safe { ; }
500
- end
501
-
502
- it "return its value if used during an assignment" do
503
- expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
504
- b = ExceptionHandling.ensure_completely_safe { 5 }
505
- expect(b).to eq(5)
506
- end
507
-
508
- it "return nil if an exception is raised during an assignment" do
509
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args) { nil }
510
- b = ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
511
- expect(b).to be_nil
512
- end
513
-
514
- it "allow a message to be appended to the error when logged." do
515
- expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
516
- b = ExceptionHandling.ensure_completely_safe("mooo") { raise ArgumentError, "blah" }
517
- expect(b).to be_nil
518
- end
519
-
520
- it "rescue any instance or child of Exception" do
521
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
522
- ExceptionHandling.ensure_completely_safe { raise Exception, "blah" }
523
- end
524
-
525
- it "not rescue the special exceptions that Ruby uses" do
526
- [SystemExit, SystemStackError, NoMemoryError, SecurityError].each do |exception|
527
- expect do
528
- ExceptionHandling.ensure_completely_safe do
529
- raise exception
530
- end
531
- end.to raise_exception(exception)
532
- end
533
- end
534
- end
535
-
536
- context "ExceptionHandling.ensure_escalation" do
537
- before do
538
- capture_notifications
539
- ActionMailer::Base.deliveries.clear
540
- end
541
-
542
- it "log the exception as usual and send the proper email" do
543
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
544
- ExceptionHandling.ensure_escalation("Favorite Feature") { raise ArgumentError, "blah" }
545
- expect(ActionMailer::Base.deliveries.count).to eq(1)
546
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
547
-
548
- email = ActionMailer::Base.deliveries.last
549
- expect(email.subject).to eq("#{ExceptionHandling.email_environment} Escalation: Favorite Feature")
550
- expect(email.body.to_s).to match('ArgumentError: blah')
551
- expect(email.body.to_s).to match(ExceptionHandling.last_exception_timestamp.to_s)
552
- end
553
-
554
- it "should not escalate if an exception is not raised." do
555
- expect(ExceptionHandling.logger).to_not receive(:fatal)
556
- ExceptionHandling.ensure_escalation('Ignored') { ; }
557
- expect(ActionMailer::Base.deliveries.count).to eq(0)
558
- end
559
-
560
- it "log if the escalation email cannot be sent" do
561
- expect_any_instance_of(Mail::Message).to receive(:deliver).and_raise(RuntimeError.new, "Delivery Error")
562
- log_fatals = []
563
- expect(ExceptionHandling.logger).to receive(:fatal).with(any_args).at_least(:once) do |*args|
564
- log_fatals << args
565
- end
566
-
567
- ExceptionHandling.ensure_escalation("ensure context") { raise ArgumentError, "first_test_exception" }
568
- expect(log_fatals[0].first).to match(/ArgumentError.*first_test_exception/)
569
- expect(log_fatals[1].first).to match(/safe_email_deliver.*Delivery Error/m)
570
-
571
- expect(log_fatals.size).to eq(2), log_fatals.inspect
572
-
573
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect # still sent to honeybadger
574
- end
575
-
576
- it "allow the caller to specify custom recipients" do
577
- custom_recipients = ['something@invoca.com']
578
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
579
- ExceptionHandling.ensure_escalation("Favorite Feature", custom_recipients) { raise ArgumentError, "blah" }
580
- expect(ActionMailer::Base.deliveries.count).to eq(1)
581
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
582
-
583
- email = ActionMailer::Base.deliveries.last
584
- expect(email.subject).to eq("#{ExceptionHandling.email_environment} Escalation: Favorite Feature")
585
- expect(email.body.to_s).to match('ArgumentError: blah')
586
- expect(email.body.to_s).to match(ExceptionHandling.last_exception_timestamp.to_s)
587
- expect(email.to).to eq(custom_recipients)
588
- end
589
- end
590
-
591
- context "ExceptionHandling.ensure_alert" do
592
- it "log the exception as usual and fire a sensu event" do
593
- expect(ExceptionHandling::Sensu).to receive(:generate_event).with("Favorite Feature", "test context\nblah")
594
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
595
- ExceptionHandling.ensure_alert('Favorite Feature', 'test context') { raise ArgumentError, "blah" }
596
- end
597
-
598
- it "should not send sensu event if an exception is not raised." do
599
- expect(ExceptionHandling.logger).to_not receive(:fatal)
600
- expect(ExceptionHandling::Sensu).to_not receive(:generate_event)
601
- ExceptionHandling.ensure_alert('Ignored', 'test context') { ; }
602
- end
603
-
604
- it "log if the sensu event could not be sent" do
605
- expect(ExceptionHandling::Sensu).to receive(:send_event).with(anything) { raise "Failed to send" }
606
- expect(ExceptionHandling.logger).to receive(:fatal).with(/first_test_exception/, any_args)
607
- expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to send/, any_args)
608
- ExceptionHandling.ensure_alert("Not Used", 'test context') { raise ArgumentError, "first_test_exception" }
609
- end
610
-
611
- it "log if the exception message is nil" do
612
- expect(ExceptionHandling::Sensu).to receive(:generate_event).with("some alert", "test context\n")
613
- ExceptionHandling.ensure_alert('some alert', 'test context') { raise_exception_with_nil_message }
614
- end
615
- end
616
-
617
- context "ExceptionHandling.escalate_to_production_support" do
618
- it "notify production support" do
619
- subject = "Runtime Error found!"
620
- exception = RuntimeError.new("Test")
621
- recipients = ["prodsupport@example.com"]
622
-
623
- expect(ExceptionHandling).to receive(:production_support_recipients).and_return(recipients).exactly(2)
624
- expect(ExceptionHandling).to receive(:escalate).with(subject, exception, ExceptionHandling.last_exception_timestamp, recipients)
625
- ExceptionHandling.escalate_to_production_support(exception, subject)
626
- end
627
- end
628
-
629
- context "exception timestamp" do
630
- before do
631
- Time.now_override = Time.parse('1986-5-21 4:17 am UTC')
632
- end
633
-
634
- it "include the timestamp when the exception is logged" do
635
- capture_notifications
636
-
637
- expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:517033020\) context\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
638
- b = ExceptionHandling.ensure_safe("context") { raise ArgumentError, "blah" }
639
- expect(b).to be_nil
640
-
641
- expect(ExceptionHandling.last_exception_timestamp).to eq(517_033_020)
642
-
643
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
644
-
645
- expect(sent_notifications.last.enhanced_data['timestamp']).to eq(517_033_020)
646
- end
647
- end
648
-
649
- it "log the error if the exception message is nil" do
650
- capture_notifications
651
-
652
- ExceptionHandling.log_error(exception_with_nil_message)
653
-
654
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
655
- expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
656
- end
657
-
658
- it "log the error if the exception message is nil and the exception context is a hash" do
659
- capture_notifications
660
-
661
- ExceptionHandling.log_error(exception_with_nil_message, "SERVER_NAME" => "exceptional.com")
662
-
663
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
664
- expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
665
- end
666
-
667
- context "Honeybadger integration" do
668
- context "with Honeybadger not defined" do
669
- before do
670
- allow(ExceptionHandling).to receive(:honeybadger_defined?) { false }
671
- end
672
-
673
- it "not invoke send_exception_to_honeybadger when log_error is executed" do
674
- expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
675
- ExceptionHandling.log_error(exception_1)
676
- end
677
-
678
- it "not invoke send_exception_to_honeybadger when ensure_safe is executed" do
679
- expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
680
- ExceptionHandling.ensure_safe { raise exception_1 }
681
- end
682
- end
683
-
684
- context "with Honeybadger defined" do
685
- it "not send_exception_to_honeybadger when log_warning is executed" do
686
- expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
687
- ExceptionHandling.log_warning("This should not go to honeybadger")
688
- end
689
-
690
- it "not send_exception_to_honeybadger when log_error is called with a Warning" do
691
- expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
692
- ExceptionHandling.log_error(ExceptionHandling::Warning.new("This should not go to honeybadger"))
693
- end
694
-
695
- it "invoke send_exception_to_honeybadger when log_error is executed" do
696
- expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
697
- ExceptionHandling.log_error(exception_1)
698
- end
699
-
700
- it "invoke send_exception_to_honeybadger when log_error_rack is executed" do
701
- expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
702
- ExceptionHandling.log_error_rack(exception_1, {}, nil)
703
- end
704
-
705
- it "invoke send_exception_to_honeybadger when ensure_safe is executed" do
706
- expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
707
- ExceptionHandling.ensure_safe { raise exception_1 }
708
- end
709
-
710
- it "specify error message as an empty string when notifying honeybadger if exception message is nil" do
711
- expect(Honeybadger).to receive(:notify).with(any_args) do |args|
712
- expect(args[:error_message]).to eq("")
713
- end
714
- ExceptionHandling.log_error(exception_with_nil_message)
715
- end
716
-
717
- context "with stubbed values" do
718
- before do
719
- Time.now_override = Time.now
720
- @env = { server: "fe98" }
721
- @parameters = { advertiser_id: 435, controller: "some_controller" }
722
- @session = { username: "jsmith" }
723
- @request_uri = "host/path"
724
- @controller = create_dummy_controller(@env, @parameters, @session, @request_uri)
725
- allow(ExceptionHandling).to receive(:server_name) { "invoca_fe98" }
726
-
727
- @exception = StandardError.new("Some Exception")
728
- @exception.set_backtrace([
729
- "spec/unit/exception_handling_spec.rb:847:in `exception_1'",
730
- "spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
731
- ])
732
- @exception_context = { "SERVER_NAME" => "exceptional.com" }
733
- end
734
-
735
- it "send error details and relevant context data to Honeybadger with log_context" do
736
- honeybadger_data = nil
737
- expect(Honeybadger).to receive(:notify).with(any_args) do |data|
738
- honeybadger_data = data
739
- end
740
- ExceptionHandling.logger.global_context = { service_name: "rails", region: "AWS-us-east-1", honeybadger_tags: ['Data-Services', 'web'] }
741
- log_context = { log_source: "gem/listen", service_name: "bin/console" }
742
- ExceptionHandling.log_error(@exception, @exception_context, @controller, **log_context) do |data|
743
- data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
744
- data[:user_details] = { username: "jsmith" }
745
- data[:event_response] = "Event successfully received"
746
- data[:other_section] = "This should not be included in the response"
747
- end
748
-
749
- expected_data = {
750
- error_class: :"Test Exception",
751
- error_message: "Some Exception",
752
- controller: "some_controller",
753
- exception: @exception,
754
- tags: "Data-Services web",
755
- context: {
756
- timestamp: Time.now.to_i,
757
- error_class: "StandardError",
758
- server: "invoca_fe98",
759
- exception_context: { "SERVER_NAME" => "exceptional.com" },
760
- scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
761
- notes: "this is used by a test",
762
- user_details: { "username" => "jsmith" },
763
- request: {
764
- "params" => { "advertiser_id" => 435, "controller" => "some_controller" },
765
- "rails_root" => "Rails.root not defined. Is this a test environment?",
766
- "url" => "host/path"
767
- },
768
- session: {
769
- "key" => nil,
770
- "data" => { "username" => "jsmith" }
771
- },
772
- environment: {
773
- "SERVER_NAME" => "exceptional.com"
774
- },
775
- backtrace: [
776
- "spec/unit/exception_handling_spec.rb:847:in `exception_1'",
777
- "spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
778
- ],
779
- event_response: "Event successfully received",
780
- log_context: { "service_name" => "bin/console", "region" => "AWS-us-east-1", "log_source" => "gem/listen", "honeybadger_tags" => ['Data-Services', 'web'] }
781
- }
782
- }
783
- expect(honeybadger_data).to eq(expected_data)
784
- end
785
-
786
- it "send error details and relevant context data to Honeybadger with empty log_context" do
787
- honeybadger_data = nil
788
- expect(Honeybadger).to receive(:notify).with(any_args) do |data|
789
- honeybadger_data = data
790
- end
791
- ExceptionHandling.logger.global_context = {}
792
- log_context = {}
793
- ExceptionHandling.log_error(@exception, @exception_context, @controller, **log_context) do |data|
794
- data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
795
- data[:user_details] = { username: "jsmith" }
796
- data[:event_response] = "Event successfully received"
797
- data[:other_section] = "This should not be included in the response"
798
- end
799
-
800
- expected_data = {
801
- error_class: :"Test Exception",
802
- error_message: "Some Exception",
803
- controller: "some_controller",
804
- exception: @exception,
805
- tags: "",
806
- context: {
807
- timestamp: Time.now.to_i,
808
- error_class: "StandardError",
809
- server: "invoca_fe98",
810
- exception_context: { "SERVER_NAME" => "exceptional.com" },
811
- scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
812
- notes: "this is used by a test",
813
- user_details: { "username" => "jsmith" },
814
- request: {
815
- "params" => { "advertiser_id" => 435, "controller" => "some_controller" },
816
- "rails_root" => "Rails.root not defined. Is this a test environment?",
817
- "url" => "host/path"
818
- },
819
- session: {
820
- "key" => nil,
821
- "data" => { "username" => "jsmith" }
822
- },
823
- environment: {
824
- "SERVER_NAME" => "exceptional.com"
825
- },
826
- backtrace: [
827
- "spec/unit/exception_handling_spec.rb:847:in `exception_1'",
828
- "spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
829
- ],
830
- event_response: "Event successfully received"
831
- }
832
- }
833
- expect(honeybadger_data).to eq(expected_data)
834
- end
835
- end
836
-
837
- context "with post_log_error_hook set" do
838
- after do
839
- ExceptionHandling.post_log_error_hook = nil
840
- end
841
-
842
- it "not send notification to honeybadger when exception description has the flag turned off and call log error callback with logged_to_honeybadger set to nil" do
843
- @honeybadger_status = nil
844
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
845
- filter_list = {
846
- NoHoneybadger: {
847
- error: "suppress Honeybadger notification",
848
- send_to_honeybadger: false
849
- }
850
- }
851
- allow(File).to receive(:mtime) { incrementing_mtime }
852
- expect(YAML).to receive(:load_file).with(any_args) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)
853
-
854
- expect(ExceptionHandling).to receive(:send_exception_to_honeybadger_unless_filtered).with(any_args).exactly(1).and_call_original
855
- expect(Honeybadger).to_not receive(:notify)
856
- ExceptionHandling.log_error(StandardError.new("suppress Honeybadger notification"))
857
- expect(@honeybadger_status).to eq(:skipped)
858
- end
859
-
860
- it "call log error callback with logged_to_honeybadger set to false if an error occurs while attempting to notify honeybadger" do
861
- @honeybadger_status = nil
862
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
863
- expect(Honeybadger).to receive(:notify).with(any_args) { raise "Honeybadger Notification Failure" }
864
- ExceptionHandling.log_error(exception_1)
865
- expect(@honeybadger_status).to eq(:failure)
866
- end
867
-
868
- it "call log error callback with logged_to_honeybadger set to false on unsuccessful honeybadger notification" do
869
- @honeybadger_status = nil
870
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
871
- expect(Honeybadger).to receive(:notify).with(any_args) { false }
872
- ExceptionHandling.log_error(exception_1)
873
- expect(@honeybadger_status).to eq(:failure)
874
- end
875
-
876
- it "call log error callback with logged_to_honeybadger set to true on successful honeybadger notification" do
877
- @honeybadger_status = nil
878
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
879
- expect(Honeybadger).to receive(:notify).with(any_args) { '06220c5a-b471-41e5-baeb-de247da45a56' }
880
- ExceptionHandling.log_error(exception_1)
881
- expect(@honeybadger_status).to eq(:success)
882
- end
883
- end
884
- end
885
- end
886
-
887
- class EventResponse
888
- def to_s
889
- "message from to_s!"
890
- end
891
- end
892
-
893
- it "allow sections to have data with just a to_s method" do
894
- capture_notifications
895
-
896
- ExceptionHandling.log_error("This is my RingSwitch example.") do |data|
897
- data.merge!(event_response: EventResponse.new)
898
- end
899
-
900
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
901
- expect(sent_notifications.last.enhanced_data['event_response'].to_s).to match(/message from to_s!/)
902
- end
903
- end
904
-
905
- it "return the error ID (timestamp)" do
906
- result = ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
907
- expect(result).to eq(ExceptionHandling.last_exception_timestamp)
908
- end
909
-
910
- it "rescue exceptions that happen in log_error" do
911
- allow(ExceptionHandling).to receive(:make_exception) { raise ArgumentError, "Bad argument" }
912
- expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
913
- satisfy { |context| context['ExceptionHandlingError: log_error rescued exception while logging Runtime message'] },
914
- any_args)
915
- ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
916
- end
917
-
918
- it "rescue exceptions that happen when log_error yields" do
919
- expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
920
- satisfy { |context| context['Context message'] },
921
- anything,
922
- any_args)
923
- ExceptionHandling.log_error(ArgumentError.new("Bad argument"), "Context message") { |_data| raise 'Error!!!' }
924
- end
925
-
926
- context "Exception Filtering" do
927
- before do
928
- filter_list = { exception1: { 'error' => "my error message" },
929
- exception2: { 'error' => "some other message", :session => "misc data" } }
930
- allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
931
-
932
- # bump modified time up to get the above filter loaded
933
- allow(File).to receive(:mtime) { incrementing_mtime }
934
- end
935
-
936
- it "handle case where filter list is not found" do
937
- allow(YAML).to receive(:load_file) { raise Errno::ENOENT, "File not found" }
938
-
939
- capture_notifications
940
-
941
- ExceptionHandling.log_error("My error message is in list")
942
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
943
- end
944
-
945
- it "log exception and suppress email when exception is on filter list" do
946
- capture_notifications
947
-
948
- ExceptionHandling.log_error("Error message is not in list")
949
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
950
-
951
- sent_notifications.clear
952
- ExceptionHandling.log_error("My error message is in list")
953
- expect(sent_notifications.size).to eq(0), sent_notifications.inspect
954
- end
955
-
956
- it "allow filtering exception on any text in exception data" do
957
- filters = { exception1: { session: "data: my extra session data" } }
958
- allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filters) }
959
-
960
- capture_notifications
961
-
962
- ExceptionHandling.log_error("No match here") do |data|
963
- data[:session] = {
964
- key: "@session_id",
965
- data: "my extra session data"
966
- }
967
- end
968
- expect(sent_notifications.size).to eq(0), sent_notifications.inspect
969
-
970
- ExceptionHandling.log_error("No match here") do |data|
971
- data[:session] = {
972
- key: "@session_id",
973
- data: "my extra session <no match!> data"
974
- }
975
- end
976
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
977
- end
978
-
979
- it "reload filter list on the next exception if file was modified" do
980
- capture_notifications
981
-
982
- ExceptionHandling.log_error("Error message is not in list")
983
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
984
-
985
- filter_list = { exception1: { 'error' => "Error message is not in list" } }
986
- allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
987
- allow(File).to receive(:mtime) { incrementing_mtime }
988
-
989
- sent_notifications.clear
990
- ExceptionHandling.log_error("Error message is not in list")
991
- expect(sent_notifications.size).to eq(0), sent_notifications.inspect
992
- end
993
-
994
- it "not consider filter if both error message and body do not match" do
995
- capture_notifications
996
-
997
- # error message matches, but not full text
998
- ExceptionHandling.log_error("some other message")
999
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
1000
-
1001
- # now both match
1002
- sent_notifications.clear
1003
- ExceptionHandling.log_error("some other message") do |data|
1004
- data[:session] = { some_random_key: "misc data" }
1005
- end
1006
- expect(sent_notifications.size).to eq(0), sent_notifications.inspect
1007
- end
1008
-
1009
- it "skip environment keys not on whitelist" do
1010
- capture_notifications
1011
-
1012
- ExceptionHandling.log_error("some message") do |data|
1013
- data[:environment] = { SERVER_PROTOCOL: "HTTP/1.0", RAILS_SECRETS_YML_CONTENTS: 'password: VERY_SECRET_PASSWORD' }
1014
- end
1015
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
1016
-
1017
- mail = sent_notifications.last
1018
- environment = mail.enhanced_data['environment']
1019
-
1020
- expect(environment["RAILS_SECRETS_YML_CONTENTS"]).to be_nil, environment.inspect # this is not on whitelist).to be_nil
1021
- expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this is
1022
- end
1023
-
1024
- it "omit environment defaults" do
1025
- capture_notifications
1026
-
1027
- allow(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args) { |exception_info| sent_notifications << exception_info }
1028
-
1029
- ExceptionHandling.log_error("some message") do |data|
1030
- data[:environment] = { SERVER_PORT: '80', SERVER_PROTOCOL: "HTTP/1.0" }
1031
- end
1032
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
1033
- mail = sent_notifications.last
1034
- environment = mail.enhanced_data['environment']
1035
-
1036
- expect(environment["SERVER_PORT"]).to be_nil, environment.inspect # this was default).to be_nil
1037
- expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this was not
1038
- end
1039
-
1040
- it "reject the filter file if any contain all empty regexes" do
1041
- filter_list = { exception1: { 'error' => "", :session => "" },
1042
- exception2: { 'error' => "is not in list", :session => "" } }
1043
- allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
1044
- allow(File).to receive(:mtime) { incrementing_mtime }
1045
-
1046
- capture_notifications
1047
-
1048
- ExceptionHandling.log_error("Error message is not in list")
1049
- expect(sent_notifications.size).to eq(1), sent_notifications.inspect
1050
- end
1051
-
1052
- it "reload filter file if filename changes" do
1053
- catalog = ExceptionHandling.exception_catalog
1054
- ExceptionHandling.filter_list_filename = "./config/other_exception_filters.yml"
1055
- expect(ExceptionHandling.exception_catalog).to_not eq(catalog)
1056
- end
1057
-
1058
- context "Exception Handling Mailer" do
1059
- EXPECTED_SMTP_HASH =
1060
- {
1061
- host: '127.0.0.1',
1062
- domain: 'localhost.localdomain',
1063
- from: 'server@example.com',
1064
- to: 'escalation@example.com'
1065
- }.freeze
1066
-
1067
- [[true, false], [true, true]].each do |em_flag, synchrony_flag|
1068
- context "eventmachine_safe = #{em_flag} && eventmachine_synchrony = #{synchrony_flag}" do
1069
- before do
1070
- ExceptionHandling.eventmachine_safe = em_flag
1071
- ExceptionHandling.eventmachine_synchrony = synchrony_flag
1072
- EventMachineStub.block = nil
1073
- set_test_const('EventMachine', EventMachineStub)
1074
- set_test_const('EventMachine::Protocols', Module.new)
1075
- set_test_const('EventMachine::DNS', Module.new)
1076
- set_test_const('EventMachine::DNS::Resolver', DNSResolvStub)
1077
- end
1078
-
1079
- after do
1080
- ExceptionHandling.eventmachine_safe = false
1081
- ExceptionHandling.eventmachine_synchrony = false
1082
- end
1083
-
1084
- it "schedule EventMachine STMP when EventMachine defined" do
1085
- ActionMailer::Base.deliveries.clear
1086
-
1087
- set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
1088
-
1089
- ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
1090
- expect(EventMachineStub.block).to be_truthy
1091
- EventMachineStub.block.call
1092
- expect(DNSResolvStub.callback_block).to be_truthy
1093
- DNSResolvStub.callback_block.call ['127.0.0.1']
1094
- expect((SmtpClientStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }) .to eq(EXPECTED_SMTP_HASH), SmtpClientStub.send_hash.inspect
1095
- expect(SmtpClientStub.last_method).to eq((synchrony_flag ? :asend : :send))
1096
- expect(SmtpClientStub.send_hash[:content]).to match(/Exception to escalate/)
1097
- assert_emails 0, ActionMailer::Base.deliveries.*.to_s
1098
- end
1099
-
1100
- it "pass the content as a proper rfc 2822 message" do
1101
- set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
1102
- ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
1103
- expect(EventMachineStub.block).to be_truthy
1104
- EventMachineStub.block.call
1105
- expect(DNSResolvStub.callback_block).to be_truthy
1106
- DNSResolvStub.callback_block.call ['127.0.0.1']
1107
- expect(content = SmtpClientStub.send_hash[:content]).to be_truthy
1108
- expect(content).to match(/Content-Transfer-Encoding: 7bit/)
1109
- expect(content).to match(/\r\n\.\r\n\z/)
1110
- end
1111
-
1112
- it "log fatal on EventMachine STMP errback" do
1113
- ActionMailer::Base.deliveries.clear
1114
-
1115
- set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientErrbackStub)
1116
- expect(ExceptionHandling.logger).to receive(:fatal).with(/Exception to escalate/, any_args)
1117
- expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to email by SMTP: "credential mismatch"/)
1118
-
1119
- ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
1120
- expect(EventMachineStub.block).to be_truthy
1121
- EventMachineStub.block.call
1122
- expect(DNSResolvStub.callback_block).to be_truthy
1123
- DNSResolvStub.callback_block.call(['127.0.0.1'])
1124
- SmtpClientErrbackStub.block.call("credential mismatch")
1125
- expect((SmtpClientErrbackStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }).to eq(EXPECTED_SMTP_HASH), SmtpClientErrbackStub.send_hash.inspect end
1126
-
1127
- it "log fatal on EventMachine dns resolver errback" do
1128
- expect(ExceptionHandling.logger).to receive(:fatal).with(/Exception to escalate/, any_args)
1129
- expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to resolv DNS for localhost: "softlayer sucks"/)
1130
-
1131
- ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
1132
- expect(EventMachineStub.block).to be_truthy
1133
- EventMachineStub.block.call
1134
- DNSResolvStub.errback_block.call("softlayer sucks")
1135
- end
1136
- end
1137
- end
1138
- end
1139
- end
1140
-
1141
- context "Exception mapping" do
1142
- before do
1143
- @data = {
1144
- environment: {
1145
- 'HTTP_HOST' => "localhost",
1146
- 'HTTP_REFERER' => "http://localhost/action/controller/instance",
1147
- },
1148
- session: {
1149
- data: {
1150
- affiliate_id: defined?(Affiliate) ? Affiliate.first.id : '1',
1151
- edit_mode: true,
1152
- advertiser_id: defined?(Advertiser) ? Advertiser.first.id : '1',
1153
- username_id: defined?(Username) ? Username.first.id : '1',
1154
- user_id: defined?(User) ? User.first.id : '1',
1155
- flash: {},
1156
- impersonated_organization_pk: 'Advertiser_1'
1157
- }
1158
- },
1159
- request: {},
1160
- backtrace: ["[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:580:in `call_filters'", "[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:601:in `run_before_filters'"],
1161
- api_key: "none",
1162
- error_class: "StandardError",
1163
- error: 'Some error message'
1164
- }
1165
- end
1166
-
1167
- it "clean backtraces" do
1168
- begin
1169
- raise "test exception"
1170
- rescue => ex
1171
- backtrace = ex.backtrace
1172
- end
1173
- result = ExceptionHandling.send(:clean_backtrace, ex).to_s
1174
- expect(backtrace).to_not eq(result)
1175
- end
1176
-
1177
- it "return entire backtrace if cleaned is emtpy" do
1178
- begin
1179
- backtrace = ["/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:312:in `find_with_ids'",
1180
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:107:in `find'",
1181
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `__send__'",
1182
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `find'",
1183
- "/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `call'",
1184
- "/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `test: Exception mapping should return entire backtrace if cleaned is emtpy. '",
1185
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `__send__'",
1186
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `run'",
1187
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:447:in `_run__1913317170__setup__4__callbacks'",
1188
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `send'",
1189
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `__run_callback'",
1190
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:385:in `_run_setup_callbacks'",
1191
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `send'",
1192
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `run_callbacks'",
1193
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:70:in `run'",
1194
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:34:in `run'",
1195
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `each'",
1196
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `run'",
1197
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:46:in `old_run_suite'",
1198
- "(eval):12:in `run_suite'",
1199
- "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `send'",
1200
- "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `start_mediator'",
1201
- "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:81:in `start'",
1202
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnerutilities.rb:29:in `run'",
1203
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/autorunner.rb:12:in `run'",
1204
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit.rb:279",
1205
- "-e:1"]
1206
-
1207
- module ::Rails
1208
- class BacktraceCleaner
1209
- def clean(_backtrace)
1210
- []
1211
- end
1212
- end
1213
- end
1214
-
1215
- rails = double(Rails)
1216
- expect(rails).to receive(:backtrace_cleaner) { Rails::BacktraceCleaner.new }
1217
- rails.backtrace_cleaner
1218
-
1219
- ex = Exception.new
1220
- ex.set_backtrace(backtrace)
1221
- result = ExceptionHandling.send(:clean_backtrace, ex)
1222
- expect(result).to eq(backtrace)
1223
- ensure
1224
- Object.send(:remove_const, :Rails)
1225
- end
1226
- end
1227
- end
1228
-
1229
- context "log_perodically" do
1230
- before do
1231
- Time.now_override = Time.now # Freeze time
1232
- ExceptionHandling.logger.clear
1233
- end
1234
-
1235
- after do
1236
- Time.now_override = nil
1237
- end
1238
-
1239
- it "take in additional logging context and pass them to the logger" do
1240
- ExceptionHandling.log_periodically(:test_context_with_periodic, 30.minutes, "this will be written", service_name: 'exception_handling')
1241
- expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
1242
- expect(logged_excluding_reload_filter.last[:context]).to eq({ service_name: 'exception_handling' })
1243
- end
1244
-
1245
- it "log immediately when we are expected to log" do
1246
- ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
1247
- expect(logged_excluding_reload_filter.size).to eq(1)
1248
-
1249
- Time.now_override = Time.now + 5.minutes
1250
- ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will not be written")
1251
- expect(logged_excluding_reload_filter.size).to eq(1)
1252
-
1253
- ExceptionHandling.log_periodically(:test_another_periodic_exception, 30.minutes, "this will be written")
1254
- expect(logged_excluding_reload_filter.size).to eq(2)
1255
-
1256
- Time.now_override = Time.now + 26.minutes
1257
-
1258
- ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
1259
- expect(logged_excluding_reload_filter.size).to eq(3)
1260
- end
1261
- end
1262
-
1263
- context "#honeybadger_auto_tagger=" do
1264
- context "with proc that runs successfully" do
1265
- before do
1266
- ExceptionHandling.honeybadger_auto_tagger =
1267
- ->(exception) do
1268
- if exception.message =~ /donuts/
1269
- ['donut-error', 'high-urgency']
1270
- else
1271
- ['low-urgency']
1272
- end
1273
- end
1274
- end
1275
-
1276
- context "without manually passed tags" do
1277
- let(:exception) { StandardError.new("We are out of chocolate milk") }
1278
-
1279
- it "adds tags from autotagger" do
1280
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "low-urgency" }))
1281
- ExceptionHandling.log_error(exception, nil)
1282
- end
1283
- end
1284
-
1285
- context "with manually passed tags" do
1286
- let(:exception) { StandardError.new("The donuts are burning") }
1287
-
1288
- it "merges tags from autotagger with manually passed tags" do
1289
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "donut-error high-urgency upset-customers" }))
1290
- ExceptionHandling.log_error(exception, nil, honeybadger_tags: ["upset-customers"])
1291
- end
1292
- end
1293
- end
1294
- end
1295
-
1296
- context "with proc that raises an exception" do
1297
- let(:exception) { StandardError.new("Something else occurred") }
1298
-
1299
- before do
1300
- ExceptionHandling.honeybadger_auto_tagger = ->(_exception) { raise StandardError, "boom" }
1301
- end
1302
-
1303
- it "logs a message and returns [] for the tags" do
1304
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "some-other-tag" }))
1305
- expect(ExceptionHandling).to receive(:log_info).with(/Unable to execute honeybadger_auto_tags callback. boom/)
1306
- ExceptionHandling.log_error(exception, nil, honeybadger_tags: ["some-other-tag"])
1307
- end
1308
- end
1309
-
1310
- describe "#add_honeybadger_tag_from_log_context" do
1311
- subject(:add_hb_tag) { ExceptionHandling.add_honeybadger_tag_from_log_context(tag_name, path: path) }
1312
- let(:tag_name) { "sample-tag" }
1313
- let(:path) { ["sample", "path"] }
1314
-
1315
- it "sets the honeybadger log context tags to the tag name and path" do
1316
- add_hb_tag
1317
- expect(ExceptionHandling.honeybadger_log_context_tags).to include("sample-tag" => ["sample", "path"])
1318
- end
1319
-
1320
- context "when path is not an array" do
1321
- let(:path) { "not an array" }
1322
-
1323
- it "raises an argument error" do
1324
- expect { add_hb_tag }.to raise_error(ArgumentError, /path must be an Array/)
1325
- end
1326
- end
1327
-
1328
- context "when tag_name is not a string" do
1329
- let(:tag_name) { 1 }
1330
-
1331
- it "raises an argument error" do
1332
- expect { add_hb_tag }.to raise_error(ArgumentError, /tag_name must be a String/)
1333
- end
1334
- end
1335
-
1336
- context "when there already exists a tag with the same name" do
1337
- before(:each) do
1338
- ExceptionHandling.add_honeybadger_tag_from_log_context(tag_name, path: ["other", "path"])
1339
- end
1340
-
1341
- it "overwrites the existing tag to the new one" do
1342
- add_hb_tag
1343
- expect(ExceptionHandling.honeybadger_log_context_tags).to include("sample-tag" => ["sample", "path"])
1344
- end
1345
-
1346
- it "logs a warning" do
1347
- expect(ExceptionHandling.logger).to receive(:warn).with(/Overwriting existing tag path for 'sample-tag'/, **{})
1348
- add_hb_tag
1349
- end
1350
- end
1351
- end
1352
-
1353
- describe "honey badger tags" do
1354
- context "with log context tags" do
1355
- before(:each) do
1356
- ExceptionHandling.add_honeybadger_tag_from_log_context("kubernetes_context", path: ["kubernetes", "context"])
1357
- end
1358
-
1359
- context "when error is logged within a log context that matches the path" do
1360
- it "notifies honeybadger with the tags from the log context" do
1361
- ExceptionHandling.logger.with_context("kubernetes" => { "context" => "local" }) do
1362
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "kubernetes_context:local" }))
1363
- ExceptionHandling.log_error(StandardError.new("Error"), nil)
1364
- end
1365
- end
1366
- end
1367
-
1368
- context "when error is logged within a log context that doesn't match the path" do
1369
- it "does not specify any tags in the honeybadger notify" do
1370
- ExceptionHandling.logger.with_context("kubernetes" => { "pod" => "frontend-abc" }) do
1371
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "" }))
1372
- ExceptionHandling.log_error(StandardError.new("Error"), nil)
1373
- end
1374
- end
1375
- end
1376
-
1377
- context "when error is logged outside of a log context block" do
1378
- it "does not specify any tags in the honeybadger notify" do
1379
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "" }))
1380
- ExceptionHandling.log_error(StandardError.new("Error"), nil)
1381
- end
1382
- end
1383
- end
1384
-
1385
- context "with all different honeybadger tagging" do
1386
- subject(:log_error) { ExceptionHandling.log_error(StandardError.new("Error"), nil, honeybadger_tags: inline_tags) }
1387
- let(:inline_tags) { ["inline-tag"] }
1388
- before(:each) do
1389
- ExceptionHandling.honeybadger_auto_tagger = ->(_exception) { ["auto-tag"] }
1390
- ExceptionHandling.add_honeybadger_tag_from_log_context("log-context", path: ["inside", "context"])
1391
- end
1392
-
1393
- it "combines all the tags from different sources" do
1394
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "auto-tag inline-tag log-context:tag" }))
1395
- ExceptionHandling.logger.with_context("inside" => { "context" => "tag" }) do
1396
- log_error
1397
- end
1398
- end
1399
-
1400
- context "when there are duplicate tags" do
1401
- let(:inline_tags) { ["auto-tag"] }
1402
-
1403
- it "notifies honeybadger with the set of tags" do
1404
- expect(Honeybadger).to receive(:notify).with(hash_including({ tags: "auto-tag log-context:tag" }))
1405
- ExceptionHandling.logger.with_context("inside" => { "context" => "tag" }) do
1406
- log_error
1407
- end
1408
- end
1409
- end
1410
- end
1411
- end
1412
- end
1413
-
1414
- context "ExceptionHandling < 3.0 " do
1415
- it "should return a deprecation warning" do
1416
- ExceptionHandling.production_support_recipients = "prodsupport@example.com"
1417
- expect { ExceptionHandling.escalate_to_production_support("blah", "invoca@example.com") }
1418
- .to output(/DEPRECATION WARNING: escalate_to_production_support is deprecated and will be removed from ExceptionHandling 3.0/).to_stderr
1419
- end
1420
-
1421
- it "should return a deprecation warning" do
1422
- expect { ExceptionHandling.escalate_error("blah", "invoca@example.com") }
1423
- .to output(/DEPRECATION WARNING: escalate_error is deprecated and will be removed from ExceptionHandling 3.0/).to_stderr
1424
- end
1425
-
1426
- it "should return a deprecation warning" do
1427
- expect { ExceptionHandling.escalate_warning("blah", "invoca@example.com") }
1428
- .to output(/DEPRECATION WARNING: escalate_warning is deprecated and will be removed from ExceptionHandling 3.0/).to_stderr
1429
- end
1430
-
1431
- it "should return a deprecation warning" do
1432
- expect { ExceptionHandling.ensure_escalation("blah", "invoca@example.com") }
1433
- .to output(/DEPRECATION WARNING: ensure_escalation is deprecated and will be removed from ExceptionHandling 3.0/).to_stderr
1434
- end
1435
- end
1436
-
1437
- private
1438
-
1439
- def logged_excluding_reload_filter
1440
- ExceptionHandling.logger.logged.select { |l| l[:message] !~ /Reloading filter list/ }
1441
- end
1442
-
1443
- def incrementing_mtime
1444
- @mtime ||= Time.now
1445
- @mtime += 1.day
1446
- end
1447
-
1448
- def exception_1
1449
- @exception_1 ||=
1450
- begin
1451
- raise StandardError, "Exception 1"
1452
- rescue => ex
1453
- ex
1454
- end
1455
- end
1456
-
1457
- def exception_2
1458
- @exception_2 ||=
1459
- begin
1460
- raise StandardError, "Exception 2"
1461
- rescue => ex
1462
- ex
1463
- end
1464
- end
1465
- end