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