exception_notification 4.2.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
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