exception_handling 2.16.0 → 3.0.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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