exception_notification 3.0.1 → 4.4.0

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