exception_notification 4.2.1 → 4.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.rdoc +7 -0
- data/CONTRIBUTING.md +3 -2
- data/README.md +48 -3
- data/exception_notification.gemspec +2 -2
- data/lib/exception_notification/rack.rb +9 -0
- data/lib/exception_notification/rails.rb +1 -0
- data/lib/exception_notifier.rb +7 -0
- data/lib/exception_notifier/campfire_notifier.rb +5 -1
- data/lib/exception_notifier/email_notifier.rb +17 -11
- data/lib/exception_notifier/hipchat_notifier.rb +8 -3
- data/lib/exception_notifier/irc_notifier.rb +4 -0
- data/lib/exception_notifier/mattermost_notifier.rb +2 -1
- data/lib/exception_notifier/modules/error_grouping.rb +77 -0
- data/lib/exception_notifier/slack_notifier.rb +12 -6
- data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +0 -2
- data/test/dummy/test/functional/posts_controller_test.rb +20 -1
- data/test/exception_notification/rack_test.rb +24 -0
- data/test/exception_notifier/campfire_notifier_test.rb +14 -0
- data/test/exception_notifier/email_notifier_test.rb +14 -0
- data/test/exception_notifier/hipchat_notifier_test.rb +39 -1
- data/test/exception_notifier/irc_notifier_test.rb +16 -0
- data/test/exception_notifier/mattermost_notifier_test.rb +17 -0
- data/test/exception_notifier/modules/error_grouping_test.rb +166 -0
- data/test/exception_notifier/slack_notifier_test.rb +23 -4
- data/test/exception_notifier_test.rb +57 -12
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5eab5531d309930d48027a778d74f8491a5a995
|
4
|
+
data.tar.gz: 67d0b4176fb7decc436754ee60e7f4a9053d48f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f92d01eee01a295d84e28bcaff7618edb3a4a626afe56a447974579115d8daf4d3e6d516c1c5bb449b88f53497effafa479c6a46bb614b9cb1eed04f5510211
|
7
|
+
data.tar.gz: ca3718addeb5deb323a4f73c09d0678534381301a4fc85ac648fef406beafccb844500039c1c805d6f20d1ef897696a6f92f94631a6be25ccbc468899ef69ec8
|
data/CHANGELOG.rdoc
CHANGED
data/CONTRIBUTING.md
CHANGED
@@ -30,8 +30,9 @@ should submit a Pull Request!
|
|
30
30
|
bundle
|
31
31
|
cd test/dummy
|
32
32
|
bundle
|
33
|
-
bundle exec rake db:reset
|
34
|
-
|
33
|
+
bundle exec rake db:reset db:test:prepare
|
34
|
+
cd ../..
|
35
|
+
bundle exec rake test
|
35
36
|
```
|
36
37
|
* Create a topic branch from where you want to base your work.
|
37
38
|
* Add a test for your change. Only refactoring and documentation changes
|
data/README.md
CHANGED
@@ -82,7 +82,7 @@ Options -> sections" below.
|
|
82
82
|
|
83
83
|
## Notifiers
|
84
84
|
|
85
|
-
ExceptionNotification relies on notifiers to deliver notifications when errors occur in your applications. By default,
|
85
|
+
ExceptionNotification relies on notifiers to deliver notifications when errors occur in your applications. By default, 7 notifiers are available:
|
86
86
|
|
87
87
|
* [Campfire notifier](#campfire-notifier)
|
88
88
|
* [Email notifier](#email-notifier)
|
@@ -279,6 +279,12 @@ If enabled, include the exception message in the subject. Use `:verbose_subject
|
|
279
279
|
|
280
280
|
If enabled, remove numbers from subject so they thread as a single one. Use `:normalize_subject => true` to enable it.
|
281
281
|
|
282
|
+
##### include_controller_and_action_names_in_subject
|
283
|
+
|
284
|
+
*Boolean, default: true*
|
285
|
+
|
286
|
+
If enabled, include the controller and action names in the subject. Use `:include_controller_and_action_names_in_subject => false` to exclude them.
|
287
|
+
|
282
288
|
##### email_format
|
283
289
|
|
284
290
|
*Symbol, default: :text*
|
@@ -393,6 +399,12 @@ Color of the message. Default : 'red'.
|
|
393
399
|
|
394
400
|
Message will appear from this nickname. Default : 'Exception'.
|
395
401
|
|
402
|
+
##### server_url
|
403
|
+
|
404
|
+
*String, optional*
|
405
|
+
|
406
|
+
Custom Server URL for self-hosted, Enterprise HipChat Server
|
407
|
+
|
396
408
|
For all options & possible values see [Hipchat API](https://www.hipchat.com/docs/api/method/rooms/message).
|
397
409
|
|
398
410
|
### IRC notifier
|
@@ -590,6 +602,12 @@ more information. Default: 'incoming-webhook'
|
|
590
602
|
|
591
603
|
Contains additional payload for a message (e.g avatar, attachments, etc). See [slack-notifier](https://github.com/stevenosloan/slack-notifier#additional-parameters) for more information.. Default: '{}'
|
592
604
|
|
605
|
+
##### additional_fields
|
606
|
+
|
607
|
+
*Array of Hashes, optional*
|
608
|
+
|
609
|
+
Contains additional fields that will be added to the attachement. See [Slack documentation](https://api.slack.com/docs/message-attachments).
|
610
|
+
|
593
611
|
## Mattermost notifier
|
594
612
|
|
595
613
|
Post notification in a mattermost channel via [incoming webhook](http://docs.mattermost.com/developer/webhooks-incoming.html)
|
@@ -600,7 +618,7 @@ Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gem
|
|
600
618
|
gem 'httparty'
|
601
619
|
```
|
602
620
|
|
603
|
-
To configure it, you **need** to set the `webhook_url` option.
|
621
|
+
To configure it, you **need** to set the `webhook_url` option.
|
604
622
|
You can also specify an other channel with `channel` option.
|
605
623
|
|
606
624
|
```ruby
|
@@ -616,7 +634,7 @@ Rails.application.config.middleware.use ExceptionNotification::Rack,
|
|
616
634
|
}
|
617
635
|
```
|
618
636
|
|
619
|
-
If you are using Github or Gitlab for issues tracking, you can specify `git_url` as follow to add a *Create issue* link in you notification.
|
637
|
+
If you are using Github or Gitlab for issues tracking, you can specify `git_url` as follow to add a *Create issue* link in you notification.
|
620
638
|
By default this will use your Rails application name to match the git repository. If yours differ you can specify `app_name`.
|
621
639
|
|
622
640
|
|
@@ -810,6 +828,33 @@ Rails.application.config.middleware.use ExceptionNotification::Rack,
|
|
810
828
|
}
|
811
829
|
```
|
812
830
|
|
831
|
+
## Error Grouping
|
832
|
+
In general, exception notification will send every notification when an error occured, which may result in a problem: if your site has a high throughput and an same error raised frequently, you will receive too many notifications during a short period time, your mail box may be full of thousands of exception mails or even your mail server will be slow. To prevent this, you can choose to error errors by using `:error_grouping` option and set it to `true`.
|
833
|
+
|
834
|
+
Error grouping has a default formula `log2(errors_count)` to determine if it is needed to send the notification based on the accumulated errors count for specified exception, this makes the notifier only send notification when count is: 1, 2, 4, 8, 16, 32, 64, 128, ... (2**n). You can use `:notification_trigger` to override this default formula.
|
835
|
+
|
836
|
+
The below shows options used to enable error grouping:
|
837
|
+
|
838
|
+
```ruby
|
839
|
+
Rails.application.config.middleware.use ExceptionNotification::Rack,
|
840
|
+
:ignore_exceptions => ['ActionView::TemplateError'] + ExceptionNotifier.ignored_exceptions,
|
841
|
+
:email => {
|
842
|
+
:email_prefix => "[PREFIX] ",
|
843
|
+
:sender_address => %{"notifier" <notifier@example.com>},
|
844
|
+
:exception_recipients => %w{exceptions@example.com}
|
845
|
+
},
|
846
|
+
:error_grouping => true,
|
847
|
+
# :error_grouping_period => 5.minutes, # the time before an error is regarded as fixed
|
848
|
+
# :error_grouping_cache => Rails.cache, # for other applications such as Sinatra, use one instance of ActiveSupport::Cache::Store
|
849
|
+
#
|
850
|
+
# notification_trigger: specify a callback to determine when a notification should be sent,
|
851
|
+
# the callback will be invoked with two arguments:
|
852
|
+
# exception: the exception raised
|
853
|
+
# count: accumulated errors count for this exception
|
854
|
+
#
|
855
|
+
# :notification_trigger => lambda { |exception, count| count % 10 == 0 }
|
856
|
+
```
|
857
|
+
|
813
858
|
## Ignore Exceptions
|
814
859
|
|
815
860
|
You can choose to ignore certain exceptions, which will make ExceptionNotification avoid sending notifications for those specified. There are three ways of specifying which exceptions to ignore:
|
@@ -1,8 +1,8 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'exception_notification'
|
3
|
-
s.version = '4.2.
|
3
|
+
s.version = '4.2.2'
|
4
4
|
s.authors = ["Jamis Buck", "Josh Peek"]
|
5
|
-
s.date = %q{
|
5
|
+
s.date = %q{2017-08-12}
|
6
6
|
s.summary = "Exception notification for Rails apps"
|
7
7
|
s.homepage = "https://smartinez87.github.io/exception_notification/"
|
8
8
|
s.email = "smartinez87@gmail.com"
|
@@ -6,6 +6,15 @@ module ExceptionNotification
|
|
6
6
|
@app = app
|
7
7
|
|
8
8
|
ExceptionNotifier.ignored_exceptions = options.delete(:ignore_exceptions) if options.key?(:ignore_exceptions)
|
9
|
+
ExceptionNotifier.error_grouping = options.delete(:error_grouping) if options.key?(:error_grouping)
|
10
|
+
ExceptionNotifier.error_grouping_period = options.delete(:error_grouping_period) if options.key?(:error_grouping_period)
|
11
|
+
ExceptionNotifier.notification_trigger = options.delete(:notification_trigger) if options.key?(:notification_trigger)
|
12
|
+
|
13
|
+
if options.key?(:error_grouping_cache)
|
14
|
+
ExceptionNotifier.error_grouping_cache = options.delete(:error_grouping_cache)
|
15
|
+
elsif defined?(Rails)
|
16
|
+
ExceptionNotifier.error_grouping_cache = Rails.cache
|
17
|
+
end
|
9
18
|
|
10
19
|
if options.key?(:ignore_if)
|
11
20
|
rack_ignore = options.delete(:ignore_if)
|
@@ -2,6 +2,7 @@ module ExceptionNotification
|
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
config.exception_notification = ExceptionNotifier
|
4
4
|
config.exception_notification.logger = Rails.logger
|
5
|
+
config.exception_notification.error_grouping_cache = Rails.cache
|
5
6
|
|
6
7
|
config.app_middleware.use ExceptionNotification::Rack
|
7
8
|
end
|
data/lib/exception_notifier.rb
CHANGED
@@ -2,8 +2,10 @@ require 'logger'
|
|
2
2
|
require 'active_support/core_ext/string/inflections'
|
3
3
|
require 'active_support/core_ext/module/attribute_accessors'
|
4
4
|
require 'exception_notifier/base_notifier'
|
5
|
+
require 'exception_notifier/modules/error_grouping'
|
5
6
|
|
6
7
|
module ExceptionNotifier
|
8
|
+
include ErrorGrouping
|
7
9
|
|
8
10
|
autoload :BacktraceCleaner, 'exception_notifier/modules/backtrace_cleaner'
|
9
11
|
|
@@ -43,6 +45,11 @@ module ExceptionNotifier
|
|
43
45
|
def notify_exception(exception, options={})
|
44
46
|
return false if ignored_exception?(options[:ignore_exceptions], exception)
|
45
47
|
return false if ignored?(exception, options)
|
48
|
+
if error_grouping
|
49
|
+
errors_count = group_error!(exception, options)
|
50
|
+
return false unless send_notification?(exception, errors_count)
|
51
|
+
end
|
52
|
+
|
46
53
|
selected_notifiers = options.delete(:notifiers) || notifiers
|
47
54
|
[*selected_notifiers].each do |notifier|
|
48
55
|
fire_notification(notifier, exception, options.dup)
|
@@ -19,7 +19,11 @@ module ExceptionNotifier
|
|
19
19
|
|
20
20
|
def call(exception, options={})
|
21
21
|
if active?
|
22
|
-
message =
|
22
|
+
message = if options[:accumulated_errors_count].to_i > 1
|
23
|
+
"The exception occurred #{options[:accumulated_errors_count]} times: '#{exception.message}'"
|
24
|
+
else
|
25
|
+
"A new exception occurred: '#{exception.message}'"
|
26
|
+
end
|
23
27
|
message += " on '#{exception.backtrace.first}'" if exception.backtrace
|
24
28
|
send_notice(exception, options, message) do |msg, _|
|
25
29
|
@room.paste msg
|
@@ -9,8 +9,8 @@ module ExceptionNotifier
|
|
9
9
|
attr_accessor(:sender_address, :exception_recipients,
|
10
10
|
:pre_callback, :post_callback,
|
11
11
|
:email_prefix, :email_format, :sections, :background_sections,
|
12
|
-
:verbose_subject, :normalize_subject, :
|
13
|
-
:email_headers, :mailer_parent, :template_path, :deliver_with)
|
12
|
+
:verbose_subject, :normalize_subject, :include_controller_and_action_names_in_subject,
|
13
|
+
:delivery_method, :mailer_settings, :email_headers, :mailer_parent, :template_path, :deliver_with)
|
14
14
|
|
15
15
|
module Mailer
|
16
16
|
class MissingController
|
@@ -46,7 +46,7 @@ module ExceptionNotifier
|
|
46
46
|
load_custom_views
|
47
47
|
|
48
48
|
@exception = exception
|
49
|
-
@options = options.reverse_merge(default_options)
|
49
|
+
@options = options.reverse_merge(default_options).symbolize_keys
|
50
50
|
@backtrace = exception.backtrace || []
|
51
51
|
@timestamp = Time.current
|
52
52
|
@sections = @options[:background_sections]
|
@@ -60,7 +60,8 @@ module ExceptionNotifier
|
|
60
60
|
|
61
61
|
def compose_subject
|
62
62
|
subject = "#{@options[:email_prefix]}"
|
63
|
-
subject << "#{@
|
63
|
+
subject << "(#{@options[:accumulated_errors_count]} times) " if @options[:accumulated_errors_count].to_i > 1
|
64
|
+
subject << "#{@kontroller.controller_name}##{@kontroller.action_name}" if @kontroller && @options[:include_controller_and_action_names_in_subject]
|
64
65
|
subject << " (#{@exception.class})"
|
65
66
|
subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
|
66
67
|
subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
|
@@ -74,13 +75,17 @@ module ExceptionNotifier
|
|
74
75
|
end
|
75
76
|
|
76
77
|
helper_method :inspect_object
|
77
|
-
|
78
|
+
|
79
|
+
def truncate(string, max)
|
80
|
+
string.length > max ? "#{string[0...max]}..." : string
|
81
|
+
end
|
82
|
+
|
78
83
|
def inspect_object(object)
|
79
84
|
case object
|
80
85
|
when Hash, Array
|
81
|
-
object.inspect
|
86
|
+
truncate(object.inspect, 300)
|
82
87
|
else
|
83
|
-
|
88
|
+
object.to_s
|
84
89
|
end
|
85
90
|
end
|
86
91
|
|
@@ -138,10 +143,10 @@ module ExceptionNotifier
|
|
138
143
|
options[:mailer_settings] = options.delete(mailer_settings_key)
|
139
144
|
|
140
145
|
options.reverse_merge(EmailNotifier.default_options).select{|k,v|[
|
141
|
-
:sender_address, :exception_recipients,
|
142
|
-
:
|
143
|
-
:
|
144
|
-
:
|
146
|
+
:sender_address, :exception_recipients, :pre_callback,
|
147
|
+
:post_callback, :email_prefix, :email_format,
|
148
|
+
:sections, :background_sections, :verbose_subject, :normalize_subject,
|
149
|
+
:include_controller_and_action_names_in_subject, :delivery_method, :mailer_settings,
|
145
150
|
:email_headers, :mailer_parent, :template_path, :deliver_with].include?(k)}.each{|k,v| send("#{k}=", v)}
|
146
151
|
end
|
147
152
|
|
@@ -197,6 +202,7 @@ module ExceptionNotifier
|
|
197
202
|
:background_sections => %w(backtrace data),
|
198
203
|
:verbose_subject => true,
|
199
204
|
:normalize_subject => false,
|
205
|
+
:include_controller_and_action_names_in_subject => true,
|
200
206
|
:delivery_method => nil,
|
201
207
|
:mailer_settings => nil,
|
202
208
|
:email_headers => {},
|
@@ -13,10 +13,15 @@ module ExceptionNotifier
|
|
13
13
|
opts = {
|
14
14
|
:api_version => options.delete(:api_version) || 'v1'
|
15
15
|
}
|
16
|
+
opts[:server_url] = options.delete(:server_url) if options[:server_url]
|
16
17
|
@from = options.delete(:from) || 'Exception'
|
17
18
|
@room = HipChat::Client.new(api_token, opts)[room_name]
|
18
|
-
@message_template = options.delete(:message_template) || ->(exception) {
|
19
|
-
msg =
|
19
|
+
@message_template = options.delete(:message_template) || ->(exception, errors_count) {
|
20
|
+
msg = if errors_count > 1
|
21
|
+
"The exception occurred #{errors_count} times: '#{Rack::Utils.escape_html(exception.message)}'"
|
22
|
+
else
|
23
|
+
"A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}'"
|
24
|
+
end
|
20
25
|
msg += " on '#{exception.backtrace.first}'" if exception.backtrace
|
21
26
|
msg
|
22
27
|
}
|
@@ -30,7 +35,7 @@ module ExceptionNotifier
|
|
30
35
|
def call(exception, options={})
|
31
36
|
return if !active?
|
32
37
|
|
33
|
-
message = @message_template.call(exception)
|
38
|
+
message = @message_template.call(exception, options[:accumulated_errors_count].to_i)
|
34
39
|
send_notice(exception, options, message, @message_options) do |msg, message_opts|
|
35
40
|
@room.send(@from, msg, message_opts)
|
36
41
|
end
|
@@ -7,7 +7,11 @@ module ExceptionNotifier
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def call(exception, options={})
|
10
|
+
errors_count = options[:accumulated_errors_count].to_i
|
11
|
+
|
10
12
|
message = "'#{exception.message}'"
|
13
|
+
message.prepend("(#{errors_count} times)") if errors_count > 1
|
14
|
+
|
11
15
|
message += " on '#{exception.backtrace.first}'" if exception.backtrace
|
12
16
|
if active?
|
13
17
|
send_notice(exception, options, message) do |msg, _|
|
@@ -91,8 +91,9 @@ module ExceptionNotifier
|
|
91
91
|
def message_header
|
92
92
|
text = []
|
93
93
|
|
94
|
+
errors_count = @options[:accumulated_errors_count].to_i
|
94
95
|
text << "### :warning: Error 500 in #{Rails.env} :warning:"
|
95
|
-
text << "An *#{@exception.class}* occured" + if @controller then " in *#{controller_and_method}*." else "." end
|
96
|
+
text << "#{errors_count > 1 ? errors_count : 'An'} *#{@exception.class}* occured" + if @controller then " in *#{controller_and_method}*." else "." end
|
96
97
|
text << "*#{@exception.message}*"
|
97
98
|
|
98
99
|
text
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'active_support/core_ext/numeric/time'
|
2
|
+
require 'active_support/concern'
|
3
|
+
|
4
|
+
module ExceptionNotifier
|
5
|
+
module ErrorGrouping
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
mattr_accessor :error_grouping
|
10
|
+
self.error_grouping = false
|
11
|
+
|
12
|
+
mattr_accessor :error_grouping_period
|
13
|
+
self.error_grouping_period = 5.minutes
|
14
|
+
|
15
|
+
mattr_accessor :notification_trigger
|
16
|
+
|
17
|
+
mattr_accessor :error_grouping_cache
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
# Fallback to the memory store while the specified cache store doesn't work
|
22
|
+
#
|
23
|
+
def fallback_cache_store
|
24
|
+
@fallback_cache_store ||= ActiveSupport::Cache::MemoryStore.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def error_count(error_key)
|
28
|
+
count = begin
|
29
|
+
error_grouping_cache.read(error_key)
|
30
|
+
rescue => e
|
31
|
+
ExceptionNotifier.logger.warn("#{error_grouping_cache.inspect} failed to read, reason: #{e.message}. Falling back to memory cache store.")
|
32
|
+
fallback_cache_store.read(error_key)
|
33
|
+
end
|
34
|
+
|
35
|
+
count.to_i if count
|
36
|
+
end
|
37
|
+
|
38
|
+
def save_error_count(error_key, count)
|
39
|
+
error_grouping_cache.write(error_key, count, expires_in: error_grouping_period)
|
40
|
+
rescue => e
|
41
|
+
ExceptionNotifier.logger.warn("#{error_grouping_cache.inspect} failed to write, reason: #{e.message}. Falling back to memory cache store.")
|
42
|
+
fallback_cache_store.write(error_key, count, expires_in: error_grouping_period)
|
43
|
+
end
|
44
|
+
|
45
|
+
def group_error!(exception, options)
|
46
|
+
message_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\nmessage:#{exception.message}")}"
|
47
|
+
accumulated_errors_count = 1
|
48
|
+
|
49
|
+
if count = error_count(message_based_key)
|
50
|
+
accumulated_errors_count = count + 1
|
51
|
+
save_error_count(message_based_key, accumulated_errors_count)
|
52
|
+
else
|
53
|
+
backtrace_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
|
54
|
+
|
55
|
+
if count = Rails.cache.read(backtrace_based_key)
|
56
|
+
accumulated_errors_count = count + 1
|
57
|
+
save_error_count(backtrace_based_key, accumulated_errors_count)
|
58
|
+
else
|
59
|
+
save_error_count(backtrace_based_key, accumulated_errors_count)
|
60
|
+
save_error_count(message_based_key, accumulated_errors_count)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
options[:accumulated_errors_count] = accumulated_errors_count
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_notification?(exception, count)
|
68
|
+
if notification_trigger.respond_to?(:call)
|
69
|
+
notification_trigger.call(exception, count)
|
70
|
+
else
|
71
|
+
factor = Math.log2(count)
|
72
|
+
factor.to_i == factor
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -8,10 +8,12 @@ module ExceptionNotifier
|
|
8
8
|
super
|
9
9
|
begin
|
10
10
|
@ignore_data_if = options[:ignore_data_if]
|
11
|
-
@backtrace_lines = options
|
11
|
+
@backtrace_lines = options.fetch(:backtrace_lines, 10)
|
12
|
+
@additional_fields = options[:additional_fields]
|
12
13
|
|
13
14
|
webhook_url = options.fetch(:webhook_url)
|
14
15
|
@message_opts = options.fetch(:additional_parameters, {})
|
16
|
+
@color = @message_opts.delete(:color) { 'danger' }
|
15
17
|
@notifier = Slack::Notifier.new webhook_url, options
|
16
18
|
rescue
|
17
19
|
@notifier = nil
|
@@ -19,7 +21,9 @@ module ExceptionNotifier
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def call(exception, options={})
|
22
|
-
|
24
|
+
errors_count = options[:accumulated_errors_count].to_i
|
25
|
+
measure_word = errors_count > 1 ? errors_count : (exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A')
|
26
|
+
exception_name = "*#{measure_word}* `#{exception.class.to_s}`"
|
23
27
|
|
24
28
|
if options[:env].nil?
|
25
29
|
data = options[:data] || {}
|
@@ -30,18 +34,18 @@ module ExceptionNotifier
|
|
30
34
|
|
31
35
|
kontroller = env['action_controller.instance']
|
32
36
|
request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>"
|
33
|
-
text = "#{exception_name} *occurred while* `#{
|
37
|
+
text = "#{exception_name} *occurred while* `#{request}`"
|
34
38
|
text += " *was processed by* `#{kontroller.controller_name}##{kontroller.action_name}`" if kontroller
|
35
39
|
text += "\n"
|
36
40
|
end
|
37
41
|
|
38
42
|
clean_message = exception.message.gsub("`", "'")
|
39
|
-
fields = [ { title: 'Exception', value: clean_message} ]
|
43
|
+
fields = [ { title: 'Exception', value: clean_message } ]
|
40
44
|
|
41
45
|
fields.push({ title: 'Hostname', value: Socket.gethostname })
|
42
46
|
|
43
47
|
if exception.backtrace
|
44
|
-
formatted_backtrace =
|
48
|
+
formatted_backtrace = "```#{exception.backtrace.first(@backtrace_lines).join("\n")}```"
|
45
49
|
fields.push({ title: 'Backtrace', value: formatted_backtrace })
|
46
50
|
end
|
47
51
|
|
@@ -51,7 +55,9 @@ module ExceptionNotifier
|
|
51
55
|
fields.push({ title: 'Data', value: "```#{data_string}```" })
|
52
56
|
end
|
53
57
|
|
54
|
-
|
58
|
+
fields.concat(@additional_fields) if @additional_fields
|
59
|
+
|
60
|
+
attchs = [color: @color, text: text, fields: fields, mrkdwn_in: %w(text fields)]
|
55
61
|
|
56
62
|
if valid?
|
57
63
|
send_notice(exception, options, clean_message, @message_opts.merge(attachments: attchs)) do |msg, message_opts|
|
@@ -14,11 +14,9 @@
|
|
14
14
|
title = render("title", :title => section).strip
|
15
15
|
[title, summary]
|
16
16
|
end
|
17
|
-
|
18
17
|
rescue Exception => e
|
19
18
|
title = render("title", :title => section).strip
|
20
19
|
summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n")
|
21
|
-
|
22
20
|
[title, summary]
|
23
21
|
end
|
24
22
|
end
|
@@ -35,7 +35,7 @@ class PostsControllerTest < ActionController::TestCase
|
|
35
35
|
test "mail subject should have the proper prefix" do
|
36
36
|
assert_includes @mail.subject, "[Dummy ERROR]"
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
test "mail subject should include descriptive error message" do
|
40
40
|
assert_includes @mail.subject, "(NoMethodError) \"undefined method `nw'"
|
41
41
|
end
|
@@ -146,6 +146,25 @@ class PostsControllerTestWithoutVerboseSubject < ActionController::TestCase
|
|
146
146
|
end
|
147
147
|
end
|
148
148
|
|
149
|
+
class PostsControllerTestWithoutControllerAndActionNames < ActionController::TestCase
|
150
|
+
tests PostsController
|
151
|
+
setup do
|
152
|
+
@email_notifier = ExceptionNotifier::EmailNotifier.new(:include_controller_and_action_names_in_subject => false)
|
153
|
+
begin
|
154
|
+
post :create, method: :post
|
155
|
+
rescue => e
|
156
|
+
@exception = e
|
157
|
+
@mail = @email_notifier.create_email(@exception, {:env => request.env})
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
test "should include controller and action names in subject" do
|
162
|
+
assert_includes @mail.subject, '[ERROR]'
|
163
|
+
assert_includes @mail.subject, '(NoMethodError)'
|
164
|
+
refute_includes @mail.subject, 'posts#create'
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
149
168
|
class PostsControllerTestWithSmtpSettings < ActionController::TestCase
|
150
169
|
tests PostsController
|
151
170
|
setup do
|
@@ -5,6 +5,14 @@ class RackTest < ActiveSupport::TestCase
|
|
5
5
|
setup do
|
6
6
|
@pass_app = Object.new
|
7
7
|
@pass_app.stubs(:call).returns([nil, { 'X-Cascade' => 'pass' }, nil])
|
8
|
+
|
9
|
+
@normal_app = Object.new
|
10
|
+
@normal_app.stubs(:call).returns([nil, { }, nil])
|
11
|
+
end
|
12
|
+
|
13
|
+
teardown do
|
14
|
+
ExceptionNotifier.error_grouping = false
|
15
|
+
ExceptionNotifier.notification_trigger = nil
|
8
16
|
end
|
9
17
|
|
10
18
|
test "should ignore \"X-Cascade\" header by default" do
|
@@ -17,4 +25,20 @@ class RackTest < ActiveSupport::TestCase
|
|
17
25
|
ExceptionNotification::Rack.new(@pass_app, :ignore_cascade_pass => false).call({})
|
18
26
|
end
|
19
27
|
|
28
|
+
test "should assign error_grouping if error_grouping is specified" do
|
29
|
+
refute ExceptionNotifier.error_grouping
|
30
|
+
ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({})
|
31
|
+
assert ExceptionNotifier.error_grouping
|
32
|
+
end
|
33
|
+
|
34
|
+
test "should assign notification_trigger if notification_trigger is specified" do
|
35
|
+
assert_nil ExceptionNotifier.notification_trigger
|
36
|
+
ExceptionNotification::Rack.new(@normal_app, notification_trigger: lambda {|i| true}).call({})
|
37
|
+
assert_respond_to ExceptionNotifier.notification_trigger, :call
|
38
|
+
end
|
39
|
+
|
40
|
+
test "should set default cache to Rails cache" do
|
41
|
+
ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({})
|
42
|
+
assert_equal Rails.cache, ExceptionNotifier.error_grouping_cache
|
43
|
+
end
|
20
44
|
end
|
@@ -52,6 +52,20 @@ class CampfireNotifierTest < ActiveSupport::TestCase
|
|
52
52
|
assert_nil campfire.call(fake_exception)
|
53
53
|
end
|
54
54
|
|
55
|
+
test "should send the new exception message if no :accumulated_errors_count option" do
|
56
|
+
campfire = ExceptionNotifier::CampfireNotifier.new({})
|
57
|
+
campfire.stubs(:active?).returns(true)
|
58
|
+
campfire.expects(:send_notice).with{ |_, _, message| message.start_with?("A new exception occurred") }.once
|
59
|
+
campfire.call(fake_exception)
|
60
|
+
end
|
61
|
+
|
62
|
+
test "shoud send the exception message if :accumulated_errors_count option greater than 1" do
|
63
|
+
campfire = ExceptionNotifier::CampfireNotifier.new({})
|
64
|
+
campfire.stubs(:active?).returns(true)
|
65
|
+
campfire.expects(:send_notice).with{ |_, _, message| message.start_with?("The exception occurred 3 times:") }.once
|
66
|
+
campfire.call(fake_exception, accumulated_errors_count: 3)
|
67
|
+
end
|
68
|
+
|
55
69
|
test "should call pre/post_callback if specified" do
|
56
70
|
pre_callback_called, post_callback_called = 0,0
|
57
71
|
Tinder::Campfire.stubs(:new).returns(Object.new)
|
@@ -218,4 +218,18 @@ class EmailNotifierTest < ActiveSupport::TestCase
|
|
218
218
|
mail = email_notifier.call(@exception)
|
219
219
|
assert_equal %w{second@example.com}, mail.to
|
220
220
|
end
|
221
|
+
|
222
|
+
test "should prepend accumulated_errors_count in email subject if accumulated_errors_count larger than 1" do
|
223
|
+
ActionMailer::Base.deliveries.clear
|
224
|
+
|
225
|
+
email_notifier = ExceptionNotifier::EmailNotifier.new(
|
226
|
+
:email_prefix => '[Dummy ERROR] ',
|
227
|
+
:sender_address => %{"Dummy Notifier" <dummynotifier@example.com>},
|
228
|
+
:exception_recipients => %w{dummyexceptions@example.com},
|
229
|
+
:delivery_method => :test
|
230
|
+
)
|
231
|
+
|
232
|
+
mail = email_notifier.call(@exception, { accumulated_errors_count: 3 })
|
233
|
+
assert mail.subject.start_with?("[Dummy ERROR] (3 times) (ZeroDivisionError)")
|
234
|
+
end
|
221
235
|
end
|
@@ -101,7 +101,7 @@ class HipchatNotifierTest < ActiveSupport::TestCase
|
|
101
101
|
:api_token => 'good_token',
|
102
102
|
:room_name => 'room_name',
|
103
103
|
:color => 'yellow',
|
104
|
-
:message_template => ->(exception) { "This is custom message: '#{exception.message}'" }
|
104
|
+
:message_template => ->(exception, _) { "This is custom message: '#{exception.message}'" }
|
105
105
|
}
|
106
106
|
|
107
107
|
HipChat::Room.any_instance.expects(:send).with('Exception', "This is custom message: '#{fake_exception.message}'", { :color => 'yellow' })
|
@@ -110,6 +110,30 @@ class HipchatNotifierTest < ActiveSupport::TestCase
|
|
110
110
|
hipchat.call(fake_exception)
|
111
111
|
end
|
112
112
|
|
113
|
+
test "should send hipchat notification exclude accumulated errors count" do
|
114
|
+
options = {
|
115
|
+
:api_token => 'good_token',
|
116
|
+
:room_name => 'room_name',
|
117
|
+
:color => 'yellow'
|
118
|
+
}
|
119
|
+
|
120
|
+
HipChat::Room.any_instance.expects(:send).with{ |_, msg, _| msg.start_with?("A new exception occurred:") }
|
121
|
+
hipchat = ExceptionNotifier::HipchatNotifier.new(options)
|
122
|
+
hipchat.call(fake_exception)
|
123
|
+
end
|
124
|
+
|
125
|
+
test "should send hipchat notification include accumulated errors count" do
|
126
|
+
options = {
|
127
|
+
:api_token => 'good_token',
|
128
|
+
:room_name => 'room_name',
|
129
|
+
:color => 'yellow'
|
130
|
+
}
|
131
|
+
|
132
|
+
HipChat::Room.any_instance.expects(:send).with{ |_, msg, _| msg.start_with?("The exception occurred 3 times:") }
|
133
|
+
hipchat = ExceptionNotifier::HipchatNotifier.new(options)
|
134
|
+
hipchat.call(fake_exception, { accumulated_errors_count: 3 })
|
135
|
+
end
|
136
|
+
|
113
137
|
test "should send hipchat notification with HTML-escaped meessage if using default message_template" do
|
114
138
|
options = {
|
115
139
|
:api_token => 'good_token',
|
@@ -151,6 +175,20 @@ class HipchatNotifierTest < ActiveSupport::TestCase
|
|
151
175
|
hipchat.call(fake_exception)
|
152
176
|
end
|
153
177
|
|
178
|
+
test "should allow server_url value (for a self-hosted HipChat Server) if set" do
|
179
|
+
options = {
|
180
|
+
:api_token => 'good_token',
|
181
|
+
:room_name => 'room_name',
|
182
|
+
:api_version => 'v2',
|
183
|
+
:server_url => 'https://domain.com',
|
184
|
+
}
|
185
|
+
|
186
|
+
HipChat::Client.stubs(:new).with('good_token', {:api_version => 'v2', :server_url => 'https://domain.com'}).returns({})
|
187
|
+
|
188
|
+
hipchat = ExceptionNotifier::HipchatNotifier.new(options)
|
189
|
+
hipchat.call(fake_exception)
|
190
|
+
end
|
191
|
+
|
154
192
|
private
|
155
193
|
|
156
194
|
def fake_body
|
@@ -16,6 +16,22 @@ class IrcNotifierTest < ActiveSupport::TestCase
|
|
16
16
|
irc.call(fake_exception)
|
17
17
|
end
|
18
18
|
|
19
|
+
test "should exclude errors count in message if :accumulated_errors_count nil" do
|
20
|
+
irc = ExceptionNotifier::IrcNotifier.new({})
|
21
|
+
irc.stubs(:active?).returns(true)
|
22
|
+
|
23
|
+
irc.expects(:send_message).with{ |message| message.include?("divided by 0") }.once
|
24
|
+
irc.call(fake_exception)
|
25
|
+
end
|
26
|
+
|
27
|
+
test "should include errors count in message if :accumulated_errors_count is 3" do
|
28
|
+
irc = ExceptionNotifier::IrcNotifier.new({})
|
29
|
+
irc.stubs(:active?).returns(true)
|
30
|
+
|
31
|
+
irc.expects(:send_message).with{ |message| message.include?("(3 times)'divided by 0'") }.once
|
32
|
+
irc.call(fake_exception, accumulated_errors_count: 3)
|
33
|
+
end
|
34
|
+
|
19
35
|
test "should call pre/post_callback if specified" do
|
20
36
|
pre_callback_called, post_callback_called = 0,0
|
21
37
|
|
@@ -77,6 +77,23 @@ class MattermostNotifierTest < ActiveSupport::TestCase
|
|
77
77
|
assert 'password', options[:basic_auth][:password]
|
78
78
|
end
|
79
79
|
|
80
|
+
test "should use 'An' for exceptions count if :accumulated_errors_count option is nil" do
|
81
|
+
mattermost_notifier = ExceptionNotifier::MattermostNotifier.new
|
82
|
+
exception = ArgumentError.new("foo")
|
83
|
+
mattermost_notifier.instance_variable_set(:@exception, exception)
|
84
|
+
mattermost_notifier.instance_variable_set(:@options, {})
|
85
|
+
|
86
|
+
assert_includes mattermost_notifier.send(:message_header), "An *ArgumentError* occured."
|
87
|
+
end
|
88
|
+
|
89
|
+
test "shoud use direct errors count if :accumulated_errors_count option is 5" do
|
90
|
+
mattermost_notifier = ExceptionNotifier::MattermostNotifier.new
|
91
|
+
exception = ArgumentError.new("foo")
|
92
|
+
mattermost_notifier.instance_variable_set(:@exception, exception)
|
93
|
+
mattermost_notifier.instance_variable_set(:@options, { accumulated_errors_count: 5 })
|
94
|
+
|
95
|
+
assert_includes mattermost_notifier.send(:message_header), "5 *ArgumentError* occured."
|
96
|
+
end
|
80
97
|
end
|
81
98
|
|
82
99
|
class FakeHTTParty
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ErrorGroupTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
setup do
|
6
|
+
module TestModule
|
7
|
+
include ExceptionNotifier::ErrorGrouping
|
8
|
+
@@error_grouping_cache = ActiveSupport::Cache::FileStore.new("test/dummy/tmp/cache")
|
9
|
+
end
|
10
|
+
|
11
|
+
@exception = RuntimeError.new("ERROR")
|
12
|
+
@exception.stubs(:backtrace).returns(["/path/where/error/raised:1"])
|
13
|
+
|
14
|
+
@exception2 = RuntimeError.new("ERROR2")
|
15
|
+
@exception2.stubs(:backtrace).returns(["/path/where/error/found:2"])
|
16
|
+
end
|
17
|
+
|
18
|
+
teardown do
|
19
|
+
TestModule.error_grouping_cache.clear
|
20
|
+
TestModule.fallback_cache_store.clear
|
21
|
+
end
|
22
|
+
|
23
|
+
test "should add additional option: error_grouping" do
|
24
|
+
assert_respond_to TestModule, :error_grouping
|
25
|
+
assert_respond_to TestModule, :error_grouping=
|
26
|
+
end
|
27
|
+
|
28
|
+
test "should set error_grouping to false default" do
|
29
|
+
assert_equal false, TestModule.error_grouping
|
30
|
+
end
|
31
|
+
|
32
|
+
test "should add additional option: error_grouping_cache" do
|
33
|
+
assert_respond_to TestModule, :error_grouping_cache
|
34
|
+
assert_respond_to TestModule, :error_grouping_cache=
|
35
|
+
end
|
36
|
+
|
37
|
+
test "should add additional option: error_grouping_period" do
|
38
|
+
assert_respond_to TestModule, :error_grouping_period
|
39
|
+
assert_respond_to TestModule, :error_grouping_period=
|
40
|
+
end
|
41
|
+
|
42
|
+
test "shoud set error_grouping_period to 5.minutes default" do
|
43
|
+
assert_equal 300, TestModule.error_grouping_period
|
44
|
+
end
|
45
|
+
|
46
|
+
test "should add additional option: notification_trigger" do
|
47
|
+
assert_respond_to TestModule, :notification_trigger
|
48
|
+
assert_respond_to TestModule, :notification_trigger=
|
49
|
+
end
|
50
|
+
|
51
|
+
test "should return errors count nil when not same error for .error_count" do
|
52
|
+
assert_nil TestModule.error_count("something")
|
53
|
+
end
|
54
|
+
|
55
|
+
test "should return errors count when same error for .error_count" do
|
56
|
+
TestModule.error_grouping_cache.write("error_key", 13)
|
57
|
+
assert_equal 13, TestModule.error_count("error_key")
|
58
|
+
end
|
59
|
+
|
60
|
+
test "should fallback to memory store cache if specified cache store failed to read" do
|
61
|
+
TestModule.error_grouping_cache.stubs(:read).raises(RuntimeError.new "Failed to read")
|
62
|
+
original_fallback = TestModule.fallback_cache_store
|
63
|
+
TestModule.expects(:fallback_cache_store).returns(original_fallback).at_least_once
|
64
|
+
|
65
|
+
assert_nil TestModule.error_count("something_to_read")
|
66
|
+
end
|
67
|
+
|
68
|
+
test "should save error with count for .save_error_count" do
|
69
|
+
count = rand(1..10)
|
70
|
+
|
71
|
+
TestModule.save_error_count("error_key", count)
|
72
|
+
assert_equal count, TestModule.error_grouping_cache.read("error_key")
|
73
|
+
end
|
74
|
+
|
75
|
+
test "should fallback to memory store cache if specified cache store failed to write" do
|
76
|
+
TestModule.error_grouping_cache.stubs(:write).raises(RuntimeError.new "Failed to write")
|
77
|
+
original_fallback = TestModule.fallback_cache_store
|
78
|
+
TestModule.expects(:fallback_cache_store).returns(original_fallback).at_least_once
|
79
|
+
|
80
|
+
assert TestModule.save_error_count("something_to_cache", rand(1..10))
|
81
|
+
end
|
82
|
+
|
83
|
+
test "should save accumulated_errors_count into options" do
|
84
|
+
options = {}
|
85
|
+
TestModule.group_error!(@exception, options)
|
86
|
+
|
87
|
+
assert_equal 1, options[:accumulated_errors_count]
|
88
|
+
end
|
89
|
+
|
90
|
+
test "should not group error if different exception in .group_error!" do
|
91
|
+
options1 = {}
|
92
|
+
TestModule.expects(:save_error_count).with{|key, count| key.is_a?(String) && count == 1}.times(4).returns(true)
|
93
|
+
TestModule.group_error!(@exception, options1)
|
94
|
+
|
95
|
+
options2 = {}
|
96
|
+
TestModule.group_error!(NoMethodError.new("method not found"), options2)
|
97
|
+
|
98
|
+
assert_equal 1, options1[:accumulated_errors_count]
|
99
|
+
assert_equal 1, options2[:accumulated_errors_count]
|
100
|
+
end
|
101
|
+
|
102
|
+
test "should not group error is same exception but different message or backtrace" do
|
103
|
+
options1 = {}
|
104
|
+
TestModule.expects(:save_error_count).with{|key, count| key.is_a?(String) && count == 1}.times(4).returns(true)
|
105
|
+
TestModule.group_error!(@exception, options1)
|
106
|
+
|
107
|
+
options2 = {}
|
108
|
+
TestModule.group_error!(@exception2, options2)
|
109
|
+
|
110
|
+
assert_equal 1, options1[:accumulated_errors_count]
|
111
|
+
assert_equal 1, options2[:accumulated_errors_count]
|
112
|
+
end
|
113
|
+
|
114
|
+
test "should group error if same exception and message" do
|
115
|
+
options = {}
|
116
|
+
|
117
|
+
10.times do |i|
|
118
|
+
@exception2.stubs(:backtrace).returns(["/path:#{i}"])
|
119
|
+
TestModule.group_error!(@exception2, options)
|
120
|
+
end
|
121
|
+
|
122
|
+
assert_equal 10, options[:accumulated_errors_count]
|
123
|
+
end
|
124
|
+
|
125
|
+
test "should group error if same exception and backtrace" do
|
126
|
+
options = {}
|
127
|
+
|
128
|
+
10.times do |i|
|
129
|
+
@exception2.stubs(:message).returns("ERRORS#{i}")
|
130
|
+
TestModule.group_error!(@exception2, options)
|
131
|
+
end
|
132
|
+
|
133
|
+
assert_equal 10, options[:accumulated_errors_count]
|
134
|
+
end
|
135
|
+
|
136
|
+
test "should group error by that message have high priority" do
|
137
|
+
message_based_key = "exception:#{Zlib.crc32("RuntimeError\nmessage:ERROR")}"
|
138
|
+
backtrace_based_key = "exception:#{Zlib.crc32("RuntimeError\n/path/where/error/raised:1")}"
|
139
|
+
|
140
|
+
TestModule.save_error_count(message_based_key, 1)
|
141
|
+
TestModule.save_error_count(backtrace_based_key, 1)
|
142
|
+
|
143
|
+
TestModule.expects(:save_error_count).with(message_based_key, 2).once
|
144
|
+
TestModule.expects(:save_error_count).with(backtrace_based_key, 2).never
|
145
|
+
|
146
|
+
TestModule.group_error!(@exception, {})
|
147
|
+
end
|
148
|
+
|
149
|
+
test "use default formula if not specify notification_trigger in .send_notification?" do
|
150
|
+
TestModule.stubs(:notification_trigger).returns(nil)
|
151
|
+
|
152
|
+
count = 16
|
153
|
+
Math.expects(:log2).with(count).returns(4)
|
154
|
+
|
155
|
+
assert TestModule.send_notification?(@exception, count)
|
156
|
+
end
|
157
|
+
|
158
|
+
test "use specified trigger in .send_notification?" do
|
159
|
+
trigger = Proc.new { |exception, count| count % 4 == 0 }
|
160
|
+
TestModule.stubs(:notification_trigger).returns(trigger)
|
161
|
+
|
162
|
+
count = 16
|
163
|
+
trigger.expects(:call).with(@exception, count).returns(true)
|
164
|
+
assert TestModule.send_notification?(@exception, count)
|
165
|
+
end
|
166
|
+
end
|
@@ -43,7 +43,8 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
43
43
|
slack_notifier = ExceptionNotifier::SlackNotifier.new(options)
|
44
44
|
slack_notifier.call(@exception)
|
45
45
|
|
46
|
-
|
46
|
+
channel = slack_notifier.notifier.config.defaults[:channel]
|
47
|
+
assert_equal channel, options[:channel]
|
47
48
|
end
|
48
49
|
|
49
50
|
test "should send the notification to the specified username" do
|
@@ -57,7 +58,8 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
57
58
|
slack_notifier = ExceptionNotifier::SlackNotifier.new(options)
|
58
59
|
slack_notifier.call(@exception)
|
59
60
|
|
60
|
-
|
61
|
+
username = slack_notifier.notifier.config.defaults[:username]
|
62
|
+
assert_equal username, options[:username]
|
61
63
|
end
|
62
64
|
|
63
65
|
test "should send the notification with specific backtrace lines" do
|
@@ -72,6 +74,22 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
72
74
|
slack_notifier.call(@exception)
|
73
75
|
end
|
74
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
|
+
|
75
93
|
test "should pass the additional parameters to Slack::Notifier.ping" do
|
76
94
|
options = {
|
77
95
|
webhook_url: "http://slack.webhook.url",
|
@@ -177,7 +195,7 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
177
195
|
]
|
178
196
|
end
|
179
197
|
|
180
|
-
def fake_notification(exception = @exception, notification_options = {}, data_string = nil, expected_backtrace_lines =
|
198
|
+
def fake_notification(exception = @exception, notification_options = {}, data_string = nil, expected_backtrace_lines = 10, additional_fields = [])
|
181
199
|
exception_name = "*#{exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'}* `#{exception.class.to_s}`"
|
182
200
|
if notification_options[:env].nil?
|
183
201
|
text = "#{exception_name} *occured in background*"
|
@@ -196,10 +214,11 @@ class SlackNotifierTest < ActiveSupport::TestCase
|
|
196
214
|
fields = [ { title: 'Exception', value: exception.message} ]
|
197
215
|
fields.push({ title: 'Hostname', value: 'example.com' })
|
198
216
|
if exception.backtrace
|
199
|
-
formatted_backtrace =
|
217
|
+
formatted_backtrace = "```#{exception.backtrace.first(expected_backtrace_lines).join("\n")}```"
|
200
218
|
fields.push({ title: 'Backtrace', value: formatted_backtrace })
|
201
219
|
end
|
202
220
|
fields.push({ title: 'Data', value: "```#{data_string}```" }) if data_string
|
221
|
+
additional_fields.each { |f| fields.push(f) }
|
203
222
|
|
204
223
|
{ attachments: [ color: 'danger', text: text, fields: fields, mrkdwn_in: %w(text fields) ] }
|
205
224
|
end
|
@@ -1,6 +1,21 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
+
class ExceptionOne < StandardError;end
|
4
|
+
class ExceptionTwo < StandardError;end
|
5
|
+
|
3
6
|
class ExceptionNotifierTest < ActiveSupport::TestCase
|
7
|
+
setup do
|
8
|
+
@notifier_calls = 0
|
9
|
+
@test_notifier = lambda { |exception, options| @notifier_calls += 1 }
|
10
|
+
end
|
11
|
+
|
12
|
+
teardown do
|
13
|
+
ExceptionNotifier.error_grouping = false
|
14
|
+
ExceptionNotifier.notification_trigger = nil
|
15
|
+
ExceptionNotifier.class_eval("@@notifiers.delete_if { |k, _| k.to_s != \"email\"}") # reset notifiers
|
16
|
+
Rails.cache.clear
|
17
|
+
end
|
18
|
+
|
4
19
|
test "should have default ignored exceptions" do
|
5
20
|
assert_equal ExceptionNotifier.ignored_exceptions,
|
6
21
|
['ActiveRecord::RecordNotFound', 'Mongoid::Errors::DocumentNotFound', 'AbstractController::ActionNotFound',
|
@@ -69,37 +84,67 @@ class ExceptionNotifierTest < ActiveSupport::TestCase
|
|
69
84
|
env != "production"
|
70
85
|
end
|
71
86
|
|
72
|
-
|
73
|
-
test_notifier = lambda { |exception, options| notifier_calls += 1 }
|
74
|
-
ExceptionNotifier.register_exception_notifier(:test, test_notifier)
|
87
|
+
ExceptionNotifier.register_exception_notifier(:test, @test_notifier)
|
75
88
|
|
76
89
|
exception = StandardError.new
|
77
90
|
|
78
91
|
ExceptionNotifier.notify_exception(exception, {:notifiers => :test})
|
79
|
-
assert_equal notifier_calls, 1
|
92
|
+
assert_equal @notifier_calls, 1
|
80
93
|
|
81
94
|
env = "development"
|
82
95
|
ExceptionNotifier.notify_exception(exception, {:notifiers => :test})
|
83
|
-
assert_equal notifier_calls, 1
|
96
|
+
assert_equal @notifier_calls, 1
|
84
97
|
|
85
98
|
ExceptionNotifier.clear_ignore_conditions!
|
86
|
-
ExceptionNotifier.unregister_exception_notifier(:test)
|
87
99
|
end
|
88
100
|
|
89
101
|
test "should not send notification if one of ignored exceptions" do
|
90
|
-
|
91
|
-
test_notifier = lambda { |exception, options| notifier_calls += 1 }
|
92
|
-
ExceptionNotifier.register_exception_notifier(:test, test_notifier)
|
102
|
+
ExceptionNotifier.register_exception_notifier(:test, @test_notifier)
|
93
103
|
|
94
104
|
exception = StandardError.new
|
95
105
|
|
96
106
|
ExceptionNotifier.notify_exception(exception, {:notifiers => :test})
|
97
|
-
assert_equal notifier_calls, 1
|
107
|
+
assert_equal @notifier_calls, 1
|
98
108
|
|
99
109
|
ExceptionNotifier.notify_exception(exception, {:notifiers => :test, :ignore_exceptions => 'StandardError' })
|
100
|
-
assert_equal notifier_calls, 1
|
110
|
+
assert_equal @notifier_calls, 1
|
111
|
+
end
|
112
|
+
|
113
|
+
test "should not call group_error! or send_notification? if error_grouping false" do
|
114
|
+
exception = StandardError.new
|
115
|
+
ExceptionNotifier.expects(:group_error!).never
|
116
|
+
ExceptionNotifier.expects(:send_notification?).never
|
117
|
+
|
118
|
+
ExceptionNotifier.notify_exception(exception)
|
119
|
+
end
|
120
|
+
|
121
|
+
test "should call group_error! and send_notification? if error_grouping true" do
|
122
|
+
ExceptionNotifier.error_grouping = true
|
101
123
|
|
102
|
-
|
124
|
+
exception = StandardError.new
|
125
|
+
ExceptionNotifier.expects(:group_error!).once
|
126
|
+
ExceptionNotifier.expects(:send_notification?).once
|
127
|
+
|
128
|
+
ExceptionNotifier.notify_exception(exception)
|
129
|
+
end
|
130
|
+
|
131
|
+
test "should skip notification if send_notification? is false" do
|
132
|
+
ExceptionNotifier.error_grouping = true
|
133
|
+
|
134
|
+
exception = StandardError.new
|
135
|
+
ExceptionNotifier.expects(:group_error!).once.returns(1)
|
136
|
+
ExceptionNotifier.expects(:send_notification?).with(exception, 1).once.returns(false)
|
137
|
+
|
138
|
+
refute ExceptionNotifier.notify_exception(exception)
|
103
139
|
end
|
104
140
|
|
141
|
+
test "should send notification if send_notification? is true" do
|
142
|
+
ExceptionNotifier.error_grouping = true
|
143
|
+
|
144
|
+
exception = StandardError.new
|
145
|
+
ExceptionNotifier.expects(:group_error!).once.returns(1)
|
146
|
+
ExceptionNotifier.expects(:send_notification?).with(exception, 1).once.returns(true)
|
147
|
+
|
148
|
+
assert ExceptionNotifier.notify_exception(exception)
|
149
|
+
end
|
105
150
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: exception_notification
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.2.
|
4
|
+
version: 4.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jamis Buck
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2017-08-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: actionmailer
|
@@ -269,6 +269,7 @@ files:
|
|
269
269
|
- lib/exception_notifier/irc_notifier.rb
|
270
270
|
- lib/exception_notifier/mattermost_notifier.rb
|
271
271
|
- lib/exception_notifier/modules/backtrace_cleaner.rb
|
272
|
+
- lib/exception_notifier/modules/error_grouping.rb
|
272
273
|
- lib/exception_notifier/notifier.rb
|
273
274
|
- lib/exception_notifier/slack_notifier.rb
|
274
275
|
- lib/exception_notifier/views/exception_notifier/_backtrace.html.erb
|
@@ -348,6 +349,7 @@ files:
|
|
348
349
|
- test/exception_notifier/hipchat_notifier_test.rb
|
349
350
|
- test/exception_notifier/irc_notifier_test.rb
|
350
351
|
- test/exception_notifier/mattermost_notifier_test.rb
|
352
|
+
- test/exception_notifier/modules/error_grouping_test.rb
|
351
353
|
- test/exception_notifier/sidekiq_test.rb
|
352
354
|
- test/exception_notifier/slack_notifier_test.rb
|
353
355
|
- test/exception_notifier/webhook_notifier_test.rb
|
@@ -436,6 +438,7 @@ test_files:
|
|
436
438
|
- test/exception_notifier/hipchat_notifier_test.rb
|
437
439
|
- test/exception_notifier/irc_notifier_test.rb
|
438
440
|
- test/exception_notifier/mattermost_notifier_test.rb
|
441
|
+
- test/exception_notifier/modules/error_grouping_test.rb
|
439
442
|
- test/exception_notifier/sidekiq_test.rb
|
440
443
|
- test/exception_notifier/slack_notifier_test.rb
|
441
444
|
- test/exception_notifier/webhook_notifier_test.rb
|