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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6de21b765af1465df6205f174cbab7e10b403048
4
- data.tar.gz: cade5525679b528aded7b1ad6369e97f40b58947
3
+ metadata.gz: a5eab5531d309930d48027a778d74f8491a5a995
4
+ data.tar.gz: 67d0b4176fb7decc436754ee60e7f4a9053d48f8
5
5
  SHA512:
6
- metadata.gz: fe4b7770171a25a7e131d97c922e5c518531f71add293ce42359eb9f2fc84ea7330f2f935657cbf4dc13fa4a8ba2d3d492397469555d665d12d36c52603a82fd
7
- data.tar.gz: 92774d00ec26d97246ca9d4e6334f3aac06f04c5242a555eb3a5bd5addfd8a8485bdd72787b0d304146c50e79a7ff4500aaf2a08aca744be963f3a779221b193
6
+ metadata.gz: 1f92d01eee01a295d84e28bcaff7618edb3a4a626afe56a447974579115d8daf4d3e6d516c1c5bb449b88f53497effafa479c6a46bb614b9cb1eed04f5510211
7
+ data.tar.gz: ca3718addeb5deb323a4f73c09d0678534381301a4fc85ac648fef406beafccb844500039c1c805d6f20d1ef897696a6f92f94631a6be25ccbc468899ef69ec8
@@ -1,3 +1,10 @@
1
+ == 4.2.2
2
+
3
+ * enhancements
4
+ * Error groupiong (by @Martin91)
5
+ * Additional fields for Slack support (by @schurig)
6
+ * Enterprise HipChat support (by @seanhuber)
7
+
1
8
  == 4.2.1
2
9
 
3
10
  * enhancements
@@ -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
- bundle exec rake db:test:prepare
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, six notifiers are available:
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.1'
3
+ s.version = '4.2.2'
4
4
  s.authors = ["Jamis Buck", "Josh Peek"]
5
- s.date = %q{2016-07-17}
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
@@ -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 = "A new exception occurred: '#{exception.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, :delivery_method, :mailer_settings,
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 << "#{@kontroller.controller_name}##{@kontroller.action_name}" if @kontroller
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
- object.to_s
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
- :pre_callback, :post_callback,
143
- :email_prefix, :email_format, :sections, :background_sections,
144
- :verbose_subject, :normalize_subject, :delivery_method, :mailer_settings,
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 = "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}'"
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[:backtrace_lines]
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
- exception_name = "*#{exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'}* `#{exception.class.to_s}`"
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* `#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>`"
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 = @backtrace_lines ? "```#{exception.backtrace.first(@backtrace_lines).join("\n")}```" : "```#{exception.backtrace.join("\n")}```"
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
- attchs = [color: 'danger', text: text, fields: fields, mrkdwn_in: %w(text fields)]
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
- assert_equal slack_notifier.notifier.channel, options[:channel]
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
- assert_equal slack_notifier.notifier.username, options[:username]
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 = nil)
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 = expected_backtrace_lines ? "```#{exception.backtrace.first(expected_backtrace_lines).join("\n")}```" : "```#{exception.backtrace.join("\n")}```"
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
- notifier_calls = 0
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
- notifier_calls = 0
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
- ExceptionNotifier.unregister_exception_notifier(:test)
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.1
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: 2016-07-17 00:00:00.000000000 Z
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