exception_notification 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. checksums.yaml +5 -5
  2. data/Appraisals +4 -2
  3. data/CHANGELOG.rdoc +47 -0
  4. data/CONTRIBUTING.md +18 -0
  5. data/Gemfile +3 -1
  6. data/README.md +97 -945
  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 -24
  25. data/gemfiles/{rails4_0.gemfile → rails5_2.gemfile} +2 -2
  26. data/gemfiles/{rails4_1.gemfile → rails6_0.gemfile} +2 -2
  27. data/gemfiles/{rails4_2.gemfile → rails6_1.gemfile} +2 -2
  28. data/gemfiles/{rails5_0.gemfile → rails7_0.gemfile} +2 -2
  29. data/lib/exception_notification/rack.rb +28 -30
  30. data/lib/exception_notification/rails.rb +2 -0
  31. data/lib/exception_notification/resque.rb +10 -10
  32. data/lib/exception_notification/sidekiq.rb +10 -12
  33. data/lib/exception_notification/version.rb +5 -0
  34. data/lib/exception_notification.rb +3 -0
  35. data/lib/exception_notifier/base_notifier.rb +10 -5
  36. data/lib/exception_notifier/datadog_notifier.rb +156 -0
  37. data/lib/exception_notifier/email_notifier.rb +73 -88
  38. data/lib/exception_notifier/google_chat_notifier.rb +27 -119
  39. data/lib/exception_notifier/hipchat_notifier.rb +13 -12
  40. data/lib/exception_notifier/irc_notifier.rb +36 -33
  41. data/lib/exception_notifier/mattermost_notifier.rb +54 -137
  42. data/lib/exception_notifier/modules/backtrace_cleaner.rb +2 -2
  43. data/lib/exception_notifier/modules/error_grouping.rb +24 -13
  44. data/lib/exception_notifier/modules/formatter.rb +125 -0
  45. data/lib/exception_notifier/notifier.rb +9 -6
  46. data/lib/exception_notifier/slack_notifier.rb +65 -40
  47. data/lib/exception_notifier/sns_notifier.rb +23 -13
  48. data/lib/exception_notifier/teams_notifier.rb +67 -46
  49. data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +1 -1
  50. data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -1
  51. data/lib/exception_notifier/views/exception_notifier/_request.text.erb +1 -1
  52. data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +2 -2
  53. data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -2
  54. data/lib/exception_notifier/webhook_notifier.rb +17 -14
  55. data/lib/exception_notifier.rb +65 -10
  56. data/lib/generators/exception_notification/install_generator.rb +11 -5
  57. data/lib/generators/exception_notification/templates/{exception_notification.rb → exception_notification.rb.erb} +13 -11
  58. data/test/exception_notification/rack_test.rb +75 -13
  59. data/test/exception_notification/resque_test.rb +54 -0
  60. data/test/exception_notifier/datadog_notifier_test.rb +153 -0
  61. data/test/exception_notifier/email_notifier_test.rb +275 -153
  62. data/test/exception_notifier/google_chat_notifier_test.rb +158 -101
  63. data/test/exception_notifier/hipchat_notifier_test.rb +84 -81
  64. data/test/exception_notifier/irc_notifier_test.rb +36 -34
  65. data/test/exception_notifier/mattermost_notifier_test.rb +213 -67
  66. data/test/exception_notifier/modules/error_grouping_test.rb +41 -40
  67. data/test/exception_notifier/modules/formatter_test.rb +152 -0
  68. data/test/exception_notifier/sidekiq_test.rb +9 -17
  69. data/test/exception_notifier/slack_notifier_test.rb +66 -63
  70. data/test/exception_notifier/sns_notifier_test.rb +84 -32
  71. data/test/exception_notifier/teams_notifier_test.rb +25 -26
  72. data/test/exception_notifier/webhook_notifier_test.rb +52 -48
  73. data/test/exception_notifier_test.rb +150 -41
  74. data/test/support/exception_notifier_helper.rb +14 -0
  75. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.html.erb +0 -0
  76. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.text.erb +0 -0
  77. data/test/{dummy/app → support}/views/exception_notifier/_new_section.html.erb +0 -0
  78. data/test/{dummy/app → support}/views/exception_notifier/_new_section.text.erb +0 -0
  79. data/test/test_helper.rb +14 -13
  80. metadata +134 -175
  81. data/gemfiles/rails5_1.gemfile +0 -7
  82. data/lib/exception_notifier/campfire_notifier.rb +0 -40
  83. data/test/dummy/.gitignore +0 -4
  84. data/test/dummy/Rakefile +0 -7
  85. data/test/dummy/app/controllers/application_controller.rb +0 -3
  86. data/test/dummy/app/controllers/posts_controller.rb +0 -30
  87. data/test/dummy/app/helpers/application_helper.rb +0 -2
  88. data/test/dummy/app/helpers/posts_helper.rb +0 -2
  89. data/test/dummy/app/models/post.rb +0 -2
  90. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  91. data/test/dummy/app/views/posts/_form.html.erb +0 -0
  92. data/test/dummy/app/views/posts/new.html.erb +0 -0
  93. data/test/dummy/app/views/posts/show.html.erb +0 -0
  94. data/test/dummy/config/application.rb +0 -42
  95. data/test/dummy/config/boot.rb +0 -6
  96. data/test/dummy/config/database.yml +0 -22
  97. data/test/dummy/config/environment.rb +0 -17
  98. data/test/dummy/config/environments/development.rb +0 -25
  99. data/test/dummy/config/environments/production.rb +0 -50
  100. data/test/dummy/config/environments/test.rb +0 -35
  101. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  102. data/test/dummy/config/initializers/inflections.rb +0 -10
  103. data/test/dummy/config/initializers/mime_types.rb +0 -5
  104. data/test/dummy/config/initializers/secret_token.rb +0 -8
  105. data/test/dummy/config/initializers/session_store.rb +0 -8
  106. data/test/dummy/config/locales/en.yml +0 -5
  107. data/test/dummy/config/routes.rb +0 -3
  108. data/test/dummy/config.ru +0 -4
  109. data/test/dummy/db/migrate/20110729022608_create_posts.rb +0 -15
  110. data/test/dummy/db/schema.rb +0 -24
  111. data/test/dummy/db/seeds.rb +0 -7
  112. data/test/dummy/lib/tasks/.gitkeep +0 -0
  113. data/test/dummy/public/404.html +0 -26
  114. data/test/dummy/public/422.html +0 -26
  115. data/test/dummy/public/500.html +0 -26
  116. data/test/dummy/public/favicon.ico +0 -0
  117. data/test/dummy/public/images/rails.png +0 -0
  118. data/test/dummy/public/index.html +0 -239
  119. data/test/dummy/public/javascripts/application.js +0 -2
  120. data/test/dummy/public/javascripts/controls.js +0 -965
  121. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  122. data/test/dummy/public/javascripts/effects.js +0 -1123
  123. data/test/dummy/public/javascripts/prototype.js +0 -6001
  124. data/test/dummy/public/javascripts/rails.js +0 -191
  125. data/test/dummy/public/robots.txt +0 -5
  126. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  127. data/test/dummy/public/stylesheets/scaffold.css +0 -56
  128. data/test/dummy/script/rails +0 -6
  129. data/test/dummy/test/functional/posts_controller_test.rb +0 -237
  130. data/test/dummy/test/test_helper.rb +0 -7
  131. data/test/exception_notifier/campfire_notifier_test.rb +0 -120
@@ -1,15 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'action_dispatch'
2
4
  require 'active_support/core_ext/time'
3
5
 
4
6
  module ExceptionNotifier
5
7
  class WebhookNotifier < BaseNotifier
6
-
7
8
  def initialize(options)
8
9
  super
9
10
  @default_options = options
10
11
  end
11
12
 
12
- def call(exception, options={})
13
+ def call(exception, options = {})
13
14
  env = options[:env]
14
15
 
15
16
  options = options.reverse_merge(@default_options)
@@ -18,23 +19,25 @@ module ExceptionNotifier
18
19
 
19
20
  options[:body] ||= {}
20
21
  options[:body][:server] = Socket.gethostname
21
- options[:body][:process] = $$
22
- if defined?(Rails) && Rails.respond_to?(:root)
23
- options[:body][:rails_root] = Rails.root
24
- end
25
- options[:body][:exception] = {:error_class => exception.class.to_s,
26
- :message => exception.message.inspect,
27
- :backtrace => exception.backtrace}
22
+ options[:body][:process] = $PROCESS_ID
23
+ options[:body][:rails_root] = Rails.root if defined?(Rails) && Rails.respond_to?(:root)
24
+ options[:body][:exception] = {
25
+ error_class: exception.class.to_s,
26
+ message: exception.message.inspect,
27
+ backtrace: exception.backtrace
28
+ }
28
29
  options[:body][:data] = (env && env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
29
30
 
30
31
  unless env.nil?
31
32
  request = ActionDispatch::Request.new(env)
32
33
 
33
- request_items = {:url => request.original_url,
34
- :http_method => request.method,
35
- :ip_address => request.remote_ip,
36
- :parameters => request.filtered_parameters,
37
- :timestamp => Time.current }
34
+ request_items = {
35
+ url: request.original_url,
36
+ http_method: request.method,
37
+ ip_address: request.remote_ip,
38
+ parameters: request.filtered_parameters,
39
+ timestamp: Time.current
40
+ }
38
41
 
39
42
  options[:body][:request] = request_items
40
43
  options[:body][:session] = request.session
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
2
4
  require 'active_support/core_ext/string/inflections'
3
5
  require 'active_support/core_ext/module/attribute_accessors'
@@ -8,10 +10,10 @@ module ExceptionNotifier
8
10
  include ErrorGrouping
9
11
 
10
12
  autoload :BacktraceCleaner, 'exception_notifier/modules/backtrace_cleaner'
13
+ autoload :Formatter, 'exception_notifier/modules/formatter'
11
14
 
12
15
  autoload :Notifier, 'exception_notifier/notifier'
13
16
  autoload :EmailNotifier, 'exception_notifier/email_notifier'
14
- autoload :CampfireNotifier, 'exception_notifier/campfire_notifier'
15
17
  autoload :HipchatNotifier, 'exception_notifier/hipchat_notifier'
16
18
  autoload :WebhookNotifier, 'exception_notifier/webhook_notifier'
17
19
  autoload :IrcNotifier, 'exception_notifier/irc_notifier'
@@ -20,6 +22,7 @@ module ExceptionNotifier
20
22
  autoload :TeamsNotifier, 'exception_notifier/teams_notifier'
21
23
  autoload :SnsNotifier, 'exception_notifier/sns_notifier'
22
24
  autoload :GoogleChatNotifier, 'exception_notifier/google_chat_notifier'
25
+ autoload :DatadogNotifier, 'exception_notifier/datadog_notifier'
23
26
 
24
27
  class UndefinedNotifierError < StandardError; end
25
28
 
@@ -29,7 +32,10 @@ module ExceptionNotifier
29
32
 
30
33
  # Define a set of exceptions to be ignored, ie, dont send notifications when any of them are raised.
31
34
  mattr_accessor :ignored_exceptions
32
- @@ignored_exceptions = %w{ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError}
35
+ @@ignored_exceptions = %w[
36
+ ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound
37
+ ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError
38
+ ]
33
39
 
34
40
  mattr_accessor :testing_mode
35
41
  @@testing_mode = false
@@ -38,6 +44,9 @@ module ExceptionNotifier
38
44
  # Store conditions that decide when exceptions must be ignored or not.
39
45
  @@ignores = []
40
46
 
47
+ # Store by-notifier conditions that decide when exceptions must be ignored or not.
48
+ @@by_notifier_ignores = {}
49
+
41
50
  # Store notifiers that send notifications when exceptions are raised.
42
51
  @@notifiers = {}
43
52
 
@@ -45,19 +54,25 @@ module ExceptionNotifier
45
54
  self.testing_mode = true
46
55
  end
47
56
 
48
- def notify_exception(exception, options={}, &block)
57
+ def notify_exception(exception, options = {}, &block)
49
58
  return false if ignored_exception?(options[:ignore_exceptions], exception)
50
59
  return false if ignored?(exception, options)
60
+
51
61
  if error_grouping
52
62
  errors_count = group_error!(exception, options)
53
63
  return false unless send_notification?(exception, errors_count)
54
64
  end
55
65
 
66
+ notification_fired = false
56
67
  selected_notifiers = options.delete(:notifiers) || notifiers
57
68
  [*selected_notifiers].each do |notifier|
58
- fire_notification(notifier, exception, options.dup, &block)
69
+ unless notifier_ignored?(exception, options, notifier: notifier)
70
+ fire_notification(notifier, exception, options.dup, &block)
71
+ notification_fired = true
72
+ end
59
73
  end
60
- true
74
+
75
+ notification_fired
61
76
  end
62
77
 
63
78
  def register_exception_notifier(name, notifier_or_options)
@@ -92,23 +107,52 @@ module ExceptionNotifier
92
107
  @@ignores << block
93
108
  end
94
109
 
110
+ def ignore_notifier_if(notifier, &block)
111
+ @@by_notifier_ignores[notifier] = block
112
+ end
113
+
114
+ def ignore_crawlers(crawlers)
115
+ ignore_if do |_exception, opts|
116
+ opts.key?(:env) && from_crawler(opts[:env], crawlers)
117
+ end
118
+ end
119
+
95
120
  def clear_ignore_conditions!
96
121
  @@ignores.clear
122
+ @@by_notifier_ignores.clear
97
123
  end
98
124
 
99
125
  private
126
+
100
127
  def ignored?(exception, options)
101
- @@ignores.any?{ |condition| condition.call(exception, options) }
128
+ @@ignores.any? { |condition| condition.call(exception, options) }
102
129
  rescue Exception => e
103
130
  raise e if @@testing_mode
104
131
 
105
- logger.warn "An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
132
+ logger.warn(
133
+ "An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
134
+ )
135
+ false
136
+ end
137
+
138
+ def notifier_ignored?(exception, options, notifier:)
139
+ return false unless @@by_notifier_ignores.key?(notifier)
140
+
141
+ condition = @@by_notifier_ignores[notifier]
142
+ condition.call(exception, options)
143
+ rescue Exception => e
144
+ raise e if @@testing_mode
145
+
146
+ logger.warn(<<~"MESSAGE")
147
+ An error occurred when evaluating a by-notifier ignore condition. #{e.class}: #{e.message}
148
+ #{e.backtrace.join("\n")}
149
+ MESSAGE
106
150
  false
107
151
  end
108
152
 
109
153
  def ignored_exception?(ignore_array, exception)
110
154
  all_ignored_exceptions = (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s)
111
- exception_ancestors = exception.class.ancestors.map(&:to_s)
155
+ exception_ancestors = exception.singleton_class.ancestors.map(&:to_s)
112
156
  !(all_ignored_exceptions & exception_ancestors).empty?
113
157
  end
114
158
 
@@ -118,7 +162,10 @@ module ExceptionNotifier
118
162
  rescue Exception => e
119
163
  raise e if @@testing_mode
120
164
 
121
- logger.warn "An error occurred when sending a notification using '#{notifier_name}' notifier. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
165
+ logger.warn(
166
+ "An error occurred when sending a notification using '#{notifier_name}' notifier." \
167
+ "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
168
+ )
122
169
  false
123
170
  end
124
171
 
@@ -128,7 +175,15 @@ module ExceptionNotifier
128
175
  notifier = notifier_class.new(options)
129
176
  register_exception_notifier(name, notifier)
130
177
  rescue NameError => e
131
- raise UndefinedNotifierError, "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}"
178
+ raise UndefinedNotifierError,
179
+ "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}"
180
+ end
181
+
182
+ def from_crawler(env, ignored_crawlers)
183
+ agent = env['HTTP_USER_AGENT']
184
+ Array(ignored_crawlers).any? do |crawler|
185
+ agent =~ Regexp.new(crawler)
186
+ end
132
187
  end
133
188
  end
134
189
  end
@@ -1,14 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotification
2
4
  module Generators
3
5
  class InstallGenerator < Rails::Generators::Base
4
- desc "Creates a ExceptionNotification initializer."
6
+ desc 'Creates a ExceptionNotification initializer.'
5
7
 
6
- source_root File.expand_path('../templates', __FILE__)
7
- class_option :resque, :type => :boolean, :desc => 'Add support for sending notifications when errors occur in Resque jobs.'
8
- class_option :sidekiq, :type => :boolean, :desc => 'Add support for sending notifications when errors occur in Sidekiq jobs.'
8
+ source_root File.expand_path('templates', __dir__)
9
+ class_option :resque,
10
+ type: :boolean,
11
+ desc: 'Add support for sending notifications when errors occur in Resque jobs.'
12
+ class_option :sidekiq,
13
+ type: :boolean,
14
+ desc: 'Add support for sending notifications when errors occur in Sidekiq jobs.'
9
15
 
10
16
  def copy_initializer
11
- template 'exception_notification.rb', 'config/initializers/exception_notification.rb'
17
+ template 'exception_notification.rb.erb', 'config/initializers/exception_notification.rb'
12
18
  end
13
19
  end
14
20
  end
@@ -22,32 +22,34 @@ ExceptionNotification.configure do |config|
22
22
  # not Rails.env.production?
23
23
  # end
24
24
 
25
+ # Ignore exceptions generated by crawlers
26
+ # config.ignore_crawlers %w{Googlebot bingbot}
27
+
25
28
  # Notifiers =================================================================
26
29
 
27
30
  # Email notifier sends notifications by email.
28
31
  config.add_notifier :email, {
29
- :email_prefix => "[ERROR] ",
30
- :sender_address => %{"Notifier" <notifier@example.com>},
31
- :exception_recipients => %w{exceptions@example.com}
32
+ email_prefix: '[ERROR] ',
33
+ sender_address: %{"Notifier" <notifier@example.com>},
34
+ exception_recipients: %w{exceptions@example.com}
32
35
  }
33
36
 
34
37
  # Campfire notifier sends notifications to your Campfire room. Requires 'tinder' gem.
35
38
  # config.add_notifier :campfire, {
36
- # :subdomain => 'my_subdomain',
37
- # :token => 'my_token',
38
- # :room_name => 'my_room'
39
+ # subdomain: 'my_subdomain',
40
+ # token: 'my_token',
41
+ # room_name: 'my_room'
39
42
  # }
40
43
 
41
44
  # HipChat notifier sends notifications to your HipChat room. Requires 'hipchat' gem.
42
45
  # config.add_notifier :hipchat, {
43
- # :api_token => 'my_token',
44
- # :room_name => 'my_room'
46
+ # api_token: 'my_token',
47
+ # room_name: 'my_room'
45
48
  # }
46
49
 
47
50
  # Webhook notifier sends notifications over HTTP protocol. Requires 'httparty' gem.
48
51
  # config.add_notifier :webhook, {
49
- # :url => 'http://example.com:5555/hubot/path',
50
- # :http_method => :post
52
+ # url: 'http://example.com:5555/hubot/path',
53
+ # http_method: :post
51
54
  # }
52
-
53
55
  end
@@ -1,44 +1,106 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class RackTest < ActiveSupport::TestCase
4
-
5
6
  setup do
6
7
  @pass_app = Object.new
7
8
  @pass_app.stubs(:call).returns([nil, { 'X-Cascade' => 'pass' }, nil])
8
9
 
9
10
  @normal_app = Object.new
10
- @normal_app.stubs(:call).returns([nil, { }, nil])
11
+ @normal_app.stubs(:call).returns([nil, {}, nil])
11
12
  end
12
13
 
13
14
  teardown do
14
- ExceptionNotifier.error_grouping = false
15
- ExceptionNotifier.notification_trigger = nil
15
+ ExceptionNotifier.reset_notifiers!
16
16
  end
17
17
 
18
- test "should ignore \"X-Cascade\" header by default" do
18
+ test 'should ignore "X-Cascade" header by default' do
19
19
  ExceptionNotifier.expects(:notify_exception).never
20
20
  ExceptionNotification::Rack.new(@pass_app).call({})
21
21
  end
22
22
 
23
- test "should notify on \"X-Cascade\" = \"pass\" if ignore_cascade_pass option is false" do
23
+ test 'should notify on "X-Cascade" = "pass" if ignore_cascade_pass option is false' do
24
24
  ExceptionNotifier.expects(:notify_exception).once
25
- ExceptionNotification::Rack.new(@pass_app, :ignore_cascade_pass => false).call({})
25
+ ExceptionNotification::Rack.new(@pass_app, ignore_cascade_pass: false).call({})
26
26
  end
27
27
 
28
- test "should assign error_grouping if error_grouping is specified" do
28
+ test 'should assign error_grouping if error_grouping is specified' do
29
29
  refute ExceptionNotifier.error_grouping
30
30
  ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({})
31
31
  assert ExceptionNotifier.error_grouping
32
32
  end
33
33
 
34
- test "should assign notification_trigger if notification_trigger is specified" do
34
+ test 'should assign notification_trigger if notification_trigger is specified' do
35
35
  assert_nil ExceptionNotifier.notification_trigger
36
- ExceptionNotification::Rack.new(@normal_app, notification_trigger: lambda {|i| true}).call({})
36
+ ExceptionNotification::Rack.new(@normal_app, notification_trigger: ->(_i) { true }).call({})
37
37
  assert_respond_to ExceptionNotifier.notification_trigger, :call
38
38
  end
39
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
40
+ if defined?(Rails) && Rails.respond_to?(:cache)
41
+ test 'should set default cache to Rails cache' do
42
+ ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({})
43
+ assert_equal Rails.cache, ExceptionNotifier.error_grouping_cache
44
+ end
45
+ end
46
+
47
+ test 'should ignore exceptions with Usar Agent in ignore_crawlers' do
48
+ exception_app = Object.new
49
+ exception_app.stubs(:call).raises(RuntimeError)
50
+
51
+ env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; Crawlerbot/2.1;)' }
52
+
53
+ begin
54
+ ExceptionNotification::Rack.new(exception_app, ignore_crawlers: %w[Crawlerbot]).call(env)
55
+
56
+ flunk
57
+ rescue StandardError
58
+ refute env['exception_notifier.delivered']
59
+ end
60
+ end
61
+
62
+ test 'should ignore exceptions if ignore_if condition is met' do
63
+ exception_app = Object.new
64
+ exception_app.stubs(:call).raises(RuntimeError)
65
+
66
+ env = {}
67
+
68
+ begin
69
+ ExceptionNotification::Rack.new(
70
+ exception_app,
71
+ ignore_if: ->(_env, exception) { exception.is_a? RuntimeError }
72
+ ).call(env)
73
+
74
+ flunk
75
+ rescue StandardError
76
+ refute env['exception_notifier.delivered']
77
+ end
78
+ end
79
+
80
+ test 'should ignore exceptions with notifiers that satisfies ignore_notifier_if condition' do
81
+ exception_app = Object.new
82
+ exception_app.stubs(:call).raises(RuntimeError)
83
+
84
+ notifier1_called = notifier2_called = false
85
+ notifier1 = ->(_exception, _options) { notifier1_called = true }
86
+ notifier2 = ->(_exception, _options) { notifier2_called = true }
87
+
88
+ env = {}
89
+
90
+ begin
91
+ ExceptionNotification::Rack.new(
92
+ exception_app,
93
+ ignore_notifier_if: {
94
+ notifier1: ->(_env, exception) { exception.is_a? RuntimeError }
95
+ },
96
+ notifier1: notifier1,
97
+ notifier2: notifier2
98
+ ).call(env)
99
+
100
+ flunk
101
+ rescue StandardError
102
+ refute notifier1_called
103
+ assert notifier2_called
104
+ end
43
105
  end
44
106
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ require 'exception_notification/resque'
6
+ require 'resque'
7
+ require 'mock_redis'
8
+ require 'resque/failure/multiple'
9
+ require 'resque/failure/redis'
10
+
11
+ class ResqueTest < ActiveSupport::TestCase
12
+ setup do
13
+ # Resque.redis=() only supports a String or Redis instance in Resque 1.8
14
+ Resque.instance_variable_set(:@redis, MockRedis.new)
15
+
16
+ Resque::Failure::Multiple.classes = [Resque::Failure::Redis, ExceptionNotification::Resque]
17
+ Resque::Failure.backend = Resque::Failure::Multiple
18
+
19
+ @worker = Resque::Worker.new(:jobs)
20
+ # Forking causes issues with Mocha's `.expects`
21
+ @worker.cant_fork = true
22
+ end
23
+
24
+ test 'count returns the number of failures' do
25
+ Resque::Job.create(:jobs, BadJob)
26
+ @worker.work(0)
27
+ assert_equal 1, ExceptionNotification::Resque.count
28
+ end
29
+
30
+ test 'notifies exception when job fails' do
31
+ ExceptionNotifier.expects(:notify_exception).with do |ex, opts|
32
+ ex.is_a?(RuntimeError) &&
33
+ ex.message == 'Bad job!' &&
34
+ opts[:data][:resque][:error_class] == 'RuntimeError' &&
35
+ opts[:data][:resque][:error_message] == 'Bad job!' &&
36
+ opts[:data][:resque][:failed_at].present? &&
37
+ opts[:data][:resque][:payload] == {
38
+ 'class' => 'ResqueTest::BadJob',
39
+ 'args' => []
40
+ } &&
41
+ opts[:data][:resque][:queue] == :jobs &&
42
+ opts[:data][:resque][:worker].present?
43
+ end
44
+
45
+ Resque::Job.create(:jobs, BadJob)
46
+ @worker.work(0)
47
+ end
48
+
49
+ class BadJob
50
+ def self.perform
51
+ raise 'Bad job!'
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'dogapi/common'
5
+ require 'dogapi/event'
6
+
7
+ class DatadogNotifierTest < ActiveSupport::TestCase
8
+ def setup
9
+ @client = FakeDatadogClient.new
10
+ @options = {
11
+ client: @client
12
+ }
13
+ @notifier = ExceptionNotifier::DatadogNotifier.new(@options)
14
+ @exception = FakeException.new
15
+ @controller = FakeController.new
16
+ @request = FakeRequest.new
17
+ end
18
+
19
+ test 'should send an event to datadog' do
20
+ fake_event = Dogapi::Event.any_instance
21
+ @client.expects(:emit_event).with(fake_event)
22
+
23
+ @notifier.stubs(:datadog_event).returns(fake_event)
24
+ @notifier.call(@exception)
25
+ end
26
+
27
+ test 'should include exception class in event title' do
28
+ event = @notifier.datadog_event(@exception)
29
+ assert_includes event.msg_title, 'FakeException'
30
+ end
31
+
32
+ test 'should include prefix in event title and not append previous events' do
33
+ options = {
34
+ client: @client,
35
+ title_prefix: 'prefix'
36
+ }
37
+
38
+ notifier = ExceptionNotifier::DatadogNotifier.new(options)
39
+ event = notifier.datadog_event(@exception)
40
+ assert_equal event.msg_title, 'prefix (DatadogNotifierTest::FakeException) "Fake exception message"'
41
+
42
+ event2 = notifier.datadog_event(@exception)
43
+ assert_equal event2.msg_title, 'prefix (DatadogNotifierTest::FakeException) "Fake exception message"'
44
+ end
45
+
46
+ test 'should include exception message in event title' do
47
+ event = @notifier.datadog_event(@exception)
48
+ assert_includes event.msg_title, 'Fake exception message'
49
+ end
50
+
51
+ test 'should include controller info in event title if controller information is available' do
52
+ event = @notifier.datadog_event(@exception,
53
+ env: {
54
+ 'action_controller.instance' => @controller,
55
+ 'REQUEST_METHOD' => 'GET',
56
+ 'rack.input' => ''
57
+ })
58
+ assert_includes event.msg_title, 'Fake controller'
59
+ assert_includes event.msg_title, 'Fake action'
60
+ end
61
+
62
+ test 'should include backtrace info in event body' do
63
+ event = @notifier.datadog_event(@exception)
64
+ assert_includes event.msg_text, "backtrace line 1\nbacktrace line 2\nbacktrace line 3"
65
+ end
66
+
67
+ test 'should include request info in event body' do
68
+ ActionDispatch::Request.stubs(:new).returns(@request)
69
+
70
+ event = @notifier.datadog_event(@exception,
71
+ env: {
72
+ 'action_controller.instance' => @controller,
73
+ 'REQUEST_METHOD' => 'GET',
74
+ 'rack.input' => ''
75
+ })
76
+ assert_includes event.msg_text, 'http://localhost:8080'
77
+ assert_includes event.msg_text, 'GET'
78
+ assert_includes event.msg_text, '127.0.0.1'
79
+ assert_includes event.msg_text, '{"param 1"=>"value 1", "param 2"=>"value 2"}'
80
+ end
81
+
82
+ test 'should include tags in event' do
83
+ options = {
84
+ client: @client,
85
+ tags: %w[error production]
86
+ }
87
+ notifier = ExceptionNotifier::DatadogNotifier.new(options)
88
+ event = notifier.datadog_event(@exception)
89
+ assert_equal event.tags, %w[error production]
90
+ end
91
+
92
+ test 'should include event title in event aggregation key' do
93
+ event = @notifier.datadog_event(@exception)
94
+ assert_equal event.aggregation_key, [event.msg_title]
95
+ end
96
+
97
+ class FakeDatadogClient
98
+ def emit_event(event); end
99
+ end
100
+
101
+ class FakeController
102
+ def controller_name
103
+ 'Fake controller'
104
+ end
105
+
106
+ def action_name
107
+ 'Fake action'
108
+ end
109
+ end
110
+
111
+ class FakeException
112
+ def backtrace
113
+ [
114
+ 'backtrace line 1',
115
+ 'backtrace line 2',
116
+ 'backtrace line 3',
117
+ 'backtrace line 4',
118
+ 'backtrace line 5'
119
+ ]
120
+ end
121
+
122
+ def message
123
+ 'Fake exception message'
124
+ end
125
+ end
126
+
127
+ class FakeRequest
128
+ def url
129
+ 'http://localhost:8080'
130
+ end
131
+
132
+ def request_method
133
+ 'GET'
134
+ end
135
+
136
+ def remote_ip
137
+ '127.0.0.1'
138
+ end
139
+
140
+ def filtered_parameters
141
+ {
142
+ 'param 1' => 'value 1',
143
+ 'param 2' => 'value 2'
144
+ }
145
+ end
146
+
147
+ def session
148
+ {
149
+ 'session_id' => '1234'
150
+ }
151
+ end
152
+ end
153
+ end