exception_notification 3.0.1 → 4.4.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 (153) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +7 -0
  3. data/CHANGELOG.rdoc +129 -1
  4. data/CODE_OF_CONDUCT.md +22 -0
  5. data/CONTRIBUTING.md +29 -1
  6. data/Gemfile +1 -1
  7. data/MIT-LICENSE +23 -0
  8. data/README.md +168 -222
  9. data/Rakefile +5 -11
  10. data/docs/notifiers/campfire.md +50 -0
  11. data/docs/notifiers/custom.md +42 -0
  12. data/docs/notifiers/datadog.md +51 -0
  13. data/docs/notifiers/email.md +195 -0
  14. data/docs/notifiers/google_chat.md +31 -0
  15. data/docs/notifiers/hipchat.md +66 -0
  16. data/docs/notifiers/irc.md +97 -0
  17. data/docs/notifiers/mattermost.md +115 -0
  18. data/docs/notifiers/slack.md +161 -0
  19. data/docs/notifiers/sns.md +37 -0
  20. data/docs/notifiers/teams.md +54 -0
  21. data/docs/notifiers/webhook.md +60 -0
  22. data/examples/sample_app.rb +54 -0
  23. data/examples/sinatra/Gemfile +8 -0
  24. data/examples/sinatra/Gemfile.lock +95 -0
  25. data/examples/sinatra/Procfile +2 -0
  26. data/examples/sinatra/README.md +11 -0
  27. data/examples/sinatra/config.ru +3 -0
  28. data/examples/sinatra/sinatra_app.rb +36 -0
  29. data/exception_notification.gemspec +32 -11
  30. data/gemfiles/rails4_0.gemfile +7 -0
  31. data/gemfiles/rails4_1.gemfile +7 -0
  32. data/gemfiles/rails4_2.gemfile +7 -0
  33. data/gemfiles/rails5_0.gemfile +7 -0
  34. data/gemfiles/rails5_1.gemfile +7 -0
  35. data/gemfiles/rails5_2.gemfile +7 -0
  36. data/gemfiles/rails6_0.gemfile +7 -0
  37. data/lib/exception_notification.rb +11 -0
  38. data/lib/exception_notification/rack.rb +55 -0
  39. data/lib/exception_notification/rails.rb +9 -0
  40. data/lib/exception_notification/resque.rb +22 -0
  41. data/lib/exception_notification/sidekiq.rb +27 -0
  42. data/lib/exception_notification/version.rb +3 -0
  43. data/lib/exception_notifier.rb +137 -61
  44. data/lib/exception_notifier/base_notifier.rb +24 -0
  45. data/lib/exception_notifier/campfire_notifier.rb +16 -11
  46. data/lib/exception_notifier/datadog_notifier.rb +153 -0
  47. data/lib/exception_notifier/email_notifier.rb +196 -0
  48. data/lib/exception_notifier/google_chat_notifier.rb +42 -0
  49. data/lib/exception_notifier/hipchat_notifier.rb +49 -0
  50. data/lib/exception_notifier/irc_notifier.rb +57 -0
  51. data/lib/exception_notifier/mattermost_notifier.rb +72 -0
  52. data/lib/exception_notifier/modules/backtrace_cleaner.rb +11 -0
  53. data/lib/exception_notifier/modules/error_grouping.rb +77 -0
  54. data/lib/exception_notifier/modules/formatter.rb +118 -0
  55. data/lib/exception_notifier/notifier.rb +9 -179
  56. data/lib/exception_notifier/slack_notifier.rb +111 -0
  57. data/lib/exception_notifier/sns_notifier.rb +85 -0
  58. data/lib/exception_notifier/teams_notifier.rb +193 -0
  59. data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +3 -1
  60. data/lib/exception_notifier/views/exception_notifier/_data.html.erb +6 -1
  61. data/lib/exception_notifier/views/exception_notifier/_environment.html.erb +8 -6
  62. data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -4
  63. data/lib/exception_notifier/views/exception_notifier/_request.html.erb +36 -5
  64. data/lib/exception_notifier/views/exception_notifier/_request.text.erb +10 -5
  65. data/lib/exception_notifier/views/exception_notifier/_session.html.erb +10 -2
  66. data/lib/exception_notifier/views/exception_notifier/_session.text.erb +2 -2
  67. data/lib/exception_notifier/views/exception_notifier/_title.html.erb +3 -3
  68. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.html.erb +38 -11
  69. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +10 -11
  70. data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +38 -22
  71. data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -3
  72. data/lib/exception_notifier/webhook_notifier.rb +51 -0
  73. data/lib/generators/exception_notification/install_generator.rb +15 -0
  74. data/lib/generators/exception_notification/templates/exception_notification.rb.erb +55 -0
  75. data/test/exception_notification/rack_test.rb +60 -0
  76. data/test/exception_notification/resque_test.rb +52 -0
  77. data/test/exception_notifier/campfire_notifier_test.rb +120 -0
  78. data/test/exception_notifier/datadog_notifier_test.rb +151 -0
  79. data/test/exception_notifier/email_notifier_test.rb +351 -0
  80. data/test/exception_notifier/google_chat_notifier_test.rb +181 -0
  81. data/test/exception_notifier/hipchat_notifier_test.rb +218 -0
  82. data/test/exception_notifier/irc_notifier_test.rb +137 -0
  83. data/test/exception_notifier/mattermost_notifier_test.rb +202 -0
  84. data/test/exception_notifier/modules/error_grouping_test.rb +165 -0
  85. data/test/exception_notifier/modules/formatter_test.rb +150 -0
  86. data/test/exception_notifier/sidekiq_test.rb +38 -0
  87. data/test/exception_notifier/slack_notifier_test.rb +227 -0
  88. data/test/exception_notifier/sns_notifier_test.rb +121 -0
  89. data/test/exception_notifier/teams_notifier_test.rb +90 -0
  90. data/test/exception_notifier/webhook_notifier_test.rb +96 -0
  91. data/test/exception_notifier_test.rb +182 -0
  92. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.html.erb +0 -0
  93. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.text.erb +0 -0
  94. data/test/{dummy/app → support}/views/exception_notifier/_new_section.html.erb +0 -0
  95. data/test/{dummy/app → support}/views/exception_notifier/_new_section.text.erb +0 -0
  96. data/test/test_helper.rb +12 -8
  97. metadata +333 -164
  98. data/.gemtest +0 -0
  99. data/Gemfile.lock +0 -122
  100. data/test/background_exception_notification_test.rb +0 -82
  101. data/test/campfire_test.rb +0 -53
  102. data/test/dummy/.gitignore +0 -4
  103. data/test/dummy/Gemfile +0 -33
  104. data/test/dummy/Gemfile.lock +0 -118
  105. data/test/dummy/Rakefile +0 -7
  106. data/test/dummy/app/controllers/application_controller.rb +0 -3
  107. data/test/dummy/app/controllers/posts_controller.rb +0 -30
  108. data/test/dummy/app/helpers/application_helper.rb +0 -2
  109. data/test/dummy/app/helpers/posts_helper.rb +0 -2
  110. data/test/dummy/app/models/post.rb +0 -2
  111. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  112. data/test/dummy/app/views/posts/_form.html.erb +0 -0
  113. data/test/dummy/app/views/posts/new.html.erb +0 -0
  114. data/test/dummy/app/views/posts/show.html.erb +0 -0
  115. data/test/dummy/config.ru +0 -4
  116. data/test/dummy/config/application.rb +0 -42
  117. data/test/dummy/config/boot.rb +0 -6
  118. data/test/dummy/config/database.yml +0 -22
  119. data/test/dummy/config/environment.rb +0 -13
  120. data/test/dummy/config/environments/development.rb +0 -24
  121. data/test/dummy/config/environments/production.rb +0 -49
  122. data/test/dummy/config/environments/test.rb +0 -35
  123. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  124. data/test/dummy/config/initializers/inflections.rb +0 -10
  125. data/test/dummy/config/initializers/mime_types.rb +0 -5
  126. data/test/dummy/config/initializers/secret_token.rb +0 -7
  127. data/test/dummy/config/initializers/session_store.rb +0 -8
  128. data/test/dummy/config/locales/en.yml +0 -5
  129. data/test/dummy/config/routes.rb +0 -3
  130. data/test/dummy/db/migrate/20110729022608_create_posts.rb +0 -15
  131. data/test/dummy/db/schema.rb +0 -24
  132. data/test/dummy/db/seeds.rb +0 -7
  133. data/test/dummy/lib/tasks/.gitkeep +0 -0
  134. data/test/dummy/public/404.html +0 -26
  135. data/test/dummy/public/422.html +0 -26
  136. data/test/dummy/public/500.html +0 -26
  137. data/test/dummy/public/favicon.ico +0 -0
  138. data/test/dummy/public/images/rails.png +0 -0
  139. data/test/dummy/public/index.html +0 -239
  140. data/test/dummy/public/javascripts/application.js +0 -2
  141. data/test/dummy/public/javascripts/controls.js +0 -965
  142. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  143. data/test/dummy/public/javascripts/effects.js +0 -1123
  144. data/test/dummy/public/javascripts/prototype.js +0 -6001
  145. data/test/dummy/public/javascripts/rails.js +0 -191
  146. data/test/dummy/public/robots.txt +0 -5
  147. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  148. data/test/dummy/public/stylesheets/scaffold.css +0 -56
  149. data/test/dummy/script/rails +0 -6
  150. data/test/dummy/test/fixtures/posts.yml +0 -11
  151. data/test/dummy/test/functional/posts_controller_test.rb +0 -239
  152. data/test/dummy/test/test_helper.rb +0 -13
  153. data/test/exception_notification_test.rb +0 -73
@@ -8,18 +8,17 @@
8
8
  begin
9
9
  summary = render(section).strip
10
10
  unless summary.blank?
11
- title = render("title", :title => section).strip
11
+ title = render("title", title: section).strip
12
12
  "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n"
13
13
  end
14
14
 
15
15
  rescue Exception => e
16
- title = render("title", :title => section).strip
16
+ title = render("title", title: section).strip
17
17
  summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n")
18
18
 
19
19
  [title, summary.gsub(/^/, " "), nil].join("\n\n")
20
20
  end
21
21
  end.join
22
- sections = sections.force_encoding('UTF-8').encode('UTF-16LE', :invalid => :replace).encode('UTF-8') if sections.respond_to?(:force_encoding)
23
22
  %>
24
23
 
25
24
  <%= raw sections %>
@@ -0,0 +1,51 @@
1
+ require 'action_dispatch'
2
+ require 'active_support/core_ext/time'
3
+
4
+ module ExceptionNotifier
5
+ class WebhookNotifier < BaseNotifier
6
+ def initialize(options)
7
+ super
8
+ @default_options = options
9
+ end
10
+
11
+ def call(exception, options = {})
12
+ env = options[:env]
13
+
14
+ options = options.reverse_merge(@default_options)
15
+ url = options.delete(:url)
16
+ http_method = options.delete(:http_method) || :post
17
+
18
+ options[:body] ||= {}
19
+ options[:body][:server] = Socket.gethostname
20
+ options[:body][:process] = $PROCESS_ID
21
+ if defined?(Rails) && Rails.respond_to?(:root)
22
+ options[:body][:rails_root] = Rails.root
23
+ end
24
+ options[:body][:exception] = {
25
+ error_class: exception.class.to_s,
26
+ message: exception.message.inspect,
27
+ backtrace: exception.backtrace
28
+ }
29
+ options[:body][:data] = (env && env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
30
+
31
+ unless env.nil?
32
+ request = ActionDispatch::Request.new(env)
33
+
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
+ }
41
+
42
+ options[:body][:request] = request_items
43
+ options[:body][:session] = request.session
44
+ options[:body][:environment] = request.filtered_env
45
+ end
46
+ send_notice(exception, options, nil, @default_options) do |_, _|
47
+ HTTParty.send(http_method, url, options)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ module ExceptionNotification
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ desc 'Creates a ExceptionNotification initializer.'
5
+
6
+ source_root File.expand_path('templates', __dir__)
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.'
9
+
10
+ def copy_initializer
11
+ template 'exception_notification.rb.erb', 'config/initializers/exception_notification.rb'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,55 @@
1
+ require 'exception_notification/rails'
2
+ <% if options.sidekiq? %>
3
+ require 'exception_notification/sidekiq'
4
+ <% end %>
5
+ <% if options.resque? %>
6
+ require 'resque/failure/multiple'
7
+ require 'resque/failure/redis'
8
+ require 'exception_notification/resque'
9
+
10
+ Resque::Failure::Multiple.classes = [Resque::Failure::Redis, ExceptionNotification::Resque]
11
+ Resque::Failure.backend = Resque::Failure::Multiple
12
+ <% end %>
13
+
14
+ ExceptionNotification.configure do |config|
15
+ # Ignore additional exception types.
16
+ # ActiveRecord::RecordNotFound, Mongoid::Errors::DocumentNotFound, AbstractController::ActionNotFound and ActionController::RoutingError are already added.
17
+ # config.ignored_exceptions += %w{ActionView::TemplateError CustomError}
18
+
19
+ # Adds a condition to decide when an exception must be ignored or not.
20
+ # The ignore_if method can be invoked multiple times to add extra conditions.
21
+ # config.ignore_if do |exception, options|
22
+ # not Rails.env.production?
23
+ # end
24
+
25
+ # Ignore exceptions generated by crawlers
26
+ # config.ignore_crawlers %w{Googlebot bingbot}
27
+
28
+ # Notifiers =================================================================
29
+
30
+ # Email notifier sends notifications by email.
31
+ config.add_notifier :email, {
32
+ email_prefix: '[ERROR] ',
33
+ sender_address: %{"Notifier" <notifier@example.com>},
34
+ exception_recipients: %w{exceptions@example.com}
35
+ }
36
+
37
+ # Campfire notifier sends notifications to your Campfire room. Requires 'tinder' gem.
38
+ # config.add_notifier :campfire, {
39
+ # subdomain: 'my_subdomain',
40
+ # token: 'my_token',
41
+ # room_name: 'my_room'
42
+ # }
43
+
44
+ # HipChat notifier sends notifications to your HipChat room. Requires 'hipchat' gem.
45
+ # config.add_notifier :hipchat, {
46
+ # api_token: 'my_token',
47
+ # room_name: 'my_room'
48
+ # }
49
+
50
+ # Webhook notifier sends notifications over HTTP protocol. Requires 'httparty' gem.
51
+ # config.add_notifier :webhook, {
52
+ # url: 'http://example.com:5555/hubot/path',
53
+ # http_method: :post
54
+ # }
55
+ end
@@ -0,0 +1,60 @@
1
+ require 'test_helper'
2
+
3
+ class RackTest < ActiveSupport::TestCase
4
+ setup do
5
+ @pass_app = Object.new
6
+ @pass_app.stubs(:call).returns([nil, { 'X-Cascade' => 'pass' }, nil])
7
+
8
+ @normal_app = Object.new
9
+ @normal_app.stubs(:call).returns([nil, {}, nil])
10
+ end
11
+
12
+ teardown do
13
+ ExceptionNotifier.error_grouping = false
14
+ ExceptionNotifier.notification_trigger = nil
15
+ end
16
+
17
+ test 'should ignore "X-Cascade" header by default' do
18
+ ExceptionNotifier.expects(:notify_exception).never
19
+ ExceptionNotification::Rack.new(@pass_app).call({})
20
+ end
21
+
22
+ test 'should notify on "X-Cascade" = "pass" if ignore_cascade_pass option is false' do
23
+ ExceptionNotifier.expects(:notify_exception).once
24
+ ExceptionNotification::Rack.new(@pass_app, ignore_cascade_pass: false).call({})
25
+ end
26
+
27
+ test 'should assign error_grouping if error_grouping is specified' do
28
+ refute ExceptionNotifier.error_grouping
29
+ ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({})
30
+ assert ExceptionNotifier.error_grouping
31
+ end
32
+
33
+ test 'should assign notification_trigger if notification_trigger is specified' do
34
+ assert_nil ExceptionNotifier.notification_trigger
35
+ ExceptionNotification::Rack.new(@normal_app, notification_trigger: ->(_i) { true }).call({})
36
+ assert_respond_to ExceptionNotifier.notification_trigger, :call
37
+ end
38
+
39
+ if defined?(Rails) && Rails.respond_to?(:cache)
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
44
+ end
45
+
46
+ test 'should ignore exceptions with Usar Agent in ignore_crawlers' do
47
+ exception_app = Object.new
48
+ exception_app.stubs(:call).raises(RuntimeError)
49
+
50
+ env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; Crawlerbot/2.1;)' }
51
+
52
+ begin
53
+ ExceptionNotification::Rack.new(exception_app, ignore_crawlers: %w[Crawlerbot]).call(env)
54
+
55
+ flunk
56
+ rescue StandardError
57
+ refute env['exception_notifier.delivered']
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,52 @@
1
+ require 'test_helper'
2
+
3
+ require 'exception_notification/resque'
4
+ require 'resque'
5
+ require 'mock_redis'
6
+ require 'resque/failure/multiple'
7
+ require 'resque/failure/redis'
8
+
9
+ class ResqueTest < ActiveSupport::TestCase
10
+ setup do
11
+ # Resque.redis=() only supports a String or Redis instance in Resque 1.8
12
+ Resque.instance_variable_set(:@redis, MockRedis.new)
13
+
14
+ Resque::Failure::Multiple.classes = [Resque::Failure::Redis, ExceptionNotification::Resque]
15
+ Resque::Failure.backend = Resque::Failure::Multiple
16
+
17
+ @worker = Resque::Worker.new(:jobs)
18
+ # Forking causes issues with Mocha's `.expects`
19
+ @worker.cant_fork = true
20
+ end
21
+
22
+ test 'count returns the number of failures' do
23
+ Resque::Job.create(:jobs, BadJob)
24
+ @worker.work(0)
25
+ assert_equal 1, ExceptionNotification::Resque.count
26
+ end
27
+
28
+ test 'notifies exception when job fails' do
29
+ ExceptionNotifier.expects(:notify_exception).with do |ex, opts|
30
+ ex.is_a?(RuntimeError) &&
31
+ ex.message == 'Bad job!' &&
32
+ opts[:data][:resque][:error_class] == 'RuntimeError' &&
33
+ opts[:data][:resque][:error_message] == 'Bad job!' &&
34
+ opts[:data][:resque][:failed_at].present? &&
35
+ opts[:data][:resque][:payload] == {
36
+ 'class' => 'ResqueTest::BadJob',
37
+ 'args' => []
38
+ } &&
39
+ opts[:data][:resque][:queue] == :jobs &&
40
+ opts[:data][:resque][:worker].present?
41
+ end
42
+
43
+ Resque::Job.create(:jobs, BadJob)
44
+ @worker.work(0)
45
+ end
46
+
47
+ class BadJob
48
+ def self.perform
49
+ raise 'Bad job!'
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,120 @@
1
+ require 'test_helper'
2
+
3
+ # silence_warnings trick around require can be removed once
4
+ # https://github.com/collectiveidea/tinder/pull/77
5
+ # gets merged and released
6
+ silence_warnings do
7
+ require 'tinder'
8
+ end
9
+
10
+ class CampfireNotifierTest < ActiveSupport::TestCase
11
+ test 'should send campfire notification if properly configured' do
12
+ ExceptionNotifier::CampfireNotifier.stubs(:new).returns(Object.new)
13
+ campfire = ExceptionNotifier::CampfireNotifier.new(subdomain: 'test', token: 'test_token', room_name: 'test_room')
14
+ campfire.stubs(:call).returns(fake_notification)
15
+ notif = campfire.call(fake_exception)
16
+
17
+ assert !notif[:message].empty?
18
+ assert_equal notif[:message][:type], 'PasteMessage'
19
+ assert_includes notif[:message][:body], 'A new exception occurred:'
20
+ assert_includes notif[:message][:body], 'divided by 0'
21
+ assert_includes notif[:message][:body], '/exception_notification/test/campfire_test.rb:45'
22
+ end
23
+
24
+ test 'should send campfire notification without backtrace info if properly configured' do
25
+ ExceptionNotifier::CampfireNotifier.stubs(:new).returns(Object.new)
26
+ campfire = ExceptionNotifier::CampfireNotifier.new(subdomain: 'test', token: 'test_token', room_name: 'test_room')
27
+ campfire.stubs(:call).returns(fake_notification_without_backtrace)
28
+ notif = campfire.call(fake_exception_without_backtrace)
29
+
30
+ assert !notif[:message].empty?
31
+ assert_equal notif[:message][:type], 'PasteMessage'
32
+ assert_includes notif[:message][:body], 'A new exception occurred:'
33
+ assert_includes notif[:message][:body], 'my custom error'
34
+ end
35
+
36
+ test 'should not send campfire notification if badly configured' do
37
+ wrong_params = { subdomain: 'test', token: 'bad_token', room_name: 'test_room' }
38
+ Tinder::Campfire.stubs(:new).with('test', token: 'bad_token').returns(nil)
39
+ campfire = ExceptionNotifier::CampfireNotifier.new(wrong_params)
40
+
41
+ assert_nil campfire.room
42
+ assert_nil campfire.call(fake_exception)
43
+ end
44
+
45
+ test 'should not send campfire notification if config attr missing' do
46
+ wrong_params = { subdomain: 'test', room_name: 'test_room' }
47
+ Tinder::Campfire.stubs(:new).with('test', {}).returns(nil)
48
+ campfire = ExceptionNotifier::CampfireNotifier.new(wrong_params)
49
+
50
+ assert_nil campfire.room
51
+ assert_nil campfire.call(fake_exception)
52
+ end
53
+
54
+ test 'should send the new exception message if no :accumulated_errors_count option' do
55
+ campfire = ExceptionNotifier::CampfireNotifier.new({})
56
+ campfire.stubs(:active?).returns(true)
57
+ campfire.expects(:send_notice).with { |_, _, message| message.start_with?('A new exception occurred') }.once
58
+ campfire.call(fake_exception)
59
+ end
60
+
61
+ test 'shoud send the exception message if :accumulated_errors_count option greater than 1' do
62
+ campfire = ExceptionNotifier::CampfireNotifier.new({})
63
+ campfire.stubs(:active?).returns(true)
64
+ campfire.expects(:send_notice).with { |_, _, message| message.start_with?('The exception occurred 3 times:') }.once
65
+ campfire.call(fake_exception, accumulated_errors_count: 3)
66
+ end
67
+
68
+ test 'should call pre/post_callback if specified' do
69
+ pre_callback_called = 0
70
+ post_callback_called = 0
71
+ Tinder::Campfire.stubs(:new).returns(Object.new)
72
+
73
+ campfire = ExceptionNotifier::CampfireNotifier.new(
74
+ subdomain: 'test',
75
+ token: 'test_token',
76
+ room_name: 'test_room',
77
+ pre_callback: proc { |_opts, _notifier, _backtrace, _message, _message_opts|
78
+ pre_callback_called += 1
79
+ },
80
+ post_callback: proc { |_opts, _notifier, _backtrace, _message, _message_opts|
81
+ post_callback_called += 1
82
+ }
83
+ )
84
+ campfire.room = Object.new
85
+ campfire.room.stubs(:paste).returns(fake_notification)
86
+ campfire.call(fake_exception)
87
+ assert_equal(1, pre_callback_called)
88
+ assert_equal(1, post_callback_called)
89
+ end
90
+
91
+ private
92
+
93
+ def fake_notification
94
+ {
95
+ message: {
96
+ type: 'PasteMessage',
97
+ body: "A new exception occurred: 'divided by 0' on '/Users/sebastian/exception_notification/test/campfire_test.rb:45:in `/'"
98
+ }
99
+ }
100
+ end
101
+
102
+ def fake_exception
103
+ 5 / 0
104
+ rescue StandardError => e
105
+ e
106
+ end
107
+
108
+ def fake_notification_without_backtrace
109
+ {
110
+ message: {
111
+ type: 'PasteMessage',
112
+ body: "A new exception occurred: 'my custom error'"
113
+ }
114
+ }
115
+ end
116
+
117
+ def fake_exception_without_backtrace
118
+ StandardError.new('my custom error')
119
+ end
120
+ end
@@ -0,0 +1,151 @@
1
+ require 'test_helper'
2
+ require 'dogapi/common'
3
+ require 'dogapi/event'
4
+
5
+ class DatadogNotifierTest < ActiveSupport::TestCase
6
+ def setup
7
+ @client = FakeDatadogClient.new
8
+ @options = {
9
+ client: @client
10
+ }
11
+ @notifier = ExceptionNotifier::DatadogNotifier.new(@options)
12
+ @exception = FakeException.new
13
+ @controller = FakeController.new
14
+ @request = FakeRequest.new
15
+ end
16
+
17
+ test 'should send an event to datadog' do
18
+ fake_event = Dogapi::Event.any_instance
19
+ @client.expects(:emit_event).with(fake_event)
20
+
21
+ @notifier.stubs(:datadog_event).returns(fake_event)
22
+ @notifier.call(@exception)
23
+ end
24
+
25
+ test 'should include exception class in event title' do
26
+ event = @notifier.datadog_event(@exception)
27
+ assert_includes event.msg_title, 'FakeException'
28
+ end
29
+
30
+ test 'should include prefix in event title and not append previous events' do
31
+ options = {
32
+ client: @client,
33
+ title_prefix: 'prefix'
34
+ }
35
+
36
+ notifier = ExceptionNotifier::DatadogNotifier.new(options)
37
+ event = notifier.datadog_event(@exception)
38
+ assert_equal event.msg_title, 'prefix (DatadogNotifierTest::FakeException) "Fake exception message"'
39
+
40
+ event2 = notifier.datadog_event(@exception)
41
+ assert_equal event2.msg_title, 'prefix (DatadogNotifierTest::FakeException) "Fake exception message"'
42
+ end
43
+
44
+ test 'should include exception message in event title' do
45
+ event = @notifier.datadog_event(@exception)
46
+ assert_includes event.msg_title, 'Fake exception message'
47
+ end
48
+
49
+ test 'should include controller info in event title if controller information is available' do
50
+ event = @notifier.datadog_event(@exception,
51
+ env: {
52
+ 'action_controller.instance' => @controller,
53
+ 'REQUEST_METHOD' => 'GET',
54
+ 'rack.input' => ''
55
+ })
56
+ assert_includes event.msg_title, 'Fake controller'
57
+ assert_includes event.msg_title, 'Fake action'
58
+ end
59
+
60
+ test 'should include backtrace info in event body' do
61
+ event = @notifier.datadog_event(@exception)
62
+ assert_includes event.msg_text, "backtrace line 1\nbacktrace line 2\nbacktrace line 3"
63
+ end
64
+
65
+ test 'should include request info in event body' do
66
+ ActionDispatch::Request.stubs(:new).returns(@request)
67
+
68
+ event = @notifier.datadog_event(@exception,
69
+ env: {
70
+ 'action_controller.instance' => @controller,
71
+ 'REQUEST_METHOD' => 'GET',
72
+ 'rack.input' => ''
73
+ })
74
+ assert_includes event.msg_text, 'http://localhost:8080'
75
+ assert_includes event.msg_text, 'GET'
76
+ assert_includes event.msg_text, '127.0.0.1'
77
+ assert_includes event.msg_text, '{"param 1"=>"value 1", "param 2"=>"value 2"}'
78
+ end
79
+
80
+ test 'should include tags in event' do
81
+ options = {
82
+ client: @client,
83
+ tags: %w[error production]
84
+ }
85
+ notifier = ExceptionNotifier::DatadogNotifier.new(options)
86
+ event = notifier.datadog_event(@exception)
87
+ assert_equal event.tags, %w[error production]
88
+ end
89
+
90
+ test 'should include event title in event aggregation key' do
91
+ event = @notifier.datadog_event(@exception)
92
+ assert_equal event.aggregation_key, [event.msg_title]
93
+ end
94
+
95
+ class FakeDatadogClient
96
+ def emit_event(event); end
97
+ end
98
+
99
+ class FakeController
100
+ def controller_name
101
+ 'Fake controller'
102
+ end
103
+
104
+ def action_name
105
+ 'Fake action'
106
+ end
107
+ end
108
+
109
+ class FakeException
110
+ def backtrace
111
+ [
112
+ 'backtrace line 1',
113
+ 'backtrace line 2',
114
+ 'backtrace line 3',
115
+ 'backtrace line 4',
116
+ 'backtrace line 5'
117
+ ]
118
+ end
119
+
120
+ def message
121
+ 'Fake exception message'
122
+ end
123
+ end
124
+
125
+ class FakeRequest
126
+ def url
127
+ 'http://localhost:8080'
128
+ end
129
+
130
+ def request_method
131
+ 'GET'
132
+ end
133
+
134
+ def remote_ip
135
+ '127.0.0.1'
136
+ end
137
+
138
+ def filtered_parameters
139
+ {
140
+ 'param 1' => 'value 1',
141
+ 'param 2' => 'value 2'
142
+ }
143
+ end
144
+
145
+ def session
146
+ {
147
+ 'session_id' => '1234'
148
+ }
149
+ end
150
+ end
151
+ end