exception_notification 4.2.0 → 4.4.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.
- checksums.yaml +5 -5
- data/Appraisals +4 -3
- data/CHANGELOG.rdoc +57 -1
- data/CONTRIBUTING.md +21 -2
- data/Gemfile +3 -1
- data/README.md +106 -789
- data/Rakefile +4 -2
- data/docs/notifiers/campfire.md +50 -0
- data/docs/notifiers/custom.md +42 -0
- data/docs/notifiers/datadog.md +51 -0
- data/docs/notifiers/email.md +195 -0
- data/docs/notifiers/google_chat.md +31 -0
- data/docs/notifiers/hipchat.md +66 -0
- data/docs/notifiers/irc.md +97 -0
- data/docs/notifiers/mattermost.md +115 -0
- data/docs/notifiers/slack.md +161 -0
- data/docs/notifiers/sns.md +37 -0
- data/docs/notifiers/teams.md +54 -0
- data/docs/notifiers/webhook.md +60 -0
- data/examples/sample_app.rb +56 -0
- data/examples/sinatra/Gemfile +8 -6
- data/examples/sinatra/config.ru +3 -1
- data/examples/sinatra/sinatra_app.rb +19 -11
- data/exception_notification.gemspec +30 -23
- data/gemfiles/rails4_0.gemfile +1 -2
- data/gemfiles/rails4_1.gemfile +1 -2
- data/gemfiles/rails4_2.gemfile +1 -2
- data/gemfiles/rails5_0.gemfile +1 -2
- data/gemfiles/rails5_1.gemfile +7 -0
- data/gemfiles/rails5_2.gemfile +7 -0
- data/gemfiles/rails6_0.gemfile +7 -0
- data/lib/exception_notification.rb +3 -0
- data/lib/exception_notification/rack.rb +34 -27
- data/lib/exception_notification/rails.rb +3 -0
- data/lib/exception_notification/resque.rb +10 -10
- data/lib/exception_notification/sidekiq.rb +10 -12
- data/lib/exception_notification/version.rb +5 -0
- data/lib/exception_notifier.rb +79 -11
- data/lib/exception_notifier/base_notifier.rb +10 -5
- data/lib/exception_notifier/campfire_notifier.rb +14 -9
- data/lib/exception_notifier/datadog_notifier.rb +156 -0
- data/lib/exception_notifier/email_notifier.rb +78 -87
- data/lib/exception_notifier/google_chat_notifier.rb +44 -0
- data/lib/exception_notifier/hipchat_notifier.rb +16 -10
- data/lib/exception_notifier/irc_notifier.rb +38 -31
- data/lib/exception_notifier/mattermost_notifier.rb +54 -131
- data/lib/exception_notifier/modules/backtrace_cleaner.rb +2 -2
- data/lib/exception_notifier/modules/error_grouping.rb +87 -0
- data/lib/exception_notifier/modules/formatter.rb +121 -0
- data/lib/exception_notifier/notifier.rb +9 -6
- data/lib/exception_notifier/slack_notifier.rb +75 -32
- data/lib/exception_notifier/sns_notifier.rb +86 -0
- data/lib/exception_notifier/teams_notifier.rb +200 -0
- data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +1 -1
- data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -1
- data/lib/exception_notifier/views/exception_notifier/_request.text.erb +1 -1
- data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +9 -9
- data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +2 -4
- data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -2
- data/lib/exception_notifier/webhook_notifier.rb +19 -16
- data/lib/generators/exception_notification/install_generator.rb +11 -5
- data/lib/generators/exception_notification/templates/{exception_notification.rb → exception_notification.rb.erb} +14 -12
- data/test/exception_notification/rack_test.rb +90 -4
- data/test/exception_notification/resque_test.rb +54 -0
- data/test/exception_notifier/campfire_notifier_test.rb +66 -39
- data/test/exception_notifier/datadog_notifier_test.rb +153 -0
- data/test/exception_notifier/email_notifier_test.rb +301 -145
- data/test/exception_notifier/google_chat_notifier_test.rb +185 -0
- data/test/exception_notifier/hipchat_notifier_test.rb +112 -65
- data/test/exception_notifier/irc_notifier_test.rb +48 -30
- data/test/exception_notifier/mattermost_notifier_test.rb +218 -55
- data/test/exception_notifier/modules/error_grouping_test.rb +167 -0
- data/test/exception_notifier/modules/formatter_test.rb +152 -0
- data/test/exception_notifier/sidekiq_test.rb +9 -6
- data/test/exception_notifier/slack_notifier_test.rb +109 -59
- data/test/exception_notifier/sns_notifier_test.rb +123 -0
- data/test/exception_notifier/teams_notifier_test.rb +92 -0
- data/test/exception_notifier/webhook_notifier_test.rb +68 -38
- data/test/exception_notifier_test.rb +220 -37
- data/test/support/exception_notifier_helper.rb +14 -0
- data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.html.erb +0 -0
- data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.text.erb +0 -0
- data/test/{dummy/app → support}/views/exception_notifier/_new_section.html.erb +0 -0
- data/test/{dummy/app → support}/views/exception_notifier/_new_section.text.erb +0 -0
- data/test/test_helper.rb +14 -13
- metadata +154 -162
- data/test/dummy/.gitignore +0 -4
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/controllers/application_controller.rb +0 -3
- data/test/dummy/app/controllers/posts_controller.rb +0 -30
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/helpers/posts_helper.rb +0 -2
- data/test/dummy/app/models/post.rb +0 -2
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/app/views/posts/_form.html.erb +0 -0
- data/test/dummy/app/views/posts/new.html.erb +0 -0
- data/test/dummy/app/views/posts/show.html.erb +0 -0
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -42
- data/test/dummy/config/boot.rb +0 -6
- data/test/dummy/config/database.yml +0 -22
- data/test/dummy/config/environment.rb +0 -17
- data/test/dummy/config/environments/development.rb +0 -25
- data/test/dummy/config/environments/production.rb +0 -50
- data/test/dummy/config/environments/test.rb +0 -38
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/inflections.rb +0 -10
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/secret_token.rb +0 -8
- data/test/dummy/config/initializers/session_store.rb +0 -8
- data/test/dummy/config/locales/en.yml +0 -5
- data/test/dummy/config/routes.rb +0 -3
- data/test/dummy/db/migrate/20110729022608_create_posts.rb +0 -15
- data/test/dummy/db/schema.rb +0 -24
- data/test/dummy/db/seeds.rb +0 -7
- data/test/dummy/lib/tasks/.gitkeep +0 -0
- data/test/dummy/public/404.html +0 -26
- data/test/dummy/public/422.html +0 -26
- data/test/dummy/public/500.html +0 -26
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/images/rails.png +0 -0
- data/test/dummy/public/index.html +0 -239
- data/test/dummy/public/javascripts/application.js +0 -2
- data/test/dummy/public/javascripts/controls.js +0 -965
- data/test/dummy/public/javascripts/dragdrop.js +0 -974
- data/test/dummy/public/javascripts/effects.js +0 -1123
- data/test/dummy/public/javascripts/prototype.js +0 -6001
- data/test/dummy/public/javascripts/rails.js +0 -191
- data/test/dummy/public/robots.txt +0 -5
- data/test/dummy/public/stylesheets/.gitkeep +0 -0
- data/test/dummy/public/stylesheets/scaffold.css +0 -56
- data/test/dummy/script/rails +0 -6
- data/test/dummy/test/functional/posts_controller_test.rb +0 -218
- data/test/dummy/test/test_helper.rb +0 -7
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
|
|
5
|
+
class ErrorGroupTest < ActiveSupport::TestCase
|
|
6
|
+
setup do
|
|
7
|
+
module TestModule
|
|
8
|
+
include ExceptionNotifier::ErrorGrouping
|
|
9
|
+
@@error_grouping_cache = ActiveSupport::Cache::FileStore.new('test/dummy/tmp/non_default_location')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
@exception = RuntimeError.new('ERROR')
|
|
13
|
+
@exception.stubs(:backtrace).returns(['/path/where/error/raised:1'])
|
|
14
|
+
|
|
15
|
+
@exception2 = RuntimeError.new('ERROR2')
|
|
16
|
+
@exception2.stubs(:backtrace).returns(['/path/where/error/found:2'])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
teardown do
|
|
20
|
+
TestModule.error_grouping_cache.clear
|
|
21
|
+
TestModule.fallback_cache_store.clear
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
test 'should add additional option: error_grouping' do
|
|
25
|
+
assert_respond_to TestModule, :error_grouping
|
|
26
|
+
assert_respond_to TestModule, :error_grouping=
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
test 'should set error_grouping to false default' do
|
|
30
|
+
assert_equal false, TestModule.error_grouping
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
test 'should add additional option: error_grouping_cache' do
|
|
34
|
+
assert_respond_to TestModule, :error_grouping_cache
|
|
35
|
+
assert_respond_to TestModule, :error_grouping_cache=
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test 'should add additional option: error_grouping_period' do
|
|
39
|
+
assert_respond_to TestModule, :error_grouping_period
|
|
40
|
+
assert_respond_to TestModule, :error_grouping_period=
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
test 'shoud set error_grouping_period to 5.minutes default' do
|
|
44
|
+
assert_equal 300, TestModule.error_grouping_period
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
test 'should add additional option: notification_trigger' do
|
|
48
|
+
assert_respond_to TestModule, :notification_trigger
|
|
49
|
+
assert_respond_to TestModule, :notification_trigger=
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
test 'should return errors count nil when not same error for .error_count' do
|
|
53
|
+
assert_nil TestModule.error_count('something')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
test 'should return errors count when same error for .error_count' do
|
|
57
|
+
TestModule.error_grouping_cache.write('error_key', 13)
|
|
58
|
+
assert_equal 13, TestModule.error_count('error_key')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
test 'should fallback to memory store cache if specified cache store failed to read' do
|
|
62
|
+
TestModule.error_grouping_cache.stubs(:read).raises(RuntimeError.new('Failed to read'))
|
|
63
|
+
original_fallback = TestModule.fallback_cache_store
|
|
64
|
+
TestModule.expects(:fallback_cache_store).returns(original_fallback).at_least_once
|
|
65
|
+
|
|
66
|
+
assert_nil TestModule.error_count('something_to_read')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
test 'should save error with count for .save_error_count' do
|
|
70
|
+
count = rand(1..10)
|
|
71
|
+
|
|
72
|
+
TestModule.save_error_count('error_key', count)
|
|
73
|
+
assert_equal count, TestModule.error_grouping_cache.read('error_key')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
test 'should fallback to memory store cache if specified cache store failed to write' do
|
|
77
|
+
TestModule.error_grouping_cache.stubs(:write).raises(RuntimeError.new('Failed to write'))
|
|
78
|
+
original_fallback = TestModule.fallback_cache_store
|
|
79
|
+
TestModule.expects(:fallback_cache_store).returns(original_fallback).at_least_once
|
|
80
|
+
|
|
81
|
+
assert TestModule.save_error_count('something_to_cache', rand(1..10))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
test 'should save accumulated_errors_count into options' do
|
|
85
|
+
options = {}
|
|
86
|
+
TestModule.group_error!(@exception, options)
|
|
87
|
+
|
|
88
|
+
assert_equal 1, options[:accumulated_errors_count]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
test 'should not group error if different exception in .group_error!' do
|
|
92
|
+
options1 = {}
|
|
93
|
+
TestModule.expects(:save_error_count).with { |key, count| key.is_a?(String) && count == 1 }.times(4).returns(true)
|
|
94
|
+
TestModule.group_error!(@exception, options1)
|
|
95
|
+
|
|
96
|
+
options2 = {}
|
|
97
|
+
TestModule.group_error!(NoMethodError.new('method not found'), options2)
|
|
98
|
+
|
|
99
|
+
assert_equal 1, options1[:accumulated_errors_count]
|
|
100
|
+
assert_equal 1, options2[:accumulated_errors_count]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
test 'should not group error is same exception but different message or backtrace' do
|
|
104
|
+
options1 = {}
|
|
105
|
+
TestModule.expects(:save_error_count).with { |key, count| key.is_a?(String) && count == 1 }.times(4).returns(true)
|
|
106
|
+
TestModule.group_error!(@exception, options1)
|
|
107
|
+
|
|
108
|
+
options2 = {}
|
|
109
|
+
TestModule.group_error!(@exception2, options2)
|
|
110
|
+
|
|
111
|
+
assert_equal 1, options1[:accumulated_errors_count]
|
|
112
|
+
assert_equal 1, options2[:accumulated_errors_count]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
test 'should group error if same exception and message' do
|
|
116
|
+
options = {}
|
|
117
|
+
|
|
118
|
+
10.times do |i|
|
|
119
|
+
@exception2.stubs(:backtrace).returns(["/path:#{i}"])
|
|
120
|
+
TestModule.group_error!(@exception2, options)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
assert_equal 10, options[:accumulated_errors_count]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
test 'should group error if same exception and backtrace' do
|
|
127
|
+
options = {}
|
|
128
|
+
|
|
129
|
+
10.times do |i|
|
|
130
|
+
@exception2.stubs(:message).returns("ERRORS#{i}")
|
|
131
|
+
TestModule.group_error!(@exception2, options)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
assert_equal 10, options[:accumulated_errors_count]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
test 'should group error by that message have high priority' do
|
|
138
|
+
message_based_key = "exception:#{Zlib.crc32("RuntimeError\nmessage:ERROR")}"
|
|
139
|
+
backtrace_based_key = "exception:#{Zlib.crc32("RuntimeError\npath:/path/where/error/raised:1")}"
|
|
140
|
+
|
|
141
|
+
TestModule.save_error_count(message_based_key, 1)
|
|
142
|
+
TestModule.save_error_count(backtrace_based_key, 1)
|
|
143
|
+
|
|
144
|
+
TestModule.expects(:save_error_count).with(message_based_key, 2).once
|
|
145
|
+
TestModule.expects(:save_error_count).with(backtrace_based_key, 2).never
|
|
146
|
+
|
|
147
|
+
TestModule.group_error!(@exception, {})
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
test 'use default formula if not specify notification_trigger in .send_notification?' do
|
|
151
|
+
TestModule.stubs(:notification_trigger).returns(nil)
|
|
152
|
+
|
|
153
|
+
count = 16
|
|
154
|
+
Math.expects(:log2).with(count).returns(4)
|
|
155
|
+
|
|
156
|
+
assert TestModule.send_notification?(@exception, count)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
test 'use specified trigger in .send_notification?' do
|
|
160
|
+
trigger = proc { |_exception, count| (count % 4).zero? }
|
|
161
|
+
TestModule.stubs(:notification_trigger).returns(trigger)
|
|
162
|
+
|
|
163
|
+
count = 16
|
|
164
|
+
trigger.expects(:call).with(@exception, count).returns(true)
|
|
165
|
+
assert TestModule.send_notification?(@exception, count)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'timecop'
|
|
5
|
+
|
|
6
|
+
class FormatterTest < ActiveSupport::TestCase
|
|
7
|
+
setup do
|
|
8
|
+
@exception = RuntimeError.new('test')
|
|
9
|
+
Timecop.freeze('2018-12-09 12:07:16 UTC')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
teardown do
|
|
13
|
+
Timecop.return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# #title
|
|
18
|
+
#
|
|
19
|
+
test 'title returns correct content' do
|
|
20
|
+
formatter = ExceptionNotifier::Formatter.new(@exception)
|
|
21
|
+
|
|
22
|
+
title = if defined?(::Rails) && ::Rails.respond_to?(:env)
|
|
23
|
+
'⚠️ Error occurred in test ⚠️'
|
|
24
|
+
else
|
|
25
|
+
'⚠️ Error occurred ⚠️'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
assert_equal title, formatter.title
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# #subtitle
|
|
33
|
+
#
|
|
34
|
+
test 'subtitle without accumulated error' do
|
|
35
|
+
formatter = ExceptionNotifier::Formatter.new(@exception)
|
|
36
|
+
assert_equal 'A *RuntimeError* occurred.', formatter.subtitle
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
test 'subtitle with accumulated error' do
|
|
40
|
+
formatter = ExceptionNotifier::Formatter.new(@exception, accumulated_errors_count: 3)
|
|
41
|
+
assert_equal '3 *RuntimeError* occurred.', formatter.subtitle
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
test 'subtitle with controller' do
|
|
45
|
+
env = Rack::MockRequest.env_for(
|
|
46
|
+
'/', 'action_controller.instance' => test_controller
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
formatter = ExceptionNotifier::Formatter.new(@exception, env: env)
|
|
50
|
+
assert_equal 'A *RuntimeError* occurred in *home#index*.', formatter.subtitle
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
# #app_name
|
|
55
|
+
#
|
|
56
|
+
test 'app_name defaults to Rails app name' do
|
|
57
|
+
formatter = ExceptionNotifier::Formatter.new(@exception)
|
|
58
|
+
|
|
59
|
+
if defined?(::Rails) && ::Rails.respond_to?(:application)
|
|
60
|
+
assert_equal 'dummy', formatter.app_name
|
|
61
|
+
else
|
|
62
|
+
assert_nil formatter.app_name
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
test 'app_name can be overwritten using options' do
|
|
67
|
+
formatter = ExceptionNotifier::Formatter.new(@exception, app_name: 'test')
|
|
68
|
+
assert_equal 'test', formatter.app_name
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# #request_message
|
|
73
|
+
#
|
|
74
|
+
test 'request_message when env set' do
|
|
75
|
+
text = [
|
|
76
|
+
'```',
|
|
77
|
+
'* url : http://test.address/?id=foo',
|
|
78
|
+
'* http_method : GET',
|
|
79
|
+
'* ip_address : 127.0.0.1',
|
|
80
|
+
'* parameters : {"id"=>"foo"}',
|
|
81
|
+
'* timestamp : 2018-12-09 12:07:16 UTC',
|
|
82
|
+
'```'
|
|
83
|
+
].join("\n")
|
|
84
|
+
|
|
85
|
+
env = Rack::MockRequest.env_for(
|
|
86
|
+
'/',
|
|
87
|
+
'HTTP_HOST' => 'test.address',
|
|
88
|
+
'REMOTE_ADDR' => '127.0.0.1',
|
|
89
|
+
params: { id: 'foo' }
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
formatter = ExceptionNotifier::Formatter.new(@exception, env: env)
|
|
93
|
+
assert_equal text, formatter.request_message
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
test 'request_message when env not set' do
|
|
97
|
+
formatter = ExceptionNotifier::Formatter.new(@exception)
|
|
98
|
+
assert_nil formatter.request_message
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#
|
|
102
|
+
# #backtrace_message
|
|
103
|
+
#
|
|
104
|
+
test 'backtrace_message when backtrace set' do
|
|
105
|
+
text = [
|
|
106
|
+
'```',
|
|
107
|
+
"* app/controllers/my_controller.rb:53:in `my_controller_params'",
|
|
108
|
+
"* app/controllers/my_controller.rb:34:in `update'",
|
|
109
|
+
'```'
|
|
110
|
+
].join("\n")
|
|
111
|
+
|
|
112
|
+
@exception.set_backtrace([
|
|
113
|
+
"app/controllers/my_controller.rb:53:in `my_controller_params'",
|
|
114
|
+
"app/controllers/my_controller.rb:34:in `update'"
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
formatter = ExceptionNotifier::Formatter.new(@exception)
|
|
118
|
+
assert_equal text, formatter.backtrace_message
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
test 'backtrace_message when no backtrace' do
|
|
122
|
+
formatter = ExceptionNotifier::Formatter.new(@exception)
|
|
123
|
+
assert_nil formatter.backtrace_message
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
#
|
|
127
|
+
# #controller_and_action
|
|
128
|
+
#
|
|
129
|
+
test 'correct controller_and_action if controller is present' do
|
|
130
|
+
env = Rack::MockRequest.env_for(
|
|
131
|
+
'/', 'action_controller.instance' => test_controller
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
formatter = ExceptionNotifier::Formatter.new(@exception, env: env)
|
|
135
|
+
assert_equal 'home#index', formatter.controller_and_action
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
test 'controller_and_action is nil if no controller' do
|
|
139
|
+
env = Rack::MockRequest.env_for('/')
|
|
140
|
+
|
|
141
|
+
formatter = ExceptionNotifier::Formatter.new(@exception, env: env)
|
|
142
|
+
assert_nil formatter.controller_and_action
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_controller
|
|
146
|
+
controller = mock('controller')
|
|
147
|
+
controller.stubs(:action_name).returns('index')
|
|
148
|
+
controller.stubs(:controller_name).returns('home')
|
|
149
|
+
|
|
150
|
+
controller
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
2
4
|
|
|
3
5
|
# To allow sidekiq error handlers to be registered, sidekiq must be in
|
|
4
6
|
# "server mode". This mode is triggered by loading sidekiq/cli. Note this
|
|
5
7
|
# has to be loaded before exception_notification/sidekiq.
|
|
6
|
-
require
|
|
8
|
+
require 'sidekiq/cli'
|
|
9
|
+
require 'sidekiq/testing'
|
|
7
10
|
|
|
8
|
-
require
|
|
11
|
+
require 'exception_notification/sidekiq'
|
|
9
12
|
|
|
10
13
|
class MockSidekiqServer
|
|
11
14
|
include ::Sidekiq::ExceptionHandler
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
class SidekiqTest < ActiveSupport::TestCase
|
|
15
|
-
test
|
|
18
|
+
test 'should call notify_exception when sidekiq raises an error' do
|
|
16
19
|
server = MockSidekiqServer.new
|
|
17
|
-
message =
|
|
20
|
+
message = {}
|
|
18
21
|
exception = RuntimeError.new
|
|
19
22
|
|
|
20
23
|
ExceptionNotifier.expects(:notify_exception).with(
|
|
21
24
|
exception,
|
|
22
|
-
:
|
|
25
|
+
data: { sidekiq: message }
|
|
23
26
|
)
|
|
24
27
|
|
|
25
28
|
server.handle_exception(exception, message)
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'test_helper'
|
|
2
4
|
require 'slack-notifier'
|
|
3
5
|
|
|
4
6
|
class SlackNotifierTest < ActiveSupport::TestCase
|
|
5
|
-
|
|
6
7
|
def setup
|
|
7
8
|
@exception = fake_exception
|
|
8
9
|
@exception.stubs(:backtrace).returns(fake_backtrace)
|
|
9
10
|
@exception.stubs(:message).returns('exception message')
|
|
11
|
+
ExceptionNotifier::SlackNotifier.any_instance.stubs(:clean_backtrace).returns(fake_cleaned_backtrace)
|
|
10
12
|
Socket.stubs(:gethostname).returns('example.com')
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
test
|
|
15
|
+
test 'should send a slack notification if properly configured' do
|
|
14
16
|
options = {
|
|
15
|
-
webhook_url:
|
|
17
|
+
webhook_url: 'http://slack.webhook.url'
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
Slack::Notifier.any_instance.expects(:ping).with('', fake_notification)
|
|
@@ -21,9 +23,9 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
|
21
23
|
slack_notifier.call(@exception)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
test
|
|
26
|
+
test 'should send a slack notification without backtrace info if properly configured' do
|
|
25
27
|
options = {
|
|
26
|
-
webhook_url:
|
|
28
|
+
webhook_url: 'http://slack.webhook.url'
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
Slack::Notifier.any_instance.expects(:ping).with('', fake_notification(fake_exception_without_backtrace))
|
|
@@ -32,10 +34,10 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
|
32
34
|
slack_notifier.call(fake_exception_without_backtrace)
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
test
|
|
37
|
+
test 'should send the notification to the specified channel' do
|
|
36
38
|
options = {
|
|
37
|
-
webhook_url:
|
|
38
|
-
channel:
|
|
39
|
+
webhook_url: 'http://slack.webhook.url',
|
|
40
|
+
channel: 'channel'
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
Slack::Notifier.any_instance.expects(:ping).with('', fake_notification)
|
|
@@ -43,13 +45,14 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
|
43
45
|
slack_notifier = ExceptionNotifier::SlackNotifier.new(options)
|
|
44
46
|
slack_notifier.call(@exception)
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
channel = slack_notifier.notifier.config.defaults[:channel]
|
|
49
|
+
assert_equal channel, options[:channel]
|
|
47
50
|
end
|
|
48
51
|
|
|
49
|
-
test
|
|
52
|
+
test 'should send the notification to the specified username' do
|
|
50
53
|
options = {
|
|
51
|
-
webhook_url:
|
|
52
|
-
username:
|
|
54
|
+
webhook_url: 'http://slack.webhook.url',
|
|
55
|
+
username: 'username'
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
Slack::Notifier.any_instance.expects(:ping).with('', fake_notification)
|
|
@@ -57,20 +60,49 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
|
57
60
|
slack_notifier = ExceptionNotifier::SlackNotifier.new(options)
|
|
58
61
|
slack_notifier.call(@exception)
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
username = slack_notifier.notifier.config.defaults[:username]
|
|
64
|
+
assert_equal username, options[:username]
|
|
61
65
|
end
|
|
62
66
|
|
|
63
|
-
test
|
|
67
|
+
test 'should send the notification with specific backtrace lines' do
|
|
64
68
|
options = {
|
|
65
|
-
webhook_url:
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
webhook_url: 'http://slack.webhook.url',
|
|
70
|
+
backtrace_lines: 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Slack::Notifier.any_instance.expects(:ping).with('', fake_notification(@exception, {}, nil, 1))
|
|
74
|
+
|
|
75
|
+
slack_notifier = ExceptionNotifier::SlackNotifier.new(options)
|
|
76
|
+
slack_notifier.call(@exception)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
test 'should send the notification with additional fields' do
|
|
80
|
+
field = { title: 'Branch', value: 'master', short: true }
|
|
81
|
+
options = {
|
|
82
|
+
webhook_url: 'http://slack.webhook.url',
|
|
83
|
+
additional_fields: [field]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Slack::Notifier.any_instance.expects(:ping).with('', fake_notification(@exception, {}, nil, 10, [field]))
|
|
87
|
+
|
|
88
|
+
slack_notifier = ExceptionNotifier::SlackNotifier.new(options)
|
|
89
|
+
slack_notifier.call(@exception)
|
|
90
|
+
|
|
91
|
+
additional_fields = slack_notifier.notifier.config.defaults[:additional_fields]
|
|
92
|
+
assert_equal additional_fields, options[:additional_fields]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
test 'should pass the additional parameters to Slack::Notifier.ping' do
|
|
96
|
+
options = {
|
|
97
|
+
webhook_url: 'http://slack.webhook.url',
|
|
98
|
+
username: 'test',
|
|
99
|
+
custom_hook: 'hook',
|
|
68
100
|
additional_parameters: {
|
|
69
|
-
icon_url:
|
|
101
|
+
icon_url: 'icon'
|
|
70
102
|
}
|
|
71
103
|
}
|
|
72
104
|
|
|
73
|
-
Slack::Notifier.any_instance.expects(:ping).with('', options[:additional_parameters].merge(fake_notification)
|
|
105
|
+
Slack::Notifier.any_instance.expects(:ping).with('', options[:additional_parameters].merge(fake_notification))
|
|
74
106
|
|
|
75
107
|
slack_notifier = ExceptionNotifier::SlackNotifier.new(options)
|
|
76
108
|
slack_notifier.call(@exception)
|
|
@@ -85,55 +117,57 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
|
85
117
|
assert_nil slack_notifier.call(@exception)
|
|
86
118
|
end
|
|
87
119
|
|
|
88
|
-
test
|
|
120
|
+
test 'should pass along environment data' do
|
|
89
121
|
options = {
|
|
90
|
-
webhook_url:
|
|
91
|
-
ignore_data_if: lambda {|k,v|
|
|
92
|
-
|
|
122
|
+
webhook_url: 'http://slack.webhook.url',
|
|
123
|
+
ignore_data_if: lambda { |k, v|
|
|
124
|
+
k.to_s == 'key_to_be_ignored' || v.is_a?(Hash)
|
|
93
125
|
}
|
|
94
126
|
}
|
|
95
127
|
|
|
96
128
|
notification_options = {
|
|
97
129
|
env: {
|
|
98
|
-
'exception_notifier.exception_data' => {foo: 'bar', john: 'doe'}
|
|
130
|
+
'exception_notifier.exception_data' => { foo: 'bar', john: 'doe' }
|
|
99
131
|
},
|
|
100
132
|
data: {
|
|
101
|
-
'user_id'
|
|
133
|
+
'user_id' => 5,
|
|
102
134
|
'key_to_be_ignored' => 'whatever',
|
|
103
|
-
'ignore_as_well'
|
|
135
|
+
'ignore_as_well' => { what: 'ever' }
|
|
104
136
|
}
|
|
105
137
|
}
|
|
106
138
|
|
|
107
139
|
expected_data_string = "foo: bar\njohn: doe\nuser_id: 5"
|
|
108
140
|
|
|
109
|
-
Slack::Notifier.any_instance
|
|
141
|
+
Slack::Notifier.any_instance
|
|
142
|
+
.expects(:ping)
|
|
143
|
+
.with('', fake_notification(@exception, notification_options, expected_data_string))
|
|
110
144
|
slack_notifier = ExceptionNotifier::SlackNotifier.new(options)
|
|
111
145
|
slack_notifier.call(@exception, notification_options)
|
|
112
146
|
end
|
|
113
147
|
|
|
114
|
-
test
|
|
148
|
+
test 'should call pre/post_callback proc if specified' do
|
|
115
149
|
post_callback_called = 0
|
|
116
150
|
options = {
|
|
117
|
-
webhook_url:
|
|
118
|
-
username:
|
|
119
|
-
custom_hook:
|
|
120
|
-
:
|
|
121
|
-
(message_opts[:attachments] = []) << { text:
|
|
151
|
+
webhook_url: 'http://slack.webhook.url',
|
|
152
|
+
username: 'test',
|
|
153
|
+
custom_hook: 'hook',
|
|
154
|
+
pre_callback: proc { |_opts, _notifier, backtrace, _message, message_opts|
|
|
155
|
+
(message_opts[:attachments] = []) << { text: backtrace.join("\n").to_s, color: 'danger' }
|
|
122
156
|
},
|
|
123
|
-
:
|
|
157
|
+
post_callback: proc { |_opts, _notifier, _backtrace, _message, _message_opts|
|
|
124
158
|
post_callback_called = 1
|
|
125
159
|
},
|
|
126
160
|
additional_parameters: {
|
|
127
|
-
icon_url:
|
|
161
|
+
icon_url: 'icon'
|
|
128
162
|
}
|
|
129
163
|
}
|
|
130
164
|
|
|
131
165
|
Slack::Notifier.any_instance.expects(:ping).with('',
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
]
|
|
166
|
+
icon_url: 'icon',
|
|
167
|
+
attachments: [{
|
|
168
|
+
text: fake_backtrace.join("\n"),
|
|
169
|
+
color: 'danger'
|
|
170
|
+
}])
|
|
137
171
|
|
|
138
172
|
slack_notifier = ExceptionNotifier::SlackNotifier.new(options)
|
|
139
173
|
slack_notifier.call(@exception)
|
|
@@ -143,11 +177,9 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
|
143
177
|
private
|
|
144
178
|
|
|
145
179
|
def fake_exception
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
e
|
|
150
|
-
end
|
|
180
|
+
5 / 0
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
e
|
|
151
183
|
end
|
|
152
184
|
|
|
153
185
|
def fake_exception_without_backtrace
|
|
@@ -156,24 +188,42 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
|
156
188
|
|
|
157
189
|
def fake_backtrace
|
|
158
190
|
[
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"backtrace line 3",
|
|
162
|
-
"backtrace line 4",
|
|
163
|
-
"backtrace line 5",
|
|
164
|
-
"backtrace line 6",
|
|
191
|
+
'backtrace line 1', 'backtrace line 2', 'backtrace line 3',
|
|
192
|
+
'backtrace line 4', 'backtrace line 5', 'backtrace line 6'
|
|
165
193
|
]
|
|
166
194
|
end
|
|
167
195
|
|
|
168
|
-
def
|
|
169
|
-
|
|
196
|
+
def fake_cleaned_backtrace
|
|
197
|
+
fake_backtrace[2..-1]
|
|
198
|
+
end
|
|
170
199
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
fields.push({ title: 'Backtrace', value: "```#{fake_backtrace.join("\n")}```" }) if exception.backtrace
|
|
174
|
-
fields.push({ title: 'Data', value: "```#{data_string}```" }) if data_string
|
|
200
|
+
def fake_notification(exception = @exception, notification_options = {},
|
|
201
|
+
data_string = nil, expected_backtrace_lines = 10, additional_fields = [])
|
|
175
202
|
|
|
176
|
-
{
|
|
177
|
-
|
|
203
|
+
exception_name = "*#{exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'}* `#{exception.class}`"
|
|
204
|
+
if notification_options[:env].nil?
|
|
205
|
+
text = "#{exception_name} *occured in background*"
|
|
206
|
+
else
|
|
207
|
+
env = notification_options[:env]
|
|
178
208
|
|
|
209
|
+
kontroller = env['action_controller.instance']
|
|
210
|
+
request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>"
|
|
211
|
+
|
|
212
|
+
text = "#{exception_name} *occurred while* `#{request}`"
|
|
213
|
+
text += " *was processed by* `#{kontroller.controller_name}##{kontroller.action_name}`" if kontroller
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
text += "\n"
|
|
217
|
+
|
|
218
|
+
fields = [{ title: 'Exception', value: exception.message }]
|
|
219
|
+
fields.push(title: 'Hostname', value: 'example.com')
|
|
220
|
+
if exception.backtrace
|
|
221
|
+
formatted_backtrace = "```#{fake_cleaned_backtrace.first(expected_backtrace_lines).join("\n")}```"
|
|
222
|
+
fields.push(title: 'Backtrace', value: formatted_backtrace)
|
|
223
|
+
end
|
|
224
|
+
fields.push(title: 'Data', value: "```#{data_string}```") if data_string
|
|
225
|
+
additional_fields.each { |f| fields.push(f) }
|
|
226
|
+
|
|
227
|
+
{ attachments: [color: 'danger', text: text, fields: fields, mrkdwn_in: %w[text fields]] }
|
|
228
|
+
end
|
|
179
229
|
end
|