exception_notification 4.2.0 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. checksums.yaml +5 -5
  2. data/Appraisals +4 -3
  3. data/CHANGELOG.rdoc +57 -1
  4. data/CONTRIBUTING.md +21 -2
  5. data/Gemfile +3 -1
  6. data/README.md +106 -789
  7. data/Rakefile +4 -2
  8. data/docs/notifiers/campfire.md +50 -0
  9. data/docs/notifiers/custom.md +42 -0
  10. data/docs/notifiers/datadog.md +51 -0
  11. data/docs/notifiers/email.md +195 -0
  12. data/docs/notifiers/google_chat.md +31 -0
  13. data/docs/notifiers/hipchat.md +66 -0
  14. data/docs/notifiers/irc.md +97 -0
  15. data/docs/notifiers/mattermost.md +115 -0
  16. data/docs/notifiers/slack.md +161 -0
  17. data/docs/notifiers/sns.md +37 -0
  18. data/docs/notifiers/teams.md +54 -0
  19. data/docs/notifiers/webhook.md +60 -0
  20. data/examples/sample_app.rb +56 -0
  21. data/examples/sinatra/Gemfile +8 -6
  22. data/examples/sinatra/config.ru +3 -1
  23. data/examples/sinatra/sinatra_app.rb +19 -11
  24. data/exception_notification.gemspec +30 -23
  25. data/gemfiles/rails4_0.gemfile +1 -2
  26. data/gemfiles/rails4_1.gemfile +1 -2
  27. data/gemfiles/rails4_2.gemfile +1 -2
  28. data/gemfiles/rails5_0.gemfile +1 -2
  29. data/gemfiles/rails5_1.gemfile +7 -0
  30. data/gemfiles/rails5_2.gemfile +7 -0
  31. data/gemfiles/rails6_0.gemfile +7 -0
  32. data/lib/exception_notification.rb +3 -0
  33. data/lib/exception_notification/rack.rb +34 -27
  34. data/lib/exception_notification/rails.rb +3 -0
  35. data/lib/exception_notification/resque.rb +10 -10
  36. data/lib/exception_notification/sidekiq.rb +10 -12
  37. data/lib/exception_notification/version.rb +5 -0
  38. data/lib/exception_notifier.rb +79 -11
  39. data/lib/exception_notifier/base_notifier.rb +10 -5
  40. data/lib/exception_notifier/campfire_notifier.rb +14 -9
  41. data/lib/exception_notifier/datadog_notifier.rb +156 -0
  42. data/lib/exception_notifier/email_notifier.rb +78 -87
  43. data/lib/exception_notifier/google_chat_notifier.rb +44 -0
  44. data/lib/exception_notifier/hipchat_notifier.rb +16 -10
  45. data/lib/exception_notifier/irc_notifier.rb +38 -31
  46. data/lib/exception_notifier/mattermost_notifier.rb +54 -131
  47. data/lib/exception_notifier/modules/backtrace_cleaner.rb +2 -2
  48. data/lib/exception_notifier/modules/error_grouping.rb +87 -0
  49. data/lib/exception_notifier/modules/formatter.rb +121 -0
  50. data/lib/exception_notifier/notifier.rb +9 -6
  51. data/lib/exception_notifier/slack_notifier.rb +75 -32
  52. data/lib/exception_notifier/sns_notifier.rb +86 -0
  53. data/lib/exception_notifier/teams_notifier.rb +200 -0
  54. data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +1 -1
  55. data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -1
  56. data/lib/exception_notifier/views/exception_notifier/_request.text.erb +1 -1
  57. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +9 -9
  58. data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +2 -4
  59. data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -2
  60. data/lib/exception_notifier/webhook_notifier.rb +19 -16
  61. data/lib/generators/exception_notification/install_generator.rb +11 -5
  62. data/lib/generators/exception_notification/templates/{exception_notification.rb → exception_notification.rb.erb} +14 -12
  63. data/test/exception_notification/rack_test.rb +90 -4
  64. data/test/exception_notification/resque_test.rb +54 -0
  65. data/test/exception_notifier/campfire_notifier_test.rb +66 -39
  66. data/test/exception_notifier/datadog_notifier_test.rb +153 -0
  67. data/test/exception_notifier/email_notifier_test.rb +301 -145
  68. data/test/exception_notifier/google_chat_notifier_test.rb +185 -0
  69. data/test/exception_notifier/hipchat_notifier_test.rb +112 -65
  70. data/test/exception_notifier/irc_notifier_test.rb +48 -30
  71. data/test/exception_notifier/mattermost_notifier_test.rb +218 -55
  72. data/test/exception_notifier/modules/error_grouping_test.rb +167 -0
  73. data/test/exception_notifier/modules/formatter_test.rb +152 -0
  74. data/test/exception_notifier/sidekiq_test.rb +9 -6
  75. data/test/exception_notifier/slack_notifier_test.rb +109 -59
  76. data/test/exception_notifier/sns_notifier_test.rb +123 -0
  77. data/test/exception_notifier/teams_notifier_test.rb +92 -0
  78. data/test/exception_notifier/webhook_notifier_test.rb +68 -38
  79. data/test/exception_notifier_test.rb +220 -37
  80. data/test/support/exception_notifier_helper.rb +14 -0
  81. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.html.erb +0 -0
  82. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.text.erb +0 -0
  83. data/test/{dummy/app → support}/views/exception_notifier/_new_section.html.erb +0 -0
  84. data/test/{dummy/app → support}/views/exception_notifier/_new_section.text.erb +0 -0
  85. data/test/test_helper.rb +14 -13
  86. metadata +154 -162
  87. data/test/dummy/.gitignore +0 -4
  88. data/test/dummy/Rakefile +0 -7
  89. data/test/dummy/app/controllers/application_controller.rb +0 -3
  90. data/test/dummy/app/controllers/posts_controller.rb +0 -30
  91. data/test/dummy/app/helpers/application_helper.rb +0 -2
  92. data/test/dummy/app/helpers/posts_helper.rb +0 -2
  93. data/test/dummy/app/models/post.rb +0 -2
  94. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  95. data/test/dummy/app/views/posts/_form.html.erb +0 -0
  96. data/test/dummy/app/views/posts/new.html.erb +0 -0
  97. data/test/dummy/app/views/posts/show.html.erb +0 -0
  98. data/test/dummy/config.ru +0 -4
  99. data/test/dummy/config/application.rb +0 -42
  100. data/test/dummy/config/boot.rb +0 -6
  101. data/test/dummy/config/database.yml +0 -22
  102. data/test/dummy/config/environment.rb +0 -17
  103. data/test/dummy/config/environments/development.rb +0 -25
  104. data/test/dummy/config/environments/production.rb +0 -50
  105. data/test/dummy/config/environments/test.rb +0 -38
  106. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  107. data/test/dummy/config/initializers/inflections.rb +0 -10
  108. data/test/dummy/config/initializers/mime_types.rb +0 -5
  109. data/test/dummy/config/initializers/secret_token.rb +0 -8
  110. data/test/dummy/config/initializers/session_store.rb +0 -8
  111. data/test/dummy/config/locales/en.yml +0 -5
  112. data/test/dummy/config/routes.rb +0 -3
  113. data/test/dummy/db/migrate/20110729022608_create_posts.rb +0 -15
  114. data/test/dummy/db/schema.rb +0 -24
  115. data/test/dummy/db/seeds.rb +0 -7
  116. data/test/dummy/lib/tasks/.gitkeep +0 -0
  117. data/test/dummy/public/404.html +0 -26
  118. data/test/dummy/public/422.html +0 -26
  119. data/test/dummy/public/500.html +0 -26
  120. data/test/dummy/public/favicon.ico +0 -0
  121. data/test/dummy/public/images/rails.png +0 -0
  122. data/test/dummy/public/index.html +0 -239
  123. data/test/dummy/public/javascripts/application.js +0 -2
  124. data/test/dummy/public/javascripts/controls.js +0 -965
  125. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  126. data/test/dummy/public/javascripts/effects.js +0 -1123
  127. data/test/dummy/public/javascripts/prototype.js +0 -6001
  128. data/test/dummy/public/javascripts/rails.js +0 -191
  129. data/test/dummy/public/robots.txt +0 -5
  130. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  131. data/test/dummy/public/stylesheets/scaffold.css +0 -56
  132. data/test/dummy/script/rails +0 -6
  133. data/test/dummy/test/functional/posts_controller_test.rb +0 -218
  134. data/test/dummy/test/test_helper.rb +0 -7
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+
5
+ module ExceptionNotifier
6
+ class GoogleChatNotifier < BaseNotifier
7
+ def call(exception, opts = {})
8
+ options = base_options.merge(opts)
9
+ formatter = Formatter.new(exception, options)
10
+
11
+ HTTParty.post(
12
+ options[:webhook_url],
13
+ body: { text: body(exception, formatter) }.to_json,
14
+ headers: { 'Content-Type' => 'application/json' }
15
+ )
16
+ end
17
+
18
+ private
19
+
20
+ def body(exception, formatter)
21
+ text = [
22
+ "\nApplication: *#{formatter.app_name}*",
23
+ formatter.subtitle,
24
+ '',
25
+ formatter.title,
26
+ "*#{exception.message.tr('`', "'")}*"
27
+ ]
28
+
29
+ if (request = formatter.request_message.presence)
30
+ text << ''
31
+ text << '*Request:*'
32
+ text << request
33
+ end
34
+
35
+ if (backtrace = formatter.backtrace_message.presence)
36
+ text << ''
37
+ text << '*Backtrace:*'
38
+ text << backtrace
39
+ end
40
+
41
+ text.compact.join("\n")
42
+ end
43
+ end
44
+ end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class HipchatNotifier < BaseNotifier
3
-
4
5
  attr_accessor :from
5
6
  attr_accessor :room
6
7
  attr_accessor :message_options
@@ -11,26 +12,31 @@ module ExceptionNotifier
11
12
  api_token = options.delete(:api_token)
12
13
  room_name = options.delete(:room_name)
13
14
  opts = {
14
- :api_version => options.delete(:api_version) || 'v1'
15
- }
15
+ api_version: options.delete(:api_version) || 'v1'
16
+ }
17
+ opts[:server_url] = options.delete(:server_url) if options[:server_url]
16
18
  @from = options.delete(:from) || 'Exception'
17
19
  @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)}'"
20
+ @message_template = options.delete(:message_template) || lambda { |exception, errors_count|
21
+ msg = if errors_count > 1
22
+ "The exception occurred #{errors_count} times: '#{Rack::Utils.escape_html(exception.message)}'"
23
+ else
24
+ "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}'"
25
+ end
20
26
  msg += " on '#{exception.backtrace.first}'" if exception.backtrace
21
27
  msg
22
28
  }
23
- @message_options = options
29
+ @message_options = options
24
30
  @message_options[:color] ||= 'red'
25
- rescue
31
+ rescue StandardError
26
32
  @room = nil
27
33
  end
28
34
  end
29
35
 
30
- def call(exception, options={})
31
- return if !active?
36
+ def call(exception, options = {})
37
+ return unless active?
32
38
 
33
- message = @message_template.call(exception)
39
+ message = @message_template.call(exception, options[:accumulated_errors_count].to_i)
34
40
  send_notice(exception, options, message, @message_options) do |msg, message_opts|
35
41
  @room.send(@from, msg, message_opts)
36
42
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class IrcNotifier < BaseNotifier
3
5
  def initialize(options)
@@ -6,46 +8,51 @@ module ExceptionNotifier
6
8
  parse_options(options)
7
9
  end
8
10
 
9
- def call(exception, options={})
10
- message = "'#{exception.message}'"
11
+ def call(exception, options = {})
12
+ errors_count = options[:accumulated_errors_count].to_i
13
+
14
+ occurrences = "(#{errors_count} times)" if errors_count > 1
15
+ message = "#{occurrences}'#{exception.message}'"
11
16
  message += " on '#{exception.backtrace.first}'" if exception.backtrace
12
- if active?
13
- send_notice(exception, options, message) do |msg, _|
14
- send_message([*@config.prefix, *msg].join(' '))
15
- end
17
+
18
+ return unless active?
19
+
20
+ send_notice(exception, options, message) do |msg, _|
21
+ send_message([*@config.prefix, *msg].join(' '))
16
22
  end
17
23
  end
18
24
 
19
25
  def send_message(message)
20
- CarrierPigeon.send @config.irc.merge({message: message})
26
+ CarrierPigeon.send @config.irc.merge(message: message)
21
27
  end
22
28
 
23
29
  private
24
- def parse_options(options)
25
- nick = options.fetch(:nick, 'ExceptionNotifierBot')
26
- password = options[:password] ? ":#{options[:password]}" : nil
27
- domain = options.fetch(:domain, nil)
28
- port = options[:port] ? ":#{options[:port]}" : nil
29
- channel = options.fetch(:channel, '#log')
30
- notice = options.fetch(:notice, false)
31
- ssl = options.fetch(:ssl, false)
32
- join = options.fetch(:join, false)
33
- uri = "irc://#{nick}#{password}@#{domain}#{port}/#{channel}"
34
- prefix = options.fetch(:prefix, nil)
35
- recipients = options[:recipients] ? options[:recipients].join(', ') + ':' : nil
36
-
37
- @config.prefix = [*prefix, *recipients].join(' ')
38
- @config.irc = { uri: uri, ssl: ssl, notice: notice, join: join }
39
- end
40
30
 
41
- def active?
42
- valid_uri? @config.irc[:uri]
43
- end
31
+ def parse_options(options)
32
+ nick = options.fetch(:nick, 'ExceptionNotifierBot')
33
+ password = options[:password] ? ":#{options[:password]}" : nil
34
+ domain = options.fetch(:domain, nil)
35
+ port = options[:port] ? ":#{options[:port]}" : nil
36
+ channel = options.fetch(:channel, '#log')
37
+ notice = options.fetch(:notice, false)
38
+ ssl = options.fetch(:ssl, false)
39
+ join = options.fetch(:join, false)
40
+ uri = "irc://#{nick}#{password}@#{domain}#{port}/#{channel}"
41
+ prefix = options.fetch(:prefix, nil)
42
+ recipients = options[:recipients] ? options[:recipients].join(', ') + ':' : nil
44
43
 
45
- def valid_uri?(uri)
46
- !!URI.parse(uri)
47
- rescue URI::InvalidURIError
48
- false
49
- end
44
+ @config.prefix = [*prefix, *recipients].join(' ')
45
+ @config.irc = { uri: uri, ssl: ssl, notice: notice, join: join }
46
+ end
47
+
48
+ def active?
49
+ valid_uri? @config.irc[:uri]
50
+ end
51
+
52
+ def valid_uri?(uri)
53
+ URI.parse(uri)
54
+ rescue URI::InvalidURIError
55
+ false
56
+ end
50
57
  end
51
58
  end
@@ -1,159 +1,82 @@
1
- require 'action_dispatch'
2
- require 'active_support/core_ext/time'
1
+ # frozen_string_literal: true
3
2
 
4
- module ExceptionNotifier
5
- class MattermostNotifier
6
- include ExceptionNotifier::BacktraceCleaner
7
-
8
- attr_accessor :httparty
9
-
10
- def initialize(options = {})
11
- super()
12
- @default_options = options
13
- @httparty = HTTParty
14
- end
3
+ require 'httparty'
15
4
 
16
- def call(exception, options = {})
17
- @options = options.merge(@default_options)
5
+ module ExceptionNotifier
6
+ class MattermostNotifier < BaseNotifier
7
+ def call(exception, opts = {})
8
+ options = opts.merge(base_options)
18
9
  @exception = exception
19
- @backtrace = exception.backtrace ? clean_backtrace(exception) : nil
20
-
21
- @env = @options.delete(:env)
22
10
 
23
- @application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore
24
- @gitlab_url = @options.delete(:git_url)
25
- @username = @options.delete(:username) || "Exception Notifier"
26
- @avatar = @options.delete(:avatar)
11
+ @formatter = Formatter.new(exception, options)
27
12
 
28
- @channel = @options.delete(:channel)
29
- @webhook_url = @options.delete(:webhook_url)
30
- raise ArgumentError.new "You must provide 'webhook_url' parameter." unless @webhook_url
13
+ @gitlab_url = options[:git_url]
31
14
 
32
- unless @env.nil?
33
- @controller = @env['action_controller.instance'] || MissingController.new
15
+ @env = options[:env] || {}
34
16
 
35
- request = ActionDispatch::Request.new(@env)
17
+ payload = {
18
+ text: message_text.compact.join("\n"),
19
+ username: options[:username] || 'Exception Notifier'
20
+ }
36
21
 
37
- @request_items = { url: request.original_url,
38
- http_method: request.method,
39
- ip_address: request.remote_ip,
40
- parameters: request.filtered_parameters,
41
- timestamp: Time.current }
42
-
43
- if request.session["warden.user.user.key"]
44
- current_user = User.find(request.session["warden.user.user.key"][0][0])
45
- @request_items.merge!({ current_user: { id: current_user.id, email: current_user.email } })
46
- end
47
- else
48
- @controller = @request_items = nil
49
- end
22
+ payload[:icon_url] = options[:avatar] if options[:avatar]
23
+ payload[:channel] = options[:channel] if options[:channel]
50
24
 
51
- payload = message_text.merge(user_info).merge(channel_info)
25
+ httparty_options = options.except(
26
+ :avatar, :channel, :username, :git_url, :webhook_url,
27
+ :env, :accumulated_errors_count, :app_name
28
+ )
52
29
 
53
- @options[:body] = payload.to_json
54
- @options[:headers] ||= {}
55
- @options[:headers].merge!({ 'Content-Type' => 'application/json' })
30
+ httparty_options[:body] = payload.to_json
31
+ httparty_options[:headers] ||= {}
32
+ httparty_options[:headers]['Content-Type'] = 'application/json'
56
33
 
57
- @httparty.post(@webhook_url, @options)
34
+ HTTParty.post(options[:webhook_url], httparty_options)
58
35
  end
59
36
 
60
37
  private
61
38
 
62
- def channel_info
63
- if @channel
64
- { channel: @channel }
65
- else
66
- {}
67
- end
68
- end
69
-
70
- def user_info
71
- infos = {}
72
-
73
- infos.merge!({ username: @username }) if @username
74
- infos.merge!({ icon_url: @avatar }) if @avatar
75
-
76
- infos
77
- end
78
-
79
- def message_text
80
- text = []
81
-
82
- text += ["@channel"]
83
- text += message_header
84
- text += message_request if @request_items
85
- text += message_backtrace if @backtrace
86
- text += message_issue_link if @gitlab_url
87
-
88
- { text: text.join("\n") }
89
- end
90
-
91
- def message_header
92
- text = []
93
-
94
- 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 << "*#{@exception.message}*"
97
-
98
- text
99
- end
100
-
101
- def message_request
102
- text = []
103
-
104
- text << "### Request"
105
- text << "```"
106
- text << hash_presentation(@request_items)
107
- text << "```"
108
-
109
- text
110
- end
111
-
112
- def message_backtrace(size = 3)
113
- text = []
39
+ attr_reader :formatter
114
40
 
115
- size = @backtrace.size < size ? @backtrace.size : size
116
- text << "### Backtrace"
117
- text << "```"
118
- size.times { |i| text << "* " + @backtrace[i] }
119
- text << "```"
41
+ def message_text
42
+ text = [
43
+ '@channel',
44
+ "### #{formatter.title}",
45
+ formatter.subtitle,
46
+ "*#{@exception.message}*"
47
+ ]
120
48
 
121
- text
49
+ if (request = formatter.request_message.presence)
50
+ text << '### Request'
51
+ text << request
122
52
  end
123
53
 
124
- def message_issue_link
125
- text = []
126
-
127
- link = [@gitlab_url, @application_name, "issues", "new"].join("/")
128
- params = {
129
- "issue[title]" => ["[BUG] Error 500 :",
130
- controller_and_method,
131
- "(#{@exception.class})",
132
- @exception.message].compact.join(" ")
133
- }.to_query
134
-
135
- text << "[Create an issue](#{link}/?#{params})"
136
-
137
- text
54
+ if (backtrace = formatter.backtrace_message.presence)
55
+ text << '### Backtrace'
56
+ text << backtrace
138
57
  end
139
58
 
140
- def controller_and_method
141
- if @controller
142
- "#{@controller.controller_name}##{@controller.action_name}"
143
- else
144
- ""
145
- end
59
+ if (exception_data = @env['exception_notifier.exception_data'])
60
+ text << '### Data'
61
+ data_string = exception_data.map { |k, v| "* #{k} : #{v}" }.join("\n")
62
+ text << "```\n#{data_string}\n```"
146
63
  end
147
64
 
148
- def hash_presentation(hash)
149
- text = []
65
+ text << message_issue_link if @gitlab_url
150
66
 
151
- hash.each do |key, value|
152
- text << "* #{key} : #{value}"
153
- end
67
+ text
68
+ end
154
69
 
155
- text.join("\n")
156
- end
70
+ def message_issue_link
71
+ link = [@gitlab_url, formatter.app_name, 'issues', 'new'].join('/')
72
+ params = {
73
+ 'issue[title]' => ['[BUG] Error 500 :',
74
+ formatter.controller_and_action || '',
75
+ "(#{@exception.class})",
76
+ @exception.message].compact.join(' ')
77
+ }.to_query
157
78
 
79
+ "[Create an issue](#{link}/?#{params})"
80
+ end
158
81
  end
159
82
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  module BacktraceCleaner
3
-
4
5
  def clean_backtrace(exception)
5
6
  if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
6
7
  Rails.backtrace_cleaner.send(:filter, exception.backtrace)
@@ -8,6 +9,5 @@ module ExceptionNotifier
8
9
  exception.backtrace
9
10
  end
10
11
  end
11
-
12
12
  end
13
13
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/numeric/time'
4
+ require 'active_support/concern'
5
+
6
+ module ExceptionNotifier
7
+ module ErrorGrouping
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ mattr_accessor :error_grouping
12
+ self.error_grouping = false
13
+
14
+ mattr_accessor :error_grouping_period
15
+ self.error_grouping_period = 5.minutes
16
+
17
+ mattr_accessor :notification_trigger
18
+
19
+ mattr_accessor :error_grouping_cache
20
+ end
21
+
22
+ module ClassMethods
23
+ # Fallback to the memory store while the specified cache store doesn't work
24
+ #
25
+ def fallback_cache_store
26
+ @fallback_cache_store ||= ActiveSupport::Cache::MemoryStore.new
27
+ end
28
+
29
+ def error_count(error_key)
30
+ count =
31
+ begin
32
+ error_grouping_cache.read(error_key)
33
+ rescue StandardError => e
34
+ log_cache_error(error_grouping_cache, e, :read)
35
+ fallback_cache_store.read(error_key)
36
+ end
37
+
38
+ count&.to_i
39
+ end
40
+
41
+ def save_error_count(error_key, count)
42
+ error_grouping_cache.write(error_key, count, expires_in: error_grouping_period)
43
+ rescue StandardError => e
44
+ log_cache_error(error_grouping_cache, e, :write)
45
+ fallback_cache_store.write(error_key, count, expires_in: error_grouping_period)
46
+ end
47
+
48
+ def group_error!(exception, options)
49
+ message_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\nmessage:#{exception.message}")}"
50
+ accumulated_errors_count = 1
51
+
52
+ if (count = error_count(message_based_key))
53
+ accumulated_errors_count = count + 1
54
+ save_error_count(message_based_key, accumulated_errors_count)
55
+ else
56
+ backtrace_based_key =
57
+ "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
58
+
59
+ if (count = error_grouping_cache.read(backtrace_based_key))
60
+ accumulated_errors_count = count + 1
61
+ save_error_count(backtrace_based_key, accumulated_errors_count)
62
+ else
63
+ save_error_count(backtrace_based_key, accumulated_errors_count)
64
+ save_error_count(message_based_key, accumulated_errors_count)
65
+ end
66
+ end
67
+
68
+ options[:accumulated_errors_count] = accumulated_errors_count
69
+ end
70
+
71
+ def send_notification?(exception, count)
72
+ if notification_trigger.respond_to?(:call)
73
+ notification_trigger.call(exception, count)
74
+ else
75
+ factor = Math.log2(count)
76
+ factor.to_i == factor
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def log_cache_error(cache, exception, action)
83
+ "#{cache.inspect} failed to #{action}, reason: #{exception.message}. Falling back to memory cache store."
84
+ end
85
+ end
86
+ end
87
+ end