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