From 4c0fbbc2f2d3f77ebae98b6643909cce75996af2 Mon Sep 17 00:00:00 2001 From: WGautier Date: Mon, 19 Jun 2017 11:55:55 -0700 Subject: [PATCH 001/156] Ignore subclasses of ignored exceptions --- lib/exception_notifier.rb | 4 +++- test/exception_notifier_test.rb | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index e99e5dd1..b4682e13 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -104,7 +104,9 @@ def ignored?(exception, options) end def ignored_exception?(ignore_array, exception) - (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s).include?(exception.class.name) + all_ignored_exceptions = (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s) + exception_ancestors = exception.class.ancestors.map(&:to_s) + !(all_ignored_exceptions & exception_ancestors).empty? end def fire_notification(notifier_name, exception, options) diff --git a/test/exception_notifier_test.rb b/test/exception_notifier_test.rb index f1110e59..920ba400 100644 --- a/test/exception_notifier_test.rb +++ b/test/exception_notifier_test.rb @@ -110,6 +110,21 @@ class ExceptionNotifierTest < ActiveSupport::TestCase assert_equal @notifier_calls, 1 end + test "should not send notification if subclass of one of ignored exceptions" do + ExceptionNotifier.register_exception_notifier(:test, @test_notifier) + + class StandardErrorSubclass < StandardError + end + + exception = StandardErrorSubclass.new + + ExceptionNotifier.notify_exception(exception, {:notifiers => :test}) + assert_equal @notifier_calls, 1 + + ExceptionNotifier.notify_exception(exception, {:notifiers => :test, :ignore_exceptions => 'StandardError' }) + assert_equal @notifier_calls, 1 + end + test "should not call group_error! or send_notification? if error_grouping false" do exception = StandardError.new ExceptionNotifier.expects(:group_error!).never From d3e81f1095f28990e47696f38d9459e795c3d3ba Mon Sep 17 00:00:00 2001 From: Irfan Fadilah Date: Sat, 9 Dec 2017 16:24:12 +0700 Subject: [PATCH 002/156] Fix Prepend Accumulated Errors Count in Email Subject --- lib/exception_notifier/email_notifier.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index fb63f372..d5a68ddc 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -60,7 +60,7 @@ def background_exception_notification(exception, options={}, default_options={}) def compose_subject subject = "#{@options[:email_prefix]}" - subject << "(#{@options[:accumulated_errors_count]} times) " if @options[:accumulated_errors_count].to_i > 1 + subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1 subject << "#{@kontroller.controller_name} #{@kontroller.action_name}" if @kontroller && @options[:include_controller_and_action_names_in_subject] subject << " (#{@exception.class})" subject << " #{@exception.message.inspect}" if @options[:verbose_subject] @@ -75,17 +75,17 @@ def set_data_variables end helper_method :inspect_object - + def truncate(string, max) string.length > max ? "#{string[0...max]}..." : string end - + def inspect_object(object) case object when Hash, Array truncate(object.inspect, 300) else - object.to_s + object.to_s end end From edb47a482d83afca729f00823ef2415ec67e7b7d Mon Sep 17 00:00:00 2001 From: Joe Francis Date: Thu, 14 Dec 2017 14:18:19 -0600 Subject: [PATCH 003/156] update rubies for travis, add rails 5.1 appraisal --- .travis.yml | 12 ++++++++---- Appraisals | 3 +-- gemfiles/rails4_0.gemfile | 1 - gemfiles/rails4_1.gemfile | 1 - gemfiles/rails4_2.gemfile | 1 - gemfiles/rails5_0.gemfile | 1 - gemfiles/rails5_1.gemfile | 7 +++++++ 7 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 gemfiles/rails5_1.gemfile diff --git a/.travis.yml b/.travis.yml index bde8eada..08d8eefc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: ruby rvm: - - 2.1.9 - - 2.2.5 - - 2.3.1 + - 2.1.10 + - 2.2.8 + - 2.3.5 + - 2.4.2 env: - COVERALLS_SILENT=true install: @@ -14,7 +15,10 @@ gemfile: - gemfiles/rails4_1.gemfile - gemfiles/rails4_2.gemfile - gemfiles/rails5_0.gemfile + - gemfiles/rails5_1.gemfile matrix: exclude: - - rvm: 2.1.9 + - rvm: 2.1.10 gemfile: gemfiles/rails5_0.gemfile + - rvm: 2.1.10 + gemfile: gemfiles/rails5_1.gemfile diff --git a/Appraisals b/Appraisals index a8b339df..6220edeb 100644 --- a/Appraisals +++ b/Appraisals @@ -1,8 +1,7 @@ -rails_versions = ['~> 4.0.5', '~> 4.1.1', '~> 4.2.0', '~> 5.0.0'] +rails_versions = ['~> 4.0.5', '~> 4.1.1', '~> 4.2.0', '~> 5.0.0', '~> 5.1.0'] rails_versions.each do |rails_version| appraise "rails#{rails_version.slice(/\d+\.\d+/).gsub('.', '_')}" do gem 'rails', rails_version - gem "sqlite3" end end diff --git a/gemfiles/rails4_0.gemfile b/gemfiles/rails4_0.gemfile index 7236c649..39a1bd6b 100644 --- a/gemfiles/rails4_0.gemfile +++ b/gemfiles/rails4_0.gemfile @@ -3,6 +3,5 @@ source "https://rubygems.org" gem "rails", "~> 4.0.5" -gem "sqlite3" gemspec :path => "../" diff --git a/gemfiles/rails4_1.gemfile b/gemfiles/rails4_1.gemfile index 4b8d1481..8dfe1fe3 100644 --- a/gemfiles/rails4_1.gemfile +++ b/gemfiles/rails4_1.gemfile @@ -3,6 +3,5 @@ source "https://rubygems.org" gem "rails", "~> 4.1.1" -gem "sqlite3" gemspec :path => "../" diff --git a/gemfiles/rails4_2.gemfile b/gemfiles/rails4_2.gemfile index 46a47fc3..cd8b45b1 100644 --- a/gemfiles/rails4_2.gemfile +++ b/gemfiles/rails4_2.gemfile @@ -3,6 +3,5 @@ source "https://rubygems.org" gem "rails", "~> 4.2.0" -gem "sqlite3" gemspec :path => "../" diff --git a/gemfiles/rails5_0.gemfile b/gemfiles/rails5_0.gemfile index 0ee5036d..123ad559 100644 --- a/gemfiles/rails5_0.gemfile +++ b/gemfiles/rails5_0.gemfile @@ -3,6 +3,5 @@ source "https://rubygems.org" gem "rails", "~> 5.0.0" -gem "sqlite3" gemspec :path => "../" diff --git a/gemfiles/rails5_1.gemfile b/gemfiles/rails5_1.gemfile new file mode 100644 index 00000000..20a05ff9 --- /dev/null +++ b/gemfiles/rails5_1.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 5.1.0" + +gemspec :path => "../" From b8f91c26322223aa0747a80594c843d964cfc530 Mon Sep 17 00:00:00 2001 From: Joe Francis Date: Fri, 5 Jan 2018 20:20:00 -0600 Subject: [PATCH 004/156] update to latest rubies --- .travis.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 08d8eefc..1740ecf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ language: ruby rvm: - 2.1.10 - - 2.2.8 - - 2.3.5 - - 2.4.2 + - 2.2.9 + - 2.3.6 + - 2.4.3 + - 2.5.0 env: - COVERALLS_SILENT=true install: @@ -22,3 +23,12 @@ matrix: gemfile: gemfiles/rails5_0.gemfile - rvm: 2.1.10 gemfile: gemfiles/rails5_1.gemfile + # rails <=4.1 segfaults with ruby 2.4+ + - rvm: 2.4.3 + gemfile: gemfiles/rails4_0.gemfile + - rvm: 2.4.3 + gemfile: gemfiles/rails4_1.gemfile + - rvm: 2.5.0 + gemfile: gemfiles/rails4_0.gemfile + - rvm: 2.5.0 + gemfile: gemfiles/rails4_1.gemfile From 8fb461293d5c132ce1a4cb8c3b1604cc446e31b5 Mon Sep 17 00:00:00 2001 From: Nicolas Rodriguez Date: Thu, 8 Feb 2018 20:40:37 +0100 Subject: [PATCH 005/156] Fix missing MissingController class (sic) https://github.com/smartinez87/exception_notification/issues/408 --- lib/exception_notifier/mattermost_notifier.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/exception_notifier/mattermost_notifier.rb b/lib/exception_notifier/mattermost_notifier.rb index b331b6c7..c8c85fe6 100644 --- a/lib/exception_notifier/mattermost_notifier.rb +++ b/lib/exception_notifier/mattermost_notifier.rb @@ -5,6 +5,11 @@ module ExceptionNotifier class MattermostNotifier include ExceptionNotifier::BacktraceCleaner + class MissingController + def method_missing(*args, &block) + end + end + attr_accessor :httparty def initialize(options = {}) From 14ee1f5f6621192c9141071eebd9812816f94222 Mon Sep 17 00:00:00 2001 From: Masataka Pocke Kuwabara Date: Thu, 8 Mar 2018 12:23:58 +0900 Subject: [PATCH 006/156] Make `ExceptionNotifier.notify_exception` to receive a block --- lib/exception_notifier.rb | 8 ++++---- test/exception_notifier_test.rb | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index b4682e13..bae96011 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -42,7 +42,7 @@ def testing_mode! self.testing_mode = true end - def notify_exception(exception, options={}) + def notify_exception(exception, options={}, &block) return false if ignored_exception?(options[:ignore_exceptions], exception) return false if ignored?(exception, options) if error_grouping @@ -52,7 +52,7 @@ def notify_exception(exception, options={}) selected_notifiers = options.delete(:notifiers) || notifiers [*selected_notifiers].each do |notifier| - fire_notification(notifier, exception, options.dup) + fire_notification(notifier, exception, options.dup, &block) end true end @@ -109,9 +109,9 @@ def ignored_exception?(ignore_array, exception) !(all_ignored_exceptions & exception_ancestors).empty? end - def fire_notification(notifier_name, exception, options) + def fire_notification(notifier_name, exception, options, &block) notifier = registered_exception_notifier(notifier_name) - notifier.call(exception, options) + notifier.call(exception, options, &block) rescue Exception => e raise e if @@testing_mode diff --git a/test/exception_notifier_test.rb b/test/exception_notifier_test.rb index 920ba400..e2965c23 100644 --- a/test/exception_notifier_test.rb +++ b/test/exception_notifier_test.rb @@ -125,6 +125,20 @@ class StandardErrorSubclass < StandardError assert_equal @notifier_calls, 1 end + test "should call received block" do + @block_called = false + notifier = lambda { |exception, options, &block| block.call } + ExceptionNotifier.register_exception_notifier(:test, notifier) + + exception = ExceptionOne.new + + ExceptionNotifier.notify_exception(exception) do + @block_called = true + end + + assert @block_called + end + test "should not call group_error! or send_notification? if error_grouping false" do exception = StandardError.new ExceptionNotifier.expects(:group_error!).never From f8055173726b35a1d59b11d75de4e984869cde8c Mon Sep 17 00:00:00 2001 From: Matthias Viehweger Date: Thu, 31 May 2018 13:07:22 +0200 Subject: [PATCH 007/156] Align output of section-headers consistently Before, the header of the first section was prefixed by two spaces, leading to a visual disruption. --- .../background_exception_notification.text.erb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb b/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb index f484f6fd..7428de0e 100644 --- a/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +++ b/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb @@ -3,12 +3,12 @@ <%= @exception.message %> <%= @backtrace.first %> - <% sections = @sections.map do |section| - summary = render(section).strip - unless summary.blank? - title = render("title", :title => section).strip - "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n" - end - end.join - %> - <%= raw sections %> +<% sections = @sections.map do |section| + summary = render(section).strip + unless summary.blank? + title = render("title", :title => section).strip + "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n" + end + end.join +%> +<%= raw sections %> From 1360253531d3c1ccc7019e9eab773a45204cd864 Mon Sep 17 00:00:00 2001 From: Gareth Cokell Date: Thu, 19 Jul 2018 18:29:11 +0100 Subject: [PATCH 008/156] Replacing hangover reference to Rails.cache with error_grouping_cache option. Also changing dummy cache path away from rails cache path as this was causing the tests to pass in spite of this bug. --- lib/exception_notifier/modules/error_grouping.rb | 2 +- test/exception_notifier/modules/error_grouping_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/exception_notifier/modules/error_grouping.rb b/lib/exception_notifier/modules/error_grouping.rb index 85dbda4c..6eacacfd 100644 --- a/lib/exception_notifier/modules/error_grouping.rb +++ b/lib/exception_notifier/modules/error_grouping.rb @@ -52,7 +52,7 @@ def group_error!(exception, options) else backtrace_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}" - if count = Rails.cache.read(backtrace_based_key) + if count = error_grouping_cache.read(backtrace_based_key) accumulated_errors_count = count + 1 save_error_count(backtrace_based_key, accumulated_errors_count) else diff --git a/test/exception_notifier/modules/error_grouping_test.rb b/test/exception_notifier/modules/error_grouping_test.rb index 0053cf11..0b4c994f 100644 --- a/test/exception_notifier/modules/error_grouping_test.rb +++ b/test/exception_notifier/modules/error_grouping_test.rb @@ -5,7 +5,7 @@ class ErrorGroupTest < ActiveSupport::TestCase setup do module TestModule include ExceptionNotifier::ErrorGrouping - @@error_grouping_cache = ActiveSupport::Cache::FileStore.new("test/dummy/tmp/cache") + @@error_grouping_cache = ActiveSupport::Cache::FileStore.new("test/dummy/tmp/non_default_location") end @exception = RuntimeError.new("ERROR") From 230d234b5532368af2a0c5369782fca495c3b032 Mon Sep 17 00:00:00 2001 From: Gareth Cokell Date: Thu, 19 Jul 2018 18:29:11 +0100 Subject: [PATCH 009/156] Replacing hangover reference to Rails.cache with error_grouping_cache option. Also changing dummy cache path away from rails cache path as this was causing the tests to pass in spite of this bug. --- .travis.yml | 1 + lib/exception_notifier/modules/error_grouping.rb | 2 +- test/exception_notifier/modules/error_grouping_test.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1740ecf6..b86cd4bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: - "gem install bundler" - "bundle install --jobs=3 --retry=3" - "mkdir -p test/dummy/tmp/cache" + - "mkdir -p test/dummy/tmp/non_default_location" gemfile: - gemfiles/rails4_0.gemfile - gemfiles/rails4_1.gemfile diff --git a/lib/exception_notifier/modules/error_grouping.rb b/lib/exception_notifier/modules/error_grouping.rb index 85dbda4c..6eacacfd 100644 --- a/lib/exception_notifier/modules/error_grouping.rb +++ b/lib/exception_notifier/modules/error_grouping.rb @@ -52,7 +52,7 @@ def group_error!(exception, options) else backtrace_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}" - if count = Rails.cache.read(backtrace_based_key) + if count = error_grouping_cache.read(backtrace_based_key) accumulated_errors_count = count + 1 save_error_count(backtrace_based_key, accumulated_errors_count) else diff --git a/test/exception_notifier/modules/error_grouping_test.rb b/test/exception_notifier/modules/error_grouping_test.rb index 0053cf11..0b4c994f 100644 --- a/test/exception_notifier/modules/error_grouping_test.rb +++ b/test/exception_notifier/modules/error_grouping_test.rb @@ -5,7 +5,7 @@ class ErrorGroupTest < ActiveSupport::TestCase setup do module TestModule include ExceptionNotifier::ErrorGrouping - @@error_grouping_cache = ActiveSupport::Cache::FileStore.new("test/dummy/tmp/cache") + @@error_grouping_cache = ActiveSupport::Cache::FileStore.new("test/dummy/tmp/non_default_location") end @exception = RuntimeError.new("ERROR") From 41b0bb031907de40d6a9e771f3e4acd02c381ea3 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Mon, 13 Aug 2018 15:44:01 +0200 Subject: [PATCH 010/156] Add Google Chats notifier This commit adds Google Chats notifier by using the incoming webhooks. Implementation is based off the mattermost implementation. --- README.md | 33 ++++- lib/exception_notifier.rb | 1 + .../google_chat_notifier.rb | 136 ++++++++++++++++++ .../google_chat_notifier_test.rb | 128 +++++++++++++++++ 4 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 lib/exception_notifier/google_chat_notifier.rb create mode 100644 test/exception_notifier/google_chat_notifier_test.rb diff --git a/README.md b/README.md index f5a1ee26..cb32b9c7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ --- -The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [IRC](#irc-notifier) or via custom [WebHooks](#webhook-notifier). +The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [IRC](#irc-notifier), [Google Chat](#google-chat-notifier) or via custom [WebHooks](#webhook-notifier). There's a great [Railscast about Exception Notification](http://railscasts.com/episodes/104-exception-notifications-revised) you can see that may help you getting started. @@ -90,6 +90,7 @@ ExceptionNotification relies on notifiers to deliver notifications when errors o * [IRC notifier](#irc-notifier) * [Slack notifier](#slack-notifier) * [Mattermost notifier](#mattermost-notifier) +* [Google Chat notifier](#google-chat-notifier) * [WebHook notifier](#webhook-notifier) But, you also can easily implement your own [custom notifier](#custom-notifier). @@ -723,6 +724,36 @@ Url of your gitlab or github with your organisation name for issue creation link Your application name used for issue creation link. Defaults to ``` Rails.application.class.parent_name.underscore```. +### Google Chat Notifier + +Post notifications in a Google Chats channel via [incoming webhook](https://developers.google.com/hangouts/chat/how-tos/webhooks) + +Add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: + +```ruby +gem 'httparty' +``` + +To configure it, you **need** to set the `webhook_url` option. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + :google_chat => { + :webhook_url => 'https://chat.googleapis.com/v1/spaces/XXXXXXXX/messages?key=YYYYYYYYYYYYY&token=ZZZZZZZZZZZZ' + } +``` + +##### webhook_url + +*String, required* + +The Incoming WebHook URL on Google Chats. + +##### app_name + +*String, optional* + +Your application name, shown in the notification. Defaults to `Rails.application.class.parent_name.underscore`. ### WebHook notifier diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index bae96011..6e573bc2 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -17,6 +17,7 @@ module ExceptionNotifier autoload :IrcNotifier, 'exception_notifier/irc_notifier' autoload :SlackNotifier, 'exception_notifier/slack_notifier' autoload :MattermostNotifier, 'exception_notifier/mattermost_notifier' + autoload :GoogleChatNotifier, 'exception_notifier/google_chat_notifier' class UndefinedNotifierError < StandardError; end diff --git a/lib/exception_notifier/google_chat_notifier.rb b/lib/exception_notifier/google_chat_notifier.rb new file mode 100644 index 00000000..46aa71d9 --- /dev/null +++ b/lib/exception_notifier/google_chat_notifier.rb @@ -0,0 +1,136 @@ +require 'action_dispatch' +require 'active_support/core_ext/time' + +module ExceptionNotifier + class GoogleChatNotifier + include ExceptionNotifier::BacktraceCleaner + + class MissingController + def method_missing(*args, &block) + end + end + + attr_accessor :httparty + + def initialize(options = {}) + super() + @default_options = options + @httparty = HTTParty + end + + def call(exception, options = {}) + @options = options.merge(@default_options) + @exception = exception + @backtrace = exception.backtrace ? clean_backtrace(exception) : nil + + @env = @options.delete(:env) + + @application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore + + @webhook_url = @options.delete(:webhook_url) + raise ArgumentError.new "You must provide 'webhook_url' parameter." unless @webhook_url + + unless @env.nil? + @controller = @env['action_controller.instance'] || MissingController.new + + request = ActionDispatch::Request.new(@env) + + @request_items = { url: request.original_url, + http_method: request.method, + ip_address: request.remote_ip, + parameters: request.filtered_parameters, + timestamp: Time.current } + else + @controller = @request_items = nil + end + + + @options[:body] = payload.to_json + @options[:headers] ||= {} + @options[:headers].merge!({ 'Content-Type' => 'application/json' }) + + @httparty.post(@webhook_url, @options) + end + + private + + def payload + { + text: exception_text + } + end + + def header + errors_count = @options[:accumulated_errors_count].to_i + text = [''] + + text << "Application: *#{@application_name}*" + text << "#{errors_count > 1 ? errors_count : 'An'} *#{@exception.class}* occured" + if @controller then " in *#{controller_and_method}*." else "." end + + text + end + + def exception_text + text = [] + + text << header + text << '' + + text << "⚠️ Error 500 in #{Rails.env} ⚠️" + text << "*#{@exception.message.gsub('`', %q('))}*" + + if @request_items + text << '' + text += message_request + end + + if @backtrace + text << '' + text += message_backtrace + end + + text.join("\n") + end + + def message_request + text = [] + + text << "*Request:*" + text << "```" + text << hash_presentation(@request_items) + text << "```" + + text + end + + def hash_presentation(hash) + text = [] + + hash.each do |key, value| + text << "* #{key} : #{value}" + end + + text.join("\n") + end + + def message_backtrace(size = 3) + text = [] + + size = @backtrace.size < size ? @backtrace.size : size + text << "*Backtrace:*" + text << "```" + size.times { |i| text << "* " + @backtrace[i] } + text << "```" + + text + end + + def controller_and_method + if @controller + "#{@controller.controller_name}##{@controller.action_name}" + else + "" + end + end + end +end diff --git a/test/exception_notifier/google_chat_notifier_test.rb b/test/exception_notifier/google_chat_notifier_test.rb new file mode 100644 index 00000000..9b9a9f15 --- /dev/null +++ b/test/exception_notifier/google_chat_notifier_test.rb @@ -0,0 +1,128 @@ +require 'test_helper' +require 'httparty' + +class GoogleChatNotifierTest < ActiveSupport::TestCase + + test "should send notification if properly configured" do + options = { + :webhook_url => 'http://localhost:8000' + } + google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new + google_chat_notifier.httparty = FakeHTTParty.new + + options = google_chat_notifier.call ArgumentError.new("foo"), options + + body = ActiveSupport::JSON.decode options[:body] + assert body.has_key? 'text' + + text = body['text'].split("\n") + assert_equal 6, text.size + assert_equal 'Application: *dummy*', text[1] + assert_equal 'An *ArgumentError* occured.', text[2] + assert_equal '*foo*', text[5] + end + + test "should use 'An' for exceptions count if :accumulated_errors_count option is nil" do + google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new + exception = ArgumentError.new("foo") + google_chat_notifier.instance_variable_set(:@exception, exception) + google_chat_notifier.instance_variable_set(:@options, {}) + + assert_includes google_chat_notifier.send(:header), "An *ArgumentError* occured." + end + + test "shoud use direct errors count if :accumulated_errors_count option is 5" do + google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new + exception = ArgumentError.new("foo") + google_chat_notifier.instance_variable_set(:@exception, exception) + google_chat_notifier.instance_variable_set(:@options, { accumulated_errors_count: 5 }) + + assert_includes google_chat_notifier.send(:header), "5 *ArgumentError* occured." + end + + test "Message request should be formatted as hash" do + google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new + request_items = { url: 'http://test.address', + http_method: :get, + ip_address: '127.0.0.1', + parameters: '{"id"=>"foo"}', + timestamp: Time.parse('2018-08-13 12:13:24 UTC') } + google_chat_notifier.instance_variable_set(:@request_items, request_items) + + message_request = google_chat_notifier.send(:message_request).join("\n") + assert_includes message_request, '* url : http://test.address' + assert_includes message_request, '* http_method : get' + assert_includes message_request, '* ip_address : 127.0.0.1' + assert_includes message_request, '* parameters : {"id"=>"foo"}' + assert_includes message_request, '* timestamp : 2018-08-13 12:13:24 UTC' + end + + test 'backtrace with less than 3 lines should be displayed fully' do + google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new + + backtrace = ["app/controllers/my_controller.rb:53:in `my_controller_params'", "app/controllers/my_controller.rb:34:in `update'"] + google_chat_notifier.instance_variable_set(:@backtrace, backtrace) + + message_backtrace = google_chat_notifier.send(:message_backtrace).join("\n") + assert_includes message_backtrace, "* app/controllers/my_controller.rb:53:in `my_controller_params'" + assert_includes message_backtrace, "* app/controllers/my_controller.rb:34:in `update'" + end + + test 'backtrace with more than 3 lines should display only top 3 lines' do + google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new + + backtrace = ["app/controllers/my_controller.rb:99:in `specific_function'", "app/controllers/my_controller.rb:70:in `specific_param'", "app/controllers/my_controller.rb:53:in `my_controller_params'", "app/controllers/my_controller.rb:34:in `update'"] + google_chat_notifier.instance_variable_set(:@backtrace, backtrace) + + message_backtrace = google_chat_notifier.send(:message_backtrace).join("\n") + assert_includes message_backtrace, "* app/controllers/my_controller.rb:99:in `specific_function'" + assert_includes message_backtrace, "* app/controllers/my_controller.rb:70:in `specific_param'" + assert_includes message_backtrace, "* app/controllers/my_controller.rb:53:in `my_controller_params'" + assert_not_includes message_backtrace, "* app/controllers/my_controller.rb:34:in `update'" + end + + test 'Get text with backtrace and request info' do + google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new + + backtrace = ["app/controllers/my_controller.rb:53:in `my_controller_params'", "app/controllers/my_controller.rb:34:in `update'"] + google_chat_notifier.instance_variable_set(:@backtrace, backtrace) + + request_items = { url: 'http://test.address', + http_method: :get, + ip_address: '127.0.0.1', + parameters: '{"id"=>"foo"}', + timestamp: Time.parse('2018-08-13 12:13:24 UTC') } + google_chat_notifier.instance_variable_set(:@request_items, request_items) + + google_chat_notifier.instance_variable_set(:@options, {accumulated_errors_count: 0}) + + google_chat_notifier.instance_variable_set(:@application_name, 'dummy') + + exception = ArgumentError.new("foo") + google_chat_notifier.instance_variable_set(:@exception, exception) + + text = google_chat_notifier.send(:exception_text) + expected_text = %q( +Application: *dummy* +An *ArgumentError* occured. + +⚠️ Error 500 in test ⚠️ +*foo* + +*Request:* +``` +* url : http://test.address +* http_method : get +* ip_address : 127.0.0.1 +* parameters : {"id"=>"foo"} +* timestamp : 2018-08-13 12:13:24 UTC +``` + +*Backtrace:* +``` +* app/controllers/my_controller.rb:53:in `my_controller_params' +* app/controllers/my_controller.rb:34:in `update' +```) + assert_equal text, expected_text + end +end From 7bafb150760d2426ba179af32e0e993f2b2a9ff9 Mon Sep 17 00:00:00 2001 From: Fabian Larranaga Date: Wed, 22 Aug 2018 17:34:23 -0300 Subject: [PATCH 011/156] Add SNS notifier --- exception_notification.gemspec | 1 + lib/exception_notifier.rb | 1 + lib/exception_notifier/sns_notifier.rb | 58 ++++++++++ test/exception_notifier/sns_notifier_test.rb | 109 +++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 lib/exception_notifier/sns_notifier.rb create mode 100644 test/exception_notifier/sns_notifier_test.rb diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 8c1bc8f7..b0c1a71a 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -32,4 +32,5 @@ Gem::Specification.new do |s| s.add_development_dependency "hipchat", ">= 1.0.0" s.add_development_dependency "carrier-pigeon", ">= 0.7.0" s.add_development_dependency "slack-notifier", ">= 1.0.0" + s.add_development_dependency "aws-sdk-sns", "~> 1" end diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index bae96011..d0b3500b 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -17,6 +17,7 @@ module ExceptionNotifier autoload :IrcNotifier, 'exception_notifier/irc_notifier' autoload :SlackNotifier, 'exception_notifier/slack_notifier' autoload :MattermostNotifier, 'exception_notifier/mattermost_notifier' + autoload :SnsNotifier, 'exception_notifier/sns_notifier' class UndefinedNotifierError < StandardError; end diff --git a/lib/exception_notifier/sns_notifier.rb b/lib/exception_notifier/sns_notifier.rb new file mode 100644 index 00000000..dc17fc7e --- /dev/null +++ b/lib/exception_notifier/sns_notifier.rb @@ -0,0 +1,58 @@ +module ExceptionNotifier + class SnsNotifier < BaseNotifier + def initialize(options) + super + + raise ArgumentError.new "You must provide 'region' option" unless options[:region] + raise ArgumentError.new "You must provide 'access_key_id' option" unless options[:access_key_id] + raise ArgumentError.new "You must provide 'secret_access_key' option" unless options[:secret_access_key] + + @notifier = Aws::SNS::Client.new( + region: options[:region], + access_key_id: options[:access_key_id], + secret_access_key: options[:secret_access_key] + ) + @options = default_options.merge(options) + end + + def call(exception, custom_opts = {}) + custom_options = options.merge(custom_opts) + + subject = build_subject(exception, custom_options) + message = build_message(exception, custom_options) + + notifier.publish( + topic_arn: custom_options[:topic_arn], + message: message, + subject: subject + ) + end + + private + + attr_reader :notifier, :options + + def build_subject(exception, options) + subject = "#{options[:sns_prefix]} - " + subject << accumulated_exceptions_text(exception, options) + subject << " occurred" + subject.length > 120 ? subject[0...120] + "..." : subject + end + + def build_message(exception, options) + exception.class + end + + def accumulated_exceptions_text(exception, options) + errors_count = options[:accumulated_errors_count].to_i + measure_word = errors_count > 1 ? errors_count : (exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A') + "#{measure_word} #{exception.class.to_s}" + end + + def default_options + { + sns_prefix: '[ERROR]', + } + end + end +end diff --git a/test/exception_notifier/sns_notifier_test.rb b/test/exception_notifier/sns_notifier_test.rb new file mode 100644 index 00000000..dcc3047d --- /dev/null +++ b/test/exception_notifier/sns_notifier_test.rb @@ -0,0 +1,109 @@ +require 'test_helper' +require 'aws-sdk-sns' + +class SnsNotifierTest < ActiveSupport::TestCase + def setup + @exception = fake_exception + @exception.stubs(:class).returns('MyException') + @exception.stubs(:backtrace).returns(fake_backtrace) + @exception.stubs(:message).returns('exception message') + @options = { + access_key_id: 'my-access_key_id', + secret_access_key: 'my-secret_access_key', + region: 'us-east', + topic_arn: 'topicARN', + sns_prefix: '[App Exception]', + } + end + + # initialize + + test 'should initialize aws notifier with received params' do + Aws::SNS::Client.expects(:new).with( + region: 'us-east', + access_key_id: 'my-access_key_id', + secret_access_key: 'my-secret_access_key' + ) + + ExceptionNotifier::SnsNotifier.new(@options) + end + + test 'should raise an exception if region is not received' do + @options[:region] = nil + + error = assert_raises ArgumentError do + ExceptionNotifier::SnsNotifier.new(@options) + end + assert_equal "You must provide 'region' option", error.message + end + + test 'should raise an exception on publish if access_key_id is not received' do + @options[:access_key_id] = nil + error = assert_raises ArgumentError do + ExceptionNotifier::SnsNotifier.new(@options) + end + + assert_equal "You must provide 'access_key_id' option", error.message + end + + test 'should raise an exception on publish if secret_access_key is not received' do + @options[:secret_access_key] = nil + error = assert_raises ArgumentError do + ExceptionNotifier::SnsNotifier.new(@options) + end + + assert_equal "You must provide 'secret_access_key' option", error.message + end + + # call + + test 'should send a sns notification' do + Aws::SNS::Client.any_instance.expects(:publish).with({ + topic_arn: "topicARN", + # message: "3 MyException occured in background\n"\ + # "Exception: undefined method 'method=' for Empty\n"\ + # "Hostname: hostname\n"\ + # "Backtrace:\n"\ + # "test.rb:430:in `method_missing'\n"\ + # "test2.rb:110:in `test!'", + message: 'MyException', + subject: "[App Exception] - 3 MyException occurred" + }) + + sns_notifier = ExceptionNotifier::SnsNotifier.new(@options) + sns_notifier.call(@exception, { accumulated_errors_count: 3 }) + end + + private + + def fake_exception + begin + 1/0 + rescue Exception => e + e + end + end + + def fake_exception_without_backtrace + StandardError.new('my custom error') + end + + def fake_backtrace + [ + 'backtrace line 1', + 'backtrace line 2', + 'backtrace line 3', + 'backtrace line 4', + 'backtrace line 5', + 'backtrace line 6', + ] + end + + def fake_notification(exception = @exception) + { + topic_arn: 'topicARN', + message: 'message exception', + subject: 'subject' + } + end +end From c2cc2f972452c783c2441b47981f7c41229a143b Mon Sep 17 00:00:00 2001 From: pastullo Date: Sun, 9 Sep 2018 15:44:19 +0200 Subject: [PATCH 012/156] changed documentation to Rails 5 After losing 30 minutes of my life that nobody will give back to me, i suggest we change the template provided to use a working Rails 5 syntax --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5a1ee26..9e23d133 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Save the current user in the `request` using a controller callback. ```ruby class ApplicationController < ActionController::Base - before_filter :prepare_exception_notifier + before_action :prepare_exception_notifier private def prepare_exception_notifier request.env["exception_notifier.exception_data"] = { From 7a131a34d8679e55a5bfae59d69ae8d31d031108 Mon Sep 17 00:00:00 2001 From: pastullo Date: Tue, 25 Sep 2018 15:24:49 +0200 Subject: [PATCH 013/156] changing all before_filter to before_action --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e23d133..563bbf22 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ If your new section requires information that isn't available by default, make s ```ruby class ApplicationController < ActionController::Base - before_filter :log_additional_data + before_action :log_additional_data ... protected def log_additional_data @@ -542,7 +542,7 @@ The slack notification will include any data saved under `env["exception_notifie An example of how to send the server name to Slack in Rails (put this code in application_controller.rb): ```ruby -before_filter :set_notification +before_action :set_notification def set_notification request.env['exception_notifier.exception_data'] = {"server" => request.env['SERVER_NAME']} From 78cb56550d88cdacd31e0e530e84feb89b110586 Mon Sep 17 00:00:00 2001 From: Fabian Larranaga Date: Wed, 26 Sep 2018 20:37:09 -0300 Subject: [PATCH 014/156] Build message for sns notifier --- lib/exception_notifier/sns_notifier.rb | 27 ++++++++-- test/exception_notifier/sns_notifier_test.rb | 57 +++++++++++++------- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/lib/exception_notifier/sns_notifier.rb b/lib/exception_notifier/sns_notifier.rb index dc17fc7e..72b98edb 100644 --- a/lib/exception_notifier/sns_notifier.rb +++ b/lib/exception_notifier/sns_notifier.rb @@ -34,16 +34,36 @@ def call(exception, custom_opts = {}) def build_subject(exception, options) subject = "#{options[:sns_prefix]} - " - subject << accumulated_exceptions_text(exception, options) + subject << accumulated_exception_name(exception, options) subject << " occurred" subject.length > 120 ? subject[0...120] + "..." : subject end def build_message(exception, options) - exception.class + exception_name = accumulated_exception_name(exception, options) + + if options[:env].nil? + text = "#{exception_name} occured in background\n" + else + env = options[:env] + + kontroller = env['action_controller.instance'] + request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>" + + text = "#{exception_name} occurred while #{request}" + text += " was processed by #{kontroller.controller_name}##{kontroller.action_name}\n" if kontroller + end + + text += "Exception: #{exception.message}\n" + text += "Hostname: #{Socket.gethostname}\n" + + if exception.backtrace + formatted_backtrace = "#{exception.backtrace.first(options[:backtrace_lines]).join("\n")}" + text += "Backtrace:\n#{formatted_backtrace}\n" + end end - def accumulated_exceptions_text(exception, options) + def accumulated_exception_name(exception, options) errors_count = options[:accumulated_errors_count].to_i measure_word = errors_count > 1 ? errors_count : (exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A') "#{measure_word} #{exception.class.to_s}" @@ -52,6 +72,7 @@ def accumulated_exceptions_text(exception, options) def default_options { sns_prefix: '[ERROR]', + backtrace_lines: 10 } end end diff --git a/test/exception_notifier/sns_notifier_test.rb b/test/exception_notifier/sns_notifier_test.rb index dcc3047d..382aabef 100644 --- a/test/exception_notifier/sns_notifier_test.rb +++ b/test/exception_notifier/sns_notifier_test.rb @@ -6,7 +6,7 @@ def setup @exception = fake_exception @exception.stubs(:class).returns('MyException') @exception.stubs(:backtrace).returns(fake_backtrace) - @exception.stubs(:message).returns('exception message') + @exception.stubs(:message).returns("undefined method 'method=' for Empty") @options = { access_key_id: 'my-access_key_id', secret_access_key: 'my-secret_access_key', @@ -14,6 +14,7 @@ def setup topic_arn: 'topicARN', sns_prefix: '[App Exception]', } + Socket.stubs(:gethostname).returns('example.com') end # initialize @@ -57,16 +58,14 @@ def setup # call - test 'should send a sns notification' do - Aws::SNS::Client.any_instance.expects(:publish).with({ + test 'should send a sns notification in background' do + Aws::SNS::Client.any_instance.expects(:publish).with( + { topic_arn: "topicARN", - # message: "3 MyException occured in background\n"\ - # "Exception: undefined method 'method=' for Empty\n"\ - # "Hostname: hostname\n"\ - # "Backtrace:\n"\ - # "test.rb:430:in `method_missing'\n"\ - # "test2.rb:110:in `test!'", - message: 'MyException', + message: "3 MyException occured in background\n"\ + "Exception: undefined method 'method=' for Empty\n"\ + "Hostname: example.com\n"\ + "Backtrace:\n#{fake_backtrace.join("\n")}\n", subject: "[App Exception] - 3 MyException occurred" }) @@ -74,11 +73,37 @@ def setup sns_notifier.call(@exception, { accumulated_errors_count: 3 }) end + test 'should send a sns notification with controller#action information' do + ExamplesController.any_instance.stubs(:action_name).returns('index') + + Aws::SNS::Client.any_instance.expects(:publish).with( + { + topic_arn: "topicARN", + message: "A MyException occurred while GET "\ + "was processed by examples#index\n"\ + "Exception: undefined method 'method=' for Empty\n"\ + "Hostname: example.com\n"\ + "Backtrace:\n#{fake_backtrace.join("\n")}\n", + subject: "[App Exception] - A MyException occurred" + }) + + sns_notifier = ExceptionNotifier::SnsNotifier.new(@options) + sns_notifier.call(@exception, + env: { + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/examples', + 'action_controller.instance' => ExamplesController.new + } + ) + end + private + class ExamplesController < ActionController::Base; end + def fake_exception begin - 1/0 + 1 / 0 rescue Exception => e e end @@ -95,15 +120,7 @@ def fake_backtrace 'backtrace line 3', 'backtrace line 4', 'backtrace line 5', - 'backtrace line 6', + 'backtrace line 6' ] end - - def fake_notification(exception = @exception) - { - topic_arn: 'topicARN', - message: 'message exception', - subject: 'subject' - } - end end From bcae7264953a64f687ee387ef0d85aba895b1ca9 Mon Sep 17 00:00:00 2001 From: Fabian Larranaga Date: Sat, 13 Oct 2018 00:19:48 -0300 Subject: [PATCH 015/156] Add sns readme instructions --- README.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f5a1ee26..dddc27c7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ --- -The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [IRC](#irc-notifier) or via custom [WebHooks](#webhook-notifier). +The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [IRC](#irc-notifier), [Amazon SNS](#amazon-sns-notifier) or via custom [WebHooks](#webhook-notifier). There's a great [Railscast about Exception Notification](http://railscasts.com/episodes/104-exception-notifications-revised) you can see that may help you getting started. @@ -90,6 +90,7 @@ ExceptionNotification relies on notifiers to deliver notifications when errors o * [IRC notifier](#irc-notifier) * [Slack notifier](#slack-notifier) * [Mattermost notifier](#mattermost-notifier) +* [Amazon SNS](#amazon-sns-notifier) * [WebHook notifier](#webhook-notifier) But, you also can easily implement your own [custom notifier](#custom-notifier). @@ -724,6 +725,44 @@ Url of your gitlab or github with your organisation name for issue creation link Your application name used for issue creation link. Defaults to ``` Rails.application.class.parent_name.underscore```. +### Amazon SNS Notifier + +Notify all exceptions Amazon - Simple Notification Service: [SNS](https://aws.amazon.com/sns/). + +#### Usage + +Add the [aws-sdk-sns](https://github.com/aws/aws-sdk-ruby/tree/master/gems/aws-sdk-sns) gem to your `Gemfile`: + +```ruby + gem 'aws-sdk-sns', '~> 1.5' +``` + +To configure it, you **need** to set 3 required options for aws: `region`, `access_key_id` and `secret_access_key`, and one more option for sns: `topic_arn`. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + sns: { + region: 'us-east-x', + access_key_id: 'access_key_id', + secret_access_key: 'secret_access_key', + topic_arn: 'arn:aws:sns:us-east-x:XXXX:my-topic' + } +``` + +##### sns_prefix +*String, optional * + +Prefix in the notification subject, by default: "[Error]" + +##### backtrace_lines +*Integer, optional * + +Number of backtrace lines to be displayed in the notification message. By default: 10 + +#### Note: +* You may need to update your previous `aws-sdk-*` gems in order to setup `aws-sdk-sns` correctly. +* If you need any further information about the available regions or any other SNS related topic consider: [SNS faqs](https://aws.amazon.com/sns/faqs/) + ### WebHook notifier This notifier ships notifications over the HTTP protocol. From 9edf1aa51cdf003b822e3668103dd6b6bf3a1041 Mon Sep 17 00:00:00 2001 From: Abhishek Jain Date: Thu, 11 Oct 2018 01:38:01 -0700 Subject: [PATCH 016/156] add datadog notifier and a test --- exception_notification.gemspec | 1 + lib/exception_notifier.rb | 1 + lib/exception_notifier/datadog_notifier.rb | 157 ++++++++++++++++++ .../datadog_notifier_test.rb | 143 ++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 lib/exception_notifier/datadog_notifier.rb create mode 100644 test/exception_notifier/datadog_notifier_test.rb diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 8c1bc8f7..94b21a6e 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -32,4 +32,5 @@ Gem::Specification.new do |s| s.add_development_dependency "hipchat", ">= 1.0.0" s.add_development_dependency "carrier-pigeon", ">= 0.7.0" s.add_development_dependency "slack-notifier", ">= 1.0.0" + s.add_development_dependency "dogapi", ">= 1.23.0" end diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index bae96011..379ed83e 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -17,6 +17,7 @@ module ExceptionNotifier autoload :IrcNotifier, 'exception_notifier/irc_notifier' autoload :SlackNotifier, 'exception_notifier/slack_notifier' autoload :MattermostNotifier, 'exception_notifier/mattermost_notifier' + autoload :DatadogNotifier, 'exception_notifier/datadog_notifier' class UndefinedNotifierError < StandardError; end diff --git a/lib/exception_notifier/datadog_notifier.rb b/lib/exception_notifier/datadog_notifier.rb new file mode 100644 index 00000000..cdc85eb6 --- /dev/null +++ b/lib/exception_notifier/datadog_notifier.rb @@ -0,0 +1,157 @@ +module ExceptionNotifier + + class DatadogNotifier < BaseNotifier + + attr_reader :client, + :default_options + + def initialize(options) + super + @client = options.fetch(:client) + @default_options = options + end + + def call(exception, options = {}) + client.emit_event( + datadog_event(exception, options) + ) + end + + def datadog_event(exception, options = {}) + DatadogExceptionEvent.new( + exception, + options.reverse_merge(default_options) + ).event + end + + private + + class DatadogExceptionEvent + include ExceptionNotifier::BacktraceCleaner + + MAX_TITLE_LENGTH = 120 + MAX_VALUE_LENGTH = 300 + MAX_BACKTRACE_SIZE = 3 + ALERT_TYPE = "error" + + attr_reader :exception, + :options + + def initialize(exception, options) + @exception = exception + @options = options + end + + def request + @request ||= ActionDispatch::Request.new(options[:env]) if options[:env] + end + + def controller + @controller ||= options[:env] && options[:env]['action_controller.instance'] + end + + def backtrace + @backtrace ||= exception.backtrace ? clean_backtrace(exception) : [] + end + + def tags + options[:tags] || [] + end + + def title_prefix + options[:title_prefix] || "" + end + + def event + title = formatted_title + body = formatted_body + + Dogapi::Event.new( + body, + msg_title: title, + alert_type: ALERT_TYPE, + tags: tags, + aggregation_key: [title] + ) + end + + def formatted_title + title = title_prefix + title << "#{controller.controller_name} #{controller.action_name}" if controller + title << " (#{exception.class})" + title << " #{exception.message.inspect}" + + truncate(title, MAX_TITLE_LENGTH) + end + + def formatted_body + text = [] + + text << "%%%" + text << formatted_request if request + text << formatted_session if request + text << formatted_backtrace + text << "%%%" + + text.join("\n") + end + + def formatted_key_value(key, value) + "**#{key}:** #{value}" + end + + def formatted_request + text = [] + text << "### **Request**" + text << formatted_key_value("URL", request.url) + text << formatted_key_value("HTTP Method", request.request_method) + text << formatted_key_value("IP Address", request.remote_ip) + text << formatted_key_value("Parameters", request.filtered_parameters.inspect) + text << formatted_key_value("Timestamp", Time.current) + text << formatted_key_value("Server", Socket.gethostname) + if defined?(Rails) && Rails.respond_to?(:root) + text << formatted_key_value("Rails root", Rails.root) + end + text << formatted_key_value("Process", $$) + text << "___" + text.join("\n") + end + + def formatted_session + text = [] + text << "### **Session**" + text << formatted_key_value("Data", request.session.to_hash) + text << "___" + text.join("\n") + end + + def formatted_backtrace + size = [backtrace.size, MAX_BACKTRACE_SIZE].min + + text = [] + text << "### **Backtrace**" + text << "````" + size.times { |i| text << backtrace[i] } + text << "````" + text << "___" + text.join("\n") + end + + def truncate(string, max) + string.length > max ? "#{string[0...max]}..." : string + end + + def inspect_object(object) + case object + when Hash, Array + truncate(object.inspect, MAX_VALUE_LENGTH) + else + object.to_s + end + end + + end + end + +end + diff --git a/test/exception_notifier/datadog_notifier_test.rb b/test/exception_notifier/datadog_notifier_test.rb new file mode 100644 index 00000000..989c00c0 --- /dev/null +++ b/test/exception_notifier/datadog_notifier_test.rb @@ -0,0 +1,143 @@ +require 'test_helper' +require 'dogapi/common' +require 'dogapi/event' + +class DatadogNotifierTest < ActiveSupport::TestCase + def setup + @client = FakeDatadogClient.new + @options = { + client: @client + } + @notifier = ExceptionNotifier::DatadogNotifier.new(@options) + @exception = FakeException.new + @controller = FakeController.new + @request = FakeRequest.new + end + + test "should send an event to datadog" do + fake_event = Dogapi::Event.any_instance + @client.expects(:emit_event).with(fake_event) + + @notifier.stubs(:datadog_event).returns(fake_event) + @notifier.call(@exception) + end + + test "should include exception class in event title" do + event = @notifier.datadog_event(@exception) + assert_includes event.msg_title, "FakeException" + end + + test "should include exception message in event title" do + event = @notifier.datadog_event(@exception) + assert_includes event.msg_title, "Fake exception message" + end + + test "should include controller info in event title if controller information is available" do + event = @notifier.datadog_event(@exception, { + env: { + "action_controller.instance" => @controller, + "REQUEST_METHOD" => "GET", + "rack.input" => "", + } + }) + assert_includes event.msg_title, "Fake controller" + assert_includes event.msg_title, "Fake action" + end + + test "should include backtrace info in event body" do + event = @notifier.datadog_event(@exception) + assert_includes event.msg_text, "backtrace line 1\nbacktrace line 2\nbacktrace line 3" + end + + test "should include request info in event body" do + ActionDispatch::Request.stubs(:new).returns(@request) + + event = @notifier.datadog_event(@exception, { + env: { + "action_controller.instance" => @controller, + "REQUEST_METHOD" => "GET", + "rack.input" => "", + } + }) + assert_includes event.msg_text, "http://localhost:8080" + assert_includes event.msg_text, "GET" + assert_includes event.msg_text, "127.0.0.1" + assert_includes event.msg_text, "{\"param 1\"=>\"value 1\", \"param 2\"=>\"value 2\"}" + end + + test "should include tags in event" do + options = { + client: @client, + tags: ["error", "production"] + } + notifier = ExceptionNotifier::DatadogNotifier.new(options) + event = notifier.datadog_event(@exception) + assert_equal event.tags, ["error", "production"] + end + + test "should include event title in event aggregation key" do + event = @notifier.datadog_event(@exception) + assert_equal event.aggregation_key, [event.msg_title] + end + + private + + class FakeDatadogClient + def emit_event(event) + end + end + + class FakeController + def controller_name + "Fake controller" + end + + def action_name + "Fake action" + end + end + + class FakeException + def backtrace + [ + "backtrace line 1", + "backtrace line 2", + "backtrace line 3", + "backtrace line 4", + "backtrace line 5" + ] + end + + def message + "Fake exception message" + end + end + + class FakeRequest + def url + "http://localhost:8080" + end + + def request_method + "GET" + end + + def remote_ip + "127.0.0.1" + end + + def filtered_parameters + { + "param 1" => "value 1", + "param 2" => "value 2" + } + end + + def session + { + "session_id" => "1234" + } + end + end + +end From c36095e85f34f2d005c321b8c468955f8d564150 Mon Sep 17 00:00:00 2001 From: Abhishek Jain Date: Thu, 11 Oct 2018 11:09:47 -0700 Subject: [PATCH 017/156] update README to include Datadog Notifier --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 563bbf22..55e7ee6d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ --- -The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [IRC](#irc-notifier) or via custom [WebHooks](#webhook-notifier). +The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [IRC](#irc-notifier), [Datadog](#datadog-notifier) or via custom [WebHooks](#webhook-notifier). There's a great [Railscast about Exception Notification](http://railscasts.com/episodes/104-exception-notifications-revised) you can see that may help you getting started. @@ -82,9 +82,10 @@ Options -> sections" below. ## Notifiers -ExceptionNotification relies on notifiers to deliver notifications when errors occur in your applications. By default, 7 notifiers are available: +ExceptionNotification relies on notifiers to deliver notifications when errors occur in your applications. By default, 8 notifiers are available: * [Campfire notifier](#campfire-notifier) +* [Datadog notifier](#datadog-notifier) * [Email notifier](#email-notifier) * [HipChat notifier](#hipchat-notifier) * [IRC notifier](#irc-notifier) @@ -145,6 +146,59 @@ The API token to allow access to your Campfire account. For more options to set Campfire, like _ssl_, check [here](https://github.com/collectiveidea/tinder/blob/master/lib/tinder/campfire.rb#L17). +### Datadog notifier + +This notifier sends error events to Datadog using the [Dogapi](https://github.com/DataDog/dogapi-rb) gem. + +#### Usage + +Just add the [Dogapi](https://github.com/DataDog/dogapi-rb) gem to your `Gemfile`: + +```ruby +gem 'dogapi' +``` + +To use datadog notifier, you first need to create a `Dogapi::Client` with your datadog api and application keys, like this: + +```ruby +cilent = Dogapi::Client.new(api_key, application_key) +``` + +You then need to set the `client` option, like this: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + :email => { + :email_prefix => "[PREFIX] ", + :sender_address => %{"notifier" }, + :exception_recipients => %w{exceptions@example.com} + }, + :datadog => { + :client => client + } +``` + +#### Options + +##### client + +*DogApi::Client, required* + +The API client to send events to Datadog. + +##### title_prefix + +*String, optional* + +Prefix for event title in Datadog. + +##### tags + +*Array of Strings, optional* + +Optional tags for events in Datadog. + + ### Email notifier The Email notifier sends notifications by email. The notifications/emails sent includes information about the current request, session, and environment, and also gives a backtrace of the exception. From 72dad065a1d259253ca6fbdcd9219127f6dc9e53 Mon Sep 17 00:00:00 2001 From: Chris Horne Date: Tue, 11 Sep 2018 00:04:35 -0400 Subject: [PATCH 018/156] Add Microsoft Teams notifier Updating syntax to work with ruby 2.1.10 --- .gitignore | 1 + README.md | 59 +++++- lib/exception_notifier.rb | 1 + lib/exception_notifier/teams_notifier.rb | 179 ++++++++++++++++++ .../exception_notifier/teams_notifier_test.rb | 93 +++++++++ 5 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 lib/exception_notifier/teams_notifier.rb create mode 100644 test/exception_notifier/teams_notifier_test.rb diff --git a/.gitignore b/.gitignore index abecd20e..136bc30a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /coverage/ *.gemfile.lock /Gemfile.lock +/.idea/ diff --git a/README.md b/README.md index f4d4ce98..24f8e093 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ --- -The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [IRC](#irc-notifier), [Amazon SNS](#amazon-sns-notifier), [Google Chat](#google-chat-notifier) or via custom [WebHooks](#webhook-notifier). +The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [Teams](#teams-notifier), [IRC](#irc-notifier), [Amazon SNS](#amazon-sns-notifier), [Google Chat](#google-chat-notifier) or via custom [WebHooks](#webhook-notifier). There's a great [Railscast about Exception Notification](http://railscasts.com/episodes/104-exception-notifications-revised) you can see that may help you getting started. @@ -90,6 +90,7 @@ ExceptionNotification relies on notifiers to deliver notifications when errors o * [IRC notifier](#irc-notifier) * [Slack notifier](#slack-notifier) * [Mattermost notifier](#mattermost-notifier) +* [Teams notifier](#teams-notifier) * [Amazon SNS](#amazon-sns-notifier) * [Google Chat notifier](#google-chat-notifier) * [WebHook notifier](#webhook-notifier) @@ -610,7 +611,7 @@ Contains additional payload for a message (e.g avatar, attachments, etc). See [s Contains additional fields that will be added to the attachement. See [Slack documentation](https://api.slack.com/docs/message-attachments). -## Mattermost notifier +### Mattermost notifier Post notification in a mattermost channel via [incoming webhook](http://docs.mattermost.com/developer/webhooks-incoming.html) @@ -794,6 +795,60 @@ Number of backtrace lines to be displayed in the notification message. By defaul * You may need to update your previous `aws-sdk-*` gems in order to setup `aws-sdk-sns` correctly. * If you need any further information about the available regions or any other SNS related topic consider: [SNS faqs](https://aws.amazon.com/sns/faqs/) +### Teams notifier + +Post notification in a Microsoft Teams channel via [Incoming Webhook Connector](https://docs.microsoft.com/en-us/outlook/actionable-messages/actionable-messages-via-connectors) +Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: + +```ruby +gem 'httparty' +``` + +To configure it, you **need** to set the `webhook_url` option. +If you are using GitLab for issue tracking, you can specify `git_url` as follows to add a *Create issue* button in your notification. +By default this will use your Rails application name to match the git repository. If yours differs, you can specify `app_name`. +By that same notion, you may also set a `jira_url` to get a button that will send you to the New Issue screen in Jira. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + :email => { + :email_prefix => "[PREFIX] ", + :sender_address => %{"notifier" }, + :exception_recipients => %w{exceptions@example.com} + }, + :teams => { + :webhook_url => 'https://outlook.office.com/webhook/your-guid/IncomingWebhook/team-guid', + :git_url => 'https://your-gitlab.com/Group/Project', + :jira_url => 'https://your-jira.com' + } +``` + +#### Options + +##### webhook_url + +*String, required* + +The Incoming WebHook URL on mattermost. + +##### git_url + +*String, optional* + +Url of your gitlab or github with your organisation name for issue creation link (Eg: `github.com/aschen`). Defaults to nil and doesn't add link to the notification. + +##### jira_url + +*String, optional* + +Url of your Jira instance, adds button for Create Issue screen. Defaults to nil and doesn't add a button to the card. + +##### app_name + +*String, optional* + +Your application name used for git issue creation link. Defaults to `Rails.application.class.parent_name.underscore`. + ### WebHook notifier This notifier ships notifications over the HTTP protocol. diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index 018e063e..abf27d84 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -17,6 +17,7 @@ module ExceptionNotifier autoload :IrcNotifier, 'exception_notifier/irc_notifier' autoload :SlackNotifier, 'exception_notifier/slack_notifier' autoload :MattermostNotifier, 'exception_notifier/mattermost_notifier' + autoload :TeamsNotifier, 'exception_notifier/teams_notifier' autoload :SnsNotifier, 'exception_notifier/sns_notifier' autoload :GoogleChatNotifier, 'exception_notifier/google_chat_notifier' diff --git a/lib/exception_notifier/teams_notifier.rb b/lib/exception_notifier/teams_notifier.rb new file mode 100644 index 00000000..c2f639a8 --- /dev/null +++ b/lib/exception_notifier/teams_notifier.rb @@ -0,0 +1,179 @@ +require 'action_dispatch' +require 'active_support/core_ext/time' + +module ExceptionNotifier + class TeamsNotifier < BaseNotifier + include ExceptionNotifier::BacktraceCleaner + + class MissingController + def method_missing(*args, &block) + end + end + + attr_accessor :httparty + + def initialize(options = {}) + super + @default_options = options + @httparty = HTTParty + end + + def call(exception, options={}) + @options = options.merge(@default_options) + @exception = exception + @backtrace = exception.backtrace ? clean_backtrace(exception) : nil + + @env = @options.delete(:env) + + @application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore + @gitlab_url = @options.delete(:git_url) + @jira_url = @options.delete(:jira_url) + + @webhook_url = @options.delete(:webhook_url) + raise ArgumentError.new "You must provide 'webhook_url' parameter." unless @webhook_url + + unless @env.nil? + @controller = @env['action_controller.instance'] || MissingController.new + + request = ActionDispatch::Request.new(@env) + + @request_items = { url: request.original_url, + http_method: request.method, + ip_address: request.remote_ip, + parameters: request.filtered_parameters, + timestamp: Time.current } + + if request.session["warden.user.user.key"] + current_user = User.find(request.session["warden.user.user.key"][0][0]) + @request_items.merge!({ current_user: { id: current_user.id, email: current_user.email } }) + end + else + @controller = @request_items = nil + end + + payload = message_text + + @options[:body] = payload.to_json + @options[:headers] ||= {} + @options[:headers].merge!({ 'Content-Type' => 'application/json' }) + @options[:debug_output] = $stdout + + @httparty.post(@webhook_url, @options) + end + + private + + def message_text + errors_count = @options[:accumulated_errors_count].to_i + + text = { + "@type" => "MessageCard", + "@context" => "http://schema.org/extensions", + "summary" => "#{@application_name} Exception Alert", + "title" => "⚠️ Exception Occurred in #{Rails.env} ⚠️", + "sections" => [ + { + "activityTitle" => "#{errors_count > 1 ? errors_count : 'A'} *#{@exception.class}* occurred" + if @controller then " in *#{controller_and_method}*." else "." end, + "activitySubtitle" => "#{@exception.message}" + } + ], + "potentialAction" => [] + } + + text['sections'].push details + text['potentialAction'].push gitlab_view_link unless @gitlab_url.nil? + text['potentialAction'].push gitlab_issue_link unless @gitlab_url.nil? + text['potentialAction'].push jira_issue_link unless @jira_url.nil? + + text + end + + def details + details = { + "title" => "Details", + "facts" => [] + } + + details['facts'].push message_request unless @request_items.nil? + details['facts'].push message_backtrace unless @backtrace.nil? + + details + end + + def message_request + { + "name" => "Request", + "value" => "#{hash_presentation(@request_items)}\n " + } + end + + def message_backtrace(size = 3) + text = [] + size = @backtrace.size < size ? @backtrace.size : size + text << "```" + size.times { |i| text << "* " + @backtrace[i] } + text << "```" + + { + "name" => "Backtrace", + "value" => "#{text.join(" \n")}" + } + end + + def gitlab_view_link + { + "@type" => "ViewAction", + "name" => "🦊 View in GitLab", + "target" => [ + "#{@gitlab_url}/#{@application_name}" + ] + } + end + + def gitlab_issue_link + link = [@gitlab_url, @application_name, "issues", "new"].join("/") + params = { + "issue[title]" => ["[BUG] Error 500 :", + controller_and_method, + "(#{@exception.class})", + @exception.message].compact.join(" ") + }.to_query + + { + "@type" => "ViewAction", + "name" => "🦊 Create Issue in GitLab", + "target" => [ + "#{link}/?#{params}" + ] + } + end + + def jira_issue_link + { + "@type" => "ViewAction", + "name" => "🐞 Create Issue in Jira", + "target" => [ + "#{@jira_url}/secure/CreateIssue!default.jspa" + ] + } + end + + def controller_and_method + if @controller + "#{@controller.controller_name}##{@controller.action_name}" + else + "" + end + end + + def hash_presentation(hash) + text = [] + + hash.each do |key, value| + text << "* **#{key}** : `#{value}`" + end + + text.join(" \n") + end + end +end diff --git a/test/exception_notifier/teams_notifier_test.rb b/test/exception_notifier/teams_notifier_test.rb new file mode 100644 index 00000000..d6ada4d7 --- /dev/null +++ b/test/exception_notifier/teams_notifier_test.rb @@ -0,0 +1,93 @@ +require 'test_helper' +require 'httparty' + +class TeamsNotifierTest < ActiveSupport::TestCase + + test "should send notification if properly configured" do + options = { + :webhook_url => 'http://localhost:8000' + } + teams_notifier = ExceptionNotifier::TeamsNotifier.new + teams_notifier.httparty = FakeHTTParty.new + + options = teams_notifier.call ArgumentError.new("foo"), options + + body = ActiveSupport::JSON.decode options[:body] + assert body.has_key? 'title' + assert body.has_key? 'sections' + + sections = body['sections'] + header = sections[0] + + assert_equal 2, sections.size + assert_equal 'A *ArgumentError* occurred.', header['activityTitle'] + assert_equal 'foo', header['activitySubtitle'] + end + + test "should send notification with create gitlab issue link if specified" do + options = { + :webhook_url => 'http://localhost:8000', + :git_url => 'github.com/aschen' + } + teams_notifier = ExceptionNotifier::TeamsNotifier.new + teams_notifier.httparty = FakeHTTParty.new + + options = teams_notifier.call ArgumentError.new("foo"), options + + body = ActiveSupport::JSON.decode options[:body] + + potential_action = body['potentialAction'] + assert_equal 2, potential_action.size + assert_equal '🦊 View in GitLab', potential_action[0]['name'] + assert_equal '🦊 Create Issue in GitLab', potential_action[1]['name'] + end + + test 'should add other HTTParty options to params' do + options = { + :webhook_url => 'http://localhost:8000', + :username => "Test Bot", + :avatar => 'http://site.com/icon.png', + :basic_auth => { + :username => 'clara', + :password => 'password' + } + } + teams_notifier = ExceptionNotifier::TeamsNotifier.new + teams_notifier.httparty = FakeHTTParty.new + + options = teams_notifier.call ArgumentError.new("foo"), options + + assert options.has_key? :basic_auth + assert 'clara', options[:basic_auth][:username] + assert 'password', options[:basic_auth][:password] + end + + test "should use 'A' for exceptions count if :accumulated_errors_count option is nil" do + teams_notifier = ExceptionNotifier::TeamsNotifier.new + exception = ArgumentError.new("foo") + teams_notifier.instance_variable_set(:@exception, exception) + teams_notifier.instance_variable_set(:@options, {}) + + message_text = teams_notifier.send(:message_text) + header = message_text['sections'][0] + assert_equal 'A *ArgumentError* occurred.', header['activityTitle'] + end + + test "should use direct errors count if :accumulated_errors_count option is 5" do + teams_notifier = ExceptionNotifier::TeamsNotifier.new + exception = ArgumentError.new("foo") + teams_notifier.instance_variable_set(:@exception, exception) + teams_notifier.instance_variable_set(:@options, { accumulated_errors_count: 5 }) + message_text = teams_notifier.send(:message_text) + header = message_text['sections'][0] + assert_equal '5 *ArgumentError* occurred.', header['activityTitle'] + end +end + +class FakeHTTParty + + def post(url, options) + return options + end + +end From 735628ec27e16d9d6c903e2f4818de5a220f87be Mon Sep 17 00:00:00 2001 From: Fabian Larranaga Date: Thu, 22 Nov 2018 18:38:06 -0300 Subject: [PATCH 019/156] Bump to version 4.3.0 --- CHANGELOG.rdoc | 19 +++++++++++++++++-- exception_notification.gemspec | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 683f0489..491ef292 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,7 +1,22 @@ +== 4.3.0 + +* enhancements + * Add Microsoft Teams Notifier (by @phaelin) + * Add SNS notifier (by @FLarra) + * Add Google Chats notifier (by @renatolond) + * Align output of section-headers consistently (by @kronn) + * ExceptionNotifier.notify_exception receives block & pass it to each notifier (by @pocke) + * Update Travis to latest rubies (by @lostapathy) + +* bug fixes + * Replace all before_filter to before_action on readme (by @pastullo) + * Fix error when using error grouping outside of rails (by @garethcokell) + * Fix missing MissingController Mattermost class (by @n-rodriguez) + == 4.2.2 * enhancements - * Error groupiong (by @Martin91) + * Error grouping (by @Martin91) * Additional fields for Slack support (by @schurig) * Enterprise HipChat support (by @seanhuber) @@ -131,7 +146,7 @@ * Add normalize_subject option to remove numbers from email so that they thread (by @jjb) * Allow the user to provide a custom message and hash of data (by @jjb) * Add support for configurable background sections and a data partial (by @jeffrafter) - * Include timestamp of exception in notification body + * Include timestamp of exception in notification body * Add support for rack based session management (by @phoet) * Add ignore_crawlers option to ignore exceptions generated by crawlers * Add verbode_subject option to exclude exception message from subject (by @amishyn) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index b0c1a71a..e0cbb8bb 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -1,8 +1,8 @@ Gem::Specification.new do |s| s.name = 'exception_notification' - s.version = '4.2.2' + s.version = '4.3.0' s.authors = ["Jamis Buck", "Josh Peek"] - s.date = %q{2017-08-12} + s.date = %q{2018-11-22} s.summary = "Exception notification for Rails apps" s.homepage = "https://smartinez87.github.io/exception_notification/" s.email = "smartinez87@gmail.com" From c00e59236e9a1fdfa328f0fe42563495edd70183 Mon Sep 17 00:00:00 2001 From: Chris Horne Date: Thu, 6 Dec 2018 09:33:09 -0500 Subject: [PATCH 020/156] Updates incorrect text in Teams section referring to Mattermost --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24f8e093..885bf6be 100644 --- a/README.md +++ b/README.md @@ -829,7 +829,7 @@ Rails.application.config.middleware.use ExceptionNotification::Rack, *String, required* -The Incoming WebHook URL on mattermost. +The Incoming WebHook URL on Teams. ##### git_url From 41c55f0cdef6d2118293d485b91777d53bedbe23 Mon Sep 17 00:00:00 2001 From: Fabian Larranaga Date: Fri, 19 Oct 2018 21:14:34 -0300 Subject: [PATCH 021/156] Remove hash rocket notation --- README.md | 520 +++++++++--------- Rakefile | 2 +- examples/sinatra/sinatra_app.rb | 17 +- lib/exception_notification/rack.rb | 2 +- lib/exception_notification/resque.rb | 16 +- lib/exception_notification/sidekiq.rb | 5 +- lib/exception_notifier/base_notifier.rb | 1 - lib/exception_notifier/email_notifier.rb | 40 +- lib/exception_notifier/hipchat_notifier.rb | 2 +- .../modules/backtrace_cleaner.rb | 2 - lib/exception_notifier/notifier.rb | 4 +- .../exception_notifier/_backtrace.html.erb | 2 +- .../exception_notifier/_environment.text.erb | 2 +- .../exception_notifier/_request.text.erb | 2 +- .../exception_notification.html.erb | 4 +- .../exception_notification.text.erb | 4 +- lib/exception_notifier/webhook_notifier.rb | 20 +- .../install_generator.rb | 6 +- .../templates/exception_notification.rb | 21 +- .../dummy/app/controllers/posts_controller.rb | 10 +- test/dummy/config/environment.rb | 20 +- .../config/initializers/session_store.rb | 2 +- test/dummy/config/routes.rb | 2 +- test/dummy/db/seeds.rb | 4 +- .../test/functional/posts_controller_test.rb | 38 +- test/exception_notification/rack_test.rb | 4 +- .../campfire_notifier_test.rb | 38 +- .../exception_notifier/email_notifier_test.rb | 48 +- .../google_chat_notifier_test.rb | 2 +- .../hipchat_notifier_test.rb | 106 ++-- test/exception_notifier/irc_notifier_test.rb | 20 +- .../mattermost_notifier_test.rb | 27 +- test/exception_notifier/sidekiq_test.rb | 2 +- .../exception_notifier/slack_notifier_test.rb | 20 +- .../exception_notifier/teams_notifier_test.rb | 18 +- .../webhook_notifier_test.rb | 56 +- test/exception_notifier_test.rb | 16 +- 37 files changed, 558 insertions(+), 547 deletions(-) diff --git a/README.md b/README.md index 66c49827..219975e4 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,11 @@ ExceptionNotification is used as a rack middleware, or in the environment you wa ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :deliver_with => :deliver, # Rails >= 4.2.1 do not need this option since it defaults to :deliver_now - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} + email: { + deliver_with: :deliver, # Rails >= 4.2.1 do not need this option since it defaults to :deliver_now + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} } ``` @@ -57,10 +57,12 @@ Save the current user in the `request` using a controller callback. ```ruby class ApplicationController < ActionController::Base before_action :prepare_exception_notifier + private + def prepare_exception_notifier request.env["exception_notifier.exception_data"] = { - :current_user => current_user + current_user: current_user } end end @@ -114,16 +116,16 @@ To configure it, you need to set the `subdomain`, `token` and `room_name` option ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :campfire => { - :subdomain => 'my_subdomain', - :token => 'my_token', - :room_name => 'my_room' - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + campfire: { + subdomain: 'my_subdomain', + token: 'my_token', + room_name: 'my_room' + } ``` #### Options @@ -164,20 +166,20 @@ gem 'dogapi' To use datadog notifier, you first need to create a `Dogapi::Client` with your datadog api and application keys, like this: ```ruby -cilent = Dogapi::Client.new(api_key, application_key) +client = Dogapi::Client.new(api_key, application_key) ``` You then need to set the `client` option, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} + email: { + email_prefix: "[PREFIX] ", + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} }, - :datadog => { - :client => client + datadog: { + client: client } ``` @@ -216,8 +218,8 @@ For the email to be sent, there must be a default ActionMailer `delivery_method` config.action_mailer.delivery_method = :sendmail # Defaults to: # config.action_mailer.sendmail_settings = { -# :location => '/usr/sbin/sendmail', -# :arguments => '-i -t' +# location: '/usr/sbin/sendmail', +# arguments: '-i -t' # } config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = true @@ -263,12 +265,12 @@ describe application-specific data--just add the section's name to the list (whe ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com}, - :sections => %w{my_section1 my_section2} - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + sections: %w{my_section1 my_section2} + } ``` Place your custom sections under `./app/views/exception_notifier/` with the suffix `.text.erb`, e.g. `./app/views/exception_notifier/_my_section1.text.erb`. @@ -280,12 +282,13 @@ class ApplicationController < ActionController::Base before_action :log_additional_data ... protected - def log_additional_data - request.env["exception_notifier.exception_data"] = { - :document => @document, - :person => @person - } - end + + def log_additional_data + request.env['exception_notifier.exception_data'] = { + document: @document, + person: @person + } + end ... end ``` @@ -300,12 +303,12 @@ When using [background notifications](#background-notifications) some variables ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com}, - :background_sections => %w{my_section1 my_section2 backtrace data} - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + background_sections: %w{my_section1 my_section2 backtrace data} + } ``` ##### email_headers @@ -316,37 +319,37 @@ Additionally, you may want to set customized headers on the outcoming emails. To ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com}, - :email_headers => { "X-Custom-Header" => "foobar" } - } + email: { + email_prefix: "[PREFIX] ", + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + email_headers: { "X-Custom-Header" => "foobar" } + } ``` ##### verbose_subject *Boolean, default: true* -If enabled, include the exception message in the subject. Use `:verbose_subject => false` to exclude it. +If enabled, include the exception message in the subject. Use `verbose_subject: false` to exclude it. ##### normalize_subject *Boolean, default: false* -If enabled, remove numbers from subject so they thread as a single one. Use `:normalize_subject => true` to enable it. +If enabled, remove numbers from subject so they thread as a single one. Use `normalize_subject: true` to enable it. ##### include_controller_and_action_names_in_subject *Boolean, default: true* -If enabled, include the controller and action names in the subject. Use `:include_controller_and_action_names_in_subject => false` to exclude them. +If enabled, include the controller and action names in the subject. Use `include_controller_and_action_names_in_subject: false` to exclude them. ##### email_format *Symbol, default: :text* -By default, ExceptionNotification sends emails in plain text, in order to sends multipart notifications (aka HTML emails) use `:email_format => :html`. +By default, ExceptionNotification sends emails in plain text, in order to sends multipart notifications (aka HTML emails) use `email_format: :html`. ##### delivery_method @@ -356,31 +359,31 @@ By default, ExceptionNotification sends emails using the ActionMailer configurat ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com}, - :delivery_method => :postmark, - :postmark_settings => { - :api_key => ENV["POSTMARK_API_KEY"] - } - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + delivery_method: :postmark, + postmark_settings: { + api_key: ENV['POSTMARK_API_KEY'] + } + } ``` Besides the `delivery_method` option, you also can customize the mailer settings by passing a hash under an option named `DELIVERY_METHOD_settings`. Thus, you can use override specific SMTP settings for notifications using: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com}, - :delivery_method => :smtp, - :smtp_settings => { - :user_name => "bob", - :password => "password", - } - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + delivery_method: :smtp, + smtp_settings: { + user_name: 'bob', + password: 'password', + } + } ``` A complete list of `smtp_settings` options can be found in the [ActionMailer Configuration documentation](http://api.rubyonrails.org/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Configuration+options). @@ -413,15 +416,15 @@ To configure it, you need to set the `token` and `room_name` options, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :hipchat => { - :api_token => 'my_token', - :room_name => 'my_room' - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + hipchat: { + api_token: 'my_token', + room_name: 'my_room' + } ``` #### Options @@ -480,31 +483,30 @@ To configure it, you need to set at least the 'domain' option, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :irc => { - :domain => 'irc.example.com' - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + irc: { + domain: 'irc.example.com' + } ``` There are several other options, which are described below. For example, to use ssl and a password, add a prefix, post to the '#log' channel, and include recipients in the message (so that they will be notified), your configuration might look like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :irc => { - :domain => 'irc.example.com', - :nick => 'BadNewsBot', - :password => 'secret', - :port => 6697, - :channel => '#log', - :ssl => true, - :prefix => '[Exception Notification]', - :recipients => ['peter', 'michael', 'samir'] - } - + irc: { + domain: 'irc.example.com', + nick: 'BadNewsBot', + password: 'secret', + port: 6697, + channel: '#log', + ssl: true, + prefix: '[Exception Notification]', + recipients: ['peter', 'michael', 'samir'] + } ``` #### Options @@ -579,22 +581,22 @@ To configure it, you need to set at least the 'webhook_url' option, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :slack => { - :webhook_url => "[Your webhook url]", - :channel => "#exceptions", - :additional_parameters => { - :icon_url => "http://image.jpg", - :mrkdwn => true - } - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + slack: { + webhook_url: '[Your webhook url]', + channel: '#exceptions', + additional_parameters: { + icon_url: 'http://image.jpg', + mrkdwn: true + } + } ``` -The slack notification will include any data saved under `env["exception_notifier.exception_data"]`. +The slack notification will include any data saved under `env['exception_notifier.exception_data']`. An example of how to send the server name to Slack in Rails (put this code in application_controller.rb): @@ -602,8 +604,8 @@ An example of how to send the server name to Slack in Rails (put this code in ap before_action :set_notification def set_notification - request.env['exception_notifier.exception_data'] = {"server" => request.env['SERVER_NAME']} - # can be any key-value pairs + request.env['exception_notifier.exception_data'] = { 'server' => request.env['SERVER_NAME'] } + # can be any key-value pairs end ``` @@ -611,17 +613,17 @@ If you find this too verbose, you can determine to exclude certain information b ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :slack => { - :webhook_url => "[Your webhook url]", - :channel => "#exceptions", - :additional_parameters => { - :icon_url => "http://image.jpg", - :mrkdwn => true - }, - :ignore_data_if => lambda {|key, value| - "#{key}" == 'key_to_ignore' || value.is_a?(ClassToBeIgnored) - } - } + slack: { + webhook_url: '[Your webhook url]', + channel: '#exceptions', + additional_parameters: { + icon_url: 'http://image.jpg', + mrkdwn: true + }, + ignore_data_if: lambda {|key, value| + "#{key}" == 'key_to_ignore' || value.is_a?(ClassToBeIgnored) + } + } ``` Any evaluation to `true` will cause the key / value pair not be be sent along to Slack. @@ -680,15 +682,15 @@ You can also specify an other channel with `channel` option. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :mattermost => { - :webhook_url => 'http://your-mattermost.com/hooks/blablabla', - :channel => 'my-channel' - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + mattermost: { + webhook_url: 'http://your-mattermost.com/hooks/blablabla', + channel: 'my-channel' + } ``` If you are using Github or Gitlab for issues tracking, you can specify `git_url` as follow to add a *Create issue* link in you notification. @@ -697,49 +699,49 @@ By default this will use your Rails application name to match the git repository ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :mattermost => { - :webhook_url => 'http://your-mattermost.com/hooks/blablabla', - :git_url => 'github.com/aschen' - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + mattermost: { + webhook_url: 'http://your-mattermost.com/hooks/blablabla', + git_url: 'github.com/aschen' + } ``` You can also specify the bot name and avatar with `username` and `avatar` options. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :mattermost => { - :webhook_url => 'http://your-mattermost.com/hooks/blablabla', - :avatar => 'http://example.com/your-image.png', - :username => 'Fail bot' - } + email: { + email_prefix: 'PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + mattermost: { + webhook_url: 'http://your-mattermost.com/hooks/blablabla', + avatar: 'http://example.com/your-image.png', + username: 'Fail bot' + } ``` Finally since the notifier use HTTParty, you can include all HTTParty options, like basic_auth for example. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :mattermost => { - :webhook_url => 'http://your-mattermost.com/hooks/blablabla', - :basic_auth => { - :username => 'clara', - :password => 'password' - } - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + mattermost: { + webhook_url: 'http://your-mattermost.com/hooks/blablabla', + basic_auth: { + username: 'clara', + password: 'password' + } + } ``` #### Options @@ -778,7 +780,7 @@ Url of your gitlab or github with your organisation name for issue creation link *String, optional* -Your application name used for issue creation link. Defaults to ``` Rails.application.class.parent_name.underscore```. +Your application name used for issue creation link. Defaults to ```Rails.application.class.parent_name.underscore```. ### Google Chat Notifier @@ -794,8 +796,8 @@ To configure it, you **need** to set the `webhook_url` option. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :google_chat => { - :webhook_url => 'https://chat.googleapis.com/v1/spaces/XXXXXXXX/messages?key=YYYYYYYYYYYYY&token=ZZZZZZZZZZZZ' + google_chat: { + webhook_url: 'https://chat.googleapis.com/v1/spaces/XXXXXXXX/messages?key=YYYYYYYYYYYYY&token=ZZZZZZZZZZZZ' } ``` @@ -827,12 +829,12 @@ To configure it, you **need** to set 3 required options for aws: `region`, `acce ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - sns: { - region: 'us-east-x', - access_key_id: 'access_key_id', - secret_access_key: 'secret_access_key', - topic_arn: 'arn:aws:sns:us-east-x:XXXX:my-topic' - } + sns: { + region: 'us-east-x', + access_key_id: 'access_key_id', + secret_access_key: 'secret_access_key', + topic_arn: 'arn:aws:sns:us-east-x:XXXX:my-topic' + } ``` ##### sns_prefix @@ -858,22 +860,22 @@ Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gem gem 'httparty' ``` -To configure it, you **need** to set the `webhook_url` option. -If you are using GitLab for issue tracking, you can specify `git_url` as follows to add a *Create issue* button in your notification. -By default this will use your Rails application name to match the git repository. If yours differs, you can specify `app_name`. +To configure it, you **need** to set the `webhook_url` option. +If you are using GitLab for issue tracking, you can specify `git_url` as follows to add a *Create issue* button in your notification. +By default this will use your Rails application name to match the git repository. If yours differs, you can specify `app_name`. By that same notion, you may also set a `jira_url` to get a button that will send you to the New Issue screen in Jira. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} + email: { + email_prefix: "[PREFIX] ", + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} }, - :teams => { - :webhook_url => 'https://outlook.office.com/webhook/your-guid/IncomingWebhook/team-guid', - :git_url => 'https://your-gitlab.com/Group/Project', - :jira_url => 'https://your-jira.com' + teams: { + webhook_url: 'https://outlook.office.com/webhook/your-guid/IncomingWebhook/team-guid', + git_url: 'https://your-gitlab.com/Group/Project', + jira_url: 'https://your-jira.com' } ``` @@ -919,47 +921,47 @@ To configure it, you need to set the `url` option, like this: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :webhook => { - :url => 'http://domain.com:5555/hubot/path' - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + webhook: { + url: 'http://domain.com:5555/hubot/path' + } ``` By default, the WebhookNotifier will call the URLs using the POST method. But, you can change this using the `http_method` option. ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :webhook => { - :url => 'http://domain.com:5555/hubot/path', - :http_method => :get - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + webhook: { + url: 'http://domain.com:5555/hubot/path', + http_method: :get + } ``` Besides the `url` and `http_method` options, all the other options are passed directly to HTTParty. Thus, if the HTTP server requires authentication, you can include the following options: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :webhook => { - :url => 'http://domain.com:5555/hubot/path', - :basic_auth => { - :username => 'alice', - :password => 'password' - } - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + webhook: { + url: 'http://domain.com:5555/hubot/path', + basic_auth: { + username: 'alice', + password: 'password' + } + } ``` For more HTTParty options, check out the [documentation](https://github.com/jnunemaker/httparty). @@ -997,14 +999,14 @@ Using it: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - }, - :simple => { - # simple notifier options - } + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + simple: { + # simple notifier options + } ``` ## Error Grouping @@ -1016,22 +1018,22 @@ The below shows options used to enable error grouping: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :ignore_exceptions => ['ActionView::TemplateError'] + ExceptionNotifier.ignored_exceptions, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} + ignore_exceptions: ['ActionView::TemplateError'] + ExceptionNotifier.ignored_exceptions, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} }, - :error_grouping => true, - # :error_grouping_period => 5.minutes, # the time before an error is regarded as fixed - # :error_grouping_cache => Rails.cache, # for other applications such as Sinatra, use one instance of ActiveSupport::Cache::Store + error_grouping: true, + # error_grouping_period: 5.minutes, # the time before an error is regarded as fixed + # error_grouping_cache: Rails.cache, # for other applications such as Sinatra, use one instance of ActiveSupport::Cache::Store # # notification_trigger: specify a callback to determine when a notification should be sent, # the callback will be invoked with two arguments: # exception: the exception raised # count: accumulated errors count for this exception # - # :notification_trigger => lambda { |exception, count| count % 10 == 0 } + # notification_trigger: lambda { |exception, count| count % 10 == 0 } ``` ## Ignore Exceptions @@ -1053,12 +1055,12 @@ Ignore specified exception types. To achieve that, you should use the `:ignore_e ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :ignore_exceptions => ['ActionView::TemplateError'] + ExceptionNotifier.ignored_exceptions, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - } + ignore_exceptions: ['ActionView::TemplateError'] + ExceptionNotifier.ignored_exceptions, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + } ``` The above will make ExceptionNotifier ignore a *TemplateError* exception, plus the ones ignored by default. @@ -1071,12 +1073,12 @@ In some cases you may want to avoid getting notifications from exceptions made b ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :ignore_crawlers => %w{Googlebot bingbot}, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com} - } + ignore_crawlers: %w{Googlebot bingbot}, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + } ``` ### :ignore_if @@ -1087,12 +1089,12 @@ Last but not least, you can ignore exceptions based on a condition. Take a look: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, - :ignore_if => ->(env, exception) { exception.message =~ /^Couldn't find Page with ID=/ }, - :email => { - :email_prefix => "[PREFIX] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com}, - } + ignore_if: ->(env, exception) { exception.message =~ /^Couldn't find Page with ID=/ }, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + } ``` You can make use of both the environment and the exception inside the lambda to decide wether to avoid or not sending the notification. @@ -1126,9 +1128,11 @@ You can include information about the background process that created the error ```ruby begin some code... -rescue => exception - ExceptionNotifier.notify_exception(exception, - :data => {:worker => worker.to_s, :queue => queue, :payload => payload}) +rescue => e + ExceptionNotifier.notify_exception( + e, + data: { worker: worker.to_s, queue: queue, payload: payload} + ) end ``` @@ -1137,13 +1141,15 @@ end If your controller action manually handles an error, the notifier will never be run. To manually notify of an error you can do something like the following: ```ruby -rescue_from Exception, :with => :server_error +rescue_from Exception, with: :server_error def server_error(exception) # Whatever code that handles the exception - ExceptionNotifier.notify_exception(exception, - :env => request.env, :data => {:message => "was doing something wrong"}) + ExceptionNotifier.notify_exception( + exception, + env: request.env, data: { message: 'was doing something wrong' } + ) end ``` diff --git a/Rakefile b/Rakefile index 50bf4120..df7cb906 100644 --- a/Rakefile +++ b/Rakefile @@ -5,7 +5,7 @@ require 'appraisal' require 'rake/testtask' -task :default => [:test] +task default: [:test] Rake::TestTask.new(:test) do |t| t.libs << 'lib' diff --git a/examples/sinatra/sinatra_app.rb b/examples/sinatra/sinatra_app.rb index c71315f3..fb1ba164 100644 --- a/examples/sinatra/sinatra_app.rb +++ b/examples/sinatra/sinatra_app.rb @@ -5,15 +5,18 @@ class SinatraApp < Sinatra::Base use Rack::Config do |env| - env["action_dispatch.parameter_filter"] = [:password] # This is highly recommended. It will prevent the ExceptionNotification email from including your users' passwords + env['action_dispatch.parameter_filter'] = [:password] # This is highly recommended. It will prevent the ExceptionNotification email from including your users' passwords end use ExceptionNotification::Rack, - :email => { - :email_prefix => "[Example] ", - :sender_address => %{"notifier" }, - :exception_recipients => %w{exceptions@example.com}, - :smtp_settings => { :address => "localhost", :port => 1025 } + email: { + email_prefix: '[Example] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + smtp_settings: { + address: 'localhost', + port: 1025 + } } get '/' do @@ -25,7 +28,7 @@ class SinatraApp < Sinatra::Base begin 1/0 rescue Exception => e - ExceptionNotifier.notify_exception(e, :data => {:msg => "Cannot divide by zero!"}) + ExceptionNotifier.notify_exception(e, data: { msg: 'Cannot divide by zero!' }) end 'Check email at mailcatcher.' end diff --git a/lib/exception_notification/rack.rb b/lib/exception_notification/rack.rb index bb0d1354..2706b103 100644 --- a/lib/exception_notification/rack.rb +++ b/lib/exception_notification/rack.rb @@ -48,7 +48,7 @@ def call(env) response rescue Exception => exception - if ExceptionNotifier.notify_exception(exception, :env => env) + if ExceptionNotifier.notify_exception(exception, env: env) env['exception_notifier.delivered'] = true end diff --git a/lib/exception_notification/resque.rb b/lib/exception_notification/resque.rb index 8fb9f76d..cdc415d7 100644 --- a/lib/exception_notification/resque.rb +++ b/lib/exception_notification/resque.rb @@ -2,23 +2,21 @@ module ExceptionNotification class Resque < Resque::Failure::Base - def self.count Stat[:failed] end def save data = { - :failed_at => Time.now.to_s, - :queue => queue, - :worker => worker.to_s, - :payload => payload, - :error_class => exception.class.name, - :error_message => exception.message + error_class: exception.class.name, + error_message: exception.message + failed_at: Time.now.to_s, + payload: payload, + queue: queue, + worker: worker.to_s, } - ExceptionNotifier.notify_exception(exception, :data => { :resque => data }) + ExceptionNotifier.notify_exception(exception, data: { resque: data }) end - end end diff --git a/lib/exception_notification/sidekiq.rb b/lib/exception_notification/sidekiq.rb index ed01f844..245403c5 100644 --- a/lib/exception_notification/sidekiq.rb +++ b/lib/exception_notification/sidekiq.rb @@ -3,12 +3,11 @@ # Note: this class is only needed for Sidekiq version < 3. module ExceptionNotification class Sidekiq - def call(worker, msg, queue) begin yield rescue Exception => exception - ExceptionNotifier.notify_exception(exception, :data => { :sidekiq => msg }) + ExceptionNotifier.notify_exception(exception, data: { sidekiq: msg }) raise exception end end @@ -25,7 +24,7 @@ def call(worker, msg, queue) else ::Sidekiq.configure_server do |config| config.error_handlers << Proc.new { |ex, context| - ExceptionNotifier.notify_exception(ex, :data => { :sidekiq => context }) + ExceptionNotifier.notify_exception(ex, data: { sidekiq: context }) } end end diff --git a/lib/exception_notifier/base_notifier.rb b/lib/exception_notifier/base_notifier.rb index 2de9c05e..7b5978b1 100644 --- a/lib/exception_notifier/base_notifier.rb +++ b/lib/exception_notifier/base_notifier.rb @@ -20,6 +20,5 @@ def _pre_callback(exception, options, message, message_opts) def _post_callback(exception, options, message, message_opts) @base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts) if @base_options[:post_callback].respond_to?(:call) end - end end diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index d5a68ddc..00f405a5 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -106,11 +106,11 @@ def compose_email exception_recipients = maybe_call(@options[:exception_recipients]) headers = { - :delivery_method => @options[:delivery_method], - :to => exception_recipients, - :from => @options[:sender_address], - :subject => subject, - :template_name => name + delivery_method: @options[:delivery_method], + to: exception_recipients, + from: @options[:sender_address], + subject: subject, + template_name: name }.merge(@options[:email_headers]) mail = mail(headers) do |format| @@ -194,21 +194,21 @@ def create_email(exception, options={}) def self.default_options { - :sender_address => %("Exception Notifier" ), - :exception_recipients => [], - :email_prefix => "[ERROR] ", - :email_format => :text, - :sections => %w(request session environment backtrace), - :background_sections => %w(backtrace data), - :verbose_subject => true, - :normalize_subject => false, - :include_controller_and_action_names_in_subject => true, - :delivery_method => nil, - :mailer_settings => nil, - :email_headers => {}, - :mailer_parent => 'ActionMailer::Base', - :template_path => 'exception_notifier', - :deliver_with => :default + sender_address: %("Exception Notifier" ), + exception_recipients: [], + email_prefix: '[ERROR] ', + email_format: :text, + sections: %w(request session environment backtrace), + background_sections: %w(backtrace data), + verbose_subject: true, + normalize_subject: false, + include_controller_and_action_names_in_subject: true, + delivery_method: nil, + mailer_settings: nil, + email_headers: {}, + mailer_parent: 'ActionMailer::Base', + template_path: 'exception_notifier', + deliver_with: :default } end diff --git a/lib/exception_notifier/hipchat_notifier.rb b/lib/exception_notifier/hipchat_notifier.rb index 68ebd984..39d4a156 100644 --- a/lib/exception_notifier/hipchat_notifier.rb +++ b/lib/exception_notifier/hipchat_notifier.rb @@ -11,7 +11,7 @@ def initialize(options) api_token = options.delete(:api_token) room_name = options.delete(:room_name) opts = { - :api_version => options.delete(:api_version) || 'v1' + api_version: options.delete(:api_version) || 'v1' } opts[:server_url] = options.delete(:server_url) if options[:server_url] @from = options.delete(:from) || 'Exception' diff --git a/lib/exception_notifier/modules/backtrace_cleaner.rb b/lib/exception_notifier/modules/backtrace_cleaner.rb index a2053a20..5fcbdd06 100644 --- a/lib/exception_notifier/modules/backtrace_cleaner.rb +++ b/lib/exception_notifier/modules/backtrace_cleaner.rb @@ -1,6 +1,5 @@ module ExceptionNotifier module BacktraceCleaner - def clean_backtrace(exception) if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) Rails.backtrace_cleaner.send(:filter, exception.backtrace) @@ -8,6 +7,5 @@ def clean_backtrace(exception) exception.backtrace end end - end end diff --git a/lib/exception_notifier/notifier.rb b/lib/exception_notifier/notifier.rb index 5a468dc7..e689c7e2 100644 --- a/lib/exception_notifier/notifier.rb +++ b/lib/exception_notifier/notifier.rb @@ -4,8 +4,8 @@ module ExceptionNotifier class Notifier def self.exception_notification(env, exception, options={}) - ActiveSupport::Deprecation.warn "Please use ExceptionNotifier.notify_exception(exception, options.merge(:env => env))." - ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(:env => env)) + ActiveSupport::Deprecation.warn "Please use ExceptionNotifier.notify_exception(exception, options.merge(env: env))." + ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(env: env)) end def self.background_exception_notification(exception, options={}) diff --git a/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb b/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb index 5a4fa966..c4f68ae0 100644 --- a/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +++ b/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb @@ -1,3 +1,3 @@
-<%= @backtrace.join("\n") %>
+  <%= @backtrace.join("\n") %>
 
diff --git a/lib/exception_notifier/views/exception_notifier/_environment.text.erb b/lib/exception_notifier/views/exception_notifier/_environment.text.erb index a2db7cbc..69b4a8e1 100644 --- a/lib/exception_notifier/views/exception_notifier/_environment.text.erb +++ b/lib/exception_notifier/views/exception_notifier/_environment.text.erb @@ -1,5 +1,5 @@ <% filtered_env = @request.filtered_env -%> <% max = filtered_env.keys.map(&:to_s).max { |a, b| a.length <=> b.length } -%> <% filtered_env.keys.map(&:to_s).sort.each do |key| -%> -* <%= raw safe_encode("%-*s: %s" % [max.length, key, inspect_object(filtered_env[key])]) %> + * <%= raw safe_encode("%-*s: %s" % [max.length, key, inspect_object(filtered_env[key])]) %> <% end -%> diff --git a/lib/exception_notifier/views/exception_notifier/_request.text.erb b/lib/exception_notifier/views/exception_notifier/_request.text.erb index 4150d84b..b06ef736 100644 --- a/lib/exception_notifier/views/exception_notifier/_request.text.erb +++ b/lib/exception_notifier/views/exception_notifier/_request.text.erb @@ -5,6 +5,6 @@ * Timestamp : <%= raw @timestamp %> * Server : <%= raw Socket.gethostname %> <% if defined?(Rails) && Rails.respond_to?(:root) %> -* Rails root : <%= raw Rails.root %> + * Rails root : <%= raw Rails.root %> <% end %> * Process: <%= raw $$ %> diff --git a/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb b/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb index b4b9e82b..3e050ae2 100644 --- a/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +++ b/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb @@ -11,11 +11,11 @@ begin summary = render(section).strip unless summary.blank? - title = render("title", :title => section).strip + title = render("title", title: section).strip [title, summary] end rescue Exception => e - title = render("title", :title => section).strip + title = render("title", title: section).strip summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n") [title, summary] end diff --git a/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb b/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb index cc8d4dc6..09f70868 100644 --- a/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +++ b/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb @@ -8,12 +8,12 @@ begin summary = render(section).strip unless summary.blank? - title = render("title", :title => section).strip + title = render("title", title: section).strip "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n" end rescue Exception => e - title = render("title", :title => section).strip + title = render("title", title: section).strip summary = ["ERROR: Failed to generate exception summary:", [e.class.to_s, e.message].join(": "), e.backtrace && e.backtrace.join("\n")].compact.join("\n\n") [title, summary.gsub(/^/, " "), nil].join("\n\n") diff --git a/lib/exception_notifier/webhook_notifier.rb b/lib/exception_notifier/webhook_notifier.rb index 7573cf56..b1751a45 100644 --- a/lib/exception_notifier/webhook_notifier.rb +++ b/lib/exception_notifier/webhook_notifier.rb @@ -22,19 +22,23 @@ def call(exception, options={}) if defined?(Rails) && Rails.respond_to?(:root) options[:body][:rails_root] = Rails.root end - options[:body][:exception] = {:error_class => exception.class.to_s, - :message => exception.message.inspect, - :backtrace => exception.backtrace} + options[:body][:exception] = { + error_class: exception.class.to_s, + message: exception.message.inspect, + backtrace: exception.backtrace + } options[:body][:data] = (env && env['exception_notifier.exception_data'] || {}).merge(options[:data] || {}) unless env.nil? request = ActionDispatch::Request.new(env) - request_items = {:url => request.original_url, - :http_method => request.method, - :ip_address => request.remote_ip, - :parameters => request.filtered_parameters, - :timestamp => Time.current } + request_items = { + url: request.original_url, + http_method: request.method, + ip_address: request.remote_ip, + parameters: request.filtered_parameters, + timestamp: Time.current + } options[:body][:request] = request_items options[:body][:session] = request.session diff --git a/lib/generators/exception_notification/install_generator.rb b/lib/generators/exception_notification/install_generator.rb index 81fdcc57..9193c773 100644 --- a/lib/generators/exception_notification/install_generator.rb +++ b/lib/generators/exception_notification/install_generator.rb @@ -1,11 +1,11 @@ module ExceptionNotification module Generators class InstallGenerator < Rails::Generators::Base - desc "Creates a ExceptionNotification initializer." + desc 'Creates a ExceptionNotification initializer.' source_root File.expand_path('../templates', __FILE__) - class_option :resque, :type => :boolean, :desc => 'Add support for sending notifications when errors occur in Resque jobs.' - class_option :sidekiq, :type => :boolean, :desc => 'Add support for sending notifications when errors occur in Sidekiq jobs.' + class_option :resque, type: :boolean, desc: 'Add support for sending notifications when errors occur in Resque jobs.' + class_option :sidekiq, type: :boolean, desc: 'Add support for sending notifications when errors occur in Sidekiq jobs.' def copy_initializer template 'exception_notification.rb', 'config/initializers/exception_notification.rb' diff --git a/lib/generators/exception_notification/templates/exception_notification.rb b/lib/generators/exception_notification/templates/exception_notification.rb index 1e932a80..df0ba160 100644 --- a/lib/generators/exception_notification/templates/exception_notification.rb +++ b/lib/generators/exception_notification/templates/exception_notification.rb @@ -26,28 +26,27 @@ # Email notifier sends notifications by email. config.add_notifier :email, { - :email_prefix => "[ERROR] ", - :sender_address => %{"Notifier" }, - :exception_recipients => %w{exceptions@example.com} + email_prefix: '[ERROR] ', + sender_address: %{"Notifier" }, + exception_recipients: %w{exceptions@example.com} } # Campfire notifier sends notifications to your Campfire room. Requires 'tinder' gem. # config.add_notifier :campfire, { - # :subdomain => 'my_subdomain', - # :token => 'my_token', - # :room_name => 'my_room' + # subdomain: 'my_subdomain', + # token: 'my_token', + # room_name: 'my_room' # } # HipChat notifier sends notifications to your HipChat room. Requires 'hipchat' gem. # config.add_notifier :hipchat, { - # :api_token => 'my_token', - # :room_name => 'my_room' + # api_token: 'my_token', + # room_name: 'my_room' # } # Webhook notifier sends notifications over HTTP protocol. Requires 'httparty' gem. # config.add_notifier :webhook, { - # :url => 'http://example.com:5555/hubot/path', - # :http_method => :post + # url: 'http://example.com:5555/hubot/path', + # http_method: :post # } - end diff --git a/test/dummy/app/controllers/posts_controller.rb b/test/dummy/app/controllers/posts_controller.rb index 8fa192aa..21d46b08 100644 --- a/test/dummy/app/controllers/posts_controller.rb +++ b/test/dummy/app/controllers/posts_controller.rb @@ -6,7 +6,7 @@ def show respond_to do |format| format.html # show.html.erb - format.xml { render :xml => @post } + format.xml { render xml: @post } end end @@ -19,11 +19,11 @@ def create respond_to do |format| if @post.save - format.html { redirect_to(post_path(@post), :notice => 'Post was successfully created.') } - format.xml { render :xml => @post, :status => :created, :location => @post } + format.html { redirect_to(post_path(@post), notice: 'Post was successfully created.') } + format.xml { render xml: @post, status: :created, location: @post } else - format.html { render :action => "new" } - format.xml { render :xml => @post.errors, :status => :unprocessable_entity } + format.html { render action: 'new' } + format.xml { render xml: @post.errors, status: :unprocessable_entity } end end end diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb index 843b60c6..f85d3cde 100644 --- a/test/dummy/config/environment.rb +++ b/test/dummy/config/environment.rb @@ -2,16 +2,16 @@ require File.expand_path('../application', __FILE__) Dummy::Application.config.middleware.use ExceptionNotification::Rack, - :email => { - :email_prefix => "[Dummy ERROR] ", - :sender_address => %{"Dummy Notifier" }, - :exception_recipients => %w{dummyexceptions@example.com}, - :email_headers => { "X-Custom-Header" => "foobar" }, - :sections => ['new_section', 'request', 'session', 'environment', 'backtrace'], - :background_sections => %w(new_bkg_section backtrace data), - :pre_callback => proc { |opts, notifier, backtrace, message, message_opts| message_opts[:pre_callback_called] = 1 }, - :post_callback => proc { |opts, notifier, backtrace, message, message_opts| message_opts[:post_callback_called] = 1 } - } + email: { + email_prefix: '[Dummy ERROR] ', + sender_address: %{"Dummy Notifier" }, + exception_recipients: %w{dummyexceptions@example.com}, + email_headers: { 'X-Custom-Header' => 'foobar' }, + sections: ['new_section', 'request', 'session', 'environment', 'backtrace'], + background_sections: %w(new_bkg_section backtrace data), + pre_callback: proc { |opts, notifier, backtrace, message, message_opts| message_opts[:pre_callback_called] = 1 }, + post_callback: proc { |opts, notifier, backtrace, message, message_opts| message_opts[:post_callback_called] = 1 } + } # Initialize the rails application Dummy::Application.initialize! diff --git a/test/dummy/config/initializers/session_store.rb b/test/dummy/config/initializers/session_store.rb index aa2f5129..952473ff 100644 --- a/test/dummy/config/initializers/session_store.rb +++ b/test/dummy/config/initializers/session_store.rb @@ -1,6 +1,6 @@ # Be sure to restart your server when you modify this file. -Dummy::Application.config.session_store :cookie_store, :key => '_dummy_session' +Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index 7588f3bf..98630068 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,3 +1,3 @@ Dummy::Application.routes.draw do - resources :posts, :only => [:create, :show] + resources :posts, only: [:create, :show] end diff --git a/test/dummy/db/seeds.rb b/test/dummy/db/seeds.rb index 664d8c74..4b46ca3e 100644 --- a/test/dummy/db/seeds.rb +++ b/test/dummy/db/seeds.rb @@ -3,5 +3,5 @@ # # Examples: # -# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) -# Mayor.create(:name => 'Daley', :city => cities.first) +# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) +# Mayor.create(name: 'Daley', city: cities.first) diff --git a/test/dummy/test/functional/posts_controller_test.rb b/test/dummy/test/functional/posts_controller_test.rb index 68cf967e..031ab4bf 100644 --- a/test/dummy/test/functional/posts_controller_test.rb +++ b/test/dummy/test/functional/posts_controller_test.rb @@ -8,7 +8,7 @@ class PostsControllerTest < ActionController::TestCase post :create, method: :post, params: { secret: "secret" } rescue => e @exception = e - @mail = @email_notifier.create_email(@exception, {:env => request.env, :data => {:message => 'My Custom Message'}}) + @mail = @email_notifier.create_email(@exception, { env: request.env, data: { message: 'My Custom Message' }}) end end @@ -35,7 +35,7 @@ class PostsControllerTest < ActionController::TestCase test "mail subject should have the proper prefix" do assert_includes @mail.subject, "[Dummy ERROR]" end - + test "mail subject should include descriptive error message" do assert_includes @mail.subject, "(NoMethodError) \"undefined method `nw'" end @@ -74,7 +74,7 @@ class PostsControllerTest < ActionController::TestCase rescue => e @ignored_exception = e unless ExceptionNotifier.ignored_exceptions.include?(@ignored_exception.class.name) - ignored_mail = @email_notifier.create_email(@ignored_exception, {:env => request.env}) + ignored_mail = @email_notifier.create_email(@ignored_exception, { env: request.env }) end end @@ -87,7 +87,7 @@ class PostsControllerTest < ActionController::TestCase begin post :create, method: :post rescue => e - @secured_mail = @email_notifier.create_email(e, {:env => request.env}) + @secured_mail = @email_notifier.create_email(e, { env: request.env }) end assert request.ssl? @@ -102,10 +102,10 @@ class PostsControllerTest < ActionController::TestCase @exception = e custom_env = request.env custom_env['exception_notifier.options'] ||= {} - custom_env['exception_notifier.options'].merge!(:ignore_crawlers => %w(Googlebot)) + custom_env['exception_notifier.options'].merge!(ignore_crawlers: %w(Googlebot)) ignore_array = custom_env['exception_notifier.options'][:ignore_crawlers] unless ExceptionNotification::Rack.new(Dummy::Application, custom_env['exception_notifier.options']).send(:from_crawler, custom_env, ignore_array) - ignored_mail = @email_notifier.create_email(@exception, {:env => custom_env}) + ignored_mail = @email_notifier.create_email(@exception, { env: custom_env }) end end @@ -119,8 +119,8 @@ class PostsControllerTest < ActionController::TestCase @exception = e custom_env = request.env custom_env['exception_notifier.options'] ||= {} - custom_env['exception_notifier.options'].merge!({:email_format => :html}) - @mail = @email_notifier.create_email(@exception, {:env => custom_env}) + custom_env['exception_notifier.options'].merge!({ email_format: :html }) + @mail = @email_notifier.create_email(@exception, { env: custom_env }) end assert_includes @mail.content_type, "multipart/alternative" @@ -130,12 +130,12 @@ class PostsControllerTest < ActionController::TestCase class PostsControllerTestWithoutVerboseSubject < ActionController::TestCase tests PostsController setup do - @email_notifier = ExceptionNotifier::EmailNotifier.new(:verbose_subject => false) + @email_notifier = ExceptionNotifier::EmailNotifier.new(verbose_subject: false) begin post :create, method: :post rescue => e @exception = e - @mail = @email_notifier.create_email(@exception, {:env => request.env}) + @mail = @email_notifier.create_email(@exception, { env: request.env }) end end @@ -149,12 +149,12 @@ class PostsControllerTestWithoutVerboseSubject < ActionController::TestCase class PostsControllerTestWithoutControllerAndActionNames < ActionController::TestCase tests PostsController setup do - @email_notifier = ExceptionNotifier::EmailNotifier.new(:include_controller_and_action_names_in_subject => false) + @email_notifier = ExceptionNotifier::EmailNotifier.new(include_controller_and_action_names_in_subject: false) begin post :create, method: :post rescue => e @exception = e - @mail = @email_notifier.create_email(@exception, {:env => request.env}) + @mail = @email_notifier.create_email(@exception, { env: request.env}) end end @@ -168,16 +168,18 @@ class PostsControllerTestWithoutControllerAndActionNames < ActionController::Tes class PostsControllerTestWithSmtpSettings < ActionController::TestCase tests PostsController setup do - @email_notifier = ExceptionNotifier::EmailNotifier.new(:smtp_settings => { - :user_name => "Dummy user_name", - :password => "Dummy password" - }) + @email_notifier = ExceptionNotifier::EmailNotifier.new( + smtp_settings: { + user_name: 'Dummy user_name', + password: 'Dummy password' + } + ) begin post :create, method: :post rescue => e @exception = e - @mail = @email_notifier.create_email(@exception, {:env => request.env}) + @mail = @email_notifier.create_email(@exception, { env: request.env }) end end @@ -223,7 +225,7 @@ class PostsControllerTestWithExceptionRecipientsAsProc < ActionController::TestC post :create, method: :post rescue => e @exception = e - @mail = @email_notifier.create_email(@exception, {:env => request.env}) + @mail = @email_notifier.create_email(@exception, { env: request.env }) end end end diff --git a/test/exception_notification/rack_test.rb b/test/exception_notification/rack_test.rb index c09bb8fa..4482ff7a 100644 --- a/test/exception_notification/rack_test.rb +++ b/test/exception_notification/rack_test.rb @@ -22,7 +22,7 @@ class RackTest < ActiveSupport::TestCase test "should notify on \"X-Cascade\" = \"pass\" if ignore_cascade_pass option is false" do ExceptionNotifier.expects(:notify_exception).once - ExceptionNotification::Rack.new(@pass_app, :ignore_cascade_pass => false).call({}) + ExceptionNotification::Rack.new(@pass_app, ignore_cascade_pass: false).call({}) end test "should assign error_grouping if error_grouping is specified" do @@ -33,7 +33,7 @@ class RackTest < ActiveSupport::TestCase test "should assign notification_trigger if notification_trigger is specified" do assert_nil ExceptionNotifier.notification_trigger - ExceptionNotification::Rack.new(@normal_app, notification_trigger: lambda {|i| true}).call({}) + ExceptionNotification::Rack.new(@normal_app, notification_trigger: lambda { |i| true }).call({}) assert_respond_to ExceptionNotifier.notification_trigger, :call end diff --git a/test/exception_notifier/campfire_notifier_test.rb b/test/exception_notifier/campfire_notifier_test.rb index 86cf7bc2..0a2bb646 100644 --- a/test/exception_notifier/campfire_notifier_test.rb +++ b/test/exception_notifier/campfire_notifier_test.rb @@ -11,7 +11,7 @@ class CampfireNotifierTest < ActiveSupport::TestCase test "should send campfire notification if properly configured" do ExceptionNotifier::CampfireNotifier.stubs(:new).returns(Object.new) - campfire = ExceptionNotifier::CampfireNotifier.new({:subdomain => 'test', :token => 'test_token', :room_name => 'test_room'}) + campfire = ExceptionNotifier::CampfireNotifier.new({ subdomain: 'test', token: 'test_token', room_name: 'test_room' }) campfire.stubs(:call).returns(fake_notification) notif = campfire.call(fake_exception) @@ -24,7 +24,7 @@ class CampfireNotifierTest < ActiveSupport::TestCase test "should send campfire notification without backtrace info if properly configured" do ExceptionNotifier::CampfireNotifier.stubs(:new).returns(Object.new) - campfire = ExceptionNotifier::CampfireNotifier.new({:subdomain => 'test', :token => 'test_token', :room_name => 'test_room'}) + campfire = ExceptionNotifier::CampfireNotifier.new({ subdomain: 'test', token: 'test_token', room_name: 'test_room' }) campfire.stubs(:call).returns(fake_notification_without_backtrace) notif = campfire.call(fake_exception_without_backtrace) @@ -35,8 +35,8 @@ class CampfireNotifierTest < ActiveSupport::TestCase end test "should not send campfire notification if badly configured" do - wrong_params = {:subdomain => 'test', :token => 'bad_token', :room_name => 'test_room'} - Tinder::Campfire.stubs(:new).with('test', {:token => 'bad_token'}).returns(nil) + wrong_params = { subdomain: 'test', token: 'bad_token', room_name: 'test_room' } + Tinder::Campfire.stubs(:new).with('test', { token: 'bad_token' }).returns(nil) campfire = ExceptionNotifier::CampfireNotifier.new(wrong_params) assert_nil campfire.room @@ -44,7 +44,7 @@ class CampfireNotifierTest < ActiveSupport::TestCase end test "should not send campfire notification if config attr missing" do - wrong_params = {:subdomain => 'test', :room_name => 'test_room'} + wrong_params = { subdomain: 'test', room_name: 'test_room' } Tinder::Campfire.stubs(:new).with('test', {}).returns(nil) campfire = ExceptionNotifier::CampfireNotifier.new(wrong_params) @@ -66,19 +66,19 @@ class CampfireNotifierTest < ActiveSupport::TestCase campfire.call(fake_exception, accumulated_errors_count: 3) end - test "should call pre/post_callback if specified" do + test "should call pre/post_callback if specified" do pre_callback_called, post_callback_called = 0,0 Tinder::Campfire.stubs(:new).returns(Object.new) campfire = ExceptionNotifier::CampfireNotifier.new( { - :subdomain => 'test', - :token => 'test_token', - :room_name => 'test_room', - :pre_callback => proc { |opts, notifier, backtrace, message, message_opts| + subdomain: 'test', + token: 'test_token', + room_name: 'test_room', + pre_callback: proc { |opts, notifier, backtrace, message, message_opts| pre_callback_called += 1 }, - :post_callback => proc { |opts, notifier, backtrace, message, message_opts| + post_callback: proc { |opts, notifier, backtrace, message, message_opts| post_callback_called += 1 } } @@ -93,9 +93,11 @@ class CampfireNotifierTest < ActiveSupport::TestCase private def fake_notification - {:message => {:type => 'PasteMessage', - :body => "A new exception occurred: 'divided by 0' on '/Users/sebastian/exception_notification/test/campfire_test.rb:45:in `/'" - } + { + message: { + type: 'PasteMessage', + body: "A new exception occurred: 'divided by 0' on '/Users/sebastian/exception_notification/test/campfire_test.rb:45:in `/'" + } } end @@ -108,9 +110,11 @@ def fake_exception end def fake_notification_without_backtrace - {:message => {:type => 'PasteMessage', - :body => "A new exception occurred: 'my custom error'" - } + { + message: { + type: 'PasteMessage', + body: "A new exception occurred: 'my custom error'" + } } end diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index 442f258c..29752eff 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -9,8 +9,10 @@ class EmailNotifierTest < ActiveSupport::TestCase 1/0 rescue => e @exception = e - @mail = @email_notifier.create_email(@exception, - :data => {:job => 'DivideWorkerJob', :payload => '1/0', :message => 'My Custom Message'}) + @mail = @email_notifier.create_email( + @exception, + data: { job: 'DivideWorkerJob', payload: '1/0', message: 'My Custom Message'} + ) end end @@ -150,19 +152,19 @@ class EmailNotifierTest < ActiveSupport::TestCase test "should encode environment strings" do email_notifier = ExceptionNotifier::EmailNotifier.new( - :sender_address => "", - :exception_recipients => %w{dummyexceptions@example.com}, - :deliver_with => :deliver_now + sender_address: "", + exception_recipients: %w{dummyexceptions@example.com}, + deliver_with: :deliver_now ) mail = email_notifier.create_email( @exception, - :env => { + env: { "REQUEST_METHOD" => "GET", "rack.input" => "", "invalid_encoding" => "R\xC3\xA9sum\xC3\xA9".force_encoding(Encoding::ASCII), }, - :email_format => :text + email_format: :text ) assert_match(/invalid_encoding\s+: R__sum__/, mail.encoded) @@ -172,10 +174,10 @@ class EmailNotifierTest < ActiveSupport::TestCase ActionMailer::Base.deliveries.clear email_notifier = ExceptionNotifier::EmailNotifier.new( - :email_prefix => '[Dummy ERROR] ', - :sender_address => %{"Dummy Notifier" }, - :exception_recipients => %w{dummyexceptions@example.com}, - :delivery_method => :test + email_prefix: '[Dummy ERROR] ', + sender_address: %{"Dummy Notifier" }, + exception_recipients: %w{dummyexceptions@example.com}, + delivery_method: :test ) email_notifier.call(@exception) @@ -193,10 +195,10 @@ class EmailNotifierTest < ActiveSupport::TestCase end email_notifier = ExceptionNotifier::EmailNotifier.new( - :email_prefix => '[Dummy ERROR] ', - :sender_address => %{"Dummy Notifier" }, - :exception_recipients => %w{dummyexceptions@example.com}, - :deliver_with => deliver_with + email_prefix: '[Dummy ERROR] ', + sender_address: %{"Dummy Notifier" }, + exception_recipients: %w{dummyexceptions@example.com}, + deliver_with: deliver_with ) email_notifier.call(@exception) @@ -207,10 +209,10 @@ class EmailNotifierTest < ActiveSupport::TestCase test "should lazily evaluate exception_recipients" do exception_recipients = %w{first@example.com second@example.com} email_notifier = ExceptionNotifier::EmailNotifier.new( - :email_prefix => '[Dummy ERROR] ', - :sender_address => %{"Dummy Notifier" }, - :exception_recipients => -> { [ exception_recipients.shift ] }, - :delivery_method => :test + email_prefix: '[Dummy ERROR] ', + sender_address: %{"Dummy Notifier" }, + exception_recipients: -> { [ exception_recipients.shift ] }, + delivery_method: :test ) mail = email_notifier.call(@exception) @@ -223,10 +225,10 @@ class EmailNotifierTest < ActiveSupport::TestCase ActionMailer::Base.deliveries.clear email_notifier = ExceptionNotifier::EmailNotifier.new( - :email_prefix => '[Dummy ERROR] ', - :sender_address => %{"Dummy Notifier" }, - :exception_recipients => %w{dummyexceptions@example.com}, - :delivery_method => :test + email_prefix: '[Dummy ERROR] ', + sender_address: %{"Dummy Notifier" }, + exception_recipients: %w{dummyexceptions@example.com}, + delivery_method: :test ) mail = email_notifier.call(@exception, { accumulated_errors_count: 3 }) diff --git a/test/exception_notifier/google_chat_notifier_test.rb b/test/exception_notifier/google_chat_notifier_test.rb index 9b9a9f15..3e70c738 100644 --- a/test/exception_notifier/google_chat_notifier_test.rb +++ b/test/exception_notifier/google_chat_notifier_test.rb @@ -5,7 +5,7 @@ class GoogleChatNotifierTest < ActiveSupport::TestCase test "should send notification if properly configured" do options = { - :webhook_url => 'http://localhost:8000' + webhook_url: 'http://localhost:8000' } google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new google_chat_notifier.httparty = FakeHTTParty.new diff --git a/test/exception_notifier/hipchat_notifier_test.rb b/test/exception_notifier/hipchat_notifier_test.rb index 09c44f3f..2a47e7a1 100644 --- a/test/exception_notifier/hipchat_notifier_test.rb +++ b/test/exception_notifier/hipchat_notifier_test.rb @@ -11,28 +11,28 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should send hipchat notification if properly configured" do options = { - :api_token => 'good_token', - :room_name => 'room_name', - :color => 'yellow', + api_token: 'good_token', + room_name: 'room_name', + color: 'yellow', } - HipChat::Room.any_instance.expects(:send).with('Exception', fake_body, { :color => 'yellow' }) + HipChat::Room.any_instance.expects(:send).with('Exception', fake_body, { color: 'yellow' }) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) end test "should call pre/post_callback if specified" do - pre_callback_called, post_callback_called = 0,0 + pre_callback_called, post_callback_called = 0, 0 options = { - :api_token => 'good_token', - :room_name => 'room_name', - :color => 'yellow', - :pre_callback => proc { |*| pre_callback_called += 1}, - :post_callback => proc { |*| post_callback_called += 1} + api_token: 'good_token', + room_name: 'room_name', + color: 'yellow', + pre_callback: proc { |*| pre_callback_called += 1 }, + post_callback: proc { |*| post_callback_called += 1 } } - HipChat::Room.any_instance.expects(:send).with('Exception', fake_body, { :color => 'yellow' }.merge(options.except(:api_token, :room_name))) + HipChat::Room.any_instance.expects(:send).with('Exception', fake_body, { color: 'yellow' }.merge(options.except(:api_token, :room_name))) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) @@ -42,12 +42,12 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should send hipchat notification without backtrace info if properly configured" do options = { - :api_token => 'good_token', - :room_name => 'room_name', - :color => 'yellow', + api_token: 'good_token', + room_name: 'room_name', + color: 'yellow', } - HipChat::Room.any_instance.expects(:send).with('Exception', fake_body_without_backtrace, { :color => 'yellow' }) + HipChat::Room.any_instance.expects(:send).with('Exception', fake_body_without_backtrace, { color: 'yellow' }) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception_without_backtrace) @@ -55,12 +55,12 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should allow custom from value if set" do options = { - :api_token => 'good_token', - :room_name => 'room_name', - :from => 'TrollFace', + api_token: 'good_token', + room_name: 'room_name', + from: 'TrollFace', } - HipChat::Room.any_instance.expects(:send).with('TrollFace', fake_body, { :color => 'red' }) + HipChat::Room.any_instance.expects(:send).with('TrollFace', fake_body, { color: 'red' }) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) @@ -68,29 +68,29 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should not send hipchat notification if badly configured" do wrong_params = { - :api_token => 'bad_token', - :room_name => 'test_room' + api_token: 'bad_token', + room_name: 'test_room' } - HipChat::Client.stubs(:new).with('bad_token', {:api_version => 'v1'}).returns(nil) + HipChat::Client.stubs(:new).with('bad_token', { api_version: 'v1' }).returns(nil) hipchat = ExceptionNotifier::HipchatNotifier.new(wrong_params) assert_nil hipchat.room end test "should not send hipchat notification if api_key is missing" do - wrong_params = {:room_name => 'test_room'} + wrong_params = { room_name: 'test_room' } - HipChat::Client.stubs(:new).with(nil, {:api_version => 'v1'}).returns(nil) + HipChat::Client.stubs(:new).with(nil, {api_version: 'v1'}).returns(nil) hipchat = ExceptionNotifier::HipchatNotifier.new(wrong_params) assert_nil hipchat.room end test "should not send hipchat notification if room_name is missing" do - wrong_params = {:api_token => 'good_token'} + wrong_params = { api_token: 'good_token' } - HipChat::Client.stubs(:new).with('good_token', {:api_version => 'v1'}).returns({}) + HipChat::Client.stubs(:new).with('good_token', { api_version: 'v1' }).returns({}) hipchat = ExceptionNotifier::HipchatNotifier.new(wrong_params) assert_nil hipchat.room @@ -98,13 +98,13 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should send hipchat notification with message_template" do options = { - :api_token => 'good_token', - :room_name => 'room_name', - :color => 'yellow', - :message_template => ->(exception, _) { "This is custom message: '#{exception.message}'" } + api_token: 'good_token', + room_name: 'room_name', + color: 'yellow', + message_template: ->(exception, _) { "This is custom message: '#{exception.message}'" } } - HipChat::Room.any_instance.expects(:send).with('Exception', "This is custom message: '#{fake_exception.message}'", { :color => 'yellow' }) + HipChat::Room.any_instance.expects(:send).with('Exception', "This is custom message: '#{fake_exception.message}'", { color: 'yellow' }) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) @@ -112,9 +112,9 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should send hipchat notification exclude accumulated errors count" do options = { - :api_token => 'good_token', - :room_name => 'room_name', - :color => 'yellow' + api_token: 'good_token', + room_name: 'room_name', + color: 'yellow' } HipChat::Room.any_instance.expects(:send).with{ |_, msg, _| msg.start_with?("A new exception occurred:") } @@ -124,9 +124,9 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should send hipchat notification include accumulated errors count" do options = { - :api_token => 'good_token', - :room_name => 'room_name', - :color => 'yellow' + api_token: 'good_token', + room_name: 'room_name', + color: 'yellow' } HipChat::Room.any_instance.expects(:send).with{ |_, msg, _| msg.start_with?("The exception occurred 3 times:") } @@ -136,15 +136,15 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should send hipchat notification with HTML-escaped meessage if using default message_template" do options = { - :api_token => 'good_token', - :room_name => 'room_name', - :color => 'yellow', + api_token: 'good_token', + room_name: 'room_name', + color: 'yellow', } exception = fake_exception_with_html_characters body = "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}' on '#{exception.backtrace.first}'" - HipChat::Room.any_instance.expects(:send).with('Exception', body, { :color => 'yellow' }) + HipChat::Room.any_instance.expects(:send).with('Exception', body, { color: 'yellow' }) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(exception) @@ -152,11 +152,11 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should use APIv1 if api_version is not specified" do options = { - :api_token => 'good_token', - :room_name => 'room_name', + api_token: 'good_token', + room_name: 'room_name', } - HipChat::Client.stubs(:new).with('good_token', {:api_version => 'v1'}).returns({}) + HipChat::Client.stubs(:new).with('good_token', { api_version: 'v1' }).returns({}) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) @@ -164,12 +164,12 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should use APIv2 when specified" do options = { - :api_token => 'good_token', - :room_name => 'room_name', - :api_version => 'v2', + api_token: 'good_token', + room_name: 'room_name', + api_version: 'v2', } - HipChat::Client.stubs(:new).with('good_token', {:api_version => 'v2'}).returns({}) + HipChat::Client.stubs(:new).with('good_token', { api_version: 'v2' }).returns({}) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) @@ -177,13 +177,13 @@ class HipchatNotifierTest < ActiveSupport::TestCase test "should allow server_url value (for a self-hosted HipChat Server) if set" do options = { - :api_token => 'good_token', - :room_name => 'room_name', - :api_version => 'v2', - :server_url => 'https://domain.com', + api_token: 'good_token', + room_name: 'room_name', + api_version: 'v2', + server_url: 'https://domain.com', } - HipChat::Client.stubs(:new).with('good_token', {:api_version => 'v2', :server_url => 'https://domain.com'}).returns({}) + HipChat::Client.stubs(:new).with('good_token', { api_version: 'v2', server_url: 'https://domain.com' }).returns({}) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) diff --git a/test/exception_notifier/irc_notifier_test.rb b/test/exception_notifier/irc_notifier_test.rb index 745f91de..08983988 100644 --- a/test/exception_notifier/irc_notifier_test.rb +++ b/test/exception_notifier/irc_notifier_test.rb @@ -5,7 +5,7 @@ class IrcNotifierTest < ActiveSupport::TestCase test "should send irc notification if properly configured" do options = { - :domain => 'irc.example.com' + domain: 'irc.example.com' } CarrierPigeon.expects(:send).with(has_key(:uri)) do |v| @@ -36,9 +36,9 @@ class IrcNotifierTest < ActiveSupport::TestCase pre_callback_called, post_callback_called = 0,0 options = { - :domain => 'irc.example.com', - :pre_callback => proc { |*| pre_callback_called += 1}, - :post_callback => proc { |*| post_callback_called += 1} + domain: 'irc.example.com', + pre_callback: proc { |*| pre_callback_called += 1 }, + post_callback: proc { |*| post_callback_called += 1 } } CarrierPigeon.expects(:send).with(has_key(:uri)) do |v| @@ -53,7 +53,7 @@ class IrcNotifierTest < ActiveSupport::TestCase test "should send irc notification without backtrace info if properly configured" do options = { - :domain => 'irc.example.com' + domain: 'irc.example.com' } CarrierPigeon.expects(:send).with(has_key(:uri)) do |v| @@ -66,11 +66,11 @@ class IrcNotifierTest < ActiveSupport::TestCase test "should properly construct URI from constituent parts" do options = { - :nick => 'BadNewsBot', - :password => 'secret', - :domain => 'irc.example.com', - :port => 9999, - :channel => '#exceptions' + nick: 'BadNewsBot', + password: 'secret', + domain: 'irc.example.com', + port: 9999, + channel: '#exceptions' } CarrierPigeon.expects(:send).with(has_entry(uri: "irc://BadNewsBot:secret@irc.example.com:9999/#exceptions")) diff --git a/test/exception_notifier/mattermost_notifier_test.rb b/test/exception_notifier/mattermost_notifier_test.rb index adeb30b6..76127cd1 100644 --- a/test/exception_notifier/mattermost_notifier_test.rb +++ b/test/exception_notifier/mattermost_notifier_test.rb @@ -2,10 +2,9 @@ require 'httparty' class MattermostNotifierTest < ActiveSupport::TestCase - test "should send notification if properly configured" do options = { - :webhook_url => 'http://localhost:8000' + webhook_url: 'http://localhost:8000' } mattermost_notifier = ExceptionNotifier::MattermostNotifier.new mattermost_notifier.httparty = FakeHTTParty.new @@ -25,8 +24,8 @@ class MattermostNotifierTest < ActiveSupport::TestCase test "should send notification with create issue link if specified" do options = { - :webhook_url => 'http://localhost:8000', - :git_url => 'github.com/aschen' + webhook_url: 'http://localhost:8000', + git_url: 'github.com/aschen' } mattermost_notifier = ExceptionNotifier::MattermostNotifier.new mattermost_notifier.httparty = FakeHTTParty.new @@ -42,9 +41,9 @@ class MattermostNotifierTest < ActiveSupport::TestCase test 'should add username and icon_url params to the notification if specified' do options = { - :webhook_url => 'http://localhost:8000', - :username => "Test Bot", - :avatar => 'http://site.com/icon.png' + webhook_url: 'http://localhost:8000', + username: 'Test Bot', + avatar: 'http://site.com/icon.png' } mattermost_notifier = ExceptionNotifier::MattermostNotifier.new mattermost_notifier.httparty = FakeHTTParty.new @@ -59,12 +58,12 @@ class MattermostNotifierTest < ActiveSupport::TestCase test 'should add other HTTParty options to params' do options = { - :webhook_url => 'http://localhost:8000', - :username => "Test Bot", - :avatar => 'http://site.com/icon.png', - :basic_auth => { - :username => 'clara', - :password => 'password' + webhook_url: 'http://localhost:8000', + username: 'Test Bot', + avatar: 'http://site.com/icon.png', + basic_auth: { + username: 'clara', + password: 'password' } } mattermost_notifier = ExceptionNotifier::MattermostNotifier.new @@ -97,9 +96,7 @@ class MattermostNotifierTest < ActiveSupport::TestCase end class FakeHTTParty - def post(url, options) return options end - end diff --git a/test/exception_notifier/sidekiq_test.rb b/test/exception_notifier/sidekiq_test.rb index 6e770d28..0e58c4ab 100644 --- a/test/exception_notifier/sidekiq_test.rb +++ b/test/exception_notifier/sidekiq_test.rb @@ -26,7 +26,7 @@ class SidekiqTest < ActiveSupport::TestCase ExceptionNotifier.expects(:notify_exception).with( exception, - :data => { :sidekiq => message } + data: { sidekiq: message } ) server.handle_exception(exception, message) diff --git a/test/exception_notifier/slack_notifier_test.rb b/test/exception_notifier/slack_notifier_test.rb index 14fda22d..a649076a 100644 --- a/test/exception_notifier/slack_notifier_test.rb +++ b/test/exception_notifier/slack_notifier_test.rb @@ -125,12 +125,12 @@ def setup notification_options = { env: { - 'exception_notifier.exception_data' => {foo: 'bar', john: 'doe'} + 'exception_notifier.exception_data' => { foo: 'bar', john: 'doe' } }, data: { 'user_id' => 5, 'key_to_be_ignored' => 'whatever', - 'ignore_as_well' => {what: 'ever'} + 'ignore_as_well' => { what: 'ever'} } } @@ -147,10 +147,10 @@ def setup webhook_url: "http://slack.webhook.url", username: "test", custom_hook: "hook", - :pre_callback => proc { |opts, notifier, backtrace, message, message_opts| + pre_callback: proc { |opts, notifier, backtrace, message, message_opts| (message_opts[:attachments] = []) << { text: "#{backtrace.join("\n")}", color: 'danger' } }, - :post_callback => proc { |opts, notifier, backtrace, message, message_opts| + post_callback: proc { |opts, notifier, backtrace, message, message_opts| post_callback_called = 1 }, additional_parameters: { @@ -158,12 +158,12 @@ def setup } } - Slack::Notifier.any_instance.expects(:ping).with('', - {:icon_url => 'icon', - :attachments => [ - {:text => fake_backtrace.join("\n"), - :color => 'danger'} - ]}) + Slack::Notifier.any_instance.expects(:ping).with('', { + icon_url: 'icon', + attachments: [{ + text: fake_backtrace.join("\n"), + color: 'danger' } + ]}) slack_notifier = ExceptionNotifier::SlackNotifier.new(options) slack_notifier.call(@exception) diff --git a/test/exception_notifier/teams_notifier_test.rb b/test/exception_notifier/teams_notifier_test.rb index d6ada4d7..df98206a 100644 --- a/test/exception_notifier/teams_notifier_test.rb +++ b/test/exception_notifier/teams_notifier_test.rb @@ -5,7 +5,7 @@ class TeamsNotifierTest < ActiveSupport::TestCase test "should send notification if properly configured" do options = { - :webhook_url => 'http://localhost:8000' + webhook_url: 'http://localhost:8000' } teams_notifier = ExceptionNotifier::TeamsNotifier.new teams_notifier.httparty = FakeHTTParty.new @@ -26,8 +26,8 @@ class TeamsNotifierTest < ActiveSupport::TestCase test "should send notification with create gitlab issue link if specified" do options = { - :webhook_url => 'http://localhost:8000', - :git_url => 'github.com/aschen' + webhook_url: 'http://localhost:8000', + git_url: 'github.com/aschen' } teams_notifier = ExceptionNotifier::TeamsNotifier.new teams_notifier.httparty = FakeHTTParty.new @@ -44,12 +44,12 @@ class TeamsNotifierTest < ActiveSupport::TestCase test 'should add other HTTParty options to params' do options = { - :webhook_url => 'http://localhost:8000', - :username => "Test Bot", - :avatar => 'http://site.com/icon.png', - :basic_auth => { - :username => 'clara', - :password => 'password' + webhook_url: 'http://localhost:8000', + username: "Test Bot", + avatar: 'http://site.com/icon.png', + basic_auth: { + username: 'clara', + password: 'password' } } teams_notifier = ExceptionNotifier::TeamsNotifier.new diff --git a/test/exception_notifier/webhook_notifier_test.rb b/test/exception_notifier/webhook_notifier_test.rb index 63f0b1c1..3990e085 100644 --- a/test/exception_notifier/webhook_notifier_test.rb +++ b/test/exception_notifier/webhook_notifier_test.rb @@ -5,7 +5,7 @@ class WebhookNotifierTest < ActiveSupport::TestCase test "should send webhook notification if properly configured" do ExceptionNotifier::WebhookNotifier.stubs(:new).returns(Object.new) - webhook = ExceptionNotifier::WebhookNotifier.new({:url => 'http://localhost:8000'}) + webhook = ExceptionNotifier::WebhookNotifier.new({ url: 'http://localhost:8000' }) webhook.stubs(:call).returns(fake_response) response = webhook.call(fake_exception) @@ -28,7 +28,7 @@ class WebhookNotifierTest < ActiveSupport::TestCase test "should send webhook notification with correct params data" do url = 'http://localhost:8000' fake_exception.stubs(:backtrace).returns('the backtrace') - webhook = ExceptionNotifier::WebhookNotifier.new({:url => url}) + webhook = ExceptionNotifier::WebhookNotifier.new({ url: url }) HTTParty.expects(:send).with(:post, url, fake_params) @@ -37,7 +37,7 @@ class WebhookNotifierTest < ActiveSupport::TestCase test "should call pre/post_callback if specified" do HTTParty.stubs(:send).returns(fake_response) - webhook = ExceptionNotifier::WebhookNotifier.new({:url => 'http://localhost:8000'}) + webhook = ExceptionNotifier::WebhookNotifier.new({ url: 'http://localhost:8000' }) webhook.call(fake_exception) end @@ -45,24 +45,24 @@ class WebhookNotifierTest < ActiveSupport::TestCase def fake_response { - :status => 200, - :body => { - :exception => { - :error_class => 'ZeroDivisionError', - :message => 'divided by 0', - :backtrace => '/exception_notification/test/webhook_notifier_test.rb:48:in `/' + status: 200, + body: { + exception: { + error_class: 'ZeroDivisionError', + message: 'divided by 0', + backtrace: '/exception_notification/test/webhook_notifier_test.rb:48:in `/' }, - :data => { - :extra_data => {:data_item1 => "datavalue1", :data_item2 => "datavalue2"} + data: { + extra_data: { data_item1: 'datavalue1', data_item2: 'datavalue2' } }, - :request => { - :cookies => {:cookie_item1 => 'cookieitemvalue1', :cookie_item2 => 'cookieitemvalue2'}, - :url => 'http://example.com/example', - :ip_address => '192.168.1.1', - :environment => {:env_item1 => "envitem1", :env_item2 => "envitem2"}, - :controller => '#', - :session => {:session_item1 => "sessionitem1", :session_item2 => "sessionitem2"}, - :parameters => {:action =>"index", :controller =>"projects"} + request: { + cookies: { cookie_item1: 'cookieitemvalue1', cookie_item2: 'cookieitemvalue2' }, + url: 'http://example.com/example', + ip_address: '192.168.1.1', + environment: { env_item1: 'envitem1', env_item2: 'envitem2' }, + controller: '#', + session: { session_item1: 'sessionitem1', session_item2: 'sessionitem2' }, + parameters: { action:'index', controller:'projects' } } } } @@ -70,16 +70,16 @@ def fake_response def fake_params { - :body => { - :server => Socket.gethostname, - :process => $$, - :rails_root => Rails.root, - :exception => { - :error_class => 'ZeroDivisionError', - :message => 'divided by 0'.inspect, - :backtrace => 'the backtrace' + body: { + server: Socket.gethostname, + process: $$, + rails_root: Rails.root, + exception: { + error_class: 'ZeroDivisionError', + message: 'divided by 0'.inspect, + backtrace: 'the backtrace' }, - :data => {} + data: {} } } end diff --git a/test/exception_notifier_test.rb b/test/exception_notifier_test.rb index e2965c23..00d71c44 100644 --- a/test/exception_notifier_test.rb +++ b/test/exception_notifier_test.rb @@ -65,11 +65,11 @@ class ExceptionNotifierTest < ActiveSupport::TestCase assert_equal notifier1_calls, 1 assert_equal notifier2_calls, 1 - ExceptionNotifier.notify_exception(exception, {:notifiers => :notifier1}) + ExceptionNotifier.notify_exception(exception, {notifiers: :notifier1}) assert_equal notifier1_calls, 2 assert_equal notifier2_calls, 1 - ExceptionNotifier.notify_exception(exception, {:notifiers => :notifier2}) + ExceptionNotifier.notify_exception(exception, {notifiers: :notifier2}) assert_equal notifier1_calls, 2 assert_equal notifier2_calls, 2 @@ -88,11 +88,11 @@ class ExceptionNotifierTest < ActiveSupport::TestCase exception = StandardError.new - ExceptionNotifier.notify_exception(exception, {:notifiers => :test}) + ExceptionNotifier.notify_exception(exception, {notifiers: :test}) assert_equal @notifier_calls, 1 env = "development" - ExceptionNotifier.notify_exception(exception, {:notifiers => :test}) + ExceptionNotifier.notify_exception(exception, {notifiers: :test}) assert_equal @notifier_calls, 1 ExceptionNotifier.clear_ignore_conditions! @@ -103,10 +103,10 @@ class ExceptionNotifierTest < ActiveSupport::TestCase exception = StandardError.new - ExceptionNotifier.notify_exception(exception, {:notifiers => :test}) + ExceptionNotifier.notify_exception(exception, {notifiers: :test}) assert_equal @notifier_calls, 1 - ExceptionNotifier.notify_exception(exception, {:notifiers => :test, :ignore_exceptions => 'StandardError' }) + ExceptionNotifier.notify_exception(exception, {notifiers: :test, ignore_exceptions: 'StandardError' }) assert_equal @notifier_calls, 1 end @@ -118,10 +118,10 @@ class StandardErrorSubclass < StandardError exception = StandardErrorSubclass.new - ExceptionNotifier.notify_exception(exception, {:notifiers => :test}) + ExceptionNotifier.notify_exception(exception, {notifiers: :test}) assert_equal @notifier_calls, 1 - ExceptionNotifier.notify_exception(exception, {:notifiers => :test, :ignore_exceptions => 'StandardError' }) + ExceptionNotifier.notify_exception(exception, {notifiers: :test, ignore_exceptions: 'StandardError' }) assert_equal @notifier_calls, 1 end From 32090a82f6ecc55dc2b8eaad1e0a18a00b3d8428 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 10 Dec 2018 21:22:05 -0300 Subject: [PATCH 022/156] Move each notifier docs to its own file --- README.md | 909 ---------------------------------- docs/notifiers/campfire.md | 50 ++ docs/notifiers/custom.md | 42 ++ docs/notifiers/datadog.md | 51 ++ docs/notifiers/email.md | 195 ++++++++ docs/notifiers/google_chat.md | 30 ++ docs/notifiers/hipchat.md | 66 +++ docs/notifiers/irc.md | 97 ++++ docs/notifiers/mattermost.md | 114 +++++ docs/notifiers/slack.md | 101 ++++ docs/notifiers/sns.md | 37 ++ docs/notifiers/teams.md | 53 ++ docs/notifiers/webhook.md | 60 +++ 13 files changed, 896 insertions(+), 909 deletions(-) create mode 100644 docs/notifiers/campfire.md create mode 100644 docs/notifiers/custom.md create mode 100644 docs/notifiers/datadog.md create mode 100644 docs/notifiers/email.md create mode 100644 docs/notifiers/google_chat.md create mode 100644 docs/notifiers/hipchat.md create mode 100644 docs/notifiers/irc.md create mode 100644 docs/notifiers/mattermost.md create mode 100644 docs/notifiers/slack.md create mode 100644 docs/notifiers/sns.md create mode 100644 docs/notifiers/teams.md create mode 100644 docs/notifiers/webhook.md diff --git a/README.md b/README.md index 219975e4..36126ed9 100644 --- a/README.md +++ b/README.md @@ -100,915 +100,6 @@ ExceptionNotification relies on notifiers to deliver notifications when errors o But, you also can easily implement your own [custom notifier](#custom-notifier). -### Campfire notifier - -This notifier sends notifications to your Campfire room. - -#### Usage - -Just add the [tinder](https://github.com/collectiveidea/tinder) gem to your `Gemfile`: - -```ruby -gem 'tinder' -``` - -To configure it, you need to set the `subdomain`, `token` and `room_name` options, like this: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - campfire: { - subdomain: 'my_subdomain', - token: 'my_token', - room_name: 'my_room' - } -``` - -#### Options - -##### subdomain - -*String, required* - -Your subdomain at Campfire. - -##### room_name - -*String, required* - -The Campfire room where the notifications must be published to. - -##### token - -*String, required* - -The API token to allow access to your Campfire account. - - -For more options to set Campfire, like _ssl_, check [here](https://github.com/collectiveidea/tinder/blob/master/lib/tinder/campfire.rb#L17). - -### Datadog notifier - -This notifier sends error events to Datadog using the [Dogapi](https://github.com/DataDog/dogapi-rb) gem. - -#### Usage - -Just add the [Dogapi](https://github.com/DataDog/dogapi-rb) gem to your `Gemfile`: - -```ruby -gem 'dogapi' -``` - -To use datadog notifier, you first need to create a `Dogapi::Client` with your datadog api and application keys, like this: - -```ruby -client = Dogapi::Client.new(api_key, application_key) -``` - -You then need to set the `client` option, like this: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: "[PREFIX] ", - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - datadog: { - client: client - } -``` - -#### Options - -##### client - -*DogApi::Client, required* - -The API client to send events to Datadog. - -##### title_prefix - -*String, optional* - -Prefix for event title in Datadog. - -##### tags - -*Array of Strings, optional* - -Optional tags for events in Datadog. - - -### Email notifier - -The Email notifier sends notifications by email. The notifications/emails sent includes information about the current request, session, and environment, and also gives a backtrace of the exception. - -After an exception notification has been delivered the rack environment variable `exception_notifier.delivered` will be set to true. - -#### ActionMailer configuration - -For the email to be sent, there must be a default ActionMailer `delivery_method` setting configured. If you do not have one, you can use the following code (assuming your app server machine has `sendmail`). Depending on the environment you want ExceptionNotification to run in, put the following code in your `config/environments/production.rb` and/or `config/environments/development.rb`: - -```ruby -config.action_mailer.delivery_method = :sendmail -# Defaults to: -# config.action_mailer.sendmail_settings = { -# location: '/usr/sbin/sendmail', -# arguments: '-i -t' -# } -config.action_mailer.perform_deliveries = true -config.action_mailer.raise_delivery_errors = true -``` - -#### Options - -##### sender_address - -*String, default: %("Exception Notifier" )* - -Who the message is from. - -##### exception_recipients - -*String/Array of strings/Proc, default: []* - -Who the message is destined for, can be a string of addresses, an array of addresses, or it can be a proc that returns a string of addresses or an array of addresses. The proc will be evaluated when the mail is sent. - -##### email_prefix - -*String, default: [ERROR]* - -The subject's prefix of the message. - -##### sections - -*Array of strings, default: %w(request session environment backtrace)* - -By default, the notification email includes four parts: request, session, environment, and backtrace (in that order). You can customize how each of those sections are rendered by placing a partial named for that part in your `app/views/exception_notifier` directory (e.g., `_session.rhtml`). Each partial has access to the following variables: - -```ruby -@kontroller # the controller that caused the error -@request # the current request object -@exception # the exception that was raised -@backtrace # a sanitized version of the exception's backtrace -@data # a hash of optional data values that were passed to the notifier -@sections # the array of sections to include in the email -``` - -You can reorder the sections, or exclude sections completely, by using `sections` option. You can even add new sections that -describe application-specific data--just add the section's name to the list (wherever you'd like), and define the corresponding partial. Like the following example with two new added sections: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com}, - sections: %w{my_section1 my_section2} - } -``` - -Place your custom sections under `./app/views/exception_notifier/` with the suffix `.text.erb`, e.g. `./app/views/exception_notifier/_my_section1.text.erb`. - -If your new section requires information that isn't available by default, make sure it is made available to the email using the `exception_data` macro: - -```ruby -class ApplicationController < ActionController::Base - before_action :log_additional_data - ... - protected - - def log_additional_data - request.env['exception_notifier.exception_data'] = { - document: @document, - person: @person - } - end - ... -end -``` - -In the above case, `@document` and `@person` would be made available to the email renderer, allowing your new section(s) to access and display them. See the existing sections defined by the plugin for examples of how to write your own. - -##### background_sections - -*Array of strings, default: %w(backtrace data)* - -When using [background notifications](#background-notifications) some variables are not available in the views, like `@kontroller` and `@request`. Thus, you may want to include different sections for background notifications: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com}, - background_sections: %w{my_section1 my_section2 backtrace data} - } -``` - -##### email_headers - -*Hash of strings, default: {}* - -Additionally, you may want to set customized headers on the outcoming emails. To do so, simply use the `:email_headers` option: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: "[PREFIX] ", - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com}, - email_headers: { "X-Custom-Header" => "foobar" } - } -``` - -##### verbose_subject - -*Boolean, default: true* - -If enabled, include the exception message in the subject. Use `verbose_subject: false` to exclude it. - -##### normalize_subject - -*Boolean, default: false* - -If enabled, remove numbers from subject so they thread as a single one. Use `normalize_subject: true` to enable it. - -##### include_controller_and_action_names_in_subject - -*Boolean, default: true* - -If enabled, include the controller and action names in the subject. Use `include_controller_and_action_names_in_subject: false` to exclude them. - -##### email_format - -*Symbol, default: :text* - -By default, ExceptionNotification sends emails in plain text, in order to sends multipart notifications (aka HTML emails) use `email_format: :html`. - -##### delivery_method - -*Symbol, default: :smtp* - -By default, ExceptionNotification sends emails using the ActionMailer configuration of the application. In order to send emails by another delivery method, use the `delivery_method` option: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com}, - delivery_method: :postmark, - postmark_settings: { - api_key: ENV['POSTMARK_API_KEY'] - } - } -``` - -Besides the `delivery_method` option, you also can customize the mailer settings by passing a hash under an option named `DELIVERY_METHOD_settings`. Thus, you can use override specific SMTP settings for notifications using: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com}, - delivery_method: :smtp, - smtp_settings: { - user_name: 'bob', - password: 'password', - } - } -``` - -A complete list of `smtp_settings` options can be found in the [ActionMailer Configuration documentation](http://api.rubyonrails.org/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Configuration+options). - -##### mailer_parent - -*String, default: ActionMailer::Base* - -The parent mailer which ExceptionNotification mailer inherit from. - -##### deliver_with - -*Symbol, default: :deliver_now - -The method name to send emalis using ActionMailer. - -### HipChat notifier - -This notifier sends notifications to your Hipchat room. - -#### Usage - -Just add the [hipchat](https://github.com/hipchat/hipchat-rb) gem to your `Gemfile`: - -```ruby -gem 'hipchat' -``` - -To configure it, you need to set the `token` and `room_name` options, like this: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - hipchat: { - api_token: 'my_token', - room_name: 'my_room' - } -``` - -#### Options - -##### room_name - -*String, required* - -The HipChat room where the notifications must be published to. - -##### api_token - -*String, required* - -The API token to allow access to your HipChat account. - -##### notify - -*Boolean, optional* - -Notify users. Default : false. - -##### color - -*String, optional* - -Color of the message. Default : 'red'. - -##### from - -*String, optional, maximum length : 15* - -Message will appear from this nickname. Default : 'Exception'. - -##### server_url - -*String, optional* - -Custom Server URL for self-hosted, Enterprise HipChat Server - -For all options & possible values see [Hipchat API](https://www.hipchat.com/docs/api/method/rooms/message). - -### IRC notifier - -This notifier sends notifications to an IRC channel using the carrier-pigeon gem. - -#### Usage - -Just add the [carrier-pigeon](https://github.com/portertech/carrier-pigeon) gem to your `Gemfile`: - -```ruby -gem 'carrier-pigeon' -``` - -To configure it, you need to set at least the 'domain' option, like this: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - irc: { - domain: 'irc.example.com' - } -``` - -There are several other options, which are described below. For example, to use ssl and a password, add a prefix, post to the '#log' channel, and include recipients in the message (so that they will be notified), your configuration might look like this: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - irc: { - domain: 'irc.example.com', - nick: 'BadNewsBot', - password: 'secret', - port: 6697, - channel: '#log', - ssl: true, - prefix: '[Exception Notification]', - recipients: ['peter', 'michael', 'samir'] - } -``` - -#### Options - -##### domain - -*String, required* - -The domain name of your IRC server. - -##### nick - -*String, optional* - -The message will appear from this nick. Default : 'ExceptionNotifierBot'. - -##### password - -*String, optional* - -Password for your IRC server. - -##### port - -*String, optional* - -Port your IRC server is listening on. Default : 6667. - -##### channel - -*String, optional* - -Message will appear in this channel. Default : '#log'. - -##### notice - -*Boolean, optional* - -Send a notice. Default : false. - -##### ssl - -*Boolean, optional* - -Whether to use SSL. Default : false. - -##### join - -*Boolean, optional* - -Join a channel. Default : false. - -##### recipients - -*Array of strings, optional* - -Nicks to include in the message. Default: [] - -### Slack notifier - -This notifier sends notifications to a slack channel using the slack-notifier gem. - -#### Usage - -Just add the [slack-notifier](https://github.com/stevenosloan/slack-notifier) gem to your `Gemfile`: - -```ruby -gem 'slack-notifier' -``` - -To configure it, you need to set at least the 'webhook_url' option, like this: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - slack: { - webhook_url: '[Your webhook url]', - channel: '#exceptions', - additional_parameters: { - icon_url: 'http://image.jpg', - mrkdwn: true - } - } -``` - -The slack notification will include any data saved under `env['exception_notifier.exception_data']`. - -An example of how to send the server name to Slack in Rails (put this code in application_controller.rb): - -```ruby -before_action :set_notification - -def set_notification - request.env['exception_notifier.exception_data'] = { 'server' => request.env['SERVER_NAME'] } - # can be any key-value pairs -end -``` - -If you find this too verbose, you can determine to exclude certain information by doing the following: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - slack: { - webhook_url: '[Your webhook url]', - channel: '#exceptions', - additional_parameters: { - icon_url: 'http://image.jpg', - mrkdwn: true - }, - ignore_data_if: lambda {|key, value| - "#{key}" == 'key_to_ignore' || value.is_a?(ClassToBeIgnored) - } - } -``` - -Any evaluation to `true` will cause the key / value pair not be be sent along to Slack. - -#### Options - -##### webhook_url - -*String, required* - -The Incoming WebHook URL on slack. - -##### channel - -*String, optional* - -Message will appear in this channel. Defaults to the channel you set as such on slack. - -##### username - -*String, optional* - -Username of the bot. Defaults to the name you set as such on slack - -##### custom_hook - -*String, optional* - -Custom hook name. See [slack-notifier](https://github.com/stevenosloan/slack-notifier#custom-hook-name) for -more information. Default: 'incoming-webhook' - -##### additional_parameters - -*Hash of strings, optional* - -Contains additional payload for a message (e.g avatar, attachments, etc). See [slack-notifier](https://github.com/stevenosloan/slack-notifier#additional-parameters) for more information.. Default: '{}' - -##### additional_fields - -*Array of Hashes, optional* - -Contains additional fields that will be added to the attachement. See [Slack documentation](https://api.slack.com/docs/message-attachments). - -### Mattermost notifier - -Post notification in a mattermost channel via [incoming webhook](http://docs.mattermost.com/developer/webhooks-incoming.html) - -Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: - -```ruby -gem 'httparty' -``` - -To configure it, you **need** to set the `webhook_url` option. -You can also specify an other channel with `channel` option. - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - mattermost: { - webhook_url: 'http://your-mattermost.com/hooks/blablabla', - channel: 'my-channel' - } -``` - -If you are using Github or Gitlab for issues tracking, you can specify `git_url` as follow to add a *Create issue* link in you notification. -By default this will use your Rails application name to match the git repository. If yours differ you can specify `app_name`. - - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - mattermost: { - webhook_url: 'http://your-mattermost.com/hooks/blablabla', - git_url: 'github.com/aschen' - } -``` - -You can also specify the bot name and avatar with `username` and `avatar` options. - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: 'PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - mattermost: { - webhook_url: 'http://your-mattermost.com/hooks/blablabla', - avatar: 'http://example.com/your-image.png', - username: 'Fail bot' - } -``` - -Finally since the notifier use HTTParty, you can include all HTTParty options, like basic_auth for example. - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - mattermost: { - webhook_url: 'http://your-mattermost.com/hooks/blablabla', - basic_auth: { - username: 'clara', - password: 'password' - } - } -``` - -#### Options - -##### webhook_url - -*String, required* - -The Incoming WebHook URL on mattermost. - -##### channel - -*String, optional* - -Message will appear in this channel. Defaults to the channel you set as such on mattermost. - -##### username - -*String, optional* - -Username of the bot. Defaults to "Incoming Webhook" - -##### avatar - -*String, optional* - -Avatar of the bot. Defaults to incoming webhook icon. - -##### git_url - -*String, optional* - -Url of your gitlab or github with your organisation name for issue creation link (Eg: `github.com/aschen`). Defaults to nil and don't add link to the notification. - -##### app_name - -*String, optional* - -Your application name used for issue creation link. Defaults to ```Rails.application.class.parent_name.underscore```. - -### Google Chat Notifier - -Post notifications in a Google Chats channel via [incoming webhook](https://developers.google.com/hangouts/chat/how-tos/webhooks) - -Add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: - -```ruby -gem 'httparty' -``` - -To configure it, you **need** to set the `webhook_url` option. - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - google_chat: { - webhook_url: 'https://chat.googleapis.com/v1/spaces/XXXXXXXX/messages?key=YYYYYYYYYYYYY&token=ZZZZZZZZZZZZ' - } -``` - -##### webhook_url - -*String, required* - -The Incoming WebHook URL on Google Chats. - -##### app_name - -*String, optional* - -Your application name, shown in the notification. Defaults to `Rails.application.class.parent_name.underscore`. - -### Amazon SNS Notifier - -Notify all exceptions Amazon - Simple Notification Service: [SNS](https://aws.amazon.com/sns/). - -#### Usage - -Add the [aws-sdk-sns](https://github.com/aws/aws-sdk-ruby/tree/master/gems/aws-sdk-sns) gem to your `Gemfile`: - -```ruby - gem 'aws-sdk-sns', '~> 1.5' -``` - -To configure it, you **need** to set 3 required options for aws: `region`, `access_key_id` and `secret_access_key`, and one more option for sns: `topic_arn`. - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - sns: { - region: 'us-east-x', - access_key_id: 'access_key_id', - secret_access_key: 'secret_access_key', - topic_arn: 'arn:aws:sns:us-east-x:XXXX:my-topic' - } -``` - -##### sns_prefix -*String, optional * - -Prefix in the notification subject, by default: "[Error]" - -##### backtrace_lines -*Integer, optional * - -Number of backtrace lines to be displayed in the notification message. By default: 10 - -#### Note: -* You may need to update your previous `aws-sdk-*` gems in order to setup `aws-sdk-sns` correctly. -* If you need any further information about the available regions or any other SNS related topic consider: [SNS faqs](https://aws.amazon.com/sns/faqs/) - -### Teams notifier - -Post notification in a Microsoft Teams channel via [Incoming Webhook Connector](https://docs.microsoft.com/en-us/outlook/actionable-messages/actionable-messages-via-connectors) -Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: - -```ruby -gem 'httparty' -``` - -To configure it, you **need** to set the `webhook_url` option. -If you are using GitLab for issue tracking, you can specify `git_url` as follows to add a *Create issue* button in your notification. -By default this will use your Rails application name to match the git repository. If yours differs, you can specify `app_name`. -By that same notion, you may also set a `jira_url` to get a button that will send you to the New Issue screen in Jira. - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: "[PREFIX] ", - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - teams: { - webhook_url: 'https://outlook.office.com/webhook/your-guid/IncomingWebhook/team-guid', - git_url: 'https://your-gitlab.com/Group/Project', - jira_url: 'https://your-jira.com' - } -``` - -#### Options - -##### webhook_url - -*String, required* - -The Incoming WebHook URL on Teams. - -##### git_url - -*String, optional* - -Url of your gitlab or github with your organisation name for issue creation link (Eg: `github.com/aschen`). Defaults to nil and doesn't add link to the notification. - -##### jira_url - -*String, optional* - -Url of your Jira instance, adds button for Create Issue screen. Defaults to nil and doesn't add a button to the card. - -##### app_name - -*String, optional* - -Your application name used for git issue creation link. Defaults to `Rails.application.class.parent_name.underscore`. - -### WebHook notifier - -This notifier ships notifications over the HTTP protocol. - -#### Usage - -Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: - -```ruby -gem 'httparty' -``` - -To configure it, you need to set the `url` option, like this: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - webhook: { - url: 'http://domain.com:5555/hubot/path' - } -``` - -By default, the WebhookNotifier will call the URLs using the POST method. But, you can change this using the `http_method` option. - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - webhook: { - url: 'http://domain.com:5555/hubot/path', - http_method: :get - } -``` - -Besides the `url` and `http_method` options, all the other options are passed directly to HTTParty. Thus, if the HTTP server requires authentication, you can include the following options: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - webhook: { - url: 'http://domain.com:5555/hubot/path', - basic_auth: { - username: 'alice', - password: 'password' - } - } -``` - -For more HTTParty options, check out the [documentation](https://github.com/jnunemaker/httparty). - -### Custom notifier - -Simply put, notifiers are objects which respond to `#call(exception, options)` method. Thus, a lambda can be used as a notifier as follow: - -```ruby -ExceptionNotifier.add_notifier :custom_notifier_name, - ->(exception, options) { puts "Something goes wrong: #{exception.message}"} -``` - -More advanced users or third-party framework developers, also can create notifiers to be shipped in gems and take advantage of ExceptionNotification's Notifier API to standardize the [various](https://github.com/airbrake/airbrake) [solutions](https://www.honeybadger.io) [out](http://www.exceptional.io) [there](https://bugsnag.com). For this, beyond the `#call(exception, options)` method, the notifier class MUST BE defined under the ExceptionNotifier namespace and its name sufixed by `Notifier`, e.g: ExceptionNotifier::SimpleNotifier. - -#### Example - -Define the custom notifier: - -```ruby -module ExceptionNotifier - class SimpleNotifier - def initialize(options) - # do something with the options... - end - - def call(exception, options={}) - # send the notification - end - end -end -``` - -Using it: - -```ruby -Rails.application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[PREFIX] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com} - }, - simple: { - # simple notifier options - } -``` - ## Error Grouping In general, exception notification will send every notification when an error occured, which may result in a problem: if your site has a high throughput and an same error raised frequently, you will receive too many notifications during a short period time, your mail box may be full of thousands of exception mails or even your mail server will be slow. To prevent this, you can choose to error errors by using `:error_grouping` option and set it to `true`. diff --git a/docs/notifiers/campfire.md b/docs/notifiers/campfire.md new file mode 100644 index 00000000..430a0889 --- /dev/null +++ b/docs/notifiers/campfire.md @@ -0,0 +1,50 @@ +### Campfire notifier + +This notifier sends notifications to your Campfire room. + +#### Usage + +Just add the [tinder](https://github.com/collectiveidea/tinder) gem to your `Gemfile`: + +```ruby +gem 'tinder' +``` + +To configure it, you need to set the `subdomain`, `token` and `room_name` options, like this: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + campfire: { + subdomain: 'my_subdomain', + token: 'my_token', + room_name: 'my_room' + } +``` + +#### Options + +##### subdomain + +*String, required* + +Your subdomain at Campfire. + +##### room_name + +*String, required* + +The Campfire room where the notifications must be published to. + +##### token + +*String, required* + +The API token to allow access to your Campfire account. + + +For more options to set Campfire, like _ssl_, check [here](https://github.com/collectiveidea/tinder/blob/master/lib/tinder/campfire.rb#L17). diff --git a/docs/notifiers/custom.md b/docs/notifiers/custom.md new file mode 100644 index 00000000..38cbf6d9 --- /dev/null +++ b/docs/notifiers/custom.md @@ -0,0 +1,42 @@ +### Custom notifier + +Simply put, notifiers are objects which respond to `#call(exception, options)` method. Thus, a lambda can be used as a notifier as follow: + +```ruby +ExceptionNotifier.add_notifier :custom_notifier_name, + ->(exception, options) { puts "Something goes wrong: #{exception.message}"} +``` + +More advanced users or third-party framework developers, also can create notifiers to be shipped in gems and take advantage of ExceptionNotification's Notifier API to standardize the [various](https://github.com/airbrake/airbrake) [solutions](https://www.honeybadger.io) [out](http://www.exceptional.io) [there](https://bugsnag.com). For this, beyond the `#call(exception, options)` method, the notifier class MUST BE defined under the ExceptionNotifier namespace and its name sufixed by `Notifier`, e.g: ExceptionNotifier::SimpleNotifier. + +#### Example + +Define the custom notifier: + +```ruby +module ExceptionNotifier + class SimpleNotifier + def initialize(options) + # do something with the options... + end + + def call(exception, options={}) + # send the notification + end + end +end +``` + +Using it: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + simple: { + # simple notifier options + } +``` diff --git a/docs/notifiers/datadog.md b/docs/notifiers/datadog.md new file mode 100644 index 00000000..c5696702 --- /dev/null +++ b/docs/notifiers/datadog.md @@ -0,0 +1,51 @@ +### Datadog notifier + +This notifier sends error events to Datadog using the [Dogapi](https://github.com/DataDog/dogapi-rb) gem. + +#### Usage + +Just add the [Dogapi](https://github.com/DataDog/dogapi-rb) gem to your `Gemfile`: + +```ruby +gem 'dogapi' +``` + +To use datadog notifier, you first need to create a `Dogapi::Client` with your datadog api and application keys, like this: + +```ruby +client = Dogapi::Client.new(api_key, application_key) +``` + +You then need to set the `client` option, like this: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: "[PREFIX] ", + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + datadog: { + client: client + } +``` + +#### Options + +##### client + +*DogApi::Client, required* + +The API client to send events to Datadog. + +##### title_prefix + +*String, optional* + +Prefix for event title in Datadog. + +##### tags + +*Array of Strings, optional* + +Optional tags for events in Datadog. diff --git a/docs/notifiers/email.md b/docs/notifiers/email.md new file mode 100644 index 00000000..e63773d2 --- /dev/null +++ b/docs/notifiers/email.md @@ -0,0 +1,195 @@ +### Email notifier + +The Email notifier sends notifications by email. The notifications/emails sent includes information about the current request, session, and environment, and also gives a backtrace of the exception. + +After an exception notification has been delivered the rack environment variable `exception_notifier.delivered` will be set to true. + +#### ActionMailer configuration + +For the email to be sent, there must be a default ActionMailer `delivery_method` setting configured. If you do not have one, you can use the following code (assuming your app server machine has `sendmail`). Depending on the environment you want ExceptionNotification to run in, put the following code in your `config/environments/production.rb` and/or `config/environments/development.rb`: + +```ruby +config.action_mailer.delivery_method = :sendmail +# Defaults to: +# config.action_mailer.sendmail_settings = { +# location: '/usr/sbin/sendmail', +# arguments: '-i -t' +# } +config.action_mailer.perform_deliveries = true +config.action_mailer.raise_delivery_errors = true +``` + +#### Options + +##### sender_address + +*String, default: %("Exception Notifier" )* + +Who the message is from. + +##### exception_recipients + +*String/Array of strings/Proc, default: []* + +Who the message is destined for, can be a string of addresses, an array of addresses, or it can be a proc that returns a string of addresses or an array of addresses. The proc will be evaluated when the mail is sent. + +##### email_prefix + +*String, default: [ERROR]* + +The subject's prefix of the message. + +##### sections + +*Array of strings, default: %w(request session environment backtrace)* + +By default, the notification email includes four parts: request, session, environment, and backtrace (in that order). You can customize how each of those sections are rendered by placing a partial named for that part in your `app/views/exception_notifier` directory (e.g., `_session.rhtml`). Each partial has access to the following variables: + +```ruby +@kontroller # the controller that caused the error +@request # the current request object +@exception # the exception that was raised +@backtrace # a sanitized version of the exception's backtrace +@data # a hash of optional data values that were passed to the notifier +@sections # the array of sections to include in the email +``` + +You can reorder the sections, or exclude sections completely, by using `sections` option. You can even add new sections that +describe application-specific data--just add the section's name to the list (wherever you'd like), and define the corresponding partial. Like the following example with two new added sections: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + sections: %w{my_section1 my_section2} + } +``` + +Place your custom sections under `./app/views/exception_notifier/` with the suffix `.text.erb`, e.g. `./app/views/exception_notifier/_my_section1.text.erb`. + +If your new section requires information that isn't available by default, make sure it is made available to the email using the `exception_data` macro: + +```ruby +class ApplicationController < ActionController::Base + before_action :log_additional_data + ... + protected + + def log_additional_data + request.env['exception_notifier.exception_data'] = { + document: @document, + person: @person + } + end + ... +end +``` + +In the above case, `@document` and `@person` would be made available to the email renderer, allowing your new section(s) to access and display them. See the existing sections defined by the plugin for examples of how to write your own. + +##### background_sections + +*Array of strings, default: %w(backtrace data)* + +When using [background notifications](#background-notifications) some variables are not available in the views, like `@kontroller` and `@request`. Thus, you may want to include different sections for background notifications: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + background_sections: %w{my_section1 my_section2 backtrace data} + } +``` + +##### email_headers + +*Hash of strings, default: {}* + +Additionally, you may want to set customized headers on the outcoming emails. To do so, simply use the `:email_headers` option: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: "[PREFIX] ", + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + email_headers: { "X-Custom-Header" => "foobar" } + } +``` + +##### verbose_subject + +*Boolean, default: true* + +If enabled, include the exception message in the subject. Use `verbose_subject: false` to exclude it. + +##### normalize_subject + +*Boolean, default: false* + +If enabled, remove numbers from subject so they thread as a single one. Use `normalize_subject: true` to enable it. + +##### include_controller_and_action_names_in_subject + +*Boolean, default: true* + +If enabled, include the controller and action names in the subject. Use `include_controller_and_action_names_in_subject: false` to exclude them. + +##### email_format + +*Symbol, default: :text* + +By default, ExceptionNotification sends emails in plain text, in order to sends multipart notifications (aka HTML emails) use `email_format: :html`. + +##### delivery_method + +*Symbol, default: :smtp* + +By default, ExceptionNotification sends emails using the ActionMailer configuration of the application. In order to send emails by another delivery method, use the `delivery_method` option: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + delivery_method: :postmark, + postmark_settings: { + api_key: ENV['POSTMARK_API_KEY'] + } + } +``` + +Besides the `delivery_method` option, you also can customize the mailer settings by passing a hash under an option named `DELIVERY_METHOD_settings`. Thus, you can use override specific SMTP settings for notifications using: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com}, + delivery_method: :smtp, + smtp_settings: { + user_name: 'bob', + password: 'password', + } + } +``` + +A complete list of `smtp_settings` options can be found in the [ActionMailer Configuration documentation](http://api.rubyonrails.org/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Configuration+options). + +##### mailer_parent + +*String, default: ActionMailer::Base* + +The parent mailer which ExceptionNotification mailer inherit from. + +##### deliver_with + +*Symbol, default: :deliver_now + +The method name to send emalis using ActionMailer. diff --git a/docs/notifiers/google_chat.md b/docs/notifiers/google_chat.md new file mode 100644 index 00000000..0131b660 --- /dev/null +++ b/docs/notifiers/google_chat.md @@ -0,0 +1,30 @@ +### Google Chat Notifier + +Post notifications in a Google Chats channel via [incoming webhook](https://developers.google.com/hangouts/chat/how-tos/webhooks) + +Add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: + +```ruby +gem 'httparty' +``` + +To configure it, you **need** to set the `webhook_url` option. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + google_chat: { + webhook_url: 'https://chat.googleapis.com/v1/spaces/XXXXXXXX/messages?key=YYYYYYYYYYYYY&token=ZZZZZZZZZZZZ' + } +``` + +##### webhook_url + +*String, required* + +The Incoming WebHook URL on Google Chats. + +##### app_name + +*String, optional* + +Your application name, shown in the notification. Defaults to `Rails.application.class.parent_name.underscore`. diff --git a/docs/notifiers/hipchat.md b/docs/notifiers/hipchat.md new file mode 100644 index 00000000..dba14415 --- /dev/null +++ b/docs/notifiers/hipchat.md @@ -0,0 +1,66 @@ +### HipChat notifier + +This notifier sends notifications to your Hipchat room. + +#### Usage + +Just add the [hipchat](https://github.com/hipchat/hipchat-rb) gem to your `Gemfile`: + +```ruby +gem 'hipchat' +``` + +To configure it, you need to set the `token` and `room_name` options, like this: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + hipchat: { + api_token: 'my_token', + room_name: 'my_room' + } +``` + +#### Options + +##### room_name + +*String, required* + +The HipChat room where the notifications must be published to. + +##### api_token + +*String, required* + +The API token to allow access to your HipChat account. + +##### notify + +*Boolean, optional* + +Notify users. Default : false. + +##### color + +*String, optional* + +Color of the message. Default : 'red'. + +##### from + +*String, optional, maximum length : 15* + +Message will appear from this nickname. Default : 'Exception'. + +##### server_url + +*String, optional* + +Custom Server URL for self-hosted, Enterprise HipChat Server + +For all options & possible values see [Hipchat API](https://www.hipchat.com/docs/api/method/rooms/message). diff --git a/docs/notifiers/irc.md b/docs/notifiers/irc.md new file mode 100644 index 00000000..69e733a5 --- /dev/null +++ b/docs/notifiers/irc.md @@ -0,0 +1,97 @@ +### IRC notifier + +This notifier sends notifications to an IRC channel using the carrier-pigeon gem. + +#### Usage + +Just add the [carrier-pigeon](https://github.com/portertech/carrier-pigeon) gem to your `Gemfile`: + +```ruby +gem 'carrier-pigeon' +``` + +To configure it, you need to set at least the 'domain' option, like this: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + irc: { + domain: 'irc.example.com' + } +``` + +There are several other options, which are described below. For example, to use ssl and a password, add a prefix, post to the '#log' channel, and include recipients in the message (so that they will be notified), your configuration might look like this: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + irc: { + domain: 'irc.example.com', + nick: 'BadNewsBot', + password: 'secret', + port: 6697, + channel: '#log', + ssl: true, + prefix: '[Exception Notification]', + recipients: ['peter', 'michael', 'samir'] + } +``` + +#### Options + +##### domain + +*String, required* + +The domain name of your IRC server. + +##### nick + +*String, optional* + +The message will appear from this nick. Default : 'ExceptionNotifierBot'. + +##### password + +*String, optional* + +Password for your IRC server. + +##### port + +*String, optional* + +Port your IRC server is listening on. Default : 6667. + +##### channel + +*String, optional* + +Message will appear in this channel. Default : '#log'. + +##### notice + +*Boolean, optional* + +Send a notice. Default : false. + +##### ssl + +*Boolean, optional* + +Whether to use SSL. Default : false. + +##### join + +*Boolean, optional* + +Join a channel. Default : false. + +##### recipients + +*Array of strings, optional* + +Nicks to include in the message. Default: [] diff --git a/docs/notifiers/mattermost.md b/docs/notifiers/mattermost.md new file mode 100644 index 00000000..d60183e5 --- /dev/null +++ b/docs/notifiers/mattermost.md @@ -0,0 +1,114 @@ +### Mattermost notifier + +Post notification in a mattermost channel via [incoming webhook](http://docs.mattermost.com/developer/webhooks-incoming.html) + +Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: + +```ruby +gem 'httparty' +``` + +To configure it, you **need** to set the `webhook_url` option. +You can also specify an other channel with `channel` option. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + mattermost: { + webhook_url: 'http://your-mattermost.com/hooks/blablabla', + channel: 'my-channel' + } +``` + +If you are using Github or Gitlab for issues tracking, you can specify `git_url` as follow to add a *Create issue* link in you notification. +By default this will use your Rails application name to match the git repository. If yours differ you can specify `app_name`. + + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + mattermost: { + webhook_url: 'http://your-mattermost.com/hooks/blablabla', + git_url: 'github.com/aschen' + } +``` + +You can also specify the bot name and avatar with `username` and `avatar` options. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: 'PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + mattermost: { + webhook_url: 'http://your-mattermost.com/hooks/blablabla', + avatar: 'http://example.com/your-image.png', + username: 'Fail bot' + } +``` + +Finally since the notifier use HTTParty, you can include all HTTParty options, like basic_auth for example. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + mattermost: { + webhook_url: 'http://your-mattermost.com/hooks/blablabla', + basic_auth: { + username: 'clara', + password: 'password' + } + } +``` + +#### Options + +##### webhook_url + +*String, required* + +The Incoming WebHook URL on mattermost. + +##### channel + +*String, optional* + +Message will appear in this channel. Defaults to the channel you set as such on mattermost. + +##### username + +*String, optional* + +Username of the bot. Defaults to "Incoming Webhook" + +##### avatar + +*String, optional* + +Avatar of the bot. Defaults to incoming webhook icon. + +##### git_url + +*String, optional* + +Url of your gitlab or github with your organisation name for issue creation link (Eg: `github.com/aschen`). Defaults to nil and don't add link to the notification. + +##### app_name + +*String, optional* + +Your application name used for issue creation link. Defaults to ```Rails.application.class.parent_name.underscore```. diff --git a/docs/notifiers/slack.md b/docs/notifiers/slack.md new file mode 100644 index 00000000..d6c3e774 --- /dev/null +++ b/docs/notifiers/slack.md @@ -0,0 +1,101 @@ +### Slack notifier + +This notifier sends notifications to a slack channel using the slack-notifier gem. + +#### Usage + +Just add the [slack-notifier](https://github.com/stevenosloan/slack-notifier) gem to your `Gemfile`: + +```ruby +gem 'slack-notifier' +``` + +To configure it, you need to set at least the 'webhook_url' option, like this: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + slack: { + webhook_url: '[Your webhook url]', + channel: '#exceptions', + additional_parameters: { + icon_url: 'http://image.jpg', + mrkdwn: true + } + } +``` + +The slack notification will include any data saved under `env['exception_notifier.exception_data']`. + +An example of how to send the server name to Slack in Rails (put this code in application_controller.rb): + +```ruby +before_action :set_notification + +def set_notification + request.env['exception_notifier.exception_data'] = { 'server' => request.env['SERVER_NAME'] } + # can be any key-value pairs +end +``` + +If you find this too verbose, you can determine to exclude certain information by doing the following: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + slack: { + webhook_url: '[Your webhook url]', + channel: '#exceptions', + additional_parameters: { + icon_url: 'http://image.jpg', + mrkdwn: true + }, + ignore_data_if: lambda {|key, value| + "#{key}" == 'key_to_ignore' || value.is_a?(ClassToBeIgnored) + } + } +``` + +Any evaluation to `true` will cause the key / value pair not be be sent along to Slack. + +#### Options + +##### webhook_url + +*String, required* + +The Incoming WebHook URL on slack. + +##### channel + +*String, optional* + +Message will appear in this channel. Defaults to the channel you set as such on slack. + +##### username + +*String, optional* + +Username of the bot. Defaults to the name you set as such on slack + +##### custom_hook + +*String, optional* + +Custom hook name. See [slack-notifier](https://github.com/stevenosloan/slack-notifier#custom-hook-name) for +more information. Default: 'incoming-webhook' + +##### additional_parameters + +*Hash of strings, optional* + +Contains additional payload for a message (e.g avatar, attachments, etc). See [slack-notifier](https://github.com/stevenosloan/slack-notifier#additional-parameters) for more information.. Default: '{}' + +##### additional_fields + +*Array of Hashes, optional* + +Contains additional fields that will be added to the attachement. See [Slack documentation](https://api.slack.com/docs/message-attachments). diff --git a/docs/notifiers/sns.md b/docs/notifiers/sns.md new file mode 100644 index 00000000..7e5700f6 --- /dev/null +++ b/docs/notifiers/sns.md @@ -0,0 +1,37 @@ +### Amazon SNS Notifier + +Notify all exceptions Amazon - Simple Notification Service: [SNS](https://aws.amazon.com/sns/). + +#### Usage + +Add the [aws-sdk-sns](https://github.com/aws/aws-sdk-ruby/tree/master/gems/aws-sdk-sns) gem to your `Gemfile`: + +```ruby + gem 'aws-sdk-sns', '~> 1.5' +``` + +To configure it, you **need** to set 3 required options for aws: `region`, `access_key_id` and `secret_access_key`, and one more option for sns: `topic_arn`. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + sns: { + region: 'us-east-x', + access_key_id: 'access_key_id', + secret_access_key: 'secret_access_key', + topic_arn: 'arn:aws:sns:us-east-x:XXXX:my-topic' + } +``` + +##### sns_prefix +*String, optional * + +Prefix in the notification subject, by default: "[Error]" + +##### backtrace_lines +*Integer, optional * + +Number of backtrace lines to be displayed in the notification message. By default: 10 + +#### Note: +* You may need to update your previous `aws-sdk-*` gems in order to setup `aws-sdk-sns` correctly. +* If you need any further information about the available regions or any other SNS related topic consider: [SNS faqs](https://aws.amazon.com/sns/faqs/) diff --git a/docs/notifiers/teams.md b/docs/notifiers/teams.md new file mode 100644 index 00000000..53e0ea93 --- /dev/null +++ b/docs/notifiers/teams.md @@ -0,0 +1,53 @@ +### Teams notifier + +Post notification in a Microsoft Teams channel via [Incoming Webhook Connector](https://docs.microsoft.com/en-us/outlook/actionable-messages/actionable-messages-via-connectors) +Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: + +```ruby +gem 'httparty' +``` + +To configure it, you **need** to set the `webhook_url` option. +If you are using GitLab for issue tracking, you can specify `git_url` as follows to add a *Create issue* button in your notification. +By default this will use your Rails application name to match the git repository. If yours differs, you can specify `app_name`. +By that same notion, you may also set a `jira_url` to get a button that will send you to the New Issue screen in Jira. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: "[PREFIX] ", + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + teams: { + webhook_url: 'https://outlook.office.com/webhook/your-guid/IncomingWebhook/team-guid', + git_url: 'https://your-gitlab.com/Group/Project', + jira_url: 'https://your-jira.com' + } +``` + +#### Options + +##### webhook_url + +*String, required* + +The Incoming WebHook URL on Teams. + +##### git_url + +*String, optional* + +Url of your gitlab or github with your organisation name for issue creation link (Eg: `github.com/aschen`). Defaults to nil and doesn't add link to the notification. + +##### jira_url + +*String, optional* + +Url of your Jira instance, adds button for Create Issue screen. Defaults to nil and doesn't add a button to the card. + +##### app_name + +*String, optional* + +Your application name used for git issue creation link. Defaults to `Rails.application.class.parent_name.underscore`. diff --git a/docs/notifiers/webhook.md b/docs/notifiers/webhook.md new file mode 100644 index 00000000..a8ad2962 --- /dev/null +++ b/docs/notifiers/webhook.md @@ -0,0 +1,60 @@ +### WebHook notifier + +This notifier ships notifications over the HTTP protocol. + +#### Usage + +Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`: + +```ruby +gem 'httparty' +``` + +To configure it, you need to set the `url` option, like this: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + webhook: { + url: 'http://domain.com:5555/hubot/path' + } +``` + +By default, the WebhookNotifier will call the URLs using the POST method. But, you can change this using the `http_method` option. + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + webhook: { + url: 'http://domain.com:5555/hubot/path', + http_method: :get + } +``` + +Besides the `url` and `http_method` options, all the other options are passed directly to HTTParty. Thus, if the HTTP server requires authentication, you can include the following options: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[PREFIX] ', + sender_address: %{"notifier" }, + exception_recipients: %w{exceptions@example.com} + }, + webhook: { + url: 'http://domain.com:5555/hubot/path', + basic_auth: { + username: 'alice', + password: 'password' + } + } +``` + +For more HTTParty options, check out the [documentation](https://github.com/jnunemaker/httparty). From 620cdfba449b50c1cd4bfa9c674cdf096b2808d2 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 10 Dec 2018 21:22:09 -0300 Subject: [PATCH 023/156] Update README links --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 36126ed9..7b644d2e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ --- -The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [Teams](#teams-notifier), [IRC](#irc-notifier), [Amazon SNS](#amazon-sns-notifier), [Google Chat](#google-chat-notifier), [Datadog](#datadog-notifier) or via custom [WebHooks](#webhook-notifier). +The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](docs/notifiers/email.md), [Campfire](docs/notifiers/campfire.md), [HipChat](docs/notifiers/hipchat.md), [Slack](docs/notifiers/slack.md), [Mattermost](docs/notifiers/mattermost.md), [Teams](docs/notifiers/teams.md), [IRC](docs/notifiers/irc.md), [Amazon SNS](docs/notifiers/sns.md), [Google Chat](docs/notifiers/google_chat.md), [Datadog](docs/notifiers/datadog.md) or via custom [WebHooks](docs/notifiers/webhook.md). There's a great [Railscast about Exception Notification](http://railscasts.com/episodes/104-exception-notifications-revised) you can see that may help you getting started. @@ -86,19 +86,19 @@ Options -> sections" below. ExceptionNotification relies on notifiers to deliver notifications when errors occur in your applications. By default, 8 notifiers are available: -* [Campfire notifier](#campfire-notifier) -* [Datadog notifier](#datadog-notifier) -* [Email notifier](#email-notifier) -* [HipChat notifier](#hipchat-notifier) -* [IRC notifier](#irc-notifier) -* [Slack notifier](#slack-notifier) -* [Mattermost notifier](#mattermost-notifier) -* [Teams notifier](#teams-notifier) -* [Amazon SNS](#amazon-sns-notifier) -* [Google Chat notifier](#google-chat-notifier) -* [WebHook notifier](#webhook-notifier) - -But, you also can easily implement your own [custom notifier](#custom-notifier). +* [Campfire notifier](docs/notifiers/campfire.md) +* [Datadog notifier](docs/notifiers/datadog.md) +* [Email notifier](docs/notifiers/email.md) +* [HipChat notifier](docs/notifiers/hipchat.md) +* [IRC notifier](docs/notifiers/irc.md) +* [Slack notifier](docs/notifiers/slack.md) +* [Mattermost notifier](docs/notifiers/mattermost.md) +* [Teams notifier](docs/notifiers/teams.md) +* [Amazon SNS](docs/notifiers/sns.md) +* [Google Chat notifier](docs/notifiers/google_chat.md) +* [WebHook notifier](docs/notifiers/webhook.md) + +But, you also can easily implement your own [custom notifier](docs/notifiers/custom.md). ## Error Grouping In general, exception notification will send every notification when an error occured, which may result in a problem: if your site has a high throughput and an same error raised frequently, you will receive too many notifications during a short period time, your mail box may be full of thousands of exception mails or even your mail server will be slow. To prevent this, you can choose to error errors by using `:error_grouping` option and set it to `true`. From 8226d7ad3850c6d702a1da365b43e13c20968a37 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 10 Dec 2018 21:46:19 -0300 Subject: [PATCH 024/156] Add Timecop gem to freeze time in tests --- exception_notification.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 5be11c3f..ada87ed2 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -34,4 +34,5 @@ Gem::Specification.new do |s| s.add_development_dependency "slack-notifier", ">= 1.0.0" s.add_development_dependency "aws-sdk-sns", "~> 1" s.add_development_dependency "dogapi", ">= 1.23.0" + s.add_development_dependency "timecop", "~>0.8.0" end From 42012a233e60ce7250daaf642337d10eca4d7b95 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 10 Dec 2018 21:46:21 -0300 Subject: [PATCH 025/156] Refactor GoogleChatNotifierTest Trying to avoid testing private methods --- .../google_chat_notifier_test.rb | 246 +++++++++++------- 1 file changed, 145 insertions(+), 101 deletions(-) diff --git a/test/exception_notifier/google_chat_notifier_test.rb b/test/exception_notifier/google_chat_notifier_test.rb index 3e70c738..fcf2dfba 100644 --- a/test/exception_notifier/google_chat_notifier_test.rb +++ b/test/exception_notifier/google_chat_notifier_test.rb @@ -1,128 +1,172 @@ require 'test_helper' require 'httparty' +require 'timecop' class GoogleChatNotifierTest < ActiveSupport::TestCase + URL = 'http://localhost:8000'.freeze - test "should send notification if properly configured" do - options = { - webhook_url: 'http://localhost:8000' - } - google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new - google_chat_notifier.httparty = FakeHTTParty.new - - options = google_chat_notifier.call ArgumentError.new("foo"), options + def setup + Timecop.freeze('2018-12-09 12:07:16 UTC') + end - body = ActiveSupport::JSON.decode options[:body] - assert body.has_key? 'text' + def teardown + Timecop.return + end - text = body['text'].split("\n") - assert_equal 6, text.size - assert_equal 'Application: *dummy*', text[1] - assert_equal 'An *ArgumentError* occured.', text[2] - assert_equal '*foo*', text[5] + test 'should send notification if properly configured' do + HTTParty.expects(:post).with(URL, post_opts("#{header}\n#{body}")) + notifier.call ArgumentError.new('foo') end - test "should use 'An' for exceptions count if :accumulated_errors_count option is nil" do - google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new - exception = ArgumentError.new("foo") - google_chat_notifier.instance_variable_set(:@exception, exception) - google_chat_notifier.instance_variable_set(:@options, {}) + test 'shoud use errors count if accumulated_errors_count is provided' do + text = [ + '', + 'Application: *dummy*', + '5 *ArgumentError* occured.', + '', + body + ].join("\n") + + opts = post_opts(text, accumulated_errors_count: 5) + HTTParty.expects(:post).with(URL, opts) - assert_includes google_chat_notifier.send(:header), "An *ArgumentError* occured." + notifier.call(ArgumentError.new('foo'), accumulated_errors_count: 5) end - test "shoud use direct errors count if :accumulated_errors_count option is 5" do - google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new - exception = ArgumentError.new("foo") - google_chat_notifier.instance_variable_set(:@exception, exception) - google_chat_notifier.instance_variable_set(:@options, { accumulated_errors_count: 5 }) + test 'Message request should be formatted as hash' do + text = [ + header(true), + body, + '', + '*Request:*', + '```', + '* url : http://test.address/?id=foo', + '* http_method : GET', + '* ip_address : 127.0.0.1', + '* parameters : {"id"=>"foo"}', + '* timestamp : 2018-12-09 12:07:16 UTC', + '```' + ].join("\n") + + HTTParty.expects(:post).with(URL, post_opts(text)) + + notifier.call(ArgumentError.new('foo'), env: test_env) + end - assert_includes google_chat_notifier.send(:header), "5 *ArgumentError* occured." + test 'backtrace with less than 3 lines should be displayed fully' do + text = [ + header, + body, + '', + '*Backtrace:*', + '```', + "* app/controllers/my_controller.rb:53:in `my_controller_params'", + "* app/controllers/my_controller.rb:34:in `update'", + '```' + ].join("\n") + + HTTParty.expects(:post).with(URL, post_opts(text)) + + exception = ArgumentError.new('foo') + exception.set_backtrace([ + "app/controllers/my_controller.rb:53:in `my_controller_params'", + "app/controllers/my_controller.rb:34:in `update'" + ]) + + notifier.call(exception) end - test "Message request should be formatted as hash" do - google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new - request_items = { url: 'http://test.address', - http_method: :get, - ip_address: '127.0.0.1', - parameters: '{"id"=>"foo"}', - timestamp: Time.parse('2018-08-13 12:13:24 UTC') } - google_chat_notifier.instance_variable_set(:@request_items, request_items) - - message_request = google_chat_notifier.send(:message_request).join("\n") - assert_includes message_request, '* url : http://test.address' - assert_includes message_request, '* http_method : get' - assert_includes message_request, '* ip_address : 127.0.0.1' - assert_includes message_request, '* parameters : {"id"=>"foo"}' - assert_includes message_request, '* timestamp : 2018-08-13 12:13:24 UTC' + test 'backtrace with more than 3 lines should display only top 3 lines' do + text = [ + header, + body, + '', + '*Backtrace:*', + '```', + "* app/controllers/my_controller.rb:99:in `specific_function'", + "* app/controllers/my_controller.rb:70:in `specific_param'", + "* app/controllers/my_controller.rb:53:in `my_controller_params'", + '```' + ].join("\n") + + HTTParty.expects(:post).with(URL, post_opts(text)) + + exception = ArgumentError.new('foo') + exception.set_backtrace([ + "app/controllers/my_controller.rb:99:in `specific_function'", + "app/controllers/my_controller.rb:70:in `specific_param'", + "app/controllers/my_controller.rb:53:in `my_controller_params'", + "app/controllers/my_controller.rb:34:in `update'" + ]) + + notifier.call(exception) end - test 'backtrace with less than 3 lines should be displayed fully' do - google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new + test 'Get text with backtrace and request info' do + text = [ + header(true), + body, + '', + '*Request:*', + '```', + '* url : http://test.address/?id=foo', + '* http_method : GET', + '* ip_address : 127.0.0.1', + '* parameters : {"id"=>"foo"}', + '* timestamp : 2018-12-09 12:07:16 UTC', + '```', + '', + '*Backtrace:*', + '```', + "* app/controllers/my_controller.rb:53:in `my_controller_params'", + "* app/controllers/my_controller.rb:34:in `update'", + '```' + ].join("\n") + + HTTParty.expects(:post).with(URL, post_opts(text)) + + exception = ArgumentError.new('foo') + exception.set_backtrace([ + "app/controllers/my_controller.rb:53:in `my_controller_params'", + "app/controllers/my_controller.rb:34:in `update'" + ]) + + notifier.call(exception, env: test_env) + end - backtrace = ["app/controllers/my_controller.rb:53:in `my_controller_params'", "app/controllers/my_controller.rb:34:in `update'"] - google_chat_notifier.instance_variable_set(:@backtrace, backtrace) + private - message_backtrace = google_chat_notifier.send(:message_backtrace).join("\n") - assert_includes message_backtrace, "* app/controllers/my_controller.rb:53:in `my_controller_params'" - assert_includes message_backtrace, "* app/controllers/my_controller.rb:34:in `update'" + def notifier + ExceptionNotifier::GoogleChatNotifier.new(webhook_url: URL) end - test 'backtrace with more than 3 lines should display only top 3 lines' do - google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new + def post_opts(text, opts = {}) + { + body: { text: text }.to_json, + headers: { 'Content-Type' => 'application/json' } + }.merge(opts) + end - backtrace = ["app/controllers/my_controller.rb:99:in `specific_function'", "app/controllers/my_controller.rb:70:in `specific_param'", "app/controllers/my_controller.rb:53:in `my_controller_params'", "app/controllers/my_controller.rb:34:in `update'"] - google_chat_notifier.instance_variable_set(:@backtrace, backtrace) + def test_env + Rack::MockRequest.env_for( + '/', + 'HTTP_HOST' => 'test.address', + 'REMOTE_ADDR' => '127.0.0.1', + 'HTTP_USER_AGENT' => 'Rails Testing', + params: { id: 'foo' } + ) + end - message_backtrace = google_chat_notifier.send(:message_backtrace).join("\n") - assert_includes message_backtrace, "* app/controllers/my_controller.rb:99:in `specific_function'" - assert_includes message_backtrace, "* app/controllers/my_controller.rb:70:in `specific_param'" - assert_includes message_backtrace, "* app/controllers/my_controller.rb:53:in `my_controller_params'" - assert_not_includes message_backtrace, "* app/controllers/my_controller.rb:34:in `update'" + def header(env = false) + [ + '', + 'Application: *dummy*', + "An *ArgumentError* occured#{' in *#*' if env}.", + '' + ].join("\n") end - test 'Get text with backtrace and request info' do - google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new - - backtrace = ["app/controllers/my_controller.rb:53:in `my_controller_params'", "app/controllers/my_controller.rb:34:in `update'"] - google_chat_notifier.instance_variable_set(:@backtrace, backtrace) - - request_items = { url: 'http://test.address', - http_method: :get, - ip_address: '127.0.0.1', - parameters: '{"id"=>"foo"}', - timestamp: Time.parse('2018-08-13 12:13:24 UTC') } - google_chat_notifier.instance_variable_set(:@request_items, request_items) - - google_chat_notifier.instance_variable_set(:@options, {accumulated_errors_count: 0}) - - google_chat_notifier.instance_variable_set(:@application_name, 'dummy') - - exception = ArgumentError.new("foo") - google_chat_notifier.instance_variable_set(:@exception, exception) - - text = google_chat_notifier.send(:exception_text) - expected_text = %q( -Application: *dummy* -An *ArgumentError* occured. - -⚠️ Error 500 in test ⚠️ -*foo* - -*Request:* -``` -* url : http://test.address -* http_method : get -* ip_address : 127.0.0.1 -* parameters : {"id"=>"foo"} -* timestamp : 2018-08-13 12:13:24 UTC -``` - -*Backtrace:* -``` -* app/controllers/my_controller.rb:53:in `my_controller_params' -* app/controllers/my_controller.rb:34:in `update' -```) - assert_equal text, expected_text + def body + "⚠️ Error 500 in test ⚠️\n*foo*" end end From 12e340b17acbaf6d33a3abf357e9e90f8e97e9b2 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 13 Dec 2018 20:42:27 -0300 Subject: [PATCH 026/156] Refactor GoogleChatNotifier --- .../google_chat_notifier.rb | 155 +++++++----------- .../google_chat_notifier_test.rb | 15 +- 2 files changed, 67 insertions(+), 103 deletions(-) diff --git a/lib/exception_notifier/google_chat_notifier.rb b/lib/exception_notifier/google_chat_notifier.rb index 46aa71d9..e8140e3c 100644 --- a/lib/exception_notifier/google_chat_notifier.rb +++ b/lib/exception_notifier/google_chat_notifier.rb @@ -1,135 +1,100 @@ require 'action_dispatch' require 'active_support/core_ext/time' +require 'httparty' module ExceptionNotifier - class GoogleChatNotifier + class GoogleChatNotifier < BaseNotifier include ExceptionNotifier::BacktraceCleaner - class MissingController - def method_missing(*args, &block) - end - end - - attr_accessor :httparty - - def initialize(options = {}) - super() - @default_options = options - @httparty = HTTParty - end - - def call(exception, options = {}) - @options = options.merge(@default_options) + def call(exception, opts = {}) + @options = base_options.merge(opts) @exception = exception - @backtrace = exception.backtrace ? clean_backtrace(exception) : nil - - @env = @options.delete(:env) - - @application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore - - @webhook_url = @options.delete(:webhook_url) - raise ArgumentError.new "You must provide 'webhook_url' parameter." unless @webhook_url - - unless @env.nil? - @controller = @env['action_controller.instance'] || MissingController.new - - request = ActionDispatch::Request.new(@env) - @request_items = { url: request.original_url, - http_method: request.method, - ip_address: request.remote_ip, - parameters: request.filtered_parameters, - timestamp: Time.current } - else - @controller = @request_items = nil - end + HTTParty.post( + options[:webhook_url], + body: { text: body }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + end + private - @options[:body] = payload.to_json - @options[:headers] ||= {} - @options[:headers].merge!({ 'Content-Type' => 'application/json' }) + attr_reader :options, :exception - @httparty.post(@webhook_url, @options) - end + def body + text = [ + header, + "", + "⚠️ Error 500 in #{Rails.env} ⚠️", + "*#{exception.message.tr('`', "'")}*" + ] - private + text += message_request + text += message_backtrace - def payload - { - text: exception_text - } + text.join("\n") end def header - errors_count = @options[:accumulated_errors_count].to_i - text = [''] + text = ["\nApplication: *#{app_name}*"] - text << "Application: *#{@application_name}*" - text << "#{errors_count > 1 ? errors_count : 'An'} *#{@exception.class}* occured" + if @controller then " in *#{controller_and_method}*." else "." end + errors_text = errors_count > 1 ? errors_count : 'An' + text << "#{errors_text} *#{exception.class}* occured#{controller_text}." text end - def exception_text - text = [] - - text << header - text << '' - - text << "⚠️ Error 500 in #{Rails.env} ⚠️" - text << "*#{@exception.message.gsub('`', %q('))}*" - - if @request_items - text << '' - text += message_request - end + def message_request + return [] unless (env = options[:env]) + request = ActionDispatch::Request.new(env) + + [ + "", + "*Request:*", + "```", + "* url : #{request.original_url}", + "* http_method : #{request.method}", + "* ip_address : #{request.remote_ip}", + "* parameters : #{request.filtered_parameters}", + "* timestamp : #{Time.current}", + "```" + ] + end - if @backtrace - text << '' - text += message_backtrace - end + def message_backtrace + backtrace = exception.backtrace ? clean_backtrace(exception) : nil - text.join("\n") - end + return [] unless backtrace - def message_request text = [] - text << "*Request:*" + text << '' + text << "*Backtrace:*" text << "```" - text << hash_presentation(@request_items) + backtrace.first(3).each { |line| text << "* #{line}" } text << "```" text end - def hash_presentation(hash) - text = [] - - hash.each do |key, value| - text << "* #{key} : #{value}" - end - - text.join("\n") + def app_name + @app_name ||= options[:app_name] || rails_app_name || "N/A" end - def message_backtrace(size = 3) - text = [] - - size = @backtrace.size < size ? @backtrace.size : size - text << "*Backtrace:*" - text << "```" - size.times { |i| text << "* " + @backtrace[i] } - text << "```" + def errors_count + @errors_count ||= options[:accumulated_errors_count].to_i + end - text + def rails_app_name + Rails.application.class.parent_name.underscore if defined?(Rails) end - def controller_and_method - if @controller - "#{@controller.controller_name}##{@controller.action_name}" - else - "" + def controller_text + env = options[:env] + controller = env ? env['action_controller.instance'] : nil + + if controller + " in *#{controller.controller_name}##{controller.action_name}*" end end end diff --git a/test/exception_notifier/google_chat_notifier_test.rb b/test/exception_notifier/google_chat_notifier_test.rb index fcf2dfba..3da08add 100644 --- a/test/exception_notifier/google_chat_notifier_test.rb +++ b/test/exception_notifier/google_chat_notifier_test.rb @@ -27,15 +27,14 @@ def teardown body ].join("\n") - opts = post_opts(text, accumulated_errors_count: 5) - HTTParty.expects(:post).with(URL, opts) + HTTParty.expects(:post).with(URL, post_opts(text)) notifier.call(ArgumentError.new('foo'), accumulated_errors_count: 5) end test 'Message request should be formatted as hash' do text = [ - header(true), + header, body, '', '*Request:*', @@ -104,7 +103,7 @@ def teardown test 'Get text with backtrace and request info' do text = [ - header(true), + header, body, '', '*Request:*', @@ -140,11 +139,11 @@ def notifier ExceptionNotifier::GoogleChatNotifier.new(webhook_url: URL) end - def post_opts(text, opts = {}) + def post_opts(text) { body: { text: text }.to_json, headers: { 'Content-Type' => 'application/json' } - }.merge(opts) + } end def test_env @@ -157,11 +156,11 @@ def test_env ) end - def header(env = false) + def header [ '', 'Application: *dummy*', - "An *ArgumentError* occured#{' in *#*' if env}.", + "An *ArgumentError* occured.", '' ].join("\n") end From 8941832b3b5c973eccc7d83f8aed27c85275a443 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 13 Dec 2018 21:09:14 -0300 Subject: [PATCH 027/156] Add Rails 5.2 to Appraisal --- Appraisals | 2 +- gemfiles/rails5_2.gemfile | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 gemfiles/rails5_2.gemfile diff --git a/Appraisals b/Appraisals index 6220edeb..987feadd 100644 --- a/Appraisals +++ b/Appraisals @@ -1,4 +1,4 @@ -rails_versions = ['~> 4.0.5', '~> 4.1.1', '~> 4.2.0', '~> 5.0.0', '~> 5.1.0'] +rails_versions = ['~> 4.0.5', '~> 4.1.1', '~> 4.2.0', '~> 5.0.0', '~> 5.1.0', '~> 5.2.0'] rails_versions.each do |rails_version| appraise "rails#{rails_version.slice(/\d+\.\d+/).gsub('.', '_')}" do diff --git a/gemfiles/rails5_2.gemfile b/gemfiles/rails5_2.gemfile new file mode 100644 index 00000000..0c9d5697 --- /dev/null +++ b/gemfiles/rails5_2.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 5.2.0" + +gemspec :path => "../" From cc3c5bc03d0447faff2bba491df09c519b30d293 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 13 Dec 2018 21:14:00 -0300 Subject: [PATCH 028/156] Test with Rails 5.2 in CI --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index b86cd4bf..906a7562 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,12 +18,15 @@ gemfile: - gemfiles/rails4_2.gemfile - gemfiles/rails5_0.gemfile - gemfiles/rails5_1.gemfile + - gemfiles/rails5_2.gemfile matrix: exclude: - rvm: 2.1.10 gemfile: gemfiles/rails5_0.gemfile - rvm: 2.1.10 gemfile: gemfiles/rails5_1.gemfile + - rvm: 2.1.10 + gemfile: gemfiles/rails5_2.gemfile # rails <=4.1 segfaults with ruby 2.4+ - rvm: 2.4.3 gemfile: gemfiles/rails4_0.gemfile From 8a6d3dd8d8a133ec4e05d01f485873b4c6283e3b Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 9 Dec 2018 00:42:41 -0300 Subject: [PATCH 029/156] Fix Rails secret_token deprecation warning DEPRECATION WARNING: `secrets.secret_token` is deprecated in favor of `secret_key_base` and will be removed in Rails 6.0. (called from at /Users/emiliocristalli/workspace/tmp/exception_notification/test/dummy/config/environment.rb:17) --- test/dummy/config/initializers/secret_token.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/dummy/config/initializers/secret_token.rb b/test/dummy/config/initializers/secret_token.rb index ff5c7bfc..1de52661 100644 --- a/test/dummy/config/initializers/secret_token.rb +++ b/test/dummy/config/initializers/secret_token.rb @@ -4,5 +4,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -Dummy::Application.config.secret_token = 'cfdf538142b0b383e722e8e7ea839b8ce6c3dc94a57856b343a2d13be66f5b690a55c991cec6e98ed60ea9b7e58265af23cb40cbadee02f13f1c45c2625f482b' Dummy::Application.config.secret_key_base = 'my new secret' From c3e25ad00602346cc9e74e62988d1ecf1b34e48b Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 9 Dec 2018 00:52:36 -0300 Subject: [PATCH 030/156] Fix Rails SQLite booleans deprecation warning DEPRECATION WARNING: Leaving `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer` set to false is deprecated. SQLite databases have used 't' and 'f' to serialize boolean values and must have old data converted to 1 and 0 (its native boolean serialization) before setting this flag to true. Conversion can be accomplished by setting up a rake task which runs ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0) for all models and all boolean columns, after which the flag must be set to true by adding the following to your application.rb file: Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true --- test/dummy/config/application.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index 989b0789..a766c4db 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -38,5 +38,8 @@ class Application < Rails::Application # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password, :secret] + + rails_version = Gem::Version.new(Rails.version) + config.active_record.sqlite3.represent_boolean_as_integer = true if rails_version >= Gem::Version.new("5.2.0") end end From c4887e0fe9688fd58ef39eed0413fabd7fe635e4 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 13 Dec 2018 23:17:20 -0300 Subject: [PATCH 031/156] Update Appraisal --- exception_notification.gemspec | 2 +- gemfiles/rails4_0.gemfile | 2 +- gemfiles/rails4_1.gemfile | 2 +- gemfiles/rails4_2.gemfile | 2 +- gemfiles/rails5_0.gemfile | 2 +- gemfiles/rails5_1.gemfile | 2 +- gemfiles/rails5_2.gemfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 5be11c3f..4b7bc8f3 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -28,7 +28,7 @@ Gem::Specification.new do |s| s.add_development_dependency "mocha", ">= 0.13.0" s.add_development_dependency "sqlite3", ">= 1.3.4" s.add_development_dependency "coveralls", "~> 0.8.2" - s.add_development_dependency "appraisal", "~> 2.0.0" + s.add_development_dependency "appraisal", "~> 2.2.0" s.add_development_dependency "hipchat", ">= 1.0.0" s.add_development_dependency "carrier-pigeon", ">= 0.7.0" s.add_development_dependency "slack-notifier", ">= 1.0.0" diff --git a/gemfiles/rails4_0.gemfile b/gemfiles/rails4_0.gemfile index 39a1bd6b..4f47847a 100644 --- a/gemfiles/rails4_0.gemfile +++ b/gemfiles/rails4_0.gemfile @@ -4,4 +4,4 @@ source "https://rubygems.org" gem "rails", "~> 4.0.5" -gemspec :path => "../" +gemspec path: "../" diff --git a/gemfiles/rails4_1.gemfile b/gemfiles/rails4_1.gemfile index 8dfe1fe3..7e4bd92e 100644 --- a/gemfiles/rails4_1.gemfile +++ b/gemfiles/rails4_1.gemfile @@ -4,4 +4,4 @@ source "https://rubygems.org" gem "rails", "~> 4.1.1" -gemspec :path => "../" +gemspec path: "../" diff --git a/gemfiles/rails4_2.gemfile b/gemfiles/rails4_2.gemfile index cd8b45b1..6977eb02 100644 --- a/gemfiles/rails4_2.gemfile +++ b/gemfiles/rails4_2.gemfile @@ -4,4 +4,4 @@ source "https://rubygems.org" gem "rails", "~> 4.2.0" -gemspec :path => "../" +gemspec path: "../" diff --git a/gemfiles/rails5_0.gemfile b/gemfiles/rails5_0.gemfile index 123ad559..10f52e7a 100644 --- a/gemfiles/rails5_0.gemfile +++ b/gemfiles/rails5_0.gemfile @@ -4,4 +4,4 @@ source "https://rubygems.org" gem "rails", "~> 5.0.0" -gemspec :path => "../" +gemspec path: "../" diff --git a/gemfiles/rails5_1.gemfile b/gemfiles/rails5_1.gemfile index 20a05ff9..6100e830 100644 --- a/gemfiles/rails5_1.gemfile +++ b/gemfiles/rails5_1.gemfile @@ -4,4 +4,4 @@ source "https://rubygems.org" gem "rails", "~> 5.1.0" -gemspec :path => "../" +gemspec path: "../" diff --git a/gemfiles/rails5_2.gemfile b/gemfiles/rails5_2.gemfile index 0c9d5697..5a706dcb 100644 --- a/gemfiles/rails5_2.gemfile +++ b/gemfiles/rails5_2.gemfile @@ -4,4 +4,4 @@ source "https://rubygems.org" gem "rails", "~> 5.2.0" -gemspec :path => "../" +gemspec path: "../" From 61a8ef579b9f6c8ff01a8eb115851413bb6030a6 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 17 Dec 2018 22:42:04 -0300 Subject: [PATCH 032/156] Use latest timecop version --- exception_notification.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index ada87ed2..9bf5d677 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -34,5 +34,5 @@ Gem::Specification.new do |s| s.add_development_dependency "slack-notifier", ">= 1.0.0" s.add_development_dependency "aws-sdk-sns", "~> 1" s.add_development_dependency "dogapi", ">= 1.23.0" - s.add_development_dependency "timecop", "~>0.8.0" + s.add_development_dependency "timecop", "~>0.9.0" end From a06ae228b8f07f54d9968ce013164d9fc9613bee Mon Sep 17 00:00:00 2001 From: Matias Verdier Date: Tue, 18 Dec 2018 18:46:10 -0300 Subject: [PATCH 033/156] Create issue_template.md --- .github/issue_template.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/issue_template.md diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 00000000..c57d7f99 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,14 @@ +### Steps to reproduce + +### Expected behavior + + +### Actual behavior + + +### System configuration +**Rails version**: + +**Ruby version**: + +**Other configurations**: From 0b9d56deb3d4aff75aca40cb53dee872aa908101 Mon Sep 17 00:00:00 2001 From: Ana Date: Tue, 18 Dec 2018 18:41:41 -0300 Subject: [PATCH 034/156] Fix 'ActionMailer configured' link in README 'ActionMailer configured' links to #actionmailer-configuration but leads nowhere. Added link to action mailer configuration: https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b644d2e..efe9def7 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Rails.application.config.middleware.use ExceptionNotification::Rack, } ``` -**Note**: In order to enable delivery notifications by email make sure you have [ActionMailer configured](#actionmailer-configuration). +**Note**: In order to enable delivery notifications by email make sure you have [ActionMailer configured](docs/notifiers/email.md#actionmailer-configuration). ### Rack/Sinatra From 02db3103de11d349b62e45a042004fbf4d2a9e1c Mon Sep 17 00:00:00 2001 From: Matias Verdier Date: Tue, 18 Dec 2018 22:34:19 -0300 Subject: [PATCH 035/156] Fix datadog notifier appends previous errors --- lib/exception_notifier/datadog_notifier.rb | 3 ++- test/exception_notifier/datadog_notifier_test.rb | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/exception_notifier/datadog_notifier.rb b/lib/exception_notifier/datadog_notifier.rb index cdc85eb6..950a1e55 100644 --- a/lib/exception_notifier/datadog_notifier.rb +++ b/lib/exception_notifier/datadog_notifier.rb @@ -76,7 +76,8 @@ def event end def formatted_title - title = title_prefix + title = "" + title << title_prefix title << "#{controller.controller_name} #{controller.action_name}" if controller title << " (#{exception.class})" title << " #{exception.message.inspect}" diff --git a/test/exception_notifier/datadog_notifier_test.rb b/test/exception_notifier/datadog_notifier_test.rb index 989c00c0..cb920649 100644 --- a/test/exception_notifier/datadog_notifier_test.rb +++ b/test/exception_notifier/datadog_notifier_test.rb @@ -27,6 +27,20 @@ def setup assert_includes event.msg_title, "FakeException" end + test "should include prefix in event title and not append previous events" do + options = { + client: @client, + title_prefix: "prefix" + } + + notifier = ExceptionNotifier::DatadogNotifier.new(options) + event = notifier.datadog_event(@exception) + assert_equal event.msg_title, "prefix (DatadogNotifierTest::FakeException) \"Fake exception message\"" + + event2 = notifier.datadog_event(@exception) + assert_equal event2.msg_title, "prefix (DatadogNotifierTest::FakeException) \"Fake exception message\"" + end + test "should include exception message in event title" do event = @notifier.datadog_event(@exception) assert_includes event.msg_title, "Fake exception message" From 55da2d58864166c9494771c83165b626acdd911e Mon Sep 17 00:00:00 2001 From: Victoria Madrid Date: Wed, 19 Dec 2018 19:03:55 -0300 Subject: [PATCH 036/156] Refactor call method from Slack notifier --- lib/exception_notifier/slack_notifier.rb | 92 ++++++++++++++---------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/lib/exception_notifier/slack_notifier.rb b/lib/exception_notifier/slack_notifier.rb index 6f6447e2..f3fbeefa 100644 --- a/lib/exception_notifier/slack_notifier.rb +++ b/lib/exception_notifier/slack_notifier.rb @@ -21,46 +21,12 @@ def initialize(options) end def call(exception, options={}) - errors_count = options[:accumulated_errors_count].to_i - measure_word = errors_count > 1 ? errors_count : (exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A') - exception_name = "*#{measure_word}* `#{exception.class.to_s}`" - - if options[:env].nil? - data = options[:data] || {} - text = "#{exception_name} *occured in background*\n" - else - env = options[:env] - data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {}) - - kontroller = env['action_controller.instance'] - request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>" - text = "#{exception_name} *occurred while* `#{request}`" - text += " *was processed by* `#{kontroller.controller_name}##{kontroller.action_name}`" if kontroller - text += "\n" - end - clean_message = exception.message.gsub("`", "'") - fields = [ { title: 'Exception', value: clean_message } ] - - fields.push({ title: 'Hostname', value: Socket.gethostname }) - - if exception.backtrace - formatted_backtrace = "```#{exception.backtrace.first(@backtrace_lines).join("\n")}```" - fields.push({ title: 'Backtrace', value: formatted_backtrace }) - end - - unless data.empty? - deep_reject(data, @ignore_data_if) if @ignore_data_if.is_a?(Proc) - data_string = data.map{|k,v| "#{k}: #{v}"}.join("\n") - fields.push({ title: 'Data', value: "```#{data_string}```" }) - end - - fields.concat(@additional_fields) if @additional_fields - - attchs = [color: @color, text: text, fields: fields, mrkdwn_in: %w(text fields)] + attchs = attchs(exception, clean_message, options) if valid? - send_notice(exception, options, clean_message, @message_opts.merge(attachments: attchs)) do |msg, message_opts| + args = [exception, options, clean_message, @message_opts.merge(attachments: attchs)] + send_notice(*args) do |msg, message_opts| @notifier.ping '', message_opts end end @@ -84,5 +50,57 @@ def deep_reject(hash, block) end end + private + + def attchs(exception, clean_message, options) + text, data = information_from_options(exception.class, options) + fields = fields(clean_message, exception.backtrace, data) + + [color: @color, text: text, fields: fields, mrkdwn_in: %w(text fields)] + end + + def information_from_options(exception_class, options) + errors_count = options[:accumulated_errors_count].to_i + measure_word = errors_count > 1 ? errors_count : (exception_class.to_s =~ /^[aeiou]/i ? 'An' : 'A') + exception_name = "*#{measure_word}* `#{exception_class.to_s}`" + env = options[:env] + + if env.nil? + data = options[:data] || {} + text = "#{exception_name} *occured in background*\n" + else + data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {}) + + kontroller = env['action_controller.instance'] + request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>" + text = "#{exception_name} *occurred while* `#{request}`" + text += " *was processed by* `#{kontroller.controller_name}##{kontroller.action_name}`" if kontroller + text += "\n" + end + + [text, data] + end + + def fields(clean_message, backtrace, data) + fields = [ + { title: 'Exception', value: clean_message }, + { title: 'Hostname', value: Socket.gethostname } + ] + + if backtrace + formatted_backtrace = "```#{backtrace.first(@backtrace_lines).join("\n")}```" + fields << { title: 'Backtrace', value: formatted_backtrace } + end + + unless data.empty? + deep_reject(data, @ignore_data_if) if @ignore_data_if.is_a?(Proc) + data_string = data.map{|k,v| "#{k}: #{v}"}.join("\n") + fields << { title: 'Data', value: "```#{data_string}```" } + end + + fields.concat(@additional_fields) if @additional_fields + + fields + end end end From 4dc12df4f0b8a7120fab7ab963f9d9a703fe2ecd Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 29 Dec 2018 23:15:31 -0300 Subject: [PATCH 037/156] Bump resque version Resque 1.2 didn't have support for the Resque::Failure::Multiple failure backend, which is the class used by the ExceptionNotification generator (see https://github.com/smartinez87/exception_notification/blob/b70bc736f4df98d61f494f541dc575817a6b8eda/lib/generators/exception_notification/templates/exception_notification.rb#L11). Looks like Resque 1.4 was the first version to support it (see https://github.com/resque/resque/commit/3dfe1f55f4edca2ce840fbc1a56c8e54813e6fcb) But Resque 1.4 uses `Redis#incr(key, increment)`, which was removed from redis-rb in version 4.0.2 (see https://github.com/redis/redis-rb/commit/e3a0c35606d966609cfca2284468eecc11426d3e) Decided to use Resque 1.8 instead (the first version to use `Redis#incrby(key, increment)`, see https://github.com/resque/resque/commit/3b3281f1fada3b8e25f9dfa955d22e2b446e984a), since it will also allow us to use mock_redis, making it easier to test. --- exception_notification.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index bb179f82..e0efef31 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.add_dependency("activesupport", ">= 4.0", "< 6") s.add_development_dependency "rails", ">= 4.0", "< 6" - s.add_development_dependency "resque", "~> 1.2.0" + s.add_development_dependency "resque", "~> 1.8.0" # Sidekiq 3.2.2 does not support Ruby 1.9. s.add_development_dependency "sidekiq", "~> 3.0.0", "< 3.2.2" s.add_development_dependency "tinder", "~> 1.8" From dcfb2182204e8f7d7dd50a635a21512c19a10c7a Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 00:47:42 -0300 Subject: [PATCH 038/156] Add test for Resque failure backend --- exception_notification.gemspec | 1 + test/exception_notification/resque_test.rb | 46 ++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 test/exception_notification/resque_test.rb diff --git a/exception_notification.gemspec b/exception_notification.gemspec index e0efef31..343d5646 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -35,4 +35,5 @@ Gem::Specification.new do |s| s.add_development_dependency "aws-sdk-sns", "~> 1" s.add_development_dependency "dogapi", ">= 1.23.0" s.add_development_dependency "timecop", "~>0.9.0" + s.add_development_dependency "mock_redis", "~> 0.19.0" end diff --git a/test/exception_notification/resque_test.rb b/test/exception_notification/resque_test.rb new file mode 100644 index 00000000..45cbaccb --- /dev/null +++ b/test/exception_notification/resque_test.rb @@ -0,0 +1,46 @@ +require "test_helper" + +require "exception_notification/resque" +require "resque" +require "mock_redis" +require 'resque/failure/multiple' +require 'resque/failure/redis' + +class ResqueTest < ActiveSupport::TestCase + setup do + # Resque.redis=() only supports a String or Redis instance in Resque 1.8 + Resque.instance_variable_set(:@redis, MockRedis.new) + + Resque::Failure::Multiple.classes = [Resque::Failure::Redis, ExceptionNotification::Resque] + Resque::Failure.backend = Resque::Failure::Multiple + + @worker = Resque::Worker.new(:jobs) + # Forking causes issues with Mocha's `.expects` + @worker.cant_fork = true + end + + test "notifies exception when job fails" do + ExceptionNotifier.expects(:notify_exception).with() do |ex, opts| + RuntimeError === ex && + ex.message == "Bad job!" && + opts[:data][:resque][:error_class] == "RuntimeError" && + opts[:data][:resque][:error_message] == "Bad job!" && + opts[:data][:resque][:failed_at].present? && + opts[:data][:resque][:payload] == { + "class" => "ResqueTest::BadJob", + "args" => [] + } && + opts[:data][:resque][:queue] == :jobs && + opts[:data][:resque][:worker].present? + end + + Resque::Job.create(:jobs, BadJob) + @worker.work(0) + end + + class BadJob + def self.perform + raise "Bad job!" + end + end +end From 23f7ed7a11cf73bbd0d1c2e5f4fcd472f8976991 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 00:48:38 -0300 Subject: [PATCH 039/156] Add missing comma --- lib/exception_notification/resque.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/exception_notification/resque.rb b/lib/exception_notification/resque.rb index cdc415d7..63f954da 100644 --- a/lib/exception_notification/resque.rb +++ b/lib/exception_notification/resque.rb @@ -9,7 +9,7 @@ def self.count def save data = { error_class: exception.class.name, - error_message: exception.message + error_message: exception.message, failed_at: Time.now.to_s, payload: payload, queue: queue, From 0be0e0ebef227c7f68f8117988822092b563a3cb Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 00:49:53 -0300 Subject: [PATCH 040/156] Fix count method --- lib/exception_notification/resque.rb | 2 +- test/exception_notification/resque_test.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/exception_notification/resque.rb b/lib/exception_notification/resque.rb index 63f954da..40b9743b 100644 --- a/lib/exception_notification/resque.rb +++ b/lib/exception_notification/resque.rb @@ -3,7 +3,7 @@ module ExceptionNotification class Resque < Resque::Failure::Base def self.count - Stat[:failed] + ::Resque::Stat[:failed] end def save diff --git a/test/exception_notification/resque_test.rb b/test/exception_notification/resque_test.rb index 45cbaccb..db061e7e 100644 --- a/test/exception_notification/resque_test.rb +++ b/test/exception_notification/resque_test.rb @@ -19,6 +19,12 @@ class ResqueTest < ActiveSupport::TestCase @worker.cant_fork = true end + test "count returns the number of failures" do + Resque::Job.create(:jobs, BadJob) + @worker.work(0) + assert_equal 1, ExceptionNotification::Resque.count + end + test "notifies exception when job fails" do ExceptionNotifier.expects(:notify_exception).with() do |ex, opts| RuntimeError === ex && From 8ae7feb366e0782901358a1a4bafdd976c9c54e2 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 17:28:04 -0300 Subject: [PATCH 041/156] Use mock_redis 0.18.0 to support Ruby 2.0 --- exception_notification.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 343d5646..75cf3a87 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -35,5 +35,5 @@ Gem::Specification.new do |s| s.add_development_dependency "aws-sdk-sns", "~> 1" s.add_development_dependency "dogapi", ">= 1.23.0" s.add_development_dependency "timecop", "~>0.9.0" - s.add_development_dependency "mock_redis", "~> 0.19.0" + s.add_development_dependency "mock_redis", "~> 0.18.0" end From 38cdcac9610e9f7936e61480b62a45c0e5047b04 Mon Sep 17 00:00:00 2001 From: Fabian Larranaga Date: Wed, 23 Jan 2019 21:29:50 -0300 Subject: [PATCH 042/156] Fix bundler version < 2 --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 906a7562..4bac6809 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,11 @@ rvm: - 2.5.0 env: - COVERALLS_SILENT=true +before_install: + - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true + - gem install bundler -v '< 2' + install: - - "gem install bundler" - "bundle install --jobs=3 --retry=3" - "mkdir -p test/dummy/tmp/cache" - "mkdir -p test/dummy/tmp/non_default_location" From 39aa9b6fbc6795b2c70a58ffb0843724c547f568 Mon Sep 17 00:00:00 2001 From: Fabian Larranaga Date: Tue, 18 Dec 2018 20:02:26 -0300 Subject: [PATCH 043/156] Fix several rubocop offenses --- Appraisals | 2 +- Gemfile | 2 +- examples/sinatra/Gemfile | 12 +- examples/sinatra/config.ru | 2 +- examples/sinatra/sinatra_app.rb | 19 +- exception_notification.gemspec | 48 +++--- gemfiles/rails4_0.gemfile | 6 +- gemfiles/rails4_1.gemfile | 6 +- gemfiles/rails4_2.gemfile | 6 +- gemfiles/rails5_0.gemfile | 6 +- gemfiles/rails5_1.gemfile | 6 +- gemfiles/rails5_2.gemfile | 6 +- lib/exception_notification/rack.rb | 11 +- lib/exception_notification/sidekiq.rb | 17 +- lib/exception_notifier.rb | 8 +- lib/exception_notifier/base_notifier.rb | 4 +- lib/exception_notifier/campfire_notifier.rb | 11 +- lib/exception_notifier/datadog_notifier.rb | 59 +++---- lib/exception_notifier/email_notifier.rb | 62 +++---- .../google_chat_notifier.rb | 19 +- lib/exception_notifier/hipchat_notifier.rb | 21 ++- lib/exception_notifier/irc_notifier.rb | 53 +++--- lib/exception_notifier/mattermost_notifier.rb | 162 +++++++++--------- .../modules/error_grouping.rb | 6 +- lib/exception_notifier/notifier.rb | 9 +- lib/exception_notifier/slack_notifier.rb | 10 +- lib/exception_notifier/sns_notifier.rb | 14 +- lib/exception_notifier/teams_notifier.rb | 85 +++++---- lib/exception_notifier/webhook_notifier.rb | 5 +- .../install_generator.rb | 2 +- test/dummy/Rakefile | 2 +- test/dummy/config.ru | 2 +- test/dummy/config/application.rb | 8 +- test/dummy/config/boot.rb | 2 +- test/dummy/config/environment.rb | 14 +- test/dummy/config/environments/production.rb | 2 +- test/dummy/config/environments/test.rb | 2 +- test/dummy/config/routes.rb | 2 +- test/dummy/db/schema.rb | 17 +- test/dummy/script/rails | 4 +- .../test/functional/posts_controller_test.rb | 130 +++++++------- test/dummy/test/test_helper.rb | 4 +- test/exception_notification/rack_test.rb | 15 +- .../campfire_notifier_test.rb | 66 ++++--- .../datadog_notifier_test.rb | 96 +++++------ .../exception_notifier/email_notifier_test.rb | 142 +++++++-------- .../google_chat_notifier_test.rb | 24 +-- .../hipchat_notifier_test.rb | 94 +++++----- test/exception_notifier/irc_notifier_test.rb | 48 +++--- .../mattermost_notifier_test.rb | 34 ++-- .../modules/error_grouping_test.rb | 77 ++++----- test/exception_notifier/sidekiq_test.rb | 10 +- .../exception_notifier/slack_notifier_test.rb | 114 ++++++------ test/exception_notifier/sns_notifier_test.rb | 53 +++--- .../exception_notifier/teams_notifier_test.rb | 33 ++-- .../webhook_notifier_test.rb | 41 +++-- test/exception_notifier_test.rb | 74 ++++---- test/test_helper.rb | 14 +- 58 files changed, 880 insertions(+), 923 deletions(-) diff --git a/Appraisals b/Appraisals index 987feadd..24a06a5b 100644 --- a/Appraisals +++ b/Appraisals @@ -1,7 +1,7 @@ rails_versions = ['~> 4.0.5', '~> 4.1.1', '~> 4.2.0', '~> 5.0.0', '~> 5.1.0', '~> 5.2.0'] rails_versions.each do |rails_version| - appraise "rails#{rails_version.slice(/\d+\.\d+/).gsub('.', '_')}" do + appraise "rails#{rails_version.slice(/\d+\.\d+/).tr('.', '_')}" do gem 'rails', rails_version end end diff --git a/Gemfile b/Gemfile index b4e2a20b..fa75df15 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ -source "https://rubygems.org" +source 'https://rubygems.org' gemspec diff --git a/examples/sinatra/Gemfile b/examples/sinatra/Gemfile index b03b5372..d6c9f377 100644 --- a/examples/sinatra/Gemfile +++ b/examples/sinatra/Gemfile @@ -1,8 +1,8 @@ -source "https://rubygems.org" +source 'https://rubygems.org' -gem "exception_notification", path: "../../" +gem 'exception_notification', path: '../../' -gem "thin", "~> 1.5.1" -gem "sinatra", "~> 1.3.5" -gem "foreman" -gem "mailcatcher" +gem 'foreman' +gem 'mailcatcher' +gem 'sinatra', '~> 1.3.5' +gem 'thin', '~> 1.5.1' diff --git a/examples/sinatra/config.ru b/examples/sinatra/config.ru index 1b41b714..9b26ef06 100644 --- a/examples/sinatra/config.ru +++ b/examples/sinatra/config.ru @@ -1,3 +1,3 @@ -require ::File.expand_path('../sinatra_app', __FILE__) +require ::File.expand_path('../sinatra_app', __FILE__) run SinatraApp diff --git a/examples/sinatra/sinatra_app.rb b/examples/sinatra/sinatra_app.rb index fb1ba164..65d31bbf 100644 --- a/examples/sinatra/sinatra_app.rb +++ b/examples/sinatra/sinatra_app.rb @@ -9,24 +9,25 @@ class SinatraApp < Sinatra::Base end use ExceptionNotification::Rack, - email: { - email_prefix: '[Example] ', - sender_address: %{"notifier" }, - exception_recipients: %w{exceptions@example.com}, - smtp_settings: { - address: 'localhost', - port: 1025 + email: { + email_prefix: '[Example] ', + sender_address: %("notifier" ), + exception_recipients: %w[exceptions@example.com], + smtp_settings: { + address: 'localhost', + port: 1025 + } } - } get '/' do raise StandardError, "ERROR: #{params[:error]}" unless params[:error].blank? + 'Everything is fine! Now, lets break things clicking here . Dont forget to see the emails at mailcatcher !' end get '/background_notification' do begin - 1/0 + 1 / 0 rescue Exception => e ExceptionNotifier.notify_exception(e, data: { msg: 'Cannot divide by zero!' }) end diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 75cf3a87..154eeb03 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -1,12 +1,12 @@ Gem::Specification.new do |s| s.name = 'exception_notification' s.version = '4.3.0' - s.authors = ["Jamis Buck", "Josh Peek"] - s.date = %q{2018-11-22} - s.summary = "Exception notification for Rails apps" - s.homepage = "https://smartinez87.github.io/exception_notification/" - s.email = "smartinez87@gmail.com" - s.license = "MIT" + s.authors = ['Jamis Buck', 'Josh Peek'] + s.date = '2018-11-22' + s.summary = 'Exception notification for Rails apps' + s.homepage = 'https://smartinez87.github.io/exception_notification/' + s.email = 'smartinez87@gmail.com' + s.license = 'MIT' s.required_ruby_version = '>= 2.0' s.required_rubygems_version = '>= 1.8.11' @@ -16,24 +16,24 @@ Gem::Specification.new do |s| s.test_files = `git ls-files -- test`.split("\n") s.require_path = 'lib' - s.add_dependency("actionmailer", ">= 4.0", "< 6") - s.add_dependency("activesupport", ">= 4.0", "< 6") + s.add_dependency('actionmailer', '>= 4.0', '< 6') + s.add_dependency('activesupport', '>= 4.0', '< 6') - s.add_development_dependency "rails", ">= 4.0", "< 6" - s.add_development_dependency "resque", "~> 1.8.0" + s.add_development_dependency 'rails', '>= 4.0', '< 6' + s.add_development_dependency 'resque', '~> 1.8.0' # Sidekiq 3.2.2 does not support Ruby 1.9. - s.add_development_dependency "sidekiq", "~> 3.0.0", "< 3.2.2" - s.add_development_dependency "tinder", "~> 1.8" - s.add_development_dependency "httparty", "~> 0.10.2" - s.add_development_dependency "mocha", ">= 0.13.0" - s.add_development_dependency "sqlite3", ">= 1.3.4" - s.add_development_dependency "coveralls", "~> 0.8.2" - s.add_development_dependency "appraisal", "~> 2.2.0" - s.add_development_dependency "hipchat", ">= 1.0.0" - s.add_development_dependency "carrier-pigeon", ">= 0.7.0" - s.add_development_dependency "slack-notifier", ">= 1.0.0" - s.add_development_dependency "aws-sdk-sns", "~> 1" - s.add_development_dependency "dogapi", ">= 1.23.0" - s.add_development_dependency "timecop", "~>0.9.0" - s.add_development_dependency "mock_redis", "~> 0.18.0" + s.add_development_dependency 'appraisal', '~> 2.2.0' + s.add_development_dependency 'aws-sdk-sns', '~> 1' + s.add_development_dependency 'carrier-pigeon', '>= 0.7.0' + s.add_development_dependency 'coveralls', '~> 0.8.2' + s.add_development_dependency 'dogapi', '>= 1.23.0' + s.add_development_dependency 'hipchat', '>= 1.0.0' + s.add_development_dependency 'httparty', '~> 0.10.2' + s.add_development_dependency 'mock_redis', '~> 0.18.0' + s.add_development_dependency 'mocha', '>= 0.13.0' + s.add_development_dependency 'sidekiq', '~> 3.0.0', '< 3.2.2' + s.add_development_dependency 'slack-notifier', '>= 1.0.0' + s.add_development_dependency 'sqlite3', '>= 1.3.4' + s.add_development_dependency 'timecop', '~>0.9.0' + s.add_development_dependency 'tinder', '~> 1.8' end diff --git a/gemfiles/rails4_0.gemfile b/gemfiles/rails4_0.gemfile index 4f47847a..7117a086 100644 --- a/gemfiles/rails4_0.gemfile +++ b/gemfiles/rails4_0.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "rails", "~> 4.0.5" +gem 'rails', '~> 4.0.5' -gemspec path: "../" +gemspec path: '../' diff --git a/gemfiles/rails4_1.gemfile b/gemfiles/rails4_1.gemfile index 7e4bd92e..213be9c6 100644 --- a/gemfiles/rails4_1.gemfile +++ b/gemfiles/rails4_1.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "rails", "~> 4.1.1" +gem 'rails', '~> 4.1.1' -gemspec path: "../" +gemspec path: '../' diff --git a/gemfiles/rails4_2.gemfile b/gemfiles/rails4_2.gemfile index 6977eb02..dbed7dd7 100644 --- a/gemfiles/rails4_2.gemfile +++ b/gemfiles/rails4_2.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "rails", "~> 4.2.0" +gem 'rails', '~> 4.2.0' -gemspec path: "../" +gemspec path: '../' diff --git a/gemfiles/rails5_0.gemfile b/gemfiles/rails5_0.gemfile index 10f52e7a..49df9648 100644 --- a/gemfiles/rails5_0.gemfile +++ b/gemfiles/rails5_0.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "rails", "~> 5.0.0" +gem 'rails', '~> 5.0.0' -gemspec path: "../" +gemspec path: '../' diff --git a/gemfiles/rails5_1.gemfile b/gemfiles/rails5_1.gemfile index 6100e830..953d45c7 100644 --- a/gemfiles/rails5_1.gemfile +++ b/gemfiles/rails5_1.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "rails", "~> 5.1.0" +gem 'rails', '~> 5.1.0' -gemspec path: "../" +gemspec path: '../' diff --git a/gemfiles/rails5_2.gemfile b/gemfiles/rails5_2.gemfile index 5a706dcb..0b87fd88 100644 --- a/gemfiles/rails5_2.gemfile +++ b/gemfiles/rails5_2.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source "https://rubygems.org" +source 'https://rubygems.org' -gem "rails", "~> 5.2.0" +gem 'rails', '~> 5.2.0' -gemspec path: "../" +gemspec path: '../' diff --git a/lib/exception_notification/rack.rb b/lib/exception_notification/rack.rb index 2706b103..e8eb4fd6 100644 --- a/lib/exception_notification/rack.rb +++ b/lib/exception_notification/rack.rb @@ -1,6 +1,6 @@ module ExceptionNotification class Rack - class CascadePassException < Exception; end + class CascadePassException < RuntimeError; end def initialize(app, options = {}) @app = app @@ -25,7 +25,7 @@ def initialize(app, options = {}) if options.key?(:ignore_crawlers) ignore_crawlers = options.delete(:ignore_crawlers) - ExceptionNotifier.ignore_if do |exception, opts| + ExceptionNotifier.ignore_if do |_exception, opts| opts.key?(:env) && from_crawler(opts[:env], ignore_crawlers) end end @@ -38,11 +38,11 @@ def initialize(app, options = {}) end def call(env) - _, headers, _ = response = @app.call(env) + _, headers, = response = @app.call(env) if !@ignore_cascade_pass && headers['X-Cascade'] == 'pass' - msg = "This exception means that the preceding Rack middleware set the 'X-Cascade' header to 'pass' -- in " << - "Rails, this often means that the route was not found (404 error)." + msg = "This exception means that the preceding Rack middleware set the 'X-Cascade' header to 'pass' -- in " \ + 'Rails, this often means that the route was not found (404 error).' raise CascadePassException, msg end @@ -53,6 +53,7 @@ def call(env) end raise exception unless exception.is_a?(CascadePassException) + response end diff --git a/lib/exception_notification/sidekiq.rb b/lib/exception_notification/sidekiq.rb index 245403c5..b01e8360 100644 --- a/lib/exception_notification/sidekiq.rb +++ b/lib/exception_notification/sidekiq.rb @@ -3,15 +3,12 @@ # Note: this class is only needed for Sidekiq version < 3. module ExceptionNotification class Sidekiq - def call(worker, msg, queue) - begin - yield - rescue Exception => exception - ExceptionNotifier.notify_exception(exception, data: { sidekiq: msg }) - raise exception - end + def call(_worker, msg, _queue) + yield + rescue Exception => exception + ExceptionNotifier.notify_exception(exception, data: { sidekiq: msg }) + raise exception end - end end @@ -23,8 +20,8 @@ def call(worker, msg, queue) end else ::Sidekiq.configure_server do |config| - config.error_handlers << Proc.new { |ex, context| + config.error_handlers << proc do |ex, context| ExceptionNotifier.notify_exception(ex, data: { sidekiq: context }) - } + end end end diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index 8c85e011..d4f2aaae 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -30,7 +30,7 @@ class UndefinedNotifierError < StandardError; end # Define a set of exceptions to be ignored, ie, dont send notifications when any of them are raised. mattr_accessor :ignored_exceptions - @@ignored_exceptions = %w{ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError} + @@ignored_exceptions = %w[ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError] mattr_accessor :testing_mode @@testing_mode = false @@ -46,9 +46,10 @@ def testing_mode! self.testing_mode = true end - def notify_exception(exception, options={}, &block) + def notify_exception(exception, options = {}, &block) return false if ignored_exception?(options[:ignore_exceptions], exception) return false if ignored?(exception, options) + if error_grouping errors_count = group_error!(exception, options) return false unless send_notification?(exception, errors_count) @@ -98,8 +99,9 @@ def clear_ignore_conditions! end private + def ignored?(exception, options) - @@ignores.any?{ |condition| condition.call(exception, options) } + @@ignores.any? { |condition| condition.call(exception, options) } rescue Exception => e raise e if @@testing_mode diff --git a/lib/exception_notifier/base_notifier.rb b/lib/exception_notifier/base_notifier.rb index 7b5978b1..1276ada0 100644 --- a/lib/exception_notifier/base_notifier.rb +++ b/lib/exception_notifier/base_notifier.rb @@ -2,11 +2,11 @@ module ExceptionNotifier class BaseNotifier attr_accessor :base_options - def initialize(options={}) + def initialize(options = {}) @base_options = options end - def send_notice(exception, options, message, message_opts=nil) + def send_notice(exception, options, message, message_opts = nil) _pre_callback(exception, options, message, message_opts) result = yield(message, message_opts) _post_callback(exception, options, message, message_opts) diff --git a/lib/exception_notifier/campfire_notifier.rb b/lib/exception_notifier/campfire_notifier.rb index 27b3ccf1..bf7c743f 100644 --- a/lib/exception_notifier/campfire_notifier.rb +++ b/lib/exception_notifier/campfire_notifier.rb @@ -1,6 +1,5 @@ module ExceptionNotifier class CampfireNotifier < BaseNotifier - attr_accessor :subdomain attr_accessor :token attr_accessor :room @@ -12,17 +11,17 @@ def initialize(options) room_name = options.delete(:room_name) @campfire = Tinder::Campfire.new subdomain, options @room = @campfire.find_room_by_name room_name - rescue + rescue StandardError @campfire = @room = nil end end - def call(exception, options={}) + def call(exception, options = {}) if active? message = if options[:accumulated_errors_count].to_i > 1 - "The exception occurred #{options[:accumulated_errors_count]} times: '#{exception.message}'" - else - "A new exception occurred: '#{exception.message}'" + "The exception occurred #{options[:accumulated_errors_count]} times: '#{exception.message}'" + else + "A new exception occurred: '#{exception.message}'" end message += " on '#{exception.backtrace.first}'" if exception.backtrace send_notice(exception, options, message) do |msg, _| diff --git a/lib/exception_notifier/datadog_notifier.rb b/lib/exception_notifier/datadog_notifier.rb index 950a1e55..1697474d 100644 --- a/lib/exception_notifier/datadog_notifier.rb +++ b/lib/exception_notifier/datadog_notifier.rb @@ -1,9 +1,7 @@ module ExceptionNotifier - class DatadogNotifier < BaseNotifier - attr_reader :client, - :default_options + :default_options def initialize(options) super @@ -32,10 +30,10 @@ class DatadogExceptionEvent MAX_TITLE_LENGTH = 120 MAX_VALUE_LENGTH = 300 MAX_BACKTRACE_SIZE = 3 - ALERT_TYPE = "error" + ALERT_TYPE = 'error'.freeze attr_reader :exception, - :options + :options def initialize(exception, options) @exception = exception @@ -59,7 +57,7 @@ def tags end def title_prefix - options[:title_prefix] || "" + options[:title_prefix] || '' end def event @@ -88,11 +86,11 @@ def formatted_title def formatted_body text = [] - text << "%%%" + text << '%%%' text << formatted_request if request text << formatted_session if request text << formatted_backtrace - text << "%%%" + text << '%%%' text.join("\n") end @@ -103,26 +101,26 @@ def formatted_key_value(key, value) def formatted_request text = [] - text << "### **Request**" - text << formatted_key_value("URL", request.url) - text << formatted_key_value("HTTP Method", request.request_method) - text << formatted_key_value("IP Address", request.remote_ip) - text << formatted_key_value("Parameters", request.filtered_parameters.inspect) - text << formatted_key_value("Timestamp", Time.current) - text << formatted_key_value("Server", Socket.gethostname) + text << '### **Request**' + text << formatted_key_value('URL', request.url) + text << formatted_key_value('HTTP Method', request.request_method) + text << formatted_key_value('IP Address', request.remote_ip) + text << formatted_key_value('Parameters', request.filtered_parameters.inspect) + text << formatted_key_value('Timestamp', Time.current) + text << formatted_key_value('Server', Socket.gethostname) if defined?(Rails) && Rails.respond_to?(:root) - text << formatted_key_value("Rails root", Rails.root) + text << formatted_key_value('Rails root', Rails.root) end - text << formatted_key_value("Process", $$) - text << "___" + text << formatted_key_value('Process', $PROCESS_ID) + text << '___' text.join("\n") end def formatted_session text = [] - text << "### **Session**" - text << formatted_key_value("Data", request.session.to_hash) - text << "___" + text << '### **Session**' + text << formatted_key_value('Data', request.session.to_hash) + text << '___' text.join("\n") end @@ -130,11 +128,11 @@ def formatted_backtrace size = [backtrace.size, MAX_BACKTRACE_SIZE].min text = [] - text << "### **Backtrace**" - text << "````" + text << '### **Backtrace**' + text << '````' size.times { |i| text << backtrace[i] } - text << "````" - text << "___" + text << '````' + text << '___' text.join("\n") end @@ -144,15 +142,12 @@ def truncate(string, max) def inspect_object(object) case object - when Hash, Array - truncate(object.inspect, MAX_VALUE_LENGTH) - else - object.to_s + when Hash, Array + truncate(object.inspect, MAX_VALUE_LENGTH) + else + object.to_s end end - end end - end - diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index 00f405a5..c4eaeac3 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/hash/reverse_merge" +require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/time' require 'action_mailer' require 'action_dispatch' @@ -7,25 +7,24 @@ module ExceptionNotifier class EmailNotifier < BaseNotifier attr_accessor(:sender_address, :exception_recipients, - :pre_callback, :post_callback, - :email_prefix, :email_format, :sections, :background_sections, - :verbose_subject, :normalize_subject, :include_controller_and_action_names_in_subject, - :delivery_method, :mailer_settings, :email_headers, :mailer_parent, :template_path, :deliver_with) + :pre_callback, :post_callback, + :email_prefix, :email_format, :sections, :background_sections, + :verbose_subject, :normalize_subject, :include_controller_and_action_names_in_subject, + :delivery_method, :mailer_settings, :email_headers, :mailer_parent, :template_path, :deliver_with) module Mailer class MissingController - def method_missing(*args, &block) - end + def method_missing(*args, &block); end end def self.extended(base) base.class_eval do - self.send(:include, ExceptionNotifier::BacktraceCleaner) + send(:include, ExceptionNotifier::BacktraceCleaner) # Append application view path to the ExceptionNotifier lookup context. - self.append_view_path "#{File.dirname(__FILE__)}/views" + append_view_path "#{File.dirname(__FILE__)}/views" - def exception_notification(env, exception, options={}, default_options={}) + def exception_notification(env, exception, options = {}, default_options = {}) load_custom_views @env = env @@ -37,12 +36,12 @@ def exception_notification(env, exception, options={}, default_options={}) @timestamp = Time.current @sections = @options[:sections] @data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {}) - @sections = @sections + %w(data) unless @data.empty? + @sections += %w[data] unless @data.empty? compose_email end - def background_exception_notification(exception, options={}, default_options={}) + def background_exception_notification(exception, options = {}, default_options = {}) load_custom_views @exception = exception @@ -65,7 +64,7 @@ def compose_subject subject << " (#{@exception.class})" subject << " #{@exception.message.inspect}" if @options[:verbose_subject] subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject] - subject.length > 120 ? subject[0...120] + "..." : subject + subject.length > 120 ? subject[0...120] + '...' : subject end def set_data_variables @@ -82,17 +81,17 @@ def truncate(string, max) def inspect_object(object) case object - when Hash, Array - truncate(object.inspect, 300) - else - object.to_s + when Hash, Array + truncate(object.inspect, 300) + else + object.to_s end end helper_method :safe_encode def safe_encode(value) - value.encode("utf-8", invalid: :replace, undef: :replace, replace: "_") + value.encode('utf-8', invalid: :replace, undef: :replace, replace: '_') end def html_mail? @@ -125,7 +124,7 @@ def compose_email def load_custom_views if defined?(Rails) && Rails.respond_to?(:root) - self.prepend_view_path Rails.root.nil? ? "app/views" : "#{Rails.root}/app/views" + prepend_view_path Rails.root.nil? ? 'app/views' : "#{Rails.root}/app/views" end end @@ -142,17 +141,20 @@ def initialize(options) mailer_settings_key = "#{delivery_method}_settings".to_sym options[:mailer_settings] = options.delete(mailer_settings_key) - options.reverse_merge(EmailNotifier.default_options).select{|k,v|[ - :sender_address, :exception_recipients, :pre_callback, - :post_callback, :email_prefix, :email_format, - :sections, :background_sections, :verbose_subject, :normalize_subject, - :include_controller_and_action_names_in_subject, :delivery_method, :mailer_settings, - :email_headers, :mailer_parent, :template_path, :deliver_with].include?(k)}.each{|k,v| send("#{k}=", v)} + options.reverse_merge(EmailNotifier.default_options).select do |k, _v| + %i[ + sender_address exception_recipients pre_callback + post_callback email_prefix email_format + sections background_sections verbose_subject normalize_subject + include_controller_and_action_names_in_subject delivery_method mailer_settings + email_headers mailer_parent template_path deliver_with + ].include?(k) + end .each { |k, v| send("#{k}=", v) } end def options @options ||= {}.tap do |opts| - self.instance_variables.each { |var| opts[var[1..-1].to_sym] = self.instance_variable_get(var) } + instance_variables.each { |var| opts[var[1..-1].to_sym] = instance_variable_get(var) } end end @@ -163,7 +165,7 @@ def mailer end end - def call(exception, options={}) + def call(exception, options = {}) message = create_email(exception, options) # FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')` @@ -178,7 +180,7 @@ def call(exception, options={}) end end - def create_email(exception, options={}) + def create_email(exception, options = {}) env = options[:env] default_options = self.options if env.nil? @@ -198,8 +200,8 @@ def self.default_options exception_recipients: [], email_prefix: '[ERROR] ', email_format: :text, - sections: %w(request session environment backtrace), - background_sections: %w(backtrace data), + sections: %w[request session environment backtrace], + background_sections: %w[backtrace data], verbose_subject: true, normalize_subject: false, include_controller_and_action_names_in_subject: true, diff --git a/lib/exception_notifier/google_chat_notifier.rb b/lib/exception_notifier/google_chat_notifier.rb index e8140e3c..6e79d8a3 100644 --- a/lib/exception_notifier/google_chat_notifier.rb +++ b/lib/exception_notifier/google_chat_notifier.rb @@ -24,7 +24,7 @@ def call(exception, opts = {}) def body text = [ header, - "", + '', "⚠️ Error 500 in #{Rails.env} ⚠️", "*#{exception.message.tr('`', "'")}*" ] @@ -46,18 +46,19 @@ def header def message_request return [] unless (env = options[:env]) + request = ActionDispatch::Request.new(env) [ - "", - "*Request:*", - "```", + '', + '*Request:*', + '```', "* url : #{request.original_url}", "* http_method : #{request.method}", "* ip_address : #{request.remote_ip}", "* parameters : #{request.filtered_parameters}", "* timestamp : #{Time.current}", - "```" + '```' ] end @@ -69,16 +70,16 @@ def message_backtrace text = [] text << '' - text << "*Backtrace:*" - text << "```" + text << '*Backtrace:*' + text << '```' backtrace.first(3).each { |line| text << "* #{line}" } - text << "```" + text << '```' text end def app_name - @app_name ||= options[:app_name] || rails_app_name || "N/A" + @app_name ||= options[:app_name] || rails_app_name || 'N/A' end def errors_count diff --git a/lib/exception_notifier/hipchat_notifier.rb b/lib/exception_notifier/hipchat_notifier.rb index 39d4a156..9d851037 100644 --- a/lib/exception_notifier/hipchat_notifier.rb +++ b/lib/exception_notifier/hipchat_notifier.rb @@ -1,6 +1,5 @@ module ExceptionNotifier class HipchatNotifier < BaseNotifier - attr_accessor :from attr_accessor :room attr_accessor :message_options @@ -11,29 +10,29 @@ def initialize(options) api_token = options.delete(:api_token) room_name = options.delete(:room_name) opts = { - api_version: options.delete(:api_version) || 'v1' - } + api_version: options.delete(:api_version) || 'v1' + } opts[:server_url] = options.delete(:server_url) if options[:server_url] @from = options.delete(:from) || 'Exception' @room = HipChat::Client.new(api_token, opts)[room_name] - @message_template = options.delete(:message_template) || ->(exception, errors_count) { + @message_template = options.delete(:message_template) || lambda { |exception, errors_count| msg = if errors_count > 1 - "The exception occurred #{errors_count} times: '#{Rack::Utils.escape_html(exception.message)}'" - else - "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}'" + "The exception occurred #{errors_count} times: '#{Rack::Utils.escape_html(exception.message)}'" + else + "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}'" end msg += " on '#{exception.backtrace.first}'" if exception.backtrace msg } - @message_options = options + @message_options = options @message_options[:color] ||= 'red' - rescue + rescue StandardError @room = nil end end - def call(exception, options={}) - return if !active? + def call(exception, options = {}) + return unless active? message = @message_template.call(exception, options[:accumulated_errors_count].to_i) send_notice(exception, options, message, @message_options) do |msg, message_opts| diff --git a/lib/exception_notifier/irc_notifier.rb b/lib/exception_notifier/irc_notifier.rb index 122c22be..990036be 100644 --- a/lib/exception_notifier/irc_notifier.rb +++ b/lib/exception_notifier/irc_notifier.rb @@ -6,7 +6,7 @@ def initialize(options) parse_options(options) end - def call(exception, options={}) + def call(exception, options = {}) errors_count = options[:accumulated_errors_count].to_i message = "'#{exception.message}'" @@ -21,35 +21,36 @@ def call(exception, options={}) end def send_message(message) - CarrierPigeon.send @config.irc.merge({message: message}) + CarrierPigeon.send @config.irc.merge(message: message) end private - def parse_options(options) - nick = options.fetch(:nick, 'ExceptionNotifierBot') - password = options[:password] ? ":#{options[:password]}" : nil - domain = options.fetch(:domain, nil) - port = options[:port] ? ":#{options[:port]}" : nil - channel = options.fetch(:channel, '#log') - notice = options.fetch(:notice, false) - ssl = options.fetch(:ssl, false) - join = options.fetch(:join, false) - uri = "irc://#{nick}#{password}@#{domain}#{port}/#{channel}" - prefix = options.fetch(:prefix, nil) - recipients = options[:recipients] ? options[:recipients].join(', ') + ':' : nil - - @config.prefix = [*prefix, *recipients].join(' ') - @config.irc = { uri: uri, ssl: ssl, notice: notice, join: join } - end - def active? - valid_uri? @config.irc[:uri] - end + def parse_options(options) + nick = options.fetch(:nick, 'ExceptionNotifierBot') + password = options[:password] ? ":#{options[:password]}" : nil + domain = options.fetch(:domain, nil) + port = options[:port] ? ":#{options[:port]}" : nil + channel = options.fetch(:channel, '#log') + notice = options.fetch(:notice, false) + ssl = options.fetch(:ssl, false) + join = options.fetch(:join, false) + uri = "irc://#{nick}#{password}@#{domain}#{port}/#{channel}" + prefix = options.fetch(:prefix, nil) + recipients = options[:recipients] ? options[:recipients].join(', ') + ':' : nil + + @config.prefix = [*prefix, *recipients].join(' ') + @config.irc = { uri: uri, ssl: ssl, notice: notice, join: join } + end - def valid_uri?(uri) - !!URI.parse(uri) - rescue URI::InvalidURIError - false - end + def active? + valid_uri? @config.irc[:uri] + end + + def valid_uri?(uri) + !!URI.parse(uri) + rescue URI::InvalidURIError + false + end end end diff --git a/lib/exception_notifier/mattermost_notifier.rb b/lib/exception_notifier/mattermost_notifier.rb index c8c85fe6..827b53b9 100644 --- a/lib/exception_notifier/mattermost_notifier.rb +++ b/lib/exception_notifier/mattermost_notifier.rb @@ -6,8 +6,7 @@ class MattermostNotifier include ExceptionNotifier::BacktraceCleaner class MissingController - def method_missing(*args, &block) - end + def method_missing(*args, &block); end end attr_accessor :httparty @@ -27,14 +26,16 @@ def call(exception, options = {}) @application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore @gitlab_url = @options.delete(:git_url) - @username = @options.delete(:username) || "Exception Notifier" + @username = @options.delete(:username) || 'Exception Notifier' @avatar = @options.delete(:avatar) @channel = @options.delete(:channel) @webhook_url = @options.delete(:webhook_url) - raise ArgumentError.new "You must provide 'webhook_url' parameter." unless @webhook_url + raise ArgumentError, "You must provide 'webhook_url' parameter." unless @webhook_url - unless @env.nil? + if @env.nil? + @controller = @request_items = nil + else @controller = @env['action_controller.instance'] || MissingController.new request = ActionDispatch::Request.new(@env) @@ -45,121 +46,118 @@ def call(exception, options = {}) parameters: request.filtered_parameters, timestamp: Time.current } - if request.session["warden.user.user.key"] - current_user = User.find(request.session["warden.user.user.key"][0][0]) - @request_items.merge!({ current_user: { id: current_user.id, email: current_user.email } }) + if request.session['warden.user.user.key'] + current_user = User.find(request.session['warden.user.user.key'][0][0]) + @request_items[:current_user] = { id: current_user.id, email: current_user.email } end - else - @controller = @request_items = nil end payload = message_text.merge(user_info).merge(channel_info) @options[:body] = payload.to_json @options[:headers] ||= {} - @options[:headers].merge!({ 'Content-Type' => 'application/json' }) + @options[:headers]['Content-Type'] = 'application/json' @httparty.post(@webhook_url, @options) end private - def channel_info - if @channel - { channel: @channel } - else - {} - end + def channel_info + if @channel + { channel: @channel } + else + {} end + end - def user_info - infos = {} + def user_info + infos = {} - infos.merge!({ username: @username }) if @username - infos.merge!({ icon_url: @avatar }) if @avatar + infos[:username] = @username if @username + infos[:icon_url] = @avatar if @avatar - infos - end + infos + end - def message_text - text = [] + def message_text + text = [] - text += ["@channel"] - text += message_header - text += message_request if @request_items - text += message_backtrace if @backtrace - text += message_issue_link if @gitlab_url + text += ['@channel'] + text += message_header + text += message_request if @request_items + text += message_backtrace if @backtrace + text += message_issue_link if @gitlab_url - { text: text.join("\n") } - end + { text: text.join("\n") } + end - def message_header - text = [] + def message_header + text = [] - errors_count = @options[:accumulated_errors_count].to_i - text << "### :warning: Error 500 in #{Rails.env} :warning:" - text << "#{errors_count > 1 ? errors_count : 'An'} *#{@exception.class}* occured" + if @controller then " in *#{controller_and_method}*." else "." end - text << "*#{@exception.message}*" + errors_count = @options[:accumulated_errors_count].to_i + text << "### :warning: Error 500 in #{Rails.env} :warning:" + text << "#{errors_count > 1 ? errors_count : 'An'} *#{@exception.class}* occured" + (@controller ? " in *#{controller_and_method}*." : '.') + text << "*#{@exception.message}*" - text - end + text + end - def message_request - text = [] + def message_request + text = [] - text << "### Request" - text << "```" - text << hash_presentation(@request_items) - text << "```" + text << '### Request' + text << '```' + text << hash_presentation(@request_items) + text << '```' - text - end + text + end - def message_backtrace(size = 3) - text = [] + def message_backtrace(size = 3) + text = [] - size = @backtrace.size < size ? @backtrace.size : size - text << "### Backtrace" - text << "```" - size.times { |i| text << "* " + @backtrace[i] } - text << "```" + size = @backtrace.size < size ? @backtrace.size : size + text << '### Backtrace' + text << '```' + size.times { |i| text << '* ' + @backtrace[i] } + text << '```' - text - end + text + end - def message_issue_link - text = [] + def message_issue_link + text = [] - link = [@gitlab_url, @application_name, "issues", "new"].join("/") - params = { - "issue[title]" => ["[BUG] Error 500 :", - controller_and_method, - "(#{@exception.class})", - @exception.message].compact.join(" ") - }.to_query + link = [@gitlab_url, @application_name, 'issues', 'new'].join('/') + params = { + 'issue[title]' => ['[BUG] Error 500 :', + controller_and_method, + "(#{@exception.class})", + @exception.message].compact.join(' ') + }.to_query - text << "[Create an issue](#{link}/?#{params})" + text << "[Create an issue](#{link}/?#{params})" - text - end + text + end - def controller_and_method - if @controller - "#{@controller.controller_name}##{@controller.action_name}" - else - "" - end + def controller_and_method + if @controller + "#{@controller.controller_name}##{@controller.action_name}" + else + '' end + end - def hash_presentation(hash) - text = [] - - hash.each do |key, value| - text << "* #{key} : #{value}" - end + def hash_presentation(hash) + text = [] - text.join("\n") + hash.each do |key, value| + text << "* #{key} : #{value}" end + text.join("\n") + end end end diff --git a/lib/exception_notifier/modules/error_grouping.rb b/lib/exception_notifier/modules/error_grouping.rb index 6eacacfd..4f6c2cc2 100644 --- a/lib/exception_notifier/modules/error_grouping.rb +++ b/lib/exception_notifier/modules/error_grouping.rb @@ -27,7 +27,7 @@ def fallback_cache_store def error_count(error_key) count = begin error_grouping_cache.read(error_key) - rescue => e + rescue StandardError => e ExceptionNotifier.logger.warn("#{error_grouping_cache.inspect} failed to read, reason: #{e.message}. Falling back to memory cache store.") fallback_cache_store.read(error_key) end @@ -37,7 +37,7 @@ def error_count(error_key) def save_error_count(error_key, count) error_grouping_cache.write(error_key, count, expires_in: error_grouping_period) - rescue => e + rescue StandardError => e ExceptionNotifier.logger.warn("#{error_grouping_cache.inspect} failed to write, reason: #{e.message}. Falling back to memory cache store.") fallback_cache_store.write(error_key, count, expires_in: error_grouping_period) end @@ -74,4 +74,4 @@ def send_notification?(exception, count) end end end -end \ No newline at end of file +end diff --git a/lib/exception_notifier/notifier.rb b/lib/exception_notifier/notifier.rb index e689c7e2..6ac829fa 100644 --- a/lib/exception_notifier/notifier.rb +++ b/lib/exception_notifier/notifier.rb @@ -2,14 +2,13 @@ module ExceptionNotifier class Notifier - - def self.exception_notification(env, exception, options={}) - ActiveSupport::Deprecation.warn "Please use ExceptionNotifier.notify_exception(exception, options.merge(env: env))." + def self.exception_notification(env, exception, options = {}) + ActiveSupport::Deprecation.warn 'Please use ExceptionNotifier.notify_exception(exception, options.merge(env: env)).' ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(env: env)) end - def self.background_exception_notification(exception, options={}) - ActiveSupport::Deprecation.warn "Please use ExceptionNotifier.notify_exception(exception, options)." + def self.background_exception_notification(exception, options = {}) + ActiveSupport::Deprecation.warn 'Please use ExceptionNotifier.notify_exception(exception, options).' ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options) end end diff --git a/lib/exception_notifier/slack_notifier.rb b/lib/exception_notifier/slack_notifier.rb index f3fbeefa..8202ef52 100644 --- a/lib/exception_notifier/slack_notifier.rb +++ b/lib/exception_notifier/slack_notifier.rb @@ -15,7 +15,7 @@ def initialize(options) @message_opts = options.fetch(:additional_parameters, {}) @color = @message_opts.delete(:color) { 'danger' } @notifier = Slack::Notifier.new webhook_url, options - rescue + rescue StandardError @notifier = nil end end @@ -40,13 +40,9 @@ def valid? def deep_reject(hash, block) hash.each do |k, v| - if v.is_a?(Hash) - deep_reject(v, block) - end + deep_reject(v, block) if v.is_a?(Hash) - if block.call(k, v) - hash.delete(k) - end + hash.delete(k) if block.call(k, v) end end diff --git a/lib/exception_notifier/sns_notifier.rb b/lib/exception_notifier/sns_notifier.rb index 72b98edb..b1d67332 100644 --- a/lib/exception_notifier/sns_notifier.rb +++ b/lib/exception_notifier/sns_notifier.rb @@ -3,9 +3,9 @@ class SnsNotifier < BaseNotifier def initialize(options) super - raise ArgumentError.new "You must provide 'region' option" unless options[:region] - raise ArgumentError.new "You must provide 'access_key_id' option" unless options[:access_key_id] - raise ArgumentError.new "You must provide 'secret_access_key' option" unless options[:secret_access_key] + raise ArgumentError, "You must provide 'region' option" unless options[:region] + raise ArgumentError, "You must provide 'access_key_id' option" unless options[:access_key_id] + raise ArgumentError, "You must provide 'secret_access_key' option" unless options[:secret_access_key] @notifier = Aws::SNS::Client.new( region: options[:region], @@ -35,8 +35,8 @@ def call(exception, custom_opts = {}) def build_subject(exception, options) subject = "#{options[:sns_prefix]} - " subject << accumulated_exception_name(exception, options) - subject << " occurred" - subject.length > 120 ? subject[0...120] + "..." : subject + subject << ' occurred' + subject.length > 120 ? subject[0...120] + '...' : subject end def build_message(exception, options) @@ -58,7 +58,7 @@ def build_message(exception, options) text += "Hostname: #{Socket.gethostname}\n" if exception.backtrace - formatted_backtrace = "#{exception.backtrace.first(options[:backtrace_lines]).join("\n")}" + formatted_backtrace = exception.backtrace.first(options[:backtrace_lines]).join("\n").to_s text += "Backtrace:\n#{formatted_backtrace}\n" end end @@ -66,7 +66,7 @@ def build_message(exception, options) def accumulated_exception_name(exception, options) errors_count = options[:accumulated_errors_count].to_i measure_word = errors_count > 1 ? errors_count : (exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A') - "#{measure_word} #{exception.class.to_s}" + "#{measure_word} #{exception.class}" end def default_options diff --git a/lib/exception_notifier/teams_notifier.rb b/lib/exception_notifier/teams_notifier.rb index c2f639a8..d419f924 100644 --- a/lib/exception_notifier/teams_notifier.rb +++ b/lib/exception_notifier/teams_notifier.rb @@ -6,8 +6,7 @@ class TeamsNotifier < BaseNotifier include ExceptionNotifier::BacktraceCleaner class MissingController - def method_missing(*args, &block) - end + def method_missing(*args, &block); end end attr_accessor :httparty @@ -18,7 +17,7 @@ def initialize(options = {}) @httparty = HTTParty end - def call(exception, options={}) + def call(exception, options = {}) @options = options.merge(@default_options) @exception = exception @backtrace = exception.backtrace ? clean_backtrace(exception) : nil @@ -30,9 +29,11 @@ def call(exception, options={}) @jira_url = @options.delete(:jira_url) @webhook_url = @options.delete(:webhook_url) - raise ArgumentError.new "You must provide 'webhook_url' parameter." unless @webhook_url + raise ArgumentError, "You must provide 'webhook_url' parameter." unless @webhook_url - unless @env.nil? + if @env.nil? + @controller = @request_items = nil + else @controller = @env['action_controller.instance'] || MissingController.new request = ActionDispatch::Request.new(@env) @@ -43,19 +44,17 @@ def call(exception, options={}) parameters: request.filtered_parameters, timestamp: Time.current } - if request.session["warden.user.user.key"] - current_user = User.find(request.session["warden.user.user.key"][0][0]) - @request_items.merge!({ current_user: { id: current_user.id, email: current_user.email } }) + if request.session['warden.user.user.key'] + current_user = User.find(request.session['warden.user.user.key'][0][0]) + @request_items[:current_user] = { id: current_user.id, email: current_user.email } end - else - @controller = @request_items = nil end payload = message_text @options[:body] = payload.to_json @options[:headers] ||= {} - @options[:headers].merge!({ 'Content-Type' => 'application/json' }) + @options[:headers]['Content-Type'] = 'application/json' @options[:debug_output] = $stdout @httparty.post(@webhook_url, @options) @@ -67,17 +66,17 @@ def message_text errors_count = @options[:accumulated_errors_count].to_i text = { - "@type" => "MessageCard", - "@context" => "http://schema.org/extensions", - "summary" => "#{@application_name} Exception Alert", - "title" => "⚠️ Exception Occurred in #{Rails.env} ⚠️", - "sections" => [ + '@type' => 'MessageCard', + '@context' => 'http://schema.org/extensions', + 'summary' => "#{@application_name} Exception Alert", + 'title' => "⚠️ Exception Occurred in #{Rails.env} ⚠️", + 'sections' => [ { - "activityTitle" => "#{errors_count > 1 ? errors_count : 'A'} *#{@exception.class}* occurred" + if @controller then " in *#{controller_and_method}*." else "." end, - "activitySubtitle" => "#{@exception.message}" + 'activityTitle' => "#{errors_count > 1 ? errors_count : 'A'} *#{@exception.class}* occurred" + (@controller ? " in *#{controller_and_method}*." : '.'), + 'activitySubtitle' => @exception.message.to_s } ], - "potentialAction" => [] + 'potentialAction' => [] } text['sections'].push details @@ -90,8 +89,8 @@ def message_text def details details = { - "title" => "Details", - "facts" => [] + 'title' => 'Details', + 'facts' => [] } details['facts'].push message_request unless @request_items.nil? @@ -102,59 +101,59 @@ def details def message_request { - "name" => "Request", - "value" => "#{hash_presentation(@request_items)}\n " + 'name' => 'Request', + 'value' => "#{hash_presentation(@request_items)}\n " } end def message_backtrace(size = 3) text = [] size = @backtrace.size < size ? @backtrace.size : size - text << "```" - size.times { |i| text << "* " + @backtrace[i] } - text << "```" + text << '```' + size.times { |i| text << '* ' + @backtrace[i] } + text << '```' { - "name" => "Backtrace", - "value" => "#{text.join(" \n")}" + 'name' => 'Backtrace', + 'value' => text.join(" \n").to_s } end def gitlab_view_link { - "@type" => "ViewAction", - "name" => "🦊 View in GitLab", - "target" => [ + '@type' => 'ViewAction', + 'name' => "\u{1F98A} View in GitLab", + 'target' => [ "#{@gitlab_url}/#{@application_name}" ] } end def gitlab_issue_link - link = [@gitlab_url, @application_name, "issues", "new"].join("/") + link = [@gitlab_url, @application_name, 'issues', 'new'].join('/') params = { - "issue[title]" => ["[BUG] Error 500 :", + 'issue[title]' => ['[BUG] Error 500 :', controller_and_method, "(#{@exception.class})", - @exception.message].compact.join(" ") + @exception.message].compact.join(' ') }.to_query { - "@type" => "ViewAction", - "name" => "🦊 Create Issue in GitLab", - "target" => [ + '@type' => 'ViewAction', + 'name' => "\u{1F98A} Create Issue in GitLab", + 'target' => [ "#{link}/?#{params}" - ] + ] } end def jira_issue_link { - "@type" => "ViewAction", - "name" => "🐞 Create Issue in Jira", - "target" => [ + '@type' => 'ViewAction', + 'name' => '🐞 Create Issue in Jira', + 'target' => [ "#{@jira_url}/secure/CreateIssue!default.jspa" - ] + ] } end @@ -162,7 +161,7 @@ def controller_and_method if @controller "#{@controller.controller_name}##{@controller.action_name}" else - "" + '' end end diff --git a/lib/exception_notifier/webhook_notifier.rb b/lib/exception_notifier/webhook_notifier.rb index b1751a45..76e949b7 100644 --- a/lib/exception_notifier/webhook_notifier.rb +++ b/lib/exception_notifier/webhook_notifier.rb @@ -3,13 +3,12 @@ module ExceptionNotifier class WebhookNotifier < BaseNotifier - def initialize(options) super @default_options = options end - def call(exception, options={}) + def call(exception, options = {}) env = options[:env] options = options.reverse_merge(@default_options) @@ -18,7 +17,7 @@ def call(exception, options={}) options[:body] ||= {} options[:body][:server] = Socket.gethostname - options[:body][:process] = $$ + options[:body][:process] = $PROCESS_ID if defined?(Rails) && Rails.respond_to?(:root) options[:body][:rails_root] = Rails.root end diff --git a/lib/generators/exception_notification/install_generator.rb b/lib/generators/exception_notification/install_generator.rb index 9193c773..d4f78b38 100644 --- a/lib/generators/exception_notification/install_generator.rb +++ b/lib/generators/exception_notification/install_generator.rb @@ -3,7 +3,7 @@ module Generators class InstallGenerator < Rails::Generators::Base desc 'Creates a ExceptionNotification initializer.' - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) class_option :resque, type: :boolean, desc: 'Add support for sending notifications when errors occur in Resque jobs.' class_option :sidekiq, type: :boolean, desc: 'Add support for sending notifications when errors occur in Sidekiq jobs.' diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile index 9724472e..20ddc49e 100644 --- a/test/dummy/Rakefile +++ b/test/dummy/Rakefile @@ -1,7 +1,7 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('../config/application', __FILE__) +require File.expand_path('config/application', __dir__) require 'rake' Dummy::Application.load_tasks diff --git a/test/dummy/config.ru b/test/dummy/config.ru index 1989ed8d..cbd74159 100644 --- a/test/dummy/config.ru +++ b/test/dummy/config.ru @@ -1,4 +1,4 @@ # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require ::File.expand_path('../config/environment', __FILE__) run Dummy::Application diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index a766c4db..16768057 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -1,4 +1,4 @@ -require File.expand_path('../boot', __FILE__) +require File.expand_path('boot', __dir__) require 'rails/all' @@ -34,12 +34,12 @@ class Application < Rails::Application # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) # Configure the default encoding used in templates for Ruby 1.9. - config.encoding = "utf-8" + config.encoding = 'utf-8' # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += [:password, :secret] + config.filter_parameters += %i[password secret] rails_version = Gem::Version.new(Rails.version) - config.active_record.sqlite3.represent_boolean_as_integer = true if rails_version >= Gem::Version.new("5.2.0") + config.active_record.sqlite3.represent_boolean_as_integer = true if rails_version >= Gem::Version.new('5.2.0') end end diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb index f2830ae3..24efefdb 100644 --- a/test/dummy/config/boot.rb +++ b/test/dummy/config/boot.rb @@ -1,6 +1,6 @@ require 'rubygems' # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb index f85d3cde..00d98427 100644 --- a/test/dummy/config/environment.rb +++ b/test/dummy/config/environment.rb @@ -1,16 +1,16 @@ # Load the rails application -require File.expand_path('../application', __FILE__) +require File.expand_path('application', __dir__) Dummy::Application.config.middleware.use ExceptionNotification::Rack, email: { email_prefix: '[Dummy ERROR] ', - sender_address: %{"Dummy Notifier" }, - exception_recipients: %w{dummyexceptions@example.com}, + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], email_headers: { 'X-Custom-Header' => 'foobar' }, - sections: ['new_section', 'request', 'session', 'environment', 'backtrace'], - background_sections: %w(new_bkg_section backtrace data), - pre_callback: proc { |opts, notifier, backtrace, message, message_opts| message_opts[:pre_callback_called] = 1 }, - post_callback: proc { |opts, notifier, backtrace, message, message_opts| message_opts[:post_callback_called] = 1 } + sections: %w[new_section request session environment backtrace], + background_sections: %w[new_bkg_section backtrace data], + pre_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:pre_callback_called] = 1 }, + post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } } # Initialize the rails application diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb index e758d3d0..d276322f 100644 --- a/test/dummy/config/environments/production.rb +++ b/test/dummy/config/environments/production.rb @@ -11,7 +11,7 @@ config.action_controller.perform_caching = true # Specifies the header that your server uses for sending files - config.action_dispatch.x_sendfile_header = "X-Sendfile" + config.action_dispatch.x_sendfile_header = 'X-Sendfile' # For nginx: # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb index 40fe27d5..822f64a8 100644 --- a/test/dummy/config/environments/test.rb +++ b/test/dummy/config/environments/test.rb @@ -16,7 +16,7 @@ config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false + config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index 98630068..0ba13a15 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,3 +1,3 @@ Dummy::Application.routes.draw do - resources :posts, only: [:create, :show] + resources :posts, only: %i[create show] end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index d4de93a0..da892226 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -1,4 +1,3 @@ -# encoding: UTF-8 # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -11,14 +10,12 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20110729022608) do - - create_table "posts", force: true do |t| - t.string "title" - t.text "body" - t.string "secret" - t.datetime "created_at" - t.datetime "updated_at" +ActiveRecord::Schema.define(version: 20_110_729_022_608) do + create_table 'posts', force: true do |t| + t.string 'title' + t.text 'body' + t.string 'secret' + t.datetime 'created_at' + t.datetime 'updated_at' end - end diff --git a/test/dummy/script/rails b/test/dummy/script/rails index f8da2cff..3c234a25 100755 --- a/test/dummy/script/rails +++ b/test/dummy/script/rails @@ -1,6 +1,6 @@ #!/usr/bin/env ruby # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. -APP_PATH = File.expand_path('../../config/application', __FILE__) -require File.expand_path('../../config/boot', __FILE__) +APP_PATH = File.expand_path('../config/application', __dir__) +require File.expand_path('../config/boot', __dir__) require 'rails/commands' diff --git a/test/dummy/test/functional/posts_controller_test.rb b/test/dummy/test/functional/posts_controller_test.rb index 031ab4bf..5ba75eb9 100644 --- a/test/dummy/test/functional/posts_controller_test.rb +++ b/test/dummy/test/functional/posts_controller_test.rb @@ -5,125 +5,125 @@ class PostsControllerTest < ActionController::TestCase Time.stubs(:current).returns('Sat, 20 Apr 2013 20:58:55 UTC +00:00') @email_notifier = ExceptionNotifier.registered_exception_notifier(:email) begin - post :create, method: :post, params: { secret: "secret" } - rescue => e + post :create, method: :post, params: { secret: 'secret' } + rescue StandardError => e @exception = e - @mail = @email_notifier.create_email(@exception, { env: request.env, data: { message: 'My Custom Message' }}) + @mail = @email_notifier.create_email(@exception, env: request.env, data: { message: 'My Custom Message' }) end end - test "should have raised an exception" do + test 'should have raised an exception' do refute_nil @exception end - test "should have generated a notification email" do + test 'should have generated a notification email' do refute_nil @mail end - test "mail should be plain text and UTF-8 enconded by default" do - assert_equal @mail.content_type, "text/plain; charset=UTF-8" + test 'mail should be plain text and UTF-8 enconded by default' do + assert_equal @mail.content_type, 'text/plain; charset=UTF-8' end - test "mail should have a from address set" do - assert_equal @mail.from, ["dummynotifier@example.com"] + test 'mail should have a from address set' do + assert_equal @mail.from, ['dummynotifier@example.com'] end - test "mail should have a to address set" do - assert_equal @mail.to, ["dummyexceptions@example.com"] + test 'mail should have a to address set' do + assert_equal @mail.to, ['dummyexceptions@example.com'] end - test "mail subject should have the proper prefix" do - assert_includes @mail.subject, "[Dummy ERROR]" + test 'mail subject should have the proper prefix' do + assert_includes @mail.subject, '[Dummy ERROR]' end - test "mail subject should include descriptive error message" do + test 'mail subject should include descriptive error message' do assert_includes @mail.subject, "(NoMethodError) \"undefined method `nw'" end - test "mail should contain backtrace in body" do + test 'mail should contain backtrace in body' do assert_includes @mail.encoded, "`method_missing'\r\n app/controllers/posts_controller.rb:18:in `create'\r\n" end - test "mail should contain timestamp of exception in body" do + test 'mail should contain timestamp of exception in body' do assert_includes @mail.encoded, "Timestamp : #{Time.current}" end - test "mail should contain the newly defined section" do - assert_includes @mail.encoded, "* New text section for testing" + test 'mail should contain the newly defined section' do + assert_includes @mail.encoded, '* New text section for testing' end - test "mail should contain the custom message" do - assert_includes @mail.encoded, "My Custom Message" + test 'mail should contain the custom message' do + assert_includes @mail.encoded, 'My Custom Message' end - test "should filter sensible data" do - assert_includes @mail.encoded, "secret\"=>\"[FILTERED]" + test 'should filter sensible data' do + assert_includes @mail.encoded, 'secret"=>"[FILTERED]' end - test "mail should contain the custom header" do + test 'mail should contain the custom header' do assert_includes @mail.encoded, 'X-Custom-Header: foobar' end - test "mail should not contain any attachments" do + test 'mail should not contain any attachments' do assert_equal @mail.attachments, [] end - test "should not send notification if one of ignored exceptions" do + test 'should not send notification if one of ignored exceptions' do begin get :invalid - rescue => e + rescue StandardError => e @ignored_exception = e unless ExceptionNotifier.ignored_exceptions.include?(@ignored_exception.class.name) - ignored_mail = @email_notifier.create_email(@ignored_exception, { env: request.env }) + ignored_mail = @email_notifier.create_email(@ignored_exception, env: request.env) end end - assert_equal @ignored_exception.class.inspect, "ActionController::UrlGenerationError" + assert_equal @ignored_exception.class.inspect, 'ActionController::UrlGenerationError' assert_nil ignored_mail end - test "should filter session_id on secure requests" do + test 'should filter session_id on secure requests' do request.env['HTTPS'] = 'on' begin post :create, method: :post - rescue => e - @secured_mail = @email_notifier.create_email(e, { env: request.env }) + rescue StandardError => e + @secured_mail = @email_notifier.create_email(e, env: request.env) end assert request.ssl? assert_includes @secured_mail.encoded, "* session id: [FILTERED]\r\n *" end - test "should ignore exception if from unwanted crawler" do - request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" + test 'should ignore exception if from unwanted crawler' do + request.env['HTTP_USER_AGENT'] = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' begin post :create, method: :post - rescue => e + rescue StandardError => e @exception = e custom_env = request.env custom_env['exception_notifier.options'] ||= {} - custom_env['exception_notifier.options'].merge!(ignore_crawlers: %w(Googlebot)) + custom_env['exception_notifier.options'][:ignore_crawlers] = %w[Googlebot] ignore_array = custom_env['exception_notifier.options'][:ignore_crawlers] unless ExceptionNotification::Rack.new(Dummy::Application, custom_env['exception_notifier.options']).send(:from_crawler, custom_env, ignore_array) - ignored_mail = @email_notifier.create_email(@exception, { env: custom_env }) + ignored_mail = @email_notifier.create_email(@exception, env: custom_env) end end assert_nil ignored_mail end - test "should send html email when selected html format" do + test 'should send html email when selected html format' do begin post :create, method: :post - rescue => e + rescue StandardError => e @exception = e custom_env = request.env custom_env['exception_notifier.options'] ||= {} - custom_env['exception_notifier.options'].merge!({ email_format: :html }) - @mail = @email_notifier.create_email(@exception, { env: custom_env }) + custom_env['exception_notifier.options'][:email_format] = :html + @mail = @email_notifier.create_email(@exception, env: custom_env) end - assert_includes @mail.content_type, "multipart/alternative" + assert_includes @mail.content_type, 'multipart/alternative' end end @@ -133,13 +133,13 @@ class PostsControllerTestWithoutVerboseSubject < ActionController::TestCase @email_notifier = ExceptionNotifier::EmailNotifier.new(verbose_subject: false) begin post :create, method: :post - rescue => e + rescue StandardError => e @exception = e - @mail = @email_notifier.create_email(@exception, { env: request.env }) + @mail = @email_notifier.create_email(@exception, env: request.env) end end - test "should not include exception message in subject" do + test 'should not include exception message in subject' do assert_includes @mail.subject, '[ERROR]' assert_includes @mail.subject, '(NoMethodError)' refute_includes @mail.subject, 'undefined method' @@ -152,13 +152,13 @@ class PostsControllerTestWithoutControllerAndActionNames < ActionController::Tes @email_notifier = ExceptionNotifier::EmailNotifier.new(include_controller_and_action_names_in_subject: false) begin post :create, method: :post - rescue => e + rescue StandardError => e @exception = e - @mail = @email_notifier.create_email(@exception, { env: request.env}) + @mail = @email_notifier.create_email(@exception, env: request.env) end end - test "should include controller and action names in subject" do + test 'should include controller and action names in subject' do assert_includes @mail.subject, '[ERROR]' assert_includes @mail.subject, '(NoMethodError)' refute_includes @mail.subject, 'posts#create' @@ -177,21 +177,21 @@ class PostsControllerTestWithSmtpSettings < ActionController::TestCase begin post :create, method: :post - rescue => e + rescue StandardError => e @exception = e - @mail = @email_notifier.create_email(@exception, { env: request.env }) + @mail = @email_notifier.create_email(@exception, env: request.env) end end - test "should have overridden smtp settings" do - assert_equal "Dummy user_name", @mail.delivery_method.settings[:user_name] - assert_equal "Dummy password", @mail.delivery_method.settings[:password] + test 'should have overridden smtp settings' do + assert_equal 'Dummy user_name', @mail.delivery_method.settings[:user_name] + assert_equal 'Dummy password', @mail.delivery_method.settings[:password] end - test "should have overridden smtp settings with background notification" do + test 'should have overridden smtp settings with background notification' do @mail = @email_notifier.create_email(@exception) - assert_equal "Dummy user_name", @mail.delivery_method.settings[:user_name] - assert_equal "Dummy password", @mail.delivery_method.settings[:password] + assert_equal 'Dummy user_name', @mail.delivery_method.settings[:user_name] + assert_equal 'Dummy password', @mail.delivery_method.settings[:password] end end @@ -201,39 +201,39 @@ class PostsControllerTestBackgroundNotification < ActionController::TestCase @email_notifier = ExceptionNotifier.registered_exception_notifier(:email) begin post :create, method: :post - rescue => exception + rescue StandardError => exception @mail = @email_notifier.create_email(exception) end end - test "mail should contain the specified section" do - assert_includes @mail.encoded, "* New background section for testing" + test 'mail should contain the specified section' do + assert_includes @mail.encoded, '* New background section for testing' end end class PostsControllerTestWithExceptionRecipientsAsProc < ActionController::TestCase tests PostsController setup do - exception_recipients = %w{first@example.com second@example.com} + exception_recipients = %w[first@example.com second@example.com] @email_notifier = ExceptionNotifier::EmailNotifier.new( - exception_recipients: -> { [ exception_recipients.shift ] } + exception_recipients: -> { [exception_recipients.shift] } ) @action = proc do begin post :create, method: :post - rescue => e + rescue StandardError => e @exception = e - @mail = @email_notifier.create_email(@exception, { env: request.env }) + @mail = @email_notifier.create_email(@exception, env: request.env) end end end - test "should lazily evaluate exception_recipients" do + test 'should lazily evaluate exception_recipients' do @action.call - assert_equal [ "first@example.com" ], @mail.to + assert_equal ['first@example.com'], @mail.to @action.call - assert_equal [ "second@example.com" ], @mail.to + assert_equal ['second@example.com'], @mail.to end end diff --git a/test/dummy/test/test_helper.rb b/test/dummy/test/test_helper.rb index 82985177..77bc8b9e 100644 --- a/test/dummy/test/test_helper.rb +++ b/test/dummy/test/test_helper.rb @@ -1,5 +1,5 @@ -ENV["RAILS_ENV"] = "test" -require File.expand_path('../../config/environment', __FILE__) +ENV['RAILS_ENV'] = 'test' +require File.expand_path('../config/environment', __dir__) require 'rails/test_help' class ActiveSupport::TestCase diff --git a/test/exception_notification/rack_test.rb b/test/exception_notification/rack_test.rb index 4482ff7a..7c7653c9 100644 --- a/test/exception_notification/rack_test.rb +++ b/test/exception_notification/rack_test.rb @@ -1,13 +1,12 @@ require 'test_helper' class RackTest < ActiveSupport::TestCase - setup do @pass_app = Object.new @pass_app.stubs(:call).returns([nil, { 'X-Cascade' => 'pass' }, nil]) @normal_app = Object.new - @normal_app.stubs(:call).returns([nil, { }, nil]) + @normal_app.stubs(:call).returns([nil, {}, nil]) end teardown do @@ -15,29 +14,29 @@ class RackTest < ActiveSupport::TestCase ExceptionNotifier.notification_trigger = nil end - test "should ignore \"X-Cascade\" header by default" do + test 'should ignore "X-Cascade" header by default' do ExceptionNotifier.expects(:notify_exception).never ExceptionNotification::Rack.new(@pass_app).call({}) end - test "should notify on \"X-Cascade\" = \"pass\" if ignore_cascade_pass option is false" do + test 'should notify on "X-Cascade" = "pass" if ignore_cascade_pass option is false' do ExceptionNotifier.expects(:notify_exception).once ExceptionNotification::Rack.new(@pass_app, ignore_cascade_pass: false).call({}) end - test "should assign error_grouping if error_grouping is specified" do + test 'should assign error_grouping if error_grouping is specified' do refute ExceptionNotifier.error_grouping ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({}) assert ExceptionNotifier.error_grouping end - test "should assign notification_trigger if notification_trigger is specified" do + test 'should assign notification_trigger if notification_trigger is specified' do assert_nil ExceptionNotifier.notification_trigger - ExceptionNotification::Rack.new(@normal_app, notification_trigger: lambda { |i| true }).call({}) + ExceptionNotification::Rack.new(@normal_app, notification_trigger: ->(_i) { true }).call({}) assert_respond_to ExceptionNotifier.notification_trigger, :call end - test "should set default cache to Rails cache" do + test 'should set default cache to Rails cache' do ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({}) assert_equal Rails.cache, ExceptionNotifier.error_grouping_cache end diff --git a/test/exception_notifier/campfire_notifier_test.rb b/test/exception_notifier/campfire_notifier_test.rb index 0a2bb646..2594f7cc 100644 --- a/test/exception_notifier/campfire_notifier_test.rb +++ b/test/exception_notifier/campfire_notifier_test.rb @@ -8,43 +8,42 @@ end class CampfireNotifierTest < ActiveSupport::TestCase - - test "should send campfire notification if properly configured" do + test 'should send campfire notification if properly configured' do ExceptionNotifier::CampfireNotifier.stubs(:new).returns(Object.new) - campfire = ExceptionNotifier::CampfireNotifier.new({ subdomain: 'test', token: 'test_token', room_name: 'test_room' }) + campfire = ExceptionNotifier::CampfireNotifier.new(subdomain: 'test', token: 'test_token', room_name: 'test_room') campfire.stubs(:call).returns(fake_notification) notif = campfire.call(fake_exception) assert !notif[:message].empty? assert_equal notif[:message][:type], 'PasteMessage' - assert_includes notif[:message][:body], "A new exception occurred:" - assert_includes notif[:message][:body], "divided by 0" - assert_includes notif[:message][:body], "/exception_notification/test/campfire_test.rb:45" + assert_includes notif[:message][:body], 'A new exception occurred:' + assert_includes notif[:message][:body], 'divided by 0' + assert_includes notif[:message][:body], '/exception_notification/test/campfire_test.rb:45' end - test "should send campfire notification without backtrace info if properly configured" do + test 'should send campfire notification without backtrace info if properly configured' do ExceptionNotifier::CampfireNotifier.stubs(:new).returns(Object.new) - campfire = ExceptionNotifier::CampfireNotifier.new({ subdomain: 'test', token: 'test_token', room_name: 'test_room' }) + campfire = ExceptionNotifier::CampfireNotifier.new(subdomain: 'test', token: 'test_token', room_name: 'test_room') campfire.stubs(:call).returns(fake_notification_without_backtrace) notif = campfire.call(fake_exception_without_backtrace) assert !notif[:message].empty? assert_equal notif[:message][:type], 'PasteMessage' - assert_includes notif[:message][:body], "A new exception occurred:" - assert_includes notif[:message][:body], "my custom error" + assert_includes notif[:message][:body], 'A new exception occurred:' + assert_includes notif[:message][:body], 'my custom error' end - test "should not send campfire notification if badly configured" do + test 'should not send campfire notification if badly configured' do wrong_params = { subdomain: 'test', token: 'bad_token', room_name: 'test_room' } - Tinder::Campfire.stubs(:new).with('test', { token: 'bad_token' }).returns(nil) + Tinder::Campfire.stubs(:new).with('test', token: 'bad_token').returns(nil) campfire = ExceptionNotifier::CampfireNotifier.new(wrong_params) assert_nil campfire.room assert_nil campfire.call(fake_exception) end - test "should not send campfire notification if config attr missing" do - wrong_params = { subdomain: 'test', room_name: 'test_room' } + test 'should not send campfire notification if config attr missing' do + wrong_params = { subdomain: 'test', room_name: 'test_room' } Tinder::Campfire.stubs(:new).with('test', {}).returns(nil) campfire = ExceptionNotifier::CampfireNotifier.new(wrong_params) @@ -52,35 +51,34 @@ class CampfireNotifierTest < ActiveSupport::TestCase assert_nil campfire.call(fake_exception) end - test "should send the new exception message if no :accumulated_errors_count option" do + test 'should send the new exception message if no :accumulated_errors_count option' do campfire = ExceptionNotifier::CampfireNotifier.new({}) campfire.stubs(:active?).returns(true) - campfire.expects(:send_notice).with{ |_, _, message| message.start_with?("A new exception occurred") }.once + campfire.expects(:send_notice).with { |_, _, message| message.start_with?('A new exception occurred') }.once campfire.call(fake_exception) end - test "shoud send the exception message if :accumulated_errors_count option greater than 1" do + test 'shoud send the exception message if :accumulated_errors_count option greater than 1' do campfire = ExceptionNotifier::CampfireNotifier.new({}) campfire.stubs(:active?).returns(true) - campfire.expects(:send_notice).with{ |_, _, message| message.start_with?("The exception occurred 3 times:") }.once + campfire.expects(:send_notice).with { |_, _, message| message.start_with?('The exception occurred 3 times:') }.once campfire.call(fake_exception, accumulated_errors_count: 3) end - test "should call pre/post_callback if specified" do - pre_callback_called, post_callback_called = 0,0 + test 'should call pre/post_callback if specified' do + pre_callback_called = 0 + post_callback_called = 0 Tinder::Campfire.stubs(:new).returns(Object.new) campfire = ExceptionNotifier::CampfireNotifier.new( - { - subdomain: 'test', - token: 'test_token', - room_name: 'test_room', - pre_callback: proc { |opts, notifier, backtrace, message, message_opts| - pre_callback_called += 1 - }, - post_callback: proc { |opts, notifier, backtrace, message, message_opts| - post_callback_called += 1 - } + subdomain: 'test', + token: 'test_token', + room_name: 'test_room', + pre_callback: proc { |_opts, _notifier, _backtrace, _message, _message_opts| + pre_callback_called += 1 + }, + post_callback: proc { |_opts, _notifier, _backtrace, _message, _message_opts| + post_callback_called += 1 } ) campfire.room = Object.new @@ -102,11 +100,9 @@ def fake_notification end def fake_exception - begin - 5/0 - rescue Exception => e - e - end + 5 / 0 + rescue Exception => e + e end def fake_notification_without_backtrace diff --git a/test/exception_notifier/datadog_notifier_test.rb b/test/exception_notifier/datadog_notifier_test.rb index cb920649..78cc7804 100644 --- a/test/exception_notifier/datadog_notifier_test.rb +++ b/test/exception_notifier/datadog_notifier_test.rb @@ -14,7 +14,7 @@ def setup @request = FakeRequest.new end - test "should send an event to datadog" do + test 'should send an event to datadog' do fake_event = Dogapi::Event.any_instance @client.expects(:emit_event).with(fake_event) @@ -22,12 +22,12 @@ def setup @notifier.call(@exception) end - test "should include exception class in event title" do + test 'should include exception class in event title' do event = @notifier.datadog_event(@exception) - assert_includes event.msg_title, "FakeException" + assert_includes event.msg_title, 'FakeException' end - test "should include prefix in event title and not append previous events" do + test 'should include prefix in event title and not append previous events' do options = { client: @client, title_prefix: "prefix" @@ -41,55 +41,53 @@ def setup assert_equal event2.msg_title, "prefix (DatadogNotifierTest::FakeException) \"Fake exception message\"" end - test "should include exception message in event title" do + test 'should include exception message in event title' do event = @notifier.datadog_event(@exception) - assert_includes event.msg_title, "Fake exception message" + assert_includes event.msg_title, 'Fake exception message' end - test "should include controller info in event title if controller information is available" do - event = @notifier.datadog_event(@exception, { - env: { - "action_controller.instance" => @controller, - "REQUEST_METHOD" => "GET", - "rack.input" => "", - } - }) - assert_includes event.msg_title, "Fake controller" - assert_includes event.msg_title, "Fake action" + test 'should include controller info in event title if controller information is available' do + event = @notifier.datadog_event(@exception, + env: { + 'action_controller.instance' => @controller, + 'REQUEST_METHOD' => 'GET', + 'rack.input' => '' + }) + assert_includes event.msg_title, 'Fake controller' + assert_includes event.msg_title, 'Fake action' end - test "should include backtrace info in event body" do + test 'should include backtrace info in event body' do event = @notifier.datadog_event(@exception) assert_includes event.msg_text, "backtrace line 1\nbacktrace line 2\nbacktrace line 3" end - test "should include request info in event body" do + test 'should include request info in event body' do ActionDispatch::Request.stubs(:new).returns(@request) - event = @notifier.datadog_event(@exception, { - env: { - "action_controller.instance" => @controller, - "REQUEST_METHOD" => "GET", - "rack.input" => "", - } - }) - assert_includes event.msg_text, "http://localhost:8080" - assert_includes event.msg_text, "GET" - assert_includes event.msg_text, "127.0.0.1" - assert_includes event.msg_text, "{\"param 1\"=>\"value 1\", \"param 2\"=>\"value 2\"}" + event = @notifier.datadog_event(@exception, + env: { + 'action_controller.instance' => @controller, + 'REQUEST_METHOD' => 'GET', + 'rack.input' => '' + }) + assert_includes event.msg_text, 'http://localhost:8080' + assert_includes event.msg_text, 'GET' + assert_includes event.msg_text, '127.0.0.1' + assert_includes event.msg_text, '{"param 1"=>"value 1", "param 2"=>"value 2"}' end - test "should include tags in event" do + test 'should include tags in event' do options = { client: @client, - tags: ["error", "production"] + tags: %w[error production] } notifier = ExceptionNotifier::DatadogNotifier.new(options) event = notifier.datadog_event(@exception) - assert_equal event.tags, ["error", "production"] + assert_equal event.tags, %w[error production] end - test "should include event title in event aggregation key" do + test 'should include event title in event aggregation key' do event = @notifier.datadog_event(@exception) assert_equal event.aggregation_key, [event.msg_title] end @@ -97,61 +95,59 @@ def setup private class FakeDatadogClient - def emit_event(event) - end + def emit_event(event); end end class FakeController def controller_name - "Fake controller" + 'Fake controller' end def action_name - "Fake action" + 'Fake action' end end class FakeException def backtrace [ - "backtrace line 1", - "backtrace line 2", - "backtrace line 3", - "backtrace line 4", - "backtrace line 5" + 'backtrace line 1', + 'backtrace line 2', + 'backtrace line 3', + 'backtrace line 4', + 'backtrace line 5' ] end def message - "Fake exception message" + 'Fake exception message' end end class FakeRequest def url - "http://localhost:8080" + 'http://localhost:8080' end def request_method - "GET" + 'GET' end def remote_ip - "127.0.0.1" + '127.0.0.1' end def filtered_parameters { - "param 1" => "value 1", - "param 2" => "value 2" + 'param 1' => 'value 1', + 'param 2' => 'value 2' } end def session { - "session_id" => "1234" + 'session_id' => '1234' } end end - end diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index 29752eff..c7665e36 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -6,114 +6,114 @@ class EmailNotifierTest < ActiveSupport::TestCase Time.stubs(:current).returns('Sat, 20 Apr 2013 20:58:55 UTC +00:00') @email_notifier = ExceptionNotifier.registered_exception_notifier(:email) begin - 1/0 - rescue => e + 1 / 0 + rescue StandardError => e @exception = e @mail = @email_notifier.create_email( @exception, - data: { job: 'DivideWorkerJob', payload: '1/0', message: 'My Custom Message'} + data: { job: 'DivideWorkerJob', payload: '1/0', message: 'My Custom Message' } ) end end - test "should call pre/post_callback if specified" do + test 'should call pre/post_callback if specified' do assert_equal @email_notifier.options[:pre_callback_called], 1 assert_equal @email_notifier.options[:post_callback_called], 1 end - test "should have default sender address overridden" do + test 'should have default sender address overridden' do assert_equal @email_notifier.sender_address, %("Dummy Notifier" ) end - test "should have default exception recipients overridden" do - assert_equal @email_notifier.exception_recipients, %w(dummyexceptions@example.com) + test 'should have default exception recipients overridden' do + assert_equal @email_notifier.exception_recipients, %w[dummyexceptions@example.com] end - test "should have default email prefix overridden" do - assert_equal @email_notifier.email_prefix, "[Dummy ERROR] " + test 'should have default email prefix overridden' do + assert_equal @email_notifier.email_prefix, '[Dummy ERROR] ' end - test "should have default email headers overridden" do - assert_equal @email_notifier.email_headers, { "X-Custom-Header" => "foobar"} + test 'should have default email headers overridden' do + assert_equal @email_notifier.email_headers, 'X-Custom-Header' => 'foobar' end - test "should have default sections overridden" do - for section in %w(new_section request session environment backtrace) + test 'should have default sections overridden' do + %w[new_section request session environment backtrace].each do |section| assert_includes @email_notifier.sections, section end end - test "should have default background sections" do - for section in %w(new_bkg_section backtrace data) + test 'should have default background sections' do + %w[new_bkg_section backtrace data].each do |section| assert_includes @email_notifier.background_sections, section end end - test "should have email format by default" do + test 'should have email format by default' do assert_equal @email_notifier.email_format, :text end - test "should have verbose subject by default" do + test 'should have verbose subject by default' do assert @email_notifier.verbose_subject end - test "should have normalize_subject false by default" do + test 'should have normalize_subject false by default' do refute @email_notifier.normalize_subject end - test "should have delivery_method nil by default" do + test 'should have delivery_method nil by default' do assert_nil @email_notifier.delivery_method end - test "should have mailer_settings nil by default" do + test 'should have mailer_settings nil by default' do assert_nil @email_notifier.mailer_settings end - test "should have mailer_parent by default" do + test 'should have mailer_parent by default' do assert_equal @email_notifier.mailer_parent, 'ActionMailer::Base' end - test "should have template_path by default" do + test 'should have template_path by default' do assert_equal @email_notifier.template_path, 'exception_notifier' end - test "should normalize multiple digits into one N" do + test 'should normalize multiple digits into one N' do assert_equal 'N foo N bar N baz N', - ExceptionNotifier::EmailNotifier.normalize_digits('1 foo 12 bar 123 baz 1234') + ExceptionNotifier::EmailNotifier.normalize_digits('1 foo 12 bar 123 baz 1234') end - test "mail should be plain text and UTF-8 enconded by default" do - assert_equal @mail.content_type, "text/plain; charset=UTF-8" + test 'mail should be plain text and UTF-8 enconded by default' do + assert_equal @mail.content_type, 'text/plain; charset=UTF-8' end - test "should have raised an exception" do + test 'should have raised an exception' do refute_nil @exception end - test "should have generated a notification email" do + test 'should have generated a notification email' do refute_nil @mail end - test "mail should have a from address set" do - assert_equal @mail.from, ["dummynotifier@example.com"] + test 'mail should have a from address set' do + assert_equal @mail.from, ['dummynotifier@example.com'] end - test "mail should have a to address set" do - assert_equal @mail.to, ["dummyexceptions@example.com"] + test 'mail should have a to address set' do + assert_equal @mail.to, ['dummyexceptions@example.com'] end - test "mail should have a descriptive subject" do + test 'mail should have a descriptive subject' do assert_match(/^\[Dummy ERROR\]\s+\(ZeroDivisionError\) "divided by 0"$/, @mail.subject) end - test "mail should say exception was raised in background at show timestamp" do + test 'mail should say exception was raised in background at show timestamp' do assert_includes @mail.encoded, "A ZeroDivisionError occurred in background at #{Time.current}" end test "mail should prefix exception class with 'an' instead of 'a' when it starts with a vowel" do begin raise ActiveRecord::RecordNotFound - rescue => e + rescue StandardError => e @vowel_exception = e @vowel_mail = @email_notifier.create_email(@vowel_exception) end @@ -121,48 +121,48 @@ class EmailNotifierTest < ActiveSupport::TestCase assert_includes @vowel_mail.encoded, "An ActiveRecord::RecordNotFound occurred in background at #{Time.current}" end - test "mail should contain backtrace in body" do - assert @mail.encoded.include?("test/exception_notifier/email_notifier_test.rb:9"), "\n#{@mail.inspect}" + test 'mail should contain backtrace in body' do + assert @mail.encoded.include?('test/exception_notifier/email_notifier_test.rb:9'), "\n#{@mail.inspect}" end - test "mail should contain data in body" do + test 'mail should contain data in body' do assert_includes @mail.encoded, '* data:' assert_includes @mail.encoded, ':payload=>"1/0"' assert_includes @mail.encoded, ':job=>"DivideWorkerJob"' - assert_includes @mail.encoded, "My Custom Message" + assert_includes @mail.encoded, 'My Custom Message' end - test "mail should not contain any attachments" do + test 'mail should not contain any attachments' do assert_equal @mail.attachments, [] end - test "should not send notification if one of ignored exceptions" do + test 'should not send notification if one of ignored exceptions' do begin raise ActiveRecord::RecordNotFound - rescue => e + rescue StandardError => e @ignored_exception = e unless ExceptionNotifier.ignored_exceptions.include?(@ignored_exception.class.name) ignored_mail = @email_notifier.create_email(@ignored_exception) end end - assert_equal @ignored_exception.class.inspect, "ActiveRecord::RecordNotFound" + assert_equal @ignored_exception.class.inspect, 'ActiveRecord::RecordNotFound' assert_nil ignored_mail end - test "should encode environment strings" do + test 'should encode environment strings' do email_notifier = ExceptionNotifier::EmailNotifier.new( - sender_address: "", - exception_recipients: %w{dummyexceptions@example.com}, + sender_address: '', + exception_recipients: %w[dummyexceptions@example.com], deliver_with: :deliver_now ) mail = email_notifier.create_email( @exception, env: { - "REQUEST_METHOD" => "GET", - "rack.input" => "", - "invalid_encoding" => "R\xC3\xA9sum\xC3\xA9".force_encoding(Encoding::ASCII), + 'REQUEST_METHOD' => 'GET', + 'rack.input' => '', + 'invalid_encoding' => "R\xC3\xA9sum\xC3\xA9".force_encoding(Encoding::ASCII) }, email_format: :text ) @@ -170,13 +170,13 @@ class EmailNotifierTest < ActiveSupport::TestCase assert_match(/invalid_encoding\s+: R__sum__/, mail.encoded) end - test "should send email using ActionMailer" do + test 'should send email using ActionMailer' do ActionMailer::Base.deliveries.clear email_notifier = ExceptionNotifier::EmailNotifier.new( email_prefix: '[Dummy ERROR] ', - sender_address: %{"Dummy Notifier" }, - exception_recipients: %w{dummyexceptions@example.com}, + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], delivery_method: :test ) @@ -185,19 +185,19 @@ class EmailNotifierTest < ActiveSupport::TestCase assert_equal 1, ActionMailer::Base.deliveries.count end - test "should be able to specify ActionMailer::MessageDelivery method" do + test 'should be able to specify ActionMailer::MessageDelivery method' do ActionMailer::Base.deliveries.clear - if ActionMailer.version < Gem::Version.new("4.2") - deliver_with = :deliver - else - deliver_with = :deliver_now - end + deliver_with = if ActionMailer.version < Gem::Version.new('4.2') + :deliver + else + :deliver_now + end email_notifier = ExceptionNotifier::EmailNotifier.new( email_prefix: '[Dummy ERROR] ', - sender_address: %{"Dummy Notifier" }, - exception_recipients: %w{dummyexceptions@example.com}, + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], deliver_with: deliver_with ) @@ -206,32 +206,32 @@ class EmailNotifierTest < ActiveSupport::TestCase assert_equal 1, ActionMailer::Base.deliveries.count end - test "should lazily evaluate exception_recipients" do - exception_recipients = %w{first@example.com second@example.com} + test 'should lazily evaluate exception_recipients' do + exception_recipients = %w[first@example.com second@example.com] email_notifier = ExceptionNotifier::EmailNotifier.new( email_prefix: '[Dummy ERROR] ', - sender_address: %{"Dummy Notifier" }, - exception_recipients: -> { [ exception_recipients.shift ] }, + sender_address: %("Dummy Notifier" ), + exception_recipients: -> { [exception_recipients.shift] }, delivery_method: :test ) mail = email_notifier.call(@exception) - assert_equal %w{first@example.com}, mail.to + assert_equal %w[first@example.com], mail.to mail = email_notifier.call(@exception) - assert_equal %w{second@example.com}, mail.to + assert_equal %w[second@example.com], mail.to end - test "should prepend accumulated_errors_count in email subject if accumulated_errors_count larger than 1" do + test 'should prepend accumulated_errors_count in email subject if accumulated_errors_count larger than 1' do ActionMailer::Base.deliveries.clear email_notifier = ExceptionNotifier::EmailNotifier.new( email_prefix: '[Dummy ERROR] ', - sender_address: %{"Dummy Notifier" }, - exception_recipients: %w{dummyexceptions@example.com}, + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], delivery_method: :test ) - mail = email_notifier.call(@exception, { accumulated_errors_count: 3 }) - assert mail.subject.start_with?("[Dummy ERROR] (3 times) (ZeroDivisionError)") + mail = email_notifier.call(@exception, accumulated_errors_count: 3) + assert mail.subject.start_with?('[Dummy ERROR] (3 times) (ZeroDivisionError)') end end diff --git a/test/exception_notifier/google_chat_notifier_test.rb b/test/exception_notifier/google_chat_notifier_test.rb index 3da08add..ad39304f 100644 --- a/test/exception_notifier/google_chat_notifier_test.rb +++ b/test/exception_notifier/google_chat_notifier_test.rb @@ -68,9 +68,9 @@ def teardown exception = ArgumentError.new('foo') exception.set_backtrace([ - "app/controllers/my_controller.rb:53:in `my_controller_params'", - "app/controllers/my_controller.rb:34:in `update'" - ]) + "app/controllers/my_controller.rb:53:in `my_controller_params'", + "app/controllers/my_controller.rb:34:in `update'" + ]) notifier.call(exception) end @@ -92,11 +92,11 @@ def teardown exception = ArgumentError.new('foo') exception.set_backtrace([ - "app/controllers/my_controller.rb:99:in `specific_function'", - "app/controllers/my_controller.rb:70:in `specific_param'", - "app/controllers/my_controller.rb:53:in `my_controller_params'", - "app/controllers/my_controller.rb:34:in `update'" - ]) + "app/controllers/my_controller.rb:99:in `specific_function'", + "app/controllers/my_controller.rb:70:in `specific_param'", + "app/controllers/my_controller.rb:53:in `my_controller_params'", + "app/controllers/my_controller.rb:34:in `update'" + ]) notifier.call(exception) end @@ -126,9 +126,9 @@ def teardown exception = ArgumentError.new('foo') exception.set_backtrace([ - "app/controllers/my_controller.rb:53:in `my_controller_params'", - "app/controllers/my_controller.rb:34:in `update'" - ]) + "app/controllers/my_controller.rb:53:in `my_controller_params'", + "app/controllers/my_controller.rb:34:in `update'" + ]) notifier.call(exception, env: test_env) end @@ -160,7 +160,7 @@ def header [ '', 'Application: *dummy*', - "An *ArgumentError* occured.", + 'An *ArgumentError* occured.', '' ].join("\n") end diff --git a/test/exception_notifier/hipchat_notifier_test.rb b/test/exception_notifier/hipchat_notifier_test.rb index 2a47e7a1..c69d7ab0 100644 --- a/test/exception_notifier/hipchat_notifier_test.rb +++ b/test/exception_notifier/hipchat_notifier_test.rb @@ -8,22 +8,22 @@ end class HipchatNotifierTest < ActiveSupport::TestCase - - test "should send hipchat notification if properly configured" do + test 'should send hipchat notification if properly configured' do options = { api_token: 'good_token', room_name: 'room_name', - color: 'yellow', + color: 'yellow' } - HipChat::Room.any_instance.expects(:send).with('Exception', fake_body, { color: 'yellow' }) + HipChat::Room.any_instance.expects(:send).with('Exception', fake_body, color: 'yellow') hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) end - test "should call pre/post_callback if specified" do - pre_callback_called, post_callback_called = 0, 0 + test 'should call pre/post_callback if specified' do + pre_callback_called = 0 + post_callback_called = 0 options = { api_token: 'good_token', room_name: 'room_name', @@ -40,63 +40,63 @@ class HipchatNotifierTest < ActiveSupport::TestCase assert_equal(1, post_callback_called) end - test "should send hipchat notification without backtrace info if properly configured" do + test 'should send hipchat notification without backtrace info if properly configured' do options = { api_token: 'good_token', room_name: 'room_name', - color: 'yellow', + color: 'yellow' } - HipChat::Room.any_instance.expects(:send).with('Exception', fake_body_without_backtrace, { color: 'yellow' }) + HipChat::Room.any_instance.expects(:send).with('Exception', fake_body_without_backtrace, color: 'yellow') hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception_without_backtrace) end - test "should allow custom from value if set" do + test 'should allow custom from value if set' do options = { api_token: 'good_token', room_name: 'room_name', - from: 'TrollFace', + from: 'TrollFace' } - HipChat::Room.any_instance.expects(:send).with('TrollFace', fake_body, { color: 'red' }) + HipChat::Room.any_instance.expects(:send).with('TrollFace', fake_body, color: 'red') hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) end - test "should not send hipchat notification if badly configured" do + test 'should not send hipchat notification if badly configured' do wrong_params = { api_token: 'bad_token', room_name: 'test_room' } - HipChat::Client.stubs(:new).with('bad_token', { api_version: 'v1' }).returns(nil) + HipChat::Client.stubs(:new).with('bad_token', api_version: 'v1').returns(nil) hipchat = ExceptionNotifier::HipchatNotifier.new(wrong_params) assert_nil hipchat.room end - test "should not send hipchat notification if api_key is missing" do - wrong_params = { room_name: 'test_room' } + test 'should not send hipchat notification if api_key is missing' do + wrong_params = { room_name: 'test_room' } - HipChat::Client.stubs(:new).with(nil, {api_version: 'v1'}).returns(nil) + HipChat::Client.stubs(:new).with(nil, api_version: 'v1').returns(nil) hipchat = ExceptionNotifier::HipchatNotifier.new(wrong_params) assert_nil hipchat.room end - test "should not send hipchat notification if room_name is missing" do - wrong_params = { api_token: 'good_token' } + test 'should not send hipchat notification if room_name is missing' do + wrong_params = { api_token: 'good_token' } - HipChat::Client.stubs(:new).with('good_token', { api_version: 'v1' }).returns({}) + HipChat::Client.stubs(:new).with('good_token', api_version: 'v1').returns({}) hipchat = ExceptionNotifier::HipchatNotifier.new(wrong_params) assert_nil hipchat.room end - test "should send hipchat notification with message_template" do + test 'should send hipchat notification with message_template' do options = { api_token: 'good_token', room_name: 'room_name', @@ -104,86 +104,86 @@ class HipchatNotifierTest < ActiveSupport::TestCase message_template: ->(exception, _) { "This is custom message: '#{exception.message}'" } } - HipChat::Room.any_instance.expects(:send).with('Exception', "This is custom message: '#{fake_exception.message}'", { color: 'yellow' }) + HipChat::Room.any_instance.expects(:send).with('Exception', "This is custom message: '#{fake_exception.message}'", color: 'yellow') hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) end - test "should send hipchat notification exclude accumulated errors count" do + test 'should send hipchat notification exclude accumulated errors count' do options = { api_token: 'good_token', room_name: 'room_name', color: 'yellow' } - HipChat::Room.any_instance.expects(:send).with{ |_, msg, _| msg.start_with?("A new exception occurred:") } + HipChat::Room.any_instance.expects(:send).with { |_, msg, _| msg.start_with?('A new exception occurred:') } hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) end - test "should send hipchat notification include accumulated errors count" do + test 'should send hipchat notification include accumulated errors count' do options = { api_token: 'good_token', room_name: 'room_name', color: 'yellow' } - HipChat::Room.any_instance.expects(:send).with{ |_, msg, _| msg.start_with?("The exception occurred 3 times:") } + HipChat::Room.any_instance.expects(:send).with { |_, msg, _| msg.start_with?('The exception occurred 3 times:') } hipchat = ExceptionNotifier::HipchatNotifier.new(options) - hipchat.call(fake_exception, { accumulated_errors_count: 3 }) + hipchat.call(fake_exception, accumulated_errors_count: 3) end - test "should send hipchat notification with HTML-escaped meessage if using default message_template" do + test 'should send hipchat notification with HTML-escaped meessage if using default message_template' do options = { api_token: 'good_token', room_name: 'room_name', - color: 'yellow', + color: 'yellow' } exception = fake_exception_with_html_characters body = "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}' on '#{exception.backtrace.first}'" - HipChat::Room.any_instance.expects(:send).with('Exception', body, { color: 'yellow' }) + HipChat::Room.any_instance.expects(:send).with('Exception', body, color: 'yellow') hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(exception) end - test "should use APIv1 if api_version is not specified" do + test 'should use APIv1 if api_version is not specified' do options = { api_token: 'good_token', - room_name: 'room_name', + room_name: 'room_name' } - HipChat::Client.stubs(:new).with('good_token', { api_version: 'v1' }).returns({}) + HipChat::Client.stubs(:new).with('good_token', api_version: 'v1').returns({}) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) end - test "should use APIv2 when specified" do + test 'should use APIv2 when specified' do options = { api_token: 'good_token', room_name: 'room_name', - api_version: 'v2', + api_version: 'v2' } - HipChat::Client.stubs(:new).with('good_token', { api_version: 'v2' }).returns({}) + HipChat::Client.stubs(:new).with('good_token', api_version: 'v2').returns({}) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) end - test "should allow server_url value (for a self-hosted HipChat Server) if set" do + test 'should allow server_url value (for a self-hosted HipChat Server) if set' do options = { api_token: 'good_token', room_name: 'room_name', api_version: 'v2', - server_url: 'https://domain.com', + server_url: 'https://domain.com' } - HipChat::Client.stubs(:new).with('good_token', { api_version: 'v2', server_url: 'https://domain.com' }).returns({}) + HipChat::Client.stubs(:new).with('good_token', api_version: 'v2', server_url: 'https://domain.com').returns({}) hipchat = ExceptionNotifier::HipchatNotifier.new(options) hipchat.call(fake_exception) @@ -196,19 +196,15 @@ def fake_body end def fake_exception - begin - 5/0 - rescue Exception => e - e - end + 5 / 0 + rescue Exception => e + e end def fake_exception_with_html_characters - begin - raise StandardError.new('an error with characters') - rescue Exception => e - e - end + raise StandardError, 'an error with characters' + rescue Exception => e + e end def fake_body_without_backtrace diff --git a/test/exception_notifier/irc_notifier_test.rb b/test/exception_notifier/irc_notifier_test.rb index 08983988..dacb989d 100644 --- a/test/exception_notifier/irc_notifier_test.rb +++ b/test/exception_notifier/irc_notifier_test.rb @@ -2,8 +2,7 @@ require 'carrier-pigeon' class IrcNotifierTest < ActiveSupport::TestCase - - test "should send irc notification if properly configured" do + test 'should send irc notification if properly configured' do options = { domain: 'irc.example.com' } @@ -16,24 +15,25 @@ class IrcNotifierTest < ActiveSupport::TestCase irc.call(fake_exception) end - test "should exclude errors count in message if :accumulated_errors_count nil" do + test 'should exclude errors count in message if :accumulated_errors_count nil' do irc = ExceptionNotifier::IrcNotifier.new({}) irc.stubs(:active?).returns(true) - irc.expects(:send_message).with{ |message| message.include?("divided by 0") }.once + irc.expects(:send_message).with { |message| message.include?('divided by 0') }.once irc.call(fake_exception) end - test "should include errors count in message if :accumulated_errors_count is 3" do + test 'should include errors count in message if :accumulated_errors_count is 3' do irc = ExceptionNotifier::IrcNotifier.new({}) irc.stubs(:active?).returns(true) - irc.expects(:send_message).with{ |message| message.include?("(3 times)'divided by 0'") }.once + irc.expects(:send_message).with { |message| message.include?("(3 times)'divided by 0'") }.once irc.call(fake_exception, accumulated_errors_count: 3) end - test "should call pre/post_callback if specified" do - pre_callback_called, post_callback_called = 0,0 + test 'should call pre/post_callback if specified' do + pre_callback_called = 0 + post_callback_called = 0 options = { domain: 'irc.example.com', @@ -51,7 +51,7 @@ class IrcNotifierTest < ActiveSupport::TestCase assert_equal(1, post_callback_called) end - test "should send irc notification without backtrace info if properly configured" do + test 'should send irc notification without backtrace info if properly configured' do options = { domain: 'irc.example.com' } @@ -64,7 +64,7 @@ class IrcNotifierTest < ActiveSupport::TestCase irc.call(fake_exception_without_backtrace) end - test "should properly construct URI from constituent parts" do + test 'should properly construct URI from constituent parts' do options = { nick: 'BadNewsBot', password: 'secret', @@ -73,16 +73,16 @@ class IrcNotifierTest < ActiveSupport::TestCase channel: '#exceptions' } - CarrierPigeon.expects(:send).with(has_entry(uri: "irc://BadNewsBot:secret@irc.example.com:9999/#exceptions")) + CarrierPigeon.expects(:send).with(has_entry(uri: 'irc://BadNewsBot:secret@irc.example.com:9999/#exceptions')) irc = ExceptionNotifier::IrcNotifier.new(options) irc.call(fake_exception) end - test "should properly add recipients if specified" do + test 'should properly add recipients if specified' do options = { domain: 'irc.example.com', - recipients: ['peter', 'michael', 'samir'] + recipients: %w[peter michael samir] } CarrierPigeon.expects(:send).with(has_key(:uri)) do |v| @@ -93,7 +93,7 @@ class IrcNotifierTest < ActiveSupport::TestCase irc.call(fake_exception) end - test "should properly set miscellaneous options" do + test 'should properly set miscellaneous options' do options = { domain: 'irc.example.com', ssl: true, @@ -103,10 +103,10 @@ class IrcNotifierTest < ActiveSupport::TestCase } CarrierPigeon.expects(:send).with(has_entries( - ssl: true, - join: true, - notice: true, - )) do |v| + ssl: true, + join: true, + notice: true + )) do |v| /\[test notification\]/.match(v[:message]) end @@ -114,8 +114,8 @@ class IrcNotifierTest < ActiveSupport::TestCase irc.call(fake_exception) end - test "should not send irc notification if badly configured" do - wrong_params = { domain: '##scriptkiddie.com###'} + test 'should not send irc notification if badly configured' do + wrong_params = { domain: '##scriptkiddie.com###' } irc = ExceptionNotifier::IrcNotifier.new(wrong_params) assert_nil irc.call(fake_exception) @@ -124,11 +124,9 @@ class IrcNotifierTest < ActiveSupport::TestCase private def fake_exception - begin - 5/0 - rescue Exception => e - e - end + 5 / 0 + rescue Exception => e + e end def fake_exception_without_backtrace diff --git a/test/exception_notifier/mattermost_notifier_test.rb b/test/exception_notifier/mattermost_notifier_test.rb index 76127cd1..f2404e23 100644 --- a/test/exception_notifier/mattermost_notifier_test.rb +++ b/test/exception_notifier/mattermost_notifier_test.rb @@ -2,18 +2,18 @@ require 'httparty' class MattermostNotifierTest < ActiveSupport::TestCase - test "should send notification if properly configured" do + test 'should send notification if properly configured' do options = { webhook_url: 'http://localhost:8000' } mattermost_notifier = ExceptionNotifier::MattermostNotifier.new mattermost_notifier.httparty = FakeHTTParty.new - options = mattermost_notifier.call ArgumentError.new("foo"), options + options = mattermost_notifier.call ArgumentError.new('foo'), options body = ActiveSupport::JSON.decode options[:body] - assert body.has_key? 'text' - assert body.has_key? 'username' + assert body.key? 'text' + assert body.key? 'username' text = body['text'].split("\n") assert_equal 4, text.size @@ -22,7 +22,7 @@ class MattermostNotifierTest < ActiveSupport::TestCase assert_equal '*foo*', text[3] end - test "should send notification with create issue link if specified" do + test 'should send notification with create issue link if specified' do options = { webhook_url: 'http://localhost:8000', git_url: 'github.com/aschen' @@ -30,7 +30,7 @@ class MattermostNotifierTest < ActiveSupport::TestCase mattermost_notifier = ExceptionNotifier::MattermostNotifier.new mattermost_notifier.httparty = FakeHTTParty.new - options = mattermost_notifier.call ArgumentError.new("foo"), options + options = mattermost_notifier.call ArgumentError.new('foo'), options body = ActiveSupport::JSON.decode options[:body] @@ -48,7 +48,7 @@ class MattermostNotifierTest < ActiveSupport::TestCase mattermost_notifier = ExceptionNotifier::MattermostNotifier.new mattermost_notifier.httparty = FakeHTTParty.new - options = mattermost_notifier.call ArgumentError.new("foo"), options + options = mattermost_notifier.call ArgumentError.new('foo'), options body = ActiveSupport::JSON.decode options[:body] @@ -69,34 +69,34 @@ class MattermostNotifierTest < ActiveSupport::TestCase mattermost_notifier = ExceptionNotifier::MattermostNotifier.new mattermost_notifier.httparty = FakeHTTParty.new - options = mattermost_notifier.call ArgumentError.new("foo"), options + options = mattermost_notifier.call ArgumentError.new('foo'), options - assert options.has_key? :basic_auth + assert options.key? :basic_auth assert 'clara', options[:basic_auth][:username] assert 'password', options[:basic_auth][:password] end test "should use 'An' for exceptions count if :accumulated_errors_count option is nil" do mattermost_notifier = ExceptionNotifier::MattermostNotifier.new - exception = ArgumentError.new("foo") + exception = ArgumentError.new('foo') mattermost_notifier.instance_variable_set(:@exception, exception) mattermost_notifier.instance_variable_set(:@options, {}) - assert_includes mattermost_notifier.send(:message_header), "An *ArgumentError* occured." + assert_includes mattermost_notifier.send(:message_header), 'An *ArgumentError* occured.' end - test "shoud use direct errors count if :accumulated_errors_count option is 5" do + test 'shoud use direct errors count if :accumulated_errors_count option is 5' do mattermost_notifier = ExceptionNotifier::MattermostNotifier.new - exception = ArgumentError.new("foo") + exception = ArgumentError.new('foo') mattermost_notifier.instance_variable_set(:@exception, exception) - mattermost_notifier.instance_variable_set(:@options, { accumulated_errors_count: 5 }) + mattermost_notifier.instance_variable_set(:@options, accumulated_errors_count: 5) - assert_includes mattermost_notifier.send(:message_header), "5 *ArgumentError* occured." + assert_includes mattermost_notifier.send(:message_header), '5 *ArgumentError* occured.' end end class FakeHTTParty - def post(url, options) - return options + def post(_url, options) + options end end diff --git a/test/exception_notifier/modules/error_grouping_test.rb b/test/exception_notifier/modules/error_grouping_test.rb index 0b4c994f..56270e07 100644 --- a/test/exception_notifier/modules/error_grouping_test.rb +++ b/test/exception_notifier/modules/error_grouping_test.rb @@ -1,18 +1,17 @@ require 'test_helper' class ErrorGroupTest < ActiveSupport::TestCase - setup do module TestModule include ExceptionNotifier::ErrorGrouping - @@error_grouping_cache = ActiveSupport::Cache::FileStore.new("test/dummy/tmp/non_default_location") + @@error_grouping_cache = ActiveSupport::Cache::FileStore.new('test/dummy/tmp/non_default_location') end - @exception = RuntimeError.new("ERROR") - @exception.stubs(:backtrace).returns(["/path/where/error/raised:1"]) + @exception = RuntimeError.new('ERROR') + @exception.stubs(:backtrace).returns(['/path/where/error/raised:1']) - @exception2 = RuntimeError.new("ERROR2") - @exception2.stubs(:backtrace).returns(["/path/where/error/found:2"]) + @exception2 = RuntimeError.new('ERROR2') + @exception2.stubs(:backtrace).returns(['/path/where/error/found:2']) end teardown do @@ -20,88 +19,88 @@ module TestModule TestModule.fallback_cache_store.clear end - test "should add additional option: error_grouping" do + test 'should add additional option: error_grouping' do assert_respond_to TestModule, :error_grouping assert_respond_to TestModule, :error_grouping= end - test "should set error_grouping to false default" do + test 'should set error_grouping to false default' do assert_equal false, TestModule.error_grouping end - test "should add additional option: error_grouping_cache" do + test 'should add additional option: error_grouping_cache' do assert_respond_to TestModule, :error_grouping_cache assert_respond_to TestModule, :error_grouping_cache= end - test "should add additional option: error_grouping_period" do + test 'should add additional option: error_grouping_period' do assert_respond_to TestModule, :error_grouping_period assert_respond_to TestModule, :error_grouping_period= end - test "shoud set error_grouping_period to 5.minutes default" do + test 'shoud set error_grouping_period to 5.minutes default' do assert_equal 300, TestModule.error_grouping_period end - test "should add additional option: notification_trigger" do + test 'should add additional option: notification_trigger' do assert_respond_to TestModule, :notification_trigger assert_respond_to TestModule, :notification_trigger= end - test "should return errors count nil when not same error for .error_count" do - assert_nil TestModule.error_count("something") + test 'should return errors count nil when not same error for .error_count' do + assert_nil TestModule.error_count('something') end - test "should return errors count when same error for .error_count" do - TestModule.error_grouping_cache.write("error_key", 13) - assert_equal 13, TestModule.error_count("error_key") + test 'should return errors count when same error for .error_count' do + TestModule.error_grouping_cache.write('error_key', 13) + assert_equal 13, TestModule.error_count('error_key') end - test "should fallback to memory store cache if specified cache store failed to read" do - TestModule.error_grouping_cache.stubs(:read).raises(RuntimeError.new "Failed to read") + test 'should fallback to memory store cache if specified cache store failed to read' do + TestModule.error_grouping_cache.stubs(:read).raises(RuntimeError.new('Failed to read')) original_fallback = TestModule.fallback_cache_store TestModule.expects(:fallback_cache_store).returns(original_fallback).at_least_once - assert_nil TestModule.error_count("something_to_read") + assert_nil TestModule.error_count('something_to_read') end - test "should save error with count for .save_error_count" do + test 'should save error with count for .save_error_count' do count = rand(1..10) - TestModule.save_error_count("error_key", count) - assert_equal count, TestModule.error_grouping_cache.read("error_key") + TestModule.save_error_count('error_key', count) + assert_equal count, TestModule.error_grouping_cache.read('error_key') end - test "should fallback to memory store cache if specified cache store failed to write" do - TestModule.error_grouping_cache.stubs(:write).raises(RuntimeError.new "Failed to write") + test 'should fallback to memory store cache if specified cache store failed to write' do + TestModule.error_grouping_cache.stubs(:write).raises(RuntimeError.new('Failed to write')) original_fallback = TestModule.fallback_cache_store TestModule.expects(:fallback_cache_store).returns(original_fallback).at_least_once - assert TestModule.save_error_count("something_to_cache", rand(1..10)) + assert TestModule.save_error_count('something_to_cache', rand(1..10)) end - test "should save accumulated_errors_count into options" do + test 'should save accumulated_errors_count into options' do options = {} TestModule.group_error!(@exception, options) assert_equal 1, options[:accumulated_errors_count] end - test "should not group error if different exception in .group_error!" do + test 'should not group error if different exception in .group_error!' do options1 = {} - TestModule.expects(:save_error_count).with{|key, count| key.is_a?(String) && count == 1}.times(4).returns(true) + TestModule.expects(:save_error_count).with { |key, count| key.is_a?(String) && count == 1 }.times(4).returns(true) TestModule.group_error!(@exception, options1) options2 = {} - TestModule.group_error!(NoMethodError.new("method not found"), options2) + TestModule.group_error!(NoMethodError.new('method not found'), options2) assert_equal 1, options1[:accumulated_errors_count] assert_equal 1, options2[:accumulated_errors_count] end - test "should not group error is same exception but different message or backtrace" do + test 'should not group error is same exception but different message or backtrace' do options1 = {} - TestModule.expects(:save_error_count).with{|key, count| key.is_a?(String) && count == 1}.times(4).returns(true) + TestModule.expects(:save_error_count).with { |key, count| key.is_a?(String) && count == 1 }.times(4).returns(true) TestModule.group_error!(@exception, options1) options2 = {} @@ -111,7 +110,7 @@ module TestModule assert_equal 1, options2[:accumulated_errors_count] end - test "should group error if same exception and message" do + test 'should group error if same exception and message' do options = {} 10.times do |i| @@ -122,7 +121,7 @@ module TestModule assert_equal 10, options[:accumulated_errors_count] end - test "should group error if same exception and backtrace" do + test 'should group error if same exception and backtrace' do options = {} 10.times do |i| @@ -133,7 +132,7 @@ module TestModule assert_equal 10, options[:accumulated_errors_count] end - test "should group error by that message have high priority" do + test 'should group error by that message have high priority' do message_based_key = "exception:#{Zlib.crc32("RuntimeError\nmessage:ERROR")}" backtrace_based_key = "exception:#{Zlib.crc32("RuntimeError\n/path/where/error/raised:1")}" @@ -146,7 +145,7 @@ module TestModule TestModule.group_error!(@exception, {}) end - test "use default formula if not specify notification_trigger in .send_notification?" do + test 'use default formula if not specify notification_trigger in .send_notification?' do TestModule.stubs(:notification_trigger).returns(nil) count = 16 @@ -155,12 +154,12 @@ module TestModule assert TestModule.send_notification?(@exception, count) end - test "use specified trigger in .send_notification?" do - trigger = Proc.new { |exception, count| count % 4 == 0 } + test 'use specified trigger in .send_notification?' do + trigger = proc { |_exception, count| count % 4 == 0 } TestModule.stubs(:notification_trigger).returns(trigger) count = 16 trigger.expects(:call).with(@exception, count).returns(true) assert TestModule.send_notification?(@exception, count) end -end \ No newline at end of file +end diff --git a/test/exception_notifier/sidekiq_test.rb b/test/exception_notifier/sidekiq_test.rb index 0e58c4ab..6674240b 100644 --- a/test/exception_notifier/sidekiq_test.rb +++ b/test/exception_notifier/sidekiq_test.rb @@ -1,11 +1,11 @@ -require "test_helper" +require 'test_helper' # To allow sidekiq error handlers to be registered, sidekiq must be in # "server mode". This mode is triggered by loading sidekiq/cli. Note this # has to be loaded before exception_notification/sidekiq. -require "sidekiq/cli" +require 'sidekiq/cli' -require "exception_notification/sidekiq" +require 'exception_notification/sidekiq' class MockSidekiqServer include ::Sidekiq::ExceptionHandler @@ -19,9 +19,9 @@ class SidekiqTest < ActiveSupport::TestCase Sidekiq::Logging.logger = nil end - test "should call notify_exception when sidekiq raises an error" do + test 'should call notify_exception when sidekiq raises an error' do server = MockSidekiqServer.new - message = Hash.new + message = {} exception = RuntimeError.new ExceptionNotifier.expects(:notify_exception).with( diff --git a/test/exception_notifier/slack_notifier_test.rb b/test/exception_notifier/slack_notifier_test.rb index a649076a..c4636674 100644 --- a/test/exception_notifier/slack_notifier_test.rb +++ b/test/exception_notifier/slack_notifier_test.rb @@ -2,7 +2,6 @@ require 'slack-notifier' class SlackNotifierTest < ActiveSupport::TestCase - def setup @exception = fake_exception @exception.stubs(:backtrace).returns(fake_backtrace) @@ -10,9 +9,9 @@ def setup Socket.stubs(:gethostname).returns('example.com') end - test "should send a slack notification if properly configured" do + test 'should send a slack notification if properly configured' do options = { - webhook_url: "http://slack.webhook.url" + webhook_url: 'http://slack.webhook.url' } Slack::Notifier.any_instance.expects(:ping).with('', fake_notification) @@ -21,9 +20,9 @@ def setup slack_notifier.call(@exception) end - test "should send a slack notification without backtrace info if properly configured" do + test 'should send a slack notification without backtrace info if properly configured' do options = { - webhook_url: "http://slack.webhook.url" + webhook_url: 'http://slack.webhook.url' } Slack::Notifier.any_instance.expects(:ping).with('', fake_notification(fake_exception_without_backtrace)) @@ -32,10 +31,10 @@ def setup slack_notifier.call(fake_exception_without_backtrace) end - test "should send the notification to the specified channel" do + test 'should send the notification to the specified channel' do options = { - webhook_url: "http://slack.webhook.url", - channel: "channel" + webhook_url: 'http://slack.webhook.url', + channel: 'channel' } Slack::Notifier.any_instance.expects(:ping).with('', fake_notification) @@ -47,10 +46,10 @@ def setup assert_equal channel, options[:channel] end - test "should send the notification to the specified username" do + test 'should send the notification to the specified username' do options = { - webhook_url: "http://slack.webhook.url", - username: "username" + webhook_url: 'http://slack.webhook.url', + username: 'username' } Slack::Notifier.any_instance.expects(:ping).with('', fake_notification) @@ -62,9 +61,9 @@ def setup assert_equal username, options[:username] end - test "should send the notification with specific backtrace lines" do + test 'should send the notification with specific backtrace lines' do options = { - webhook_url: "http://slack.webhook.url", + webhook_url: 'http://slack.webhook.url', backtrace_lines: 1 } @@ -74,10 +73,10 @@ def setup slack_notifier.call(@exception) end - test "should send the notification with additional fields" do - field = {title: "Branch", value: "master", short: true} + test 'should send the notification with additional fields' do + field = { title: 'Branch', value: 'master', short: true } options = { - webhook_url: "http://slack.webhook.url", + webhook_url: 'http://slack.webhook.url', additional_fields: [field] } @@ -90,17 +89,17 @@ def setup assert_equal additional_fields, options[:additional_fields] end - test "should pass the additional parameters to Slack::Notifier.ping" do + test 'should pass the additional parameters to Slack::Notifier.ping' do options = { - webhook_url: "http://slack.webhook.url", - username: "test", - custom_hook: "hook", + webhook_url: 'http://slack.webhook.url', + username: 'test', + custom_hook: 'hook', additional_parameters: { - icon_url: "icon", + icon_url: 'icon' } } - Slack::Notifier.any_instance.expects(:ping).with('', options[:additional_parameters].merge(fake_notification) ) + Slack::Notifier.any_instance.expects(:ping).with('', options[:additional_parameters].merge(fake_notification)) slack_notifier = ExceptionNotifier::SlackNotifier.new(options) slack_notifier.call(@exception) @@ -115,11 +114,11 @@ def setup assert_nil slack_notifier.call(@exception) end - test "should pass along environment data" do + test 'should pass along environment data' do options = { - webhook_url: "http://slack.webhook.url", - ignore_data_if: lambda {|k,v| - "#{k}" == 'key_to_be_ignored' || v.is_a?(Hash) + webhook_url: 'http://slack.webhook.url', + ignore_data_if: lambda { |k, v| + k.to_s == 'key_to_be_ignored' || v.is_a?(Hash) } } @@ -130,7 +129,7 @@ def setup data: { 'user_id' => 5, 'key_to_be_ignored' => 'whatever', - 'ignore_as_well' => { what: 'ever'} + 'ignore_as_well' => { what: 'ever' } } } @@ -141,29 +140,29 @@ def setup slack_notifier.call(@exception, notification_options) end - test "should call pre/post_callback proc if specified" do + test 'should call pre/post_callback proc if specified' do post_callback_called = 0 options = { - webhook_url: "http://slack.webhook.url", - username: "test", - custom_hook: "hook", - pre_callback: proc { |opts, notifier, backtrace, message, message_opts| - (message_opts[:attachments] = []) << { text: "#{backtrace.join("\n")}", color: 'danger' } + webhook_url: 'http://slack.webhook.url', + username: 'test', + custom_hook: 'hook', + pre_callback: proc { |_opts, _notifier, backtrace, _message, message_opts| + (message_opts[:attachments] = []) << { text: backtrace.join("\n").to_s, color: 'danger' } }, - post_callback: proc { |opts, notifier, backtrace, message, message_opts| + post_callback: proc { |_opts, _notifier, _backtrace, _message, _message_opts| post_callback_called = 1 }, additional_parameters: { - icon_url: "icon", + icon_url: 'icon' } } - Slack::Notifier.any_instance.expects(:ping).with('', { - icon_url: 'icon', - attachments: [{ - text: fake_backtrace.join("\n"), - color: 'danger' } - ]}) + Slack::Notifier.any_instance.expects(:ping).with('', + icon_url: 'icon', + attachments: [{ + text: fake_backtrace.join("\n"), + color: 'danger' + }]) slack_notifier = ExceptionNotifier::SlackNotifier.new(options) slack_notifier.call(@exception) @@ -173,11 +172,9 @@ def setup private def fake_exception - begin - 5/0 - rescue Exception => e - e - end + 5 / 0 + rescue Exception => e + e end def fake_exception_without_backtrace @@ -186,17 +183,17 @@ def fake_exception_without_backtrace def fake_backtrace [ - "backtrace line 1", - "backtrace line 2", - "backtrace line 3", - "backtrace line 4", - "backtrace line 5", - "backtrace line 6", + 'backtrace line 1', + 'backtrace line 2', + 'backtrace line 3', + 'backtrace line 4', + 'backtrace line 5', + 'backtrace line 6' ] end def fake_notification(exception = @exception, notification_options = {}, data_string = nil, expected_backtrace_lines = 10, additional_fields = []) - exception_name = "*#{exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'}* `#{exception.class.to_s}`" + exception_name = "*#{exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'}* `#{exception.class}`" if notification_options[:env].nil? text = "#{exception_name} *occured in background*" else @@ -211,16 +208,15 @@ def fake_notification(exception = @exception, notification_options = {}, data_st text += "\n" - fields = [ { title: 'Exception', value: exception.message} ] - fields.push({ title: 'Hostname', value: 'example.com' }) + fields = [{ title: 'Exception', value: exception.message }] + fields.push(title: 'Hostname', value: 'example.com') if exception.backtrace formatted_backtrace = "```#{exception.backtrace.first(expected_backtrace_lines).join("\n")}```" - fields.push({ title: 'Backtrace', value: formatted_backtrace }) + fields.push(title: 'Backtrace', value: formatted_backtrace) end - fields.push({ title: 'Data', value: "```#{data_string}```" }) if data_string + fields.push(title: 'Data', value: "```#{data_string}```") if data_string additional_fields.each { |f| fields.push(f) } - { attachments: [ color: 'danger', text: text, fields: fields, mrkdwn_in: %w(text fields) ] } + { attachments: [color: 'danger', text: text, fields: fields, mrkdwn_in: %w[text fields]] } end - end diff --git a/test/exception_notifier/sns_notifier_test.rb b/test/exception_notifier/sns_notifier_test.rb index 382aabef..a68813ab 100644 --- a/test/exception_notifier/sns_notifier_test.rb +++ b/test/exception_notifier/sns_notifier_test.rb @@ -12,7 +12,7 @@ def setup secret_access_key: 'my-secret_access_key', region: 'us-east', topic_arn: 'topicARN', - sns_prefix: '[App Exception]', + sns_prefix: '[App Exception]' } Socket.stubs(:gethostname).returns('example.com') end @@ -60,41 +60,38 @@ def setup test 'should send a sns notification in background' do Aws::SNS::Client.any_instance.expects(:publish).with( - { - topic_arn: "topicARN", + topic_arn: 'topicARN', message: "3 MyException occured in background\n"\ - "Exception: undefined method 'method=' for Empty\n"\ - "Hostname: example.com\n"\ - "Backtrace:\n#{fake_backtrace.join("\n")}\n", - subject: "[App Exception] - 3 MyException occurred" - }) + "Exception: undefined method 'method=' for Empty\n"\ + "Hostname: example.com\n"\ + "Backtrace:\n#{fake_backtrace.join("\n")}\n", + subject: '[App Exception] - 3 MyException occurred' + ) sns_notifier = ExceptionNotifier::SnsNotifier.new(@options) - sns_notifier.call(@exception, { accumulated_errors_count: 3 }) + sns_notifier.call(@exception, accumulated_errors_count: 3) end test 'should send a sns notification with controller#action information' do ExamplesController.any_instance.stubs(:action_name).returns('index') Aws::SNS::Client.any_instance.expects(:publish).with( - { - topic_arn: "topicARN", - message: "A MyException occurred while GET "\ - "was processed by examples#index\n"\ - "Exception: undefined method 'method=' for Empty\n"\ - "Hostname: example.com\n"\ - "Backtrace:\n#{fake_backtrace.join("\n")}\n", - subject: "[App Exception] - A MyException occurred" - }) + topic_arn: 'topicARN', + message: 'A MyException occurred while GET '\ + "was processed by examples#index\n"\ + "Exception: undefined method 'method=' for Empty\n"\ + "Hostname: example.com\n"\ + "Backtrace:\n#{fake_backtrace.join("\n")}\n", + subject: '[App Exception] - A MyException occurred' + ) sns_notifier = ExceptionNotifier::SnsNotifier.new(@options) sns_notifier.call(@exception, - env: { - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/examples', - 'action_controller.instance' => ExamplesController.new - } - ) + env: { + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/examples', + 'action_controller.instance' => ExamplesController.new + }) end private @@ -102,11 +99,9 @@ def setup class ExamplesController < ActionController::Base; end def fake_exception - begin - 1 / 0 - rescue Exception => e - e - end + 1 / 0 + rescue Exception => e + e end def fake_exception_without_backtrace diff --git a/test/exception_notifier/teams_notifier_test.rb b/test/exception_notifier/teams_notifier_test.rb index df98206a..d21b800b 100644 --- a/test/exception_notifier/teams_notifier_test.rb +++ b/test/exception_notifier/teams_notifier_test.rb @@ -2,19 +2,18 @@ require 'httparty' class TeamsNotifierTest < ActiveSupport::TestCase - - test "should send notification if properly configured" do + test 'should send notification if properly configured' do options = { webhook_url: 'http://localhost:8000' } teams_notifier = ExceptionNotifier::TeamsNotifier.new teams_notifier.httparty = FakeHTTParty.new - options = teams_notifier.call ArgumentError.new("foo"), options + options = teams_notifier.call ArgumentError.new('foo'), options body = ActiveSupport::JSON.decode options[:body] - assert body.has_key? 'title' - assert body.has_key? 'sections' + assert body.key? 'title' + assert body.key? 'sections' sections = body['sections'] header = sections[0] @@ -24,7 +23,7 @@ class TeamsNotifierTest < ActiveSupport::TestCase assert_equal 'foo', header['activitySubtitle'] end - test "should send notification with create gitlab issue link if specified" do + test 'should send notification with create gitlab issue link if specified' do options = { webhook_url: 'http://localhost:8000', git_url: 'github.com/aschen' @@ -32,7 +31,7 @@ class TeamsNotifierTest < ActiveSupport::TestCase teams_notifier = ExceptionNotifier::TeamsNotifier.new teams_notifier.httparty = FakeHTTParty.new - options = teams_notifier.call ArgumentError.new("foo"), options + options = teams_notifier.call ArgumentError.new('foo'), options body = ActiveSupport::JSON.decode options[:body] @@ -45,7 +44,7 @@ class TeamsNotifierTest < ActiveSupport::TestCase test 'should add other HTTParty options to params' do options = { webhook_url: 'http://localhost:8000', - username: "Test Bot", + username: 'Test Bot', avatar: 'http://site.com/icon.png', basic_auth: { username: 'clara', @@ -55,16 +54,16 @@ class TeamsNotifierTest < ActiveSupport::TestCase teams_notifier = ExceptionNotifier::TeamsNotifier.new teams_notifier.httparty = FakeHTTParty.new - options = teams_notifier.call ArgumentError.new("foo"), options + options = teams_notifier.call ArgumentError.new('foo'), options - assert options.has_key? :basic_auth + assert options.key? :basic_auth assert 'clara', options[:basic_auth][:username] assert 'password', options[:basic_auth][:password] end test "should use 'A' for exceptions count if :accumulated_errors_count option is nil" do teams_notifier = ExceptionNotifier::TeamsNotifier.new - exception = ArgumentError.new("foo") + exception = ArgumentError.new('foo') teams_notifier.instance_variable_set(:@exception, exception) teams_notifier.instance_variable_set(:@options, {}) @@ -73,11 +72,11 @@ class TeamsNotifierTest < ActiveSupport::TestCase assert_equal 'A *ArgumentError* occurred.', header['activityTitle'] end - test "should use direct errors count if :accumulated_errors_count option is 5" do + test 'should use direct errors count if :accumulated_errors_count option is 5' do teams_notifier = ExceptionNotifier::TeamsNotifier.new - exception = ArgumentError.new("foo") + exception = ArgumentError.new('foo') teams_notifier.instance_variable_set(:@exception, exception) - teams_notifier.instance_variable_set(:@options, { accumulated_errors_count: 5 }) + teams_notifier.instance_variable_set(:@options, accumulated_errors_count: 5) message_text = teams_notifier.send(:message_text) header = message_text['sections'][0] assert_equal '5 *ArgumentError* occurred.', header['activityTitle'] @@ -85,9 +84,7 @@ class TeamsNotifierTest < ActiveSupport::TestCase end class FakeHTTParty - - def post(url, options) - return options + def post(_url, options) + options end - end diff --git a/test/exception_notifier/webhook_notifier_test.rb b/test/exception_notifier/webhook_notifier_test.rb index 3990e085..010f0a52 100644 --- a/test/exception_notifier/webhook_notifier_test.rb +++ b/test/exception_notifier/webhook_notifier_test.rb @@ -2,42 +2,41 @@ require 'httparty' class WebhookNotifierTest < ActiveSupport::TestCase - - test "should send webhook notification if properly configured" do + test 'should send webhook notification if properly configured' do ExceptionNotifier::WebhookNotifier.stubs(:new).returns(Object.new) - webhook = ExceptionNotifier::WebhookNotifier.new({ url: 'http://localhost:8000' }) + webhook = ExceptionNotifier::WebhookNotifier.new(url: 'http://localhost:8000') webhook.stubs(:call).returns(fake_response) response = webhook.call(fake_exception) refute_nil response assert_equal response[:status], 200 - assert_equal response[:body][:exception][:error_class], "ZeroDivisionError" - assert_includes response[:body][:exception][:message], "divided by 0" - assert_includes response[:body][:exception][:backtrace], "/exception_notification/test/webhook_notifier_test.rb:48" + assert_equal response[:body][:exception][:error_class], 'ZeroDivisionError' + assert_includes response[:body][:exception][:message], 'divided by 0' + assert_includes response[:body][:exception][:backtrace], '/exception_notification/test/webhook_notifier_test.rb:48' - assert response[:body][:request][:cookies].has_key?(:cookie_item1) - assert_equal response[:body][:request][:url], "http://example.com/example" - assert_equal response[:body][:request][:ip_address], "192.168.1.1" - assert response[:body][:request][:environment].has_key?(:env_item1) - assert_equal response[:body][:request][:controller], "#" - assert response[:body][:request][:session].has_key?(:session_item1) - assert response[:body][:request][:parameters].has_key?(:controller) - assert response[:body][:data][:extra_data].has_key?(:data_item1) + assert response[:body][:request][:cookies].key?(:cookie_item1) + assert_equal response[:body][:request][:url], 'http://example.com/example' + assert_equal response[:body][:request][:ip_address], '192.168.1.1' + assert response[:body][:request][:environment].key?(:env_item1) + assert_equal response[:body][:request][:controller], '#' + assert response[:body][:request][:session].key?(:session_item1) + assert response[:body][:request][:parameters].key?(:controller) + assert response[:body][:data][:extra_data].key?(:data_item1) end - test "should send webhook notification with correct params data" do + test 'should send webhook notification with correct params data' do url = 'http://localhost:8000' fake_exception.stubs(:backtrace).returns('the backtrace') - webhook = ExceptionNotifier::WebhookNotifier.new({ url: url }) + webhook = ExceptionNotifier::WebhookNotifier.new(url: url) HTTParty.expects(:send).with(:post, url, fake_params) webhook.call(fake_exception) end - test "should call pre/post_callback if specified" do + test 'should call pre/post_callback if specified' do HTTParty.stubs(:send).returns(fake_response) - webhook = ExceptionNotifier::WebhookNotifier.new({ url: 'http://localhost:8000' }) + webhook = ExceptionNotifier::WebhookNotifier.new(url: 'http://localhost:8000') webhook.call(fake_exception) end @@ -62,7 +61,7 @@ def fake_response environment: { env_item1: 'envitem1', env_item2: 'envitem2' }, controller: '#', session: { session_item1: 'sessionitem1', session_item2: 'sessionitem2' }, - parameters: { action:'index', controller:'projects' } + parameters: { action: 'index', controller: 'projects' } } } } @@ -72,7 +71,7 @@ def fake_params { body: { server: Socket.gethostname, - process: $$, + process: $PROCESS_ID, rails_root: Rails.root, exception: { error_class: 'ZeroDivisionError', @@ -86,7 +85,7 @@ def fake_params def fake_exception @fake_exception ||= begin - 5/0 + 5 / 0 rescue Exception => e e end diff --git a/test/exception_notifier_test.rb b/test/exception_notifier_test.rb index 00d71c44..55c9f3df 100644 --- a/test/exception_notifier_test.rb +++ b/test/exception_notifier_test.rb @@ -1,44 +1,44 @@ require 'test_helper' -class ExceptionOne < StandardError;end -class ExceptionTwo < StandardError;end +class ExceptionOne < StandardError; end +class ExceptionTwo < StandardError; end class ExceptionNotifierTest < ActiveSupport::TestCase setup do @notifier_calls = 0 - @test_notifier = lambda { |exception, options| @notifier_calls += 1 } + @test_notifier = ->(_exception, _options) { @notifier_calls += 1 } end teardown do ExceptionNotifier.error_grouping = false ExceptionNotifier.notification_trigger = nil - ExceptionNotifier.class_eval("@@notifiers.delete_if { |k, _| k.to_s != \"email\"}") # reset notifiers + ExceptionNotifier.class_eval('@@notifiers.delete_if { |k, _| k.to_s != "email"}') # reset notifiers Rails.cache.clear end - test "should have default ignored exceptions" do + test 'should have default ignored exceptions' do assert_equal ExceptionNotifier.ignored_exceptions, - ['ActiveRecord::RecordNotFound', 'Mongoid::Errors::DocumentNotFound', 'AbstractController::ActionNotFound', - 'ActionController::RoutingError', 'ActionController::UnknownFormat', 'ActionController::UrlGenerationError'] + ['ActiveRecord::RecordNotFound', 'Mongoid::Errors::DocumentNotFound', 'AbstractController::ActionNotFound', + 'ActionController::RoutingError', 'ActionController::UnknownFormat', 'ActionController::UrlGenerationError'] end - test "should have email notifier registered" do + test 'should have email notifier registered' do assert_equal ExceptionNotifier.notifiers, [:email] end - test "should have a valid email notifier" do + test 'should have a valid email notifier' do @email_notifier = ExceptionNotifier.registered_exception_notifier(:email) refute_nil @email_notifier assert_equal @email_notifier.class, ExceptionNotifier::EmailNotifier assert_respond_to @email_notifier, :call end - test "should allow register/unregister another notifier" do + test 'should allow register/unregister another notifier' do called = false - proc_notifier = lambda { |exception, options| called = true } + proc_notifier = ->(_exception, _options) { called = true } ExceptionNotifier.register_exception_notifier(:proc, proc_notifier) - assert_equal ExceptionNotifier.notifiers.sort, [:email, :proc] + assert_equal ExceptionNotifier.notifiers.sort, %i[email proc] exception = StandardError.new @@ -49,27 +49,27 @@ class ExceptionNotifierTest < ActiveSupport::TestCase assert_equal ExceptionNotifier.notifiers, [:email] end - test "should allow select notifiers to send error to" do + test 'should allow select notifiers to send error to' do notifier1_calls = 0 - notifier1 = lambda { |exception, options| notifier1_calls += 1 } + notifier1 = ->(_exception, _options) { notifier1_calls += 1 } ExceptionNotifier.register_exception_notifier(:notifier1, notifier1) notifier2_calls = 0 - notifier2 = lambda { |exception, options| notifier2_calls += 1 } + notifier2 = ->(_exception, _options) { notifier2_calls += 1 } ExceptionNotifier.register_exception_notifier(:notifier2, notifier2) - assert_equal ExceptionNotifier.notifiers.sort, [:email, :notifier1, :notifier2] + assert_equal ExceptionNotifier.notifiers.sort, %i[email notifier1 notifier2] exception = StandardError.new ExceptionNotifier.notify_exception(exception) assert_equal notifier1_calls, 1 assert_equal notifier2_calls, 1 - ExceptionNotifier.notify_exception(exception, {notifiers: :notifier1}) + ExceptionNotifier.notify_exception(exception, notifiers: :notifier1) assert_equal notifier1_calls, 2 assert_equal notifier2_calls, 1 - ExceptionNotifier.notify_exception(exception, {notifiers: :notifier2}) + ExceptionNotifier.notify_exception(exception, notifiers: :notifier2) assert_equal notifier1_calls, 2 assert_equal notifier2_calls, 2 @@ -78,39 +78,39 @@ class ExceptionNotifierTest < ActiveSupport::TestCase assert_equal ExceptionNotifier.notifiers, [:email] end - test "should ignore exception if satisfies conditional ignore" do - env = "production" - ExceptionNotifier.ignore_if do |exception, options| - env != "production" + test 'should ignore exception if satisfies conditional ignore' do + env = 'production' + ExceptionNotifier.ignore_if do |_exception, _options| + env != 'production' end ExceptionNotifier.register_exception_notifier(:test, @test_notifier) exception = StandardError.new - ExceptionNotifier.notify_exception(exception, {notifiers: :test}) + ExceptionNotifier.notify_exception(exception, notifiers: :test) assert_equal @notifier_calls, 1 - env = "development" - ExceptionNotifier.notify_exception(exception, {notifiers: :test}) + env = 'development' + ExceptionNotifier.notify_exception(exception, notifiers: :test) assert_equal @notifier_calls, 1 ExceptionNotifier.clear_ignore_conditions! end - test "should not send notification if one of ignored exceptions" do + test 'should not send notification if one of ignored exceptions' do ExceptionNotifier.register_exception_notifier(:test, @test_notifier) exception = StandardError.new - ExceptionNotifier.notify_exception(exception, {notifiers: :test}) + ExceptionNotifier.notify_exception(exception, notifiers: :test) assert_equal @notifier_calls, 1 - ExceptionNotifier.notify_exception(exception, {notifiers: :test, ignore_exceptions: 'StandardError' }) + ExceptionNotifier.notify_exception(exception, notifiers: :test, ignore_exceptions: 'StandardError') assert_equal @notifier_calls, 1 end - test "should not send notification if subclass of one of ignored exceptions" do + test 'should not send notification if subclass of one of ignored exceptions' do ExceptionNotifier.register_exception_notifier(:test, @test_notifier) class StandardErrorSubclass < StandardError @@ -118,16 +118,16 @@ class StandardErrorSubclass < StandardError exception = StandardErrorSubclass.new - ExceptionNotifier.notify_exception(exception, {notifiers: :test}) + ExceptionNotifier.notify_exception(exception, notifiers: :test) assert_equal @notifier_calls, 1 - ExceptionNotifier.notify_exception(exception, {notifiers: :test, ignore_exceptions: 'StandardError' }) + ExceptionNotifier.notify_exception(exception, notifiers: :test, ignore_exceptions: 'StandardError') assert_equal @notifier_calls, 1 end - test "should call received block" do + test 'should call received block' do @block_called = false - notifier = lambda { |exception, options, &block| block.call } + notifier = ->(_exception, _options, &block) { block.call } ExceptionNotifier.register_exception_notifier(:test, notifier) exception = ExceptionOne.new @@ -139,7 +139,7 @@ class StandardErrorSubclass < StandardError assert @block_called end - test "should not call group_error! or send_notification? if error_grouping false" do + test 'should not call group_error! or send_notification? if error_grouping false' do exception = StandardError.new ExceptionNotifier.expects(:group_error!).never ExceptionNotifier.expects(:send_notification?).never @@ -147,7 +147,7 @@ class StandardErrorSubclass < StandardError ExceptionNotifier.notify_exception(exception) end - test "should call group_error! and send_notification? if error_grouping true" do + test 'should call group_error! and send_notification? if error_grouping true' do ExceptionNotifier.error_grouping = true exception = StandardError.new @@ -157,7 +157,7 @@ class StandardErrorSubclass < StandardError ExceptionNotifier.notify_exception(exception) end - test "should skip notification if send_notification? is false" do + test 'should skip notification if send_notification? is false' do ExceptionNotifier.error_grouping = true exception = StandardError.new @@ -167,7 +167,7 @@ class StandardErrorSubclass < StandardError refute ExceptionNotifier.notify_exception(exception) end - test "should send notification if send_notification? is true" do + test 'should send notification if send_notification? is true' do ExceptionNotifier.error_grouping = true exception = StandardError.new diff --git a/test/test_helper.rb b/test/test_helper.rb index 9b1f858c..7e24f042 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,18 +1,18 @@ # Configure Rails Environment -ENV["RAILS_ENV"] = "test" +ENV['RAILS_ENV'] = 'test' begin - require "coveralls" + require 'coveralls' Coveralls.wear! rescue LoadError - warn "warning: coveralls gem not found; skipping Coveralls" + warn 'warning: coveralls gem not found; skipping Coveralls' end -require File.expand_path("../dummy/config/environment.rb", __FILE__) -require "rails/test_help" -require File.expand_path("../dummy/test/test_helper.rb", __FILE__) +require File.expand_path('dummy/config/environment.rb', __dir__) +require 'rails/test_help' +require File.expand_path('dummy/test/test_helper.rb', __dir__) -require "mocha/setup" +require 'mocha/setup' Rails.backtrace_cleaner.remove_silencers! ExceptionNotifier.testing_mode! From 06ec82501cfe510798eae42dbebdb986ed3100c6 Mon Sep 17 00:00:00 2001 From: Fabian Larranaga Date: Tue, 18 Dec 2018 20:04:43 -0300 Subject: [PATCH 044/156] Add rubocop_todo.yml --- .rubocop.yml | 1 + .rubocop_todo.yml | 180 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 .rubocop.yml create mode 100644 .rubocop_todo.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..cc32da4b --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..566eec83 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,180 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2018-12-18 20:02:43 -0300 using RuboCop version 0.59.2. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +# Configuration parameters: Include. +# Include: **/*.gemspec +Gemspec/RequiredRubyVersion: + Exclude: + - 'exception_notification.gemspec' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyleAlignWith, AutoCorrect, Severity. +# SupportedStylesAlignWith: keyword, variable, start_of_line +Layout/EndAlignment: + Exclude: + - 'lib/exception_notifier/campfire_notifier.rb' + - 'lib/exception_notifier/hipchat_notifier.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Layout/RescueEnsureAlignment: + Exclude: + - 'lib/exception_notifier/modules/error_grouping.rb' + - 'test/exception_notifier/webhook_notifier_test.rb' + +# Offense count: 2 +# Configuration parameters: AllowSafeAssignment. +Lint/AssignmentInCondition: + Exclude: + - 'lib/exception_notifier/modules/error_grouping.rb' + +# Offense count: 12 +Lint/RescueException: + Exclude: + - 'examples/sinatra/sinatra_app.rb' + - 'lib/exception_notification/rack.rb' + - 'lib/exception_notification/sidekiq.rb' + - 'lib/exception_notifier.rb' + - 'test/exception_notifier/campfire_notifier_test.rb' + - 'test/exception_notifier/hipchat_notifier_test.rb' + - 'test/exception_notifier/irc_notifier_test.rb' + - 'test/exception_notifier/slack_notifier_test.rb' + - 'test/exception_notifier/sns_notifier_test.rb' + - 'test/exception_notifier/webhook_notifier_test.rb' + +# Offense count: 2 +# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. +Lint/UselessAccessModifier: + Exclude: + - 'lib/exception_notifier/datadog_notifier.rb' + - 'test/exception_notifier/datadog_notifier_test.rb' + +# Offense count: 1 +Lint/UselessAssignment: + Exclude: + - 'lib/exception_notifier/sns_notifier.rb' + +# Offense count: 18 +Metrics/AbcSize: + Max: 97 + +# Offense count: 3 +# Configuration parameters: CountComments, ExcludedMethods. +# ExcludedMethods: refine +Metrics/BlockLength: + Max: 88 + +# Offense count: 10 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 186 + +# Offense count: 9 +Metrics/CyclomaticComplexity: + Max: 24 + +# Offense count: 28 +# Configuration parameters: CountComments, ExcludedMethods. +Metrics/MethodLength: + Max: 90 + +# Offense count: 7 +Metrics/PerceivedComplexity: + Max: 24 + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, EnforcedStyle. +# SupportedStyles: nested, compact +Style/ClassAndModuleChildren: + Exclude: + - 'test/dummy/test/test_helper.rb' + +# Offense count: 6 +Style/ClassVars: + Exclude: + - 'lib/exception_notifier.rb' + - 'test/exception_notifier/modules/error_grouping_test.rb' + +# Offense count: 28 +Style/Documentation: + Enabled: false + +# Offense count: 1 +Style/DoubleNegation: + Exclude: + - 'lib/exception_notifier/irc_notifier.rb' + +# Offense count: 1 +Style/EvalWithLocation: + Exclude: + - 'test/exception_notifier_test.rb' + +# Offense count: 6 +# Configuration parameters: MinBodyLength. +Style/GuardClause: + Exclude: + - 'lib/exception_notifier/campfire_notifier.rb' + - 'lib/exception_notifier/email_notifier.rb' + - 'lib/exception_notifier/google_chat_notifier.rb' + - 'lib/exception_notifier/irc_notifier.rb' + - 'lib/exception_notifier/slack_notifier.rb' + - 'lib/exception_notifier/sns_notifier.rb' + +# Offense count: 7 +# Cop supports --auto-correct. +Style/IfUnlessModifier: + Exclude: + - 'lib/exception_notification/rack.rb' + - 'lib/exception_notifier/datadog_notifier.rb' + - 'lib/exception_notifier/google_chat_notifier.rb' + - 'lib/exception_notifier/webhook_notifier.rb' + - 'test/dummy/test/functional/posts_controller_test.rb' + - 'test/exception_notifier/email_notifier_test.rb' + +# Offense count: 3 +Style/MethodMissingSuper: + Exclude: + - 'lib/exception_notifier/email_notifier.rb' + - 'lib/exception_notifier/mattermost_notifier.rb' + - 'lib/exception_notifier/teams_notifier.rb' + +# Offense count: 3 +Style/MissingRespondToMissing: + Exclude: + - 'lib/exception_notifier/email_notifier.rb' + - 'lib/exception_notifier/mattermost_notifier.rb' + - 'lib/exception_notifier/teams_notifier.rb' + +# Offense count: 1 +Style/MultilineBlockChain: + Exclude: + - 'lib/exception_notifier/email_notifier.rb' + +# Offense count: 2 +Style/NestedTernaryOperator: + Exclude: + - 'lib/exception_notifier/slack_notifier.rb' + - 'lib/exception_notifier/sns_notifier.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. +# SupportedStyles: predicate, comparison +Style/NumericPredicate: + Exclude: + - 'spec/**/*' + - 'test/exception_notifier/modules/error_grouping_test.rb' + +# Offense count: 253 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 226 From f825b9f9bc03df69d2d283cc7829aba5cdc4b358 Mon Sep 17 00:00:00 2001 From: Fabian Larranaga Date: Wed, 23 Jan 2019 18:48:53 -0300 Subject: [PATCH 045/156] Sort missing dev dependencies & add rubocop target --- .rubocop.yml | 4 ++++ exception_notification.gemspec | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index cc32da4b..86fc25ea 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1 +1,5 @@ inherit_from: .rubocop_todo.yml + +AllCops: + TargetRubyVersion: 2.0 + DisplayCopNames: true diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 154eeb03..23afd287 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -19,9 +19,6 @@ Gem::Specification.new do |s| s.add_dependency('actionmailer', '>= 4.0', '< 6') s.add_dependency('activesupport', '>= 4.0', '< 6') - s.add_development_dependency 'rails', '>= 4.0', '< 6' - s.add_development_dependency 'resque', '~> 1.8.0' - # Sidekiq 3.2.2 does not support Ruby 1.9. s.add_development_dependency 'appraisal', '~> 2.2.0' s.add_development_dependency 'aws-sdk-sns', '~> 1' s.add_development_dependency 'carrier-pigeon', '>= 0.7.0' @@ -31,6 +28,10 @@ Gem::Specification.new do |s| s.add_development_dependency 'httparty', '~> 0.10.2' s.add_development_dependency 'mock_redis', '~> 0.18.0' s.add_development_dependency 'mocha', '>= 0.13.0' + s.add_development_dependency 'rails', '>= 4.0', '< 6' + s.add_development_dependency 'resque', '~> 1.8.0' + s.add_development_dependency 'rubocop', '0.50.0' + # Sidekiq 3.2.2 does not support Ruby 1.9. s.add_development_dependency 'sidekiq', '~> 3.0.0', '< 3.2.2' s.add_development_dependency 'slack-notifier', '>= 1.0.0' s.add_development_dependency 'sqlite3', '>= 1.3.4' From 8211b38304543e5e2733ab2fcdafb91545966b93 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:04:38 -0300 Subject: [PATCH 046/156] Rename erb template Rubocop skips erb templates since it doesn't support them --- lib/generators/exception_notification/install_generator.rb | 2 +- ...{exception_notification.rb => exception_notification.rb.erb} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/generators/exception_notification/templates/{exception_notification.rb => exception_notification.rb.erb} (100%) diff --git a/lib/generators/exception_notification/install_generator.rb b/lib/generators/exception_notification/install_generator.rb index d4f78b38..52d5b2f0 100644 --- a/lib/generators/exception_notification/install_generator.rb +++ b/lib/generators/exception_notification/install_generator.rb @@ -8,7 +8,7 @@ class InstallGenerator < Rails::Generators::Base class_option :sidekiq, type: :boolean, desc: 'Add support for sending notifications when errors occur in Sidekiq jobs.' def copy_initializer - template 'exception_notification.rb', 'config/initializers/exception_notification.rb' + template 'exception_notification.rb.erb', 'config/initializers/exception_notification.rb' end end end diff --git a/lib/generators/exception_notification/templates/exception_notification.rb b/lib/generators/exception_notification/templates/exception_notification.rb.erb similarity index 100% rename from lib/generators/exception_notification/templates/exception_notification.rb rename to lib/generators/exception_notification/templates/exception_notification.rb.erb From 5ede10fbbb0fd78c393b09bbc2f2c28bfaa9ba39 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:09:23 -0300 Subject: [PATCH 047/156] Fix Style/UnneededInterpolation offense --- lib/exception_notifier/email_notifier.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index c4eaeac3..b28d013c 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -58,7 +58,7 @@ def background_exception_notification(exception, options = {}, default_options = private def compose_subject - subject = "#{@options[:email_prefix]}" + subject = @options[:email_prefix].to_s.dup subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1 subject << "#{@kontroller.controller_name} #{@kontroller.action_name}" if @kontroller && @options[:include_controller_and_action_names_in_subject] subject << " (#{@exception.class})" From a70ec5014b2969257bad27106ef6d19260b4299b Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:09:57 -0300 Subject: [PATCH 048/156] Fix Layout/ClosingParenthesisIndentation offense --- test/exception_notifier/irc_notifier_test.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/exception_notifier/irc_notifier_test.rb b/test/exception_notifier/irc_notifier_test.rb index dacb989d..824ae29a 100644 --- a/test/exception_notifier/irc_notifier_test.rb +++ b/test/exception_notifier/irc_notifier_test.rb @@ -102,11 +102,13 @@ class IrcNotifierTest < ActiveSupport::TestCase prefix: '[test notification]' } - CarrierPigeon.expects(:send).with(has_entries( - ssl: true, - join: true, - notice: true - )) do |v| + entries = { + ssl: true, + join: true, + notice: true + } + + CarrierPigeon.expects(:send).with(has_entries(entries)) do |v| /\[test notification\]/.match(v[:message]) end From c0cbde576010a9d963166c3f4d8a1389bd31af5c Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:10:15 -0300 Subject: [PATCH 049/156] Fix syntax error and Style/TrailingCommaInLiteral offense --- lib/exception_notification/resque.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/exception_notification/resque.rb b/lib/exception_notification/resque.rb index 40b9743b..288f117d 100644 --- a/lib/exception_notification/resque.rb +++ b/lib/exception_notification/resque.rb @@ -13,7 +13,7 @@ def save failed_at: Time.now.to_s, payload: payload, queue: queue, - worker: worker.to_s, + worker: worker.to_s } ExceptionNotifier.notify_exception(exception, data: { resque: data }) From b43d2f47ac4898b909e8e79c422e8bdfb15a1487 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:12:07 -0300 Subject: [PATCH 050/156] Fix Lint/AssignmentInCondition offenses --- .rubocop_todo.yml | 6 ------ lib/exception_notifier/modules/error_grouping.rb | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 566eec83..ae54c158 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -29,12 +29,6 @@ Layout/RescueEnsureAlignment: - 'lib/exception_notifier/modules/error_grouping.rb' - 'test/exception_notifier/webhook_notifier_test.rb' -# Offense count: 2 -# Configuration parameters: AllowSafeAssignment. -Lint/AssignmentInCondition: - Exclude: - - 'lib/exception_notifier/modules/error_grouping.rb' - # Offense count: 12 Lint/RescueException: Exclude: diff --git a/lib/exception_notifier/modules/error_grouping.rb b/lib/exception_notifier/modules/error_grouping.rb index 4f6c2cc2..3aefff0e 100644 --- a/lib/exception_notifier/modules/error_grouping.rb +++ b/lib/exception_notifier/modules/error_grouping.rb @@ -46,13 +46,13 @@ def group_error!(exception, options) message_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\nmessage:#{exception.message}")}" accumulated_errors_count = 1 - if count = error_count(message_based_key) + if (count = error_count(message_based_key)) accumulated_errors_count = count + 1 save_error_count(message_based_key, accumulated_errors_count) else backtrace_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}" - if count = error_grouping_cache.read(backtrace_based_key) + if (count = error_grouping_cache.read(backtrace_based_key)) accumulated_errors_count = count + 1 save_error_count(backtrace_based_key, accumulated_errors_count) else From 996544f37bec112114ac991a4b47f57e68c8a67c Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:17:43 -0300 Subject: [PATCH 051/156] Fix Layout/EndAlignment offenses --- .rubocop_todo.yml | 9 --------- lib/exception_notifier/campfire_notifier.rb | 2 +- lib/exception_notifier/hipchat_notifier.rb | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ae54c158..24f33f1e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -13,15 +13,6 @@ Gemspec/RequiredRubyVersion: Exclude: - 'exception_notification.gemspec' -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleAlignWith, AutoCorrect, Severity. -# SupportedStylesAlignWith: keyword, variable, start_of_line -Layout/EndAlignment: - Exclude: - - 'lib/exception_notifier/campfire_notifier.rb' - - 'lib/exception_notifier/hipchat_notifier.rb' - # Offense count: 2 # Cop supports --auto-correct. Layout/RescueEnsureAlignment: diff --git a/lib/exception_notifier/campfire_notifier.rb b/lib/exception_notifier/campfire_notifier.rb index bf7c743f..881f8965 100644 --- a/lib/exception_notifier/campfire_notifier.rb +++ b/lib/exception_notifier/campfire_notifier.rb @@ -22,7 +22,7 @@ def call(exception, options = {}) "The exception occurred #{options[:accumulated_errors_count]} times: '#{exception.message}'" else "A new exception occurred: '#{exception.message}'" - end + end message += " on '#{exception.backtrace.first}'" if exception.backtrace send_notice(exception, options, message) do |msg, _| @room.paste msg diff --git a/lib/exception_notifier/hipchat_notifier.rb b/lib/exception_notifier/hipchat_notifier.rb index 9d851037..b45cc588 100644 --- a/lib/exception_notifier/hipchat_notifier.rb +++ b/lib/exception_notifier/hipchat_notifier.rb @@ -20,7 +20,7 @@ def initialize(options) "The exception occurred #{errors_count} times: '#{Rack::Utils.escape_html(exception.message)}'" else "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}'" - end + end msg += " on '#{exception.backtrace.first}'" if exception.backtrace msg } From e4a1d98ef77a98b63fbb1bbe4a7d8e8ac5771ab9 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:24:15 -0300 Subject: [PATCH 052/156] Fix some Lint/RescueException offenses --- .rubocop_todo.yml | 7 ------- examples/sinatra/sinatra_app.rb | 2 +- test/exception_notifier/campfire_notifier_test.rb | 2 +- test/exception_notifier/hipchat_notifier_test.rb | 4 ++-- test/exception_notifier/irc_notifier_test.rb | 2 +- test/exception_notifier/slack_notifier_test.rb | 2 +- test/exception_notifier/sns_notifier_test.rb | 2 +- test/exception_notifier/webhook_notifier_test.rb | 2 +- 8 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 24f33f1e..48a72f71 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -23,16 +23,9 @@ Layout/RescueEnsureAlignment: # Offense count: 12 Lint/RescueException: Exclude: - - 'examples/sinatra/sinatra_app.rb' - 'lib/exception_notification/rack.rb' - 'lib/exception_notification/sidekiq.rb' - 'lib/exception_notifier.rb' - - 'test/exception_notifier/campfire_notifier_test.rb' - - 'test/exception_notifier/hipchat_notifier_test.rb' - - 'test/exception_notifier/irc_notifier_test.rb' - - 'test/exception_notifier/slack_notifier_test.rb' - - 'test/exception_notifier/sns_notifier_test.rb' - - 'test/exception_notifier/webhook_notifier_test.rb' # Offense count: 2 # Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. diff --git a/examples/sinatra/sinatra_app.rb b/examples/sinatra/sinatra_app.rb index 65d31bbf..6e0dec51 100644 --- a/examples/sinatra/sinatra_app.rb +++ b/examples/sinatra/sinatra_app.rb @@ -28,7 +28,7 @@ class SinatraApp < Sinatra::Base get '/background_notification' do begin 1 / 0 - rescue Exception => e + rescue StandardError => e ExceptionNotifier.notify_exception(e, data: { msg: 'Cannot divide by zero!' }) end 'Check email at mailcatcher.' diff --git a/test/exception_notifier/campfire_notifier_test.rb b/test/exception_notifier/campfire_notifier_test.rb index 2594f7cc..e3065a97 100644 --- a/test/exception_notifier/campfire_notifier_test.rb +++ b/test/exception_notifier/campfire_notifier_test.rb @@ -101,7 +101,7 @@ def fake_notification def fake_exception 5 / 0 - rescue Exception => e + rescue StandardError => e e end diff --git a/test/exception_notifier/hipchat_notifier_test.rb b/test/exception_notifier/hipchat_notifier_test.rb index c69d7ab0..b5e944bf 100644 --- a/test/exception_notifier/hipchat_notifier_test.rb +++ b/test/exception_notifier/hipchat_notifier_test.rb @@ -197,13 +197,13 @@ def fake_body def fake_exception 5 / 0 - rescue Exception => e + rescue StandardError => e e end def fake_exception_with_html_characters raise StandardError, 'an error with characters' - rescue Exception => e + rescue StandardError => e e end diff --git a/test/exception_notifier/irc_notifier_test.rb b/test/exception_notifier/irc_notifier_test.rb index 824ae29a..32d0095e 100644 --- a/test/exception_notifier/irc_notifier_test.rb +++ b/test/exception_notifier/irc_notifier_test.rb @@ -127,7 +127,7 @@ class IrcNotifierTest < ActiveSupport::TestCase def fake_exception 5 / 0 - rescue Exception => e + rescue StandardError => e e end diff --git a/test/exception_notifier/slack_notifier_test.rb b/test/exception_notifier/slack_notifier_test.rb index c4636674..69ca489c 100644 --- a/test/exception_notifier/slack_notifier_test.rb +++ b/test/exception_notifier/slack_notifier_test.rb @@ -173,7 +173,7 @@ def setup def fake_exception 5 / 0 - rescue Exception => e + rescue StandardError => e e end diff --git a/test/exception_notifier/sns_notifier_test.rb b/test/exception_notifier/sns_notifier_test.rb index a68813ab..8d3ae40a 100644 --- a/test/exception_notifier/sns_notifier_test.rb +++ b/test/exception_notifier/sns_notifier_test.rb @@ -100,7 +100,7 @@ class ExamplesController < ActionController::Base; end def fake_exception 1 / 0 - rescue Exception => e + rescue StandardError => e e end diff --git a/test/exception_notifier/webhook_notifier_test.rb b/test/exception_notifier/webhook_notifier_test.rb index 010f0a52..26c6903f 100644 --- a/test/exception_notifier/webhook_notifier_test.rb +++ b/test/exception_notifier/webhook_notifier_test.rb @@ -86,7 +86,7 @@ def fake_params def fake_exception @fake_exception ||= begin 5 / 0 - rescue Exception => e + rescue StandardError => e e end end From 6f380aa9bc60753638585126dc7a8a629c4e3f49 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:29:15 -0300 Subject: [PATCH 053/156] Fix Lint/UselessAccessModifier offenses --- .rubocop_todo.yml | 7 ------- lib/exception_notifier/datadog_notifier.rb | 2 -- test/exception_notifier/datadog_notifier_test.rb | 2 -- 3 files changed, 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 48a72f71..f2d4420b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -27,13 +27,6 @@ Lint/RescueException: - 'lib/exception_notification/sidekiq.rb' - 'lib/exception_notifier.rb' -# Offense count: 2 -# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. -Lint/UselessAccessModifier: - Exclude: - - 'lib/exception_notifier/datadog_notifier.rb' - - 'test/exception_notifier/datadog_notifier_test.rb' - # Offense count: 1 Lint/UselessAssignment: Exclude: diff --git a/lib/exception_notifier/datadog_notifier.rb b/lib/exception_notifier/datadog_notifier.rb index 1697474d..9fc3dbf8 100644 --- a/lib/exception_notifier/datadog_notifier.rb +++ b/lib/exception_notifier/datadog_notifier.rb @@ -22,8 +22,6 @@ def datadog_event(exception, options = {}) ).event end - private - class DatadogExceptionEvent include ExceptionNotifier::BacktraceCleaner diff --git a/test/exception_notifier/datadog_notifier_test.rb b/test/exception_notifier/datadog_notifier_test.rb index 78cc7804..556d1098 100644 --- a/test/exception_notifier/datadog_notifier_test.rb +++ b/test/exception_notifier/datadog_notifier_test.rb @@ -92,8 +92,6 @@ def setup assert_equal event.aggregation_key, [event.msg_title] end - private - class FakeDatadogClient def emit_event(event); end end From 580ca0604b8a09f7acfc88a03ec17c9c4f8ed2a5 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:31:24 -0300 Subject: [PATCH 054/156] Fix Lint/UselessAssignment offense --- .rubocop_todo.yml | 5 ----- lib/exception_notifier/sns_notifier.rb | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f2d4420b..d60f73ed 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -27,11 +27,6 @@ Lint/RescueException: - 'lib/exception_notification/sidekiq.rb' - 'lib/exception_notifier.rb' -# Offense count: 1 -Lint/UselessAssignment: - Exclude: - - 'lib/exception_notifier/sns_notifier.rb' - # Offense count: 18 Metrics/AbcSize: Max: 97 diff --git a/lib/exception_notifier/sns_notifier.rb b/lib/exception_notifier/sns_notifier.rb index b1d67332..9e74145a 100644 --- a/lib/exception_notifier/sns_notifier.rb +++ b/lib/exception_notifier/sns_notifier.rb @@ -59,7 +59,7 @@ def build_message(exception, options) if exception.backtrace formatted_backtrace = exception.backtrace.first(options[:backtrace_lines]).join("\n").to_s - text += "Backtrace:\n#{formatted_backtrace}\n" + text + "Backtrace:\n#{formatted_backtrace}\n" end end From 13e9e13ae6bc68bf2011203b1ed20348c11ae060 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:34:12 -0300 Subject: [PATCH 055/156] Fix Style/NumericPredicate offense --- .rubocop_todo.yml | 9 --------- test/exception_notifier/modules/error_grouping_test.rb | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d60f73ed..b297e69e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -130,15 +130,6 @@ Style/NestedTernaryOperator: - 'lib/exception_notifier/slack_notifier.rb' - 'lib/exception_notifier/sns_notifier.rb' -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: - Exclude: - - 'spec/**/*' - - 'test/exception_notifier/modules/error_grouping_test.rb' - # Offense count: 253 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/test/exception_notifier/modules/error_grouping_test.rb b/test/exception_notifier/modules/error_grouping_test.rb index 56270e07..1365cd0b 100644 --- a/test/exception_notifier/modules/error_grouping_test.rb +++ b/test/exception_notifier/modules/error_grouping_test.rb @@ -155,7 +155,7 @@ module TestModule end test 'use specified trigger in .send_notification?' do - trigger = proc { |_exception, count| count % 4 == 0 } + trigger = proc { |_exception, count| (count % 4).zero? } TestModule.stubs(:notification_trigger).returns(trigger) count = 16 From 1e54aa229dd4d06f5a2b7c347ae71b7cf773d743 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:38:34 -0300 Subject: [PATCH 056/156] Fix Style/NestedTernaryOperator offenses --- .rubocop_todo.yml | 6 ------ lib/exception_notifier/slack_notifier.rb | 8 +++++++- lib/exception_notifier/sns_notifier.rb | 8 +++++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b297e69e..e31c15da 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -124,12 +124,6 @@ Style/MultilineBlockChain: Exclude: - 'lib/exception_notifier/email_notifier.rb' -# Offense count: 2 -Style/NestedTernaryOperator: - Exclude: - - 'lib/exception_notifier/slack_notifier.rb' - - 'lib/exception_notifier/sns_notifier.rb' - # Offense count: 253 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/lib/exception_notifier/slack_notifier.rb b/lib/exception_notifier/slack_notifier.rb index 8202ef52..05005d90 100644 --- a/lib/exception_notifier/slack_notifier.rb +++ b/lib/exception_notifier/slack_notifier.rb @@ -57,7 +57,13 @@ def attchs(exception, clean_message, options) def information_from_options(exception_class, options) errors_count = options[:accumulated_errors_count].to_i - measure_word = errors_count > 1 ? errors_count : (exception_class.to_s =~ /^[aeiou]/i ? 'An' : 'A') + + measure_word = if errors_count > 1 + errors_count + else + exception_class.to_s =~ /^[aeiou]/i ? 'An' : 'A' + end + exception_name = "*#{measure_word}* `#{exception_class.to_s}`" env = options[:env] diff --git a/lib/exception_notifier/sns_notifier.rb b/lib/exception_notifier/sns_notifier.rb index 9e74145a..fbd7f72e 100644 --- a/lib/exception_notifier/sns_notifier.rb +++ b/lib/exception_notifier/sns_notifier.rb @@ -65,7 +65,13 @@ def build_message(exception, options) def accumulated_exception_name(exception, options) errors_count = options[:accumulated_errors_count].to_i - measure_word = errors_count > 1 ? errors_count : (exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A') + + measure_word = if errors_count > 1 + errors_count + else + exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' + end + "#{measure_word} #{exception.class}" end From 54a7fa63c733f5b5793437a3425a38de7070022b Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:47:50 -0300 Subject: [PATCH 057/156] Fix Style/MultilineBlockChain offense --- .rubocop_todo.yml | 5 ----- lib/exception_notifier/email_notifier.rb | 7 +++++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e31c15da..e0dc2beb 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -119,11 +119,6 @@ Style/MissingRespondToMissing: - 'lib/exception_notifier/mattermost_notifier.rb' - 'lib/exception_notifier/teams_notifier.rb' -# Offense count: 1 -Style/MultilineBlockChain: - Exclude: - - 'lib/exception_notifier/email_notifier.rb' - # Offense count: 253 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index b28d013c..ede9b1cb 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -141,7 +141,8 @@ def initialize(options) mailer_settings_key = "#{delivery_method}_settings".to_sym options[:mailer_settings] = options.delete(mailer_settings_key) - options.reverse_merge(EmailNotifier.default_options).select do |k, _v| + merged_opts = options.reverse_merge(EmailNotifier.default_options) + filtered_opts = merged_opts.select do |k, _v| %i[ sender_address exception_recipients pre_callback post_callback email_prefix email_format @@ -149,7 +150,9 @@ def initialize(options) include_controller_and_action_names_in_subject delivery_method mailer_settings email_headers mailer_parent template_path deliver_with ].include?(k) - end .each { |k, v| send("#{k}=", v) } + end + + filtered_opts.each { |k, v| send("#{k}=", v) } end def options From ffff62fee99d526ffb8a0647cb62812210585046 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 30 Dec 2018 16:52:34 -0300 Subject: [PATCH 058/156] Re-generate .rubocop_todo.yml using Rubocop 0.50.0 --- .rubocop_todo.yml | 121 +++++++++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 40 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e0dc2beb..73b2f152 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,43 +1,73 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2018-12-18 20:02:43 -0300 using RuboCop version 0.59.2. +# on 2019-01-23 23:57:48 -0300 using RuboCop version 0.50.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 1 -# Configuration parameters: Include. -# Include: **/*.gemspec -Gemspec/RequiredRubyVersion: +# Cop supports --auto-correct. +Layout/SpaceAfterComma: + Exclude: + - 'lib/exception_notifier/slack_notifier.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: space, no_space +Layout/SpaceAroundEqualsInParameterDefault: + Exclude: + - 'lib/exception_notifier/slack_notifier.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, SupportedStylesForEmptyBraces. +# SupportedStyles: space, no_space +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceBeforeBlockBraces: Exclude: - - 'exception_notification.gemspec' + - 'lib/exception_notifier/slack_notifier.rb' # Offense count: 2 # Cop supports --auto-correct. -Layout/RescueEnsureAlignment: +# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters. +# SupportedStyles: space, no_space +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceInsideBlockBraces: Exclude: - - 'lib/exception_notifier/modules/error_grouping.rb' - - 'test/exception_notifier/webhook_notifier_test.rb' + - 'lib/exception_notifier/slack_notifier.rb' -# Offense count: 12 +# Offense count: 4 Lint/RescueException: Exclude: - 'lib/exception_notification/rack.rb' - 'lib/exception_notification/sidekiq.rb' - 'lib/exception_notifier.rb' +# Offense count: 1 +# Cop supports --auto-correct. +Lint/StringConversionInInterpolation: + Exclude: + - 'lib/exception_notifier/slack_notifier.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'lib/exception_notifier/slack_notifier.rb' + # Offense count: 18 Metrics/AbcSize: - Max: 97 + Max: 98 # Offense count: 3 # Configuration parameters: CountComments, ExcludedMethods. -# ExcludedMethods: refine Metrics/BlockLength: Max: 88 -# Offense count: 10 +# Offense count: 11 # Configuration parameters: CountComments. Metrics/ClassLength: Max: 186 @@ -46,8 +76,14 @@ Metrics/ClassLength: Metrics/CyclomaticComplexity: Max: 24 -# Offense count: 28 -# Configuration parameters: CountComments, ExcludedMethods. +# Offense count: 253 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 226 + +# Offense count: 29 +# Configuration parameters: CountComments. Metrics/MethodLength: Max: 90 @@ -57,7 +93,17 @@ Metrics/PerceivedComplexity: # Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. +Performance/StringReplacement: + Exclude: + - 'lib/exception_notifier/slack_notifier.rb' + +# Offense count: 1 +Style/CaseEquality: + Exclude: + - 'test/exception_notification/resque_test.rb' + +# Offense count: 1 +# Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: nested, compact Style/ClassAndModuleChildren: Exclude: @@ -69,7 +115,7 @@ Style/ClassVars: - 'lib/exception_notifier.rb' - 'test/exception_notifier/modules/error_grouping_test.rb' -# Offense count: 28 +# Offense count: 29 Style/Documentation: Enabled: false @@ -78,11 +124,6 @@ Style/DoubleNegation: Exclude: - 'lib/exception_notifier/irc_notifier.rb' -# Offense count: 1 -Style/EvalWithLocation: - Exclude: - - 'test/exception_notifier_test.rb' - # Offense count: 6 # Configuration parameters: MinBodyLength. Style/GuardClause: @@ -94,33 +135,33 @@ Style/GuardClause: - 'lib/exception_notifier/slack_notifier.rb' - 'lib/exception_notifier/sns_notifier.rb' -# Offense count: 7 +# Offense count: 1 # Cop supports --auto-correct. -Style/IfUnlessModifier: +Style/MethodCallWithoutArgsParentheses: Exclude: - - 'lib/exception_notification/rack.rb' - - 'lib/exception_notifier/datadog_notifier.rb' - - 'lib/exception_notifier/google_chat_notifier.rb' - - 'lib/exception_notifier/webhook_notifier.rb' - - 'test/dummy/test/functional/posts_controller_test.rb' - - 'test/exception_notifier/email_notifier_test.rb' + - 'test/exception_notification/resque_test.rb' # Offense count: 3 -Style/MethodMissingSuper: +Style/MethodMissing: Exclude: - 'lib/exception_notifier/email_notifier.rb' - 'lib/exception_notifier/mattermost_notifier.rb' - 'lib/exception_notifier/teams_notifier.rb' -# Offense count: 3 -Style/MissingRespondToMissing: +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: Exclude: - - 'lib/exception_notifier/email_notifier.rb' - - 'lib/exception_notifier/mattermost_notifier.rb' - - 'lib/exception_notifier/teams_notifier.rb' + - 'lib/exception_notifier/slack_notifier.rb' -# Offense count: 253 -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. -# URISchemes: http, https -Metrics/LineLength: - Max: 226 +# Offense count: 18 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'lib/exception_notifier/datadog_notifier.rb' + - 'lib/exception_notifier/slack_notifier.rb' + - 'test/exception_notification/resque_test.rb' + - 'test/exception_notifier/datadog_notifier_test.rb' From d5b23121a65767ba54e6a47d0f9a6ef71497f50f Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 00:00:06 -0300 Subject: [PATCH 059/156] Fix slack_notifier offenses --- .rubocop_todo.yml | 59 ------------------------ lib/exception_notifier/slack_notifier.rb | 12 ++--- 2 files changed, 6 insertions(+), 65 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 73b2f152..56b6555a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,38 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -# Cop supports --auto-correct. -Layout/SpaceAfterComma: - Exclude: - - 'lib/exception_notifier/slack_notifier.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: space, no_space -Layout/SpaceAroundEqualsInParameterDefault: - Exclude: - - 'lib/exception_notifier/slack_notifier.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, SupportedStylesForEmptyBraces. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceBeforeBlockBraces: - Exclude: - - 'lib/exception_notifier/slack_notifier.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceInsideBlockBraces: - Exclude: - - 'lib/exception_notifier/slack_notifier.rb' - # Offense count: 4 Lint/RescueException: Exclude: @@ -45,19 +13,6 @@ Lint/RescueException: - 'lib/exception_notification/sidekiq.rb' - 'lib/exception_notifier.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Lint/StringConversionInInterpolation: - Exclude: - - 'lib/exception_notifier/slack_notifier.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. -Lint/UnusedBlockArgument: - Exclude: - - 'lib/exception_notifier/slack_notifier.rb' - # Offense count: 18 Metrics/AbcSize: Max: 98 @@ -91,12 +46,6 @@ Metrics/MethodLength: Metrics/PerceivedComplexity: Max: 24 -# Offense count: 1 -# Cop supports --auto-correct. -Performance/StringReplacement: - Exclude: - - 'lib/exception_notifier/slack_notifier.rb' - # Offense count: 1 Style/CaseEquality: Exclude: @@ -148,13 +97,6 @@ Style/MethodMissing: - 'lib/exception_notifier/mattermost_notifier.rb' - 'lib/exception_notifier/teams_notifier.rb' -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: PreferredDelimiters. -Style/PercentLiteralDelimiters: - Exclude: - - 'lib/exception_notifier/slack_notifier.rb' - # Offense count: 18 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. @@ -162,6 +104,5 @@ Style/PercentLiteralDelimiters: Style/StringLiterals: Exclude: - 'lib/exception_notifier/datadog_notifier.rb' - - 'lib/exception_notifier/slack_notifier.rb' - 'test/exception_notification/resque_test.rb' - 'test/exception_notifier/datadog_notifier_test.rb' diff --git a/lib/exception_notifier/slack_notifier.rb b/lib/exception_notifier/slack_notifier.rb index 05005d90..fe9ab7a1 100644 --- a/lib/exception_notifier/slack_notifier.rb +++ b/lib/exception_notifier/slack_notifier.rb @@ -20,13 +20,13 @@ def initialize(options) end end - def call(exception, options={}) - clean_message = exception.message.gsub("`", "'") + def call(exception, options = {}) + clean_message = exception.message.tr('`', "'") attchs = attchs(exception, clean_message, options) if valid? args = [exception, options, clean_message, @message_opts.merge(attachments: attchs)] - send_notice(*args) do |msg, message_opts| + send_notice(*args) do |_msg, message_opts| @notifier.ping '', message_opts end end @@ -52,7 +52,7 @@ def attchs(exception, clean_message, options) text, data = information_from_options(exception.class, options) fields = fields(clean_message, exception.backtrace, data) - [color: @color, text: text, fields: fields, mrkdwn_in: %w(text fields)] + [color: @color, text: text, fields: fields, mrkdwn_in: %w[text fields]] end def information_from_options(exception_class, options) @@ -64,7 +64,7 @@ def information_from_options(exception_class, options) exception_class.to_s =~ /^[aeiou]/i ? 'An' : 'A' end - exception_name = "*#{measure_word}* `#{exception_class.to_s}`" + exception_name = "*#{measure_word}* `#{exception_class}`" env = options[:env] if env.nil? @@ -96,7 +96,7 @@ def fields(clean_message, backtrace, data) unless data.empty? deep_reject(data, @ignore_data_if) if @ignore_data_if.is_a?(Proc) - data_string = data.map{|k,v| "#{k}: #{v}"}.join("\n") + data_string = data.map { |k, v| "#{k}: #{v}" }.join("\n") fields << { title: 'Data', value: "```#{data_string}```" } end From 643b7703fb101ceecb172bb069b10144c8244ce7 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 00:16:54 -0300 Subject: [PATCH 060/156] Cache bundler install on the CI More info: https://docs.travis-ci.com/user/caching/#bundler --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4bac6809..4d7ebb9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: ruby +cache: bundler rvm: - 2.1.10 - 2.2.9 @@ -12,7 +13,7 @@ before_install: - gem install bundler -v '< 2' install: - - "bundle install --jobs=3 --retry=3" + - "bundle install --jobs=3 --retry=3 --path=vendor/bundle" - "mkdir -p test/dummy/tmp/cache" - "mkdir -p test/dummy/tmp/non_default_location" gemfile: From 598c1a6a52f23c10810b25f5e18f07922b6a2310 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 00:20:51 -0300 Subject: [PATCH 061/156] Run Rubocop on the CI --- .rubocop.yml | 2 ++ .travis.yml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 86fc25ea..f8d304f0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,3 +3,5 @@ inherit_from: .rubocop_todo.yml AllCops: TargetRubyVersion: 2.0 DisplayCopNames: true + Exclude: + - "gemfiles/**/*" diff --git a/.travis.yml b/.travis.yml index 4d7ebb9a..0c7a406f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,9 @@ install: - "bundle install --jobs=3 --retry=3 --path=vendor/bundle" - "mkdir -p test/dummy/tmp/cache" - "mkdir -p test/dummy/tmp/non_default_location" +script: + - bundle exec rake test + - bundle exec rubocop gemfile: - gemfiles/rails4_0.gemfile - gemfiles/rails4_1.gemfile From 8d46e5246092a7a306ea588744d2ee38ca69fa5a Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 00:35:59 -0300 Subject: [PATCH 062/156] Reduce logs while running tests --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index df7cb906..9474a97f 100644 --- a/Rakefile +++ b/Rakefile @@ -11,5 +11,5 @@ Rake::TestTask.new(:test) do |t| t.libs << 'lib' t.libs << 'test' t.pattern = 'test/**/*_test.rb' - t.verbose = true + t.warning = false end From c1e81855451a1c5aa25e8664d6946ac2964d1b5f Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 01:43:44 -0300 Subject: [PATCH 063/156] Fix Style/CaseEquality --- .rubocop_todo.yml | 5 ----- test/exception_notification/resque_test.rb | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 56b6555a..2948be84 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -46,11 +46,6 @@ Metrics/MethodLength: Metrics/PerceivedComplexity: Max: 24 -# Offense count: 1 -Style/CaseEquality: - Exclude: - - 'test/exception_notification/resque_test.rb' - # Offense count: 1 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: nested, compact diff --git a/test/exception_notification/resque_test.rb b/test/exception_notification/resque_test.rb index db061e7e..6492577f 100644 --- a/test/exception_notification/resque_test.rb +++ b/test/exception_notification/resque_test.rb @@ -27,7 +27,7 @@ class ResqueTest < ActiveSupport::TestCase test "notifies exception when job fails" do ExceptionNotifier.expects(:notify_exception).with() do |ex, opts| - RuntimeError === ex && + ex.is_a?(RuntimeError) && ex.message == "Bad job!" && opts[:data][:resque][:error_class] == "RuntimeError" && opts[:data][:resque][:error_message] == "Bad job!" && From 49b3d3ba9ec3bc138387e6333c1c5ed2c8c88ced Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 01:49:57 -0300 Subject: [PATCH 064/156] Disable Style/Documentation cop --- .rubocop.yml | 3 +++ .rubocop_todo.yml | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 86fc25ea..cb08ac4c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,3 +3,6 @@ inherit_from: .rubocop_todo.yml AllCops: TargetRubyVersion: 2.0 DisplayCopNames: true + +Style/Documentation: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 2948be84..11b0462e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -59,10 +59,6 @@ Style/ClassVars: - 'lib/exception_notifier.rb' - 'test/exception_notifier/modules/error_grouping_test.rb' -# Offense count: 29 -Style/Documentation: - Enabled: false - # Offense count: 1 Style/DoubleNegation: Exclude: From abacfc625f06757d3e1ee84303990a072242320d Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 01:50:28 -0300 Subject: [PATCH 065/156] Fix Style/DoubleNegation offenses --- .rubocop_todo.yml | 5 ----- lib/exception_notifier/irc_notifier.rb | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 11b0462e..c1c0018d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -59,11 +59,6 @@ Style/ClassVars: - 'lib/exception_notifier.rb' - 'test/exception_notifier/modules/error_grouping_test.rb' -# Offense count: 1 -Style/DoubleNegation: - Exclude: - - 'lib/exception_notifier/irc_notifier.rb' - # Offense count: 6 # Configuration parameters: MinBodyLength. Style/GuardClause: diff --git a/lib/exception_notifier/irc_notifier.rb b/lib/exception_notifier/irc_notifier.rb index 990036be..7f89f8bc 100644 --- a/lib/exception_notifier/irc_notifier.rb +++ b/lib/exception_notifier/irc_notifier.rb @@ -48,7 +48,7 @@ def active? end def valid_uri?(uri) - !!URI.parse(uri) + URI.parse(uri) rescue URI::InvalidURIError false end From 183075860f2ecdaf5cbfb7b4674c9c87001ead9f Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 01:57:03 -0300 Subject: [PATCH 066/156] Fix Style/GuardClause offenses --- .rubocop_todo.yml | 11 ---------- lib/exception_notifier/campfire_notifier.rb | 20 +++++++++---------- lib/exception_notifier/email_notifier.rb | 4 +--- .../google_chat_notifier.rb | 4 +--- lib/exception_notifier/irc_notifier.rb | 9 +++++---- lib/exception_notifier/slack_notifier.rb | 10 +++++----- lib/exception_notifier/sns_notifier.rb | 8 ++++---- 7 files changed, 26 insertions(+), 40 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c1c0018d..53af77d2 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -59,17 +59,6 @@ Style/ClassVars: - 'lib/exception_notifier.rb' - 'test/exception_notifier/modules/error_grouping_test.rb' -# Offense count: 6 -# Configuration parameters: MinBodyLength. -Style/GuardClause: - Exclude: - - 'lib/exception_notifier/campfire_notifier.rb' - - 'lib/exception_notifier/email_notifier.rb' - - 'lib/exception_notifier/google_chat_notifier.rb' - - 'lib/exception_notifier/irc_notifier.rb' - - 'lib/exception_notifier/slack_notifier.rb' - - 'lib/exception_notifier/sns_notifier.rb' - # Offense count: 1 # Cop supports --auto-correct. Style/MethodCallWithoutArgsParentheses: diff --git a/lib/exception_notifier/campfire_notifier.rb b/lib/exception_notifier/campfire_notifier.rb index 881f8965..2f40d9b8 100644 --- a/lib/exception_notifier/campfire_notifier.rb +++ b/lib/exception_notifier/campfire_notifier.rb @@ -17,16 +17,16 @@ def initialize(options) end def call(exception, options = {}) - if active? - message = if options[:accumulated_errors_count].to_i > 1 - "The exception occurred #{options[:accumulated_errors_count]} times: '#{exception.message}'" - else - "A new exception occurred: '#{exception.message}'" - end - message += " on '#{exception.backtrace.first}'" if exception.backtrace - send_notice(exception, options, message) do |msg, _| - @room.paste msg - end + return unless active? + + message = if options[:accumulated_errors_count].to_i > 1 + "The exception occurred #{options[:accumulated_errors_count]} times: '#{exception.message}'" + else + "A new exception occurred: '#{exception.message}'" + end + message += " on '#{exception.backtrace.first}'" if exception.backtrace + send_notice(exception, options, message) do |msg, _| + @room.paste msg end end diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index ede9b1cb..8a07d9a2 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -123,9 +123,7 @@ def compose_email end def load_custom_views - if defined?(Rails) && Rails.respond_to?(:root) - prepend_view_path Rails.root.nil? ? 'app/views' : "#{Rails.root}/app/views" - end + prepend_view_path Rails.root.nil? ? 'app/views' : "#{Rails.root}/app/views" if defined?(Rails) && Rails.respond_to?(:root) end def maybe_call(maybe_proc) diff --git a/lib/exception_notifier/google_chat_notifier.rb b/lib/exception_notifier/google_chat_notifier.rb index 6e79d8a3..a4d67d4f 100644 --- a/lib/exception_notifier/google_chat_notifier.rb +++ b/lib/exception_notifier/google_chat_notifier.rb @@ -94,9 +94,7 @@ def controller_text env = options[:env] controller = env ? env['action_controller.instance'] : nil - if controller - " in *#{controller.controller_name}##{controller.action_name}*" - end + " in *#{controller.controller_name}##{controller.action_name}*" if controller end end end diff --git a/lib/exception_notifier/irc_notifier.rb b/lib/exception_notifier/irc_notifier.rb index 7f89f8bc..23a1ba0c 100644 --- a/lib/exception_notifier/irc_notifier.rb +++ b/lib/exception_notifier/irc_notifier.rb @@ -13,10 +13,11 @@ def call(exception, options = {}) message.prepend("(#{errors_count} times)") if errors_count > 1 message += " on '#{exception.backtrace.first}'" if exception.backtrace - if active? - send_notice(exception, options, message) do |msg, _| - send_message([*@config.prefix, *msg].join(' ')) - end + + return unless active? + + send_notice(exception, options, message) do |msg, _| + send_message([*@config.prefix, *msg].join(' ')) end end diff --git a/lib/exception_notifier/slack_notifier.rb b/lib/exception_notifier/slack_notifier.rb index fe9ab7a1..f726d9a5 100644 --- a/lib/exception_notifier/slack_notifier.rb +++ b/lib/exception_notifier/slack_notifier.rb @@ -24,11 +24,11 @@ def call(exception, options = {}) clean_message = exception.message.tr('`', "'") attchs = attchs(exception, clean_message, options) - if valid? - args = [exception, options, clean_message, @message_opts.merge(attachments: attchs)] - send_notice(*args) do |_msg, message_opts| - @notifier.ping '', message_opts - end + return unless valid? + + args = [exception, options, clean_message, @message_opts.merge(attachments: attchs)] + send_notice(*args) do |_msg, message_opts| + @notifier.ping '', message_opts end end diff --git a/lib/exception_notifier/sns_notifier.rb b/lib/exception_notifier/sns_notifier.rb index fbd7f72e..d40ab6e3 100644 --- a/lib/exception_notifier/sns_notifier.rb +++ b/lib/exception_notifier/sns_notifier.rb @@ -57,10 +57,10 @@ def build_message(exception, options) text += "Exception: #{exception.message}\n" text += "Hostname: #{Socket.gethostname}\n" - if exception.backtrace - formatted_backtrace = exception.backtrace.first(options[:backtrace_lines]).join("\n").to_s - text + "Backtrace:\n#{formatted_backtrace}\n" - end + return unless exception.backtrace + + formatted_backtrace = exception.backtrace.first(options[:backtrace_lines]).join("\n").to_s + text + "Backtrace:\n#{formatted_backtrace}\n" end def accumulated_exception_name(exception, options) From a9e70ff20688406f389c6997a7f08324b6c8c4a5 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 01:57:53 -0300 Subject: [PATCH 067/156] Fix Style/MethodCallWithoutArgsParentheses offenses --- .rubocop_todo.yml | 6 ------ test/exception_notification/resque_test.rb | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 53af77d2..8f8c44b1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -59,12 +59,6 @@ Style/ClassVars: - 'lib/exception_notifier.rb' - 'test/exception_notifier/modules/error_grouping_test.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Style/MethodCallWithoutArgsParentheses: - Exclude: - - 'test/exception_notification/resque_test.rb' - # Offense count: 3 Style/MethodMissing: Exclude: diff --git a/test/exception_notification/resque_test.rb b/test/exception_notification/resque_test.rb index 6492577f..b3f52ed6 100644 --- a/test/exception_notification/resque_test.rb +++ b/test/exception_notification/resque_test.rb @@ -26,7 +26,7 @@ class ResqueTest < ActiveSupport::TestCase end test "notifies exception when job fails" do - ExceptionNotifier.expects(:notify_exception).with() do |ex, opts| + ExceptionNotifier.expects(:notify_exception).with do |ex, opts| ex.is_a?(RuntimeError) && ex.message == "Bad job!" && opts[:data][:resque][:error_class] == "RuntimeError" && From 46f937a935ad23265b743d86c6646e395173f80e Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 01:59:46 -0300 Subject: [PATCH 068/156] Fix Style/StringLiterals offenses --- .rubocop_todo.yml | 10 -------- lib/exception_notifier/datadog_notifier.rb | 2 +- test/exception_notification/resque_test.rb | 24 +++++++++---------- .../datadog_notifier_test.rb | 6 ++--- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8f8c44b1..5f7a1d4c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -65,13 +65,3 @@ Style/MethodMissing: - 'lib/exception_notifier/email_notifier.rb' - 'lib/exception_notifier/mattermost_notifier.rb' - 'lib/exception_notifier/teams_notifier.rb' - -# Offense count: 18 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiterals: - Exclude: - - 'lib/exception_notifier/datadog_notifier.rb' - - 'test/exception_notification/resque_test.rb' - - 'test/exception_notifier/datadog_notifier_test.rb' diff --git a/lib/exception_notifier/datadog_notifier.rb b/lib/exception_notifier/datadog_notifier.rb index 9fc3dbf8..e29603d6 100644 --- a/lib/exception_notifier/datadog_notifier.rb +++ b/lib/exception_notifier/datadog_notifier.rb @@ -72,7 +72,7 @@ def event end def formatted_title - title = "" + title = '' title << title_prefix title << "#{controller.controller_name} #{controller.action_name}" if controller title << " (#{exception.class})" diff --git a/test/exception_notification/resque_test.rb b/test/exception_notification/resque_test.rb index b3f52ed6..4d146d4a 100644 --- a/test/exception_notification/resque_test.rb +++ b/test/exception_notification/resque_test.rb @@ -1,8 +1,8 @@ -require "test_helper" +require 'test_helper' -require "exception_notification/resque" -require "resque" -require "mock_redis" +require 'exception_notification/resque' +require 'resque' +require 'mock_redis' require 'resque/failure/multiple' require 'resque/failure/redis' @@ -19,22 +19,22 @@ class ResqueTest < ActiveSupport::TestCase @worker.cant_fork = true end - test "count returns the number of failures" do + test 'count returns the number of failures' do Resque::Job.create(:jobs, BadJob) @worker.work(0) assert_equal 1, ExceptionNotification::Resque.count end - test "notifies exception when job fails" do + test 'notifies exception when job fails' do ExceptionNotifier.expects(:notify_exception).with do |ex, opts| ex.is_a?(RuntimeError) && - ex.message == "Bad job!" && - opts[:data][:resque][:error_class] == "RuntimeError" && - opts[:data][:resque][:error_message] == "Bad job!" && + ex.message == 'Bad job!' && + opts[:data][:resque][:error_class] == 'RuntimeError' && + opts[:data][:resque][:error_message] == 'Bad job!' && opts[:data][:resque][:failed_at].present? && opts[:data][:resque][:payload] == { - "class" => "ResqueTest::BadJob", - "args" => [] + 'class' => 'ResqueTest::BadJob', + 'args' => [] } && opts[:data][:resque][:queue] == :jobs && opts[:data][:resque][:worker].present? @@ -46,7 +46,7 @@ class ResqueTest < ActiveSupport::TestCase class BadJob def self.perform - raise "Bad job!" + raise 'Bad job!' end end end diff --git a/test/exception_notifier/datadog_notifier_test.rb b/test/exception_notifier/datadog_notifier_test.rb index 556d1098..90d1ffbb 100644 --- a/test/exception_notifier/datadog_notifier_test.rb +++ b/test/exception_notifier/datadog_notifier_test.rb @@ -30,15 +30,15 @@ def setup test 'should include prefix in event title and not append previous events' do options = { client: @client, - title_prefix: "prefix" + title_prefix: 'prefix' } notifier = ExceptionNotifier::DatadogNotifier.new(options) event = notifier.datadog_event(@exception) - assert_equal event.msg_title, "prefix (DatadogNotifierTest::FakeException) \"Fake exception message\"" + assert_equal event.msg_title, 'prefix (DatadogNotifierTest::FakeException) "Fake exception message"' event2 = notifier.datadog_event(@exception) - assert_equal event2.msg_title, "prefix (DatadogNotifierTest::FakeException) \"Fake exception message\"" + assert_equal event2.msg_title, 'prefix (DatadogNotifierTest::FakeException) "Fake exception message"' end test 'should include exception message in event title' do From 3f68a9336db6b72d5050be2ff7e261665eac1ff4 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 24 Jan 2019 02:02:56 -0300 Subject: [PATCH 069/156] Fix Style/ClassAndModuleChildren offense --- .rubocop_todo.yml | 7 ------- test/dummy/test/test_helper.rb | 6 ++++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 5f7a1d4c..1f0ed3ac 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -46,13 +46,6 @@ Metrics/MethodLength: Metrics/PerceivedComplexity: Max: 24 -# Offense count: 1 -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: nested, compact -Style/ClassAndModuleChildren: - Exclude: - - 'test/dummy/test/test_helper.rb' - # Offense count: 6 Style/ClassVars: Exclude: diff --git a/test/dummy/test/test_helper.rb b/test/dummy/test/test_helper.rb index 77bc8b9e..3e30f622 100644 --- a/test/dummy/test/test_helper.rb +++ b/test/dummy/test/test_helper.rb @@ -2,6 +2,8 @@ require File.expand_path('../config/environment', __dir__) require 'rails/test_help' -class ActiveSupport::TestCase - # Add more helper methods to be used by all tests here... +module ActiveSupport + class TestCase + # Add more helper methods to be used by all tests here... + end end From 6cfb05b5f06bf80ac5e9277ba86727bc1c615a0f Mon Sep 17 00:00:00 2001 From: Romain Pomier Date: Wed, 30 Jan 2019 14:18:44 +0100 Subject: [PATCH 070/156] Use backtrace cleaner for Slack notifications --- lib/exception_notifier/slack_notifier.rb | 3 ++- test/exception_notifier/slack_notifier_test.rb | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/exception_notifier/slack_notifier.rb b/lib/exception_notifier/slack_notifier.rb index f726d9a5..8e8568b7 100644 --- a/lib/exception_notifier/slack_notifier.rb +++ b/lib/exception_notifier/slack_notifier.rb @@ -50,7 +50,8 @@ def deep_reject(hash, block) def attchs(exception, clean_message, options) text, data = information_from_options(exception.class, options) - fields = fields(clean_message, exception.backtrace, data) + backtrace = clean_backtrace(exception) if exception.backtrace + fields = fields(clean_message, backtrace, data) [color: @color, text: text, fields: fields, mrkdwn_in: %w[text fields]] end diff --git a/test/exception_notifier/slack_notifier_test.rb b/test/exception_notifier/slack_notifier_test.rb index 69ca489c..72db960e 100644 --- a/test/exception_notifier/slack_notifier_test.rb +++ b/test/exception_notifier/slack_notifier_test.rb @@ -6,6 +6,7 @@ def setup @exception = fake_exception @exception.stubs(:backtrace).returns(fake_backtrace) @exception.stubs(:message).returns('exception message') + ExceptionNotifier::SlackNotifier.any_instance.stubs(:clean_backtrace).returns(fake_cleaned_backtrace) Socket.stubs(:gethostname).returns('example.com') end @@ -192,6 +193,10 @@ def fake_backtrace ] end + def fake_cleaned_backtrace + fake_backtrace[2..-1] + end + def fake_notification(exception = @exception, notification_options = {}, data_string = nil, expected_backtrace_lines = 10, additional_fields = []) exception_name = "*#{exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'}* `#{exception.class}`" if notification_options[:env].nil? @@ -211,7 +216,7 @@ def fake_notification(exception = @exception, notification_options = {}, data_st fields = [{ title: 'Exception', value: exception.message }] fields.push(title: 'Hostname', value: 'example.com') if exception.backtrace - formatted_backtrace = "```#{exception.backtrace.first(expected_backtrace_lines).join("\n")}```" + formatted_backtrace = "```#{fake_cleaned_backtrace.first(expected_backtrace_lines).join("\n")}```" fields.push(title: 'Backtrace', value: formatted_backtrace) end fields.push(title: 'Data', value: "```#{data_string}```") if data_string From 902bd851bd4af4d75131afe9eac5215efc871376 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 2 Feb 2019 19:56:46 -0300 Subject: [PATCH 071/156] Move logic to generate and format notification content to its own class --- lib/exception_notifier.rb | 1 + .../google_chat_notifier.rb | 90 ++----------- lib/exception_notifier/modules/formatter.rb | 114 ++++++++++++++++ .../google_chat_notifier_test.rb | 6 +- .../modules/formatter_test.rb | 122 ++++++++++++++++++ 5 files changed, 251 insertions(+), 82 deletions(-) create mode 100644 lib/exception_notifier/modules/formatter.rb create mode 100644 test/exception_notifier/modules/formatter_test.rb diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index d4f2aaae..58ea04c3 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -8,6 +8,7 @@ module ExceptionNotifier include ErrorGrouping autoload :BacktraceCleaner, 'exception_notifier/modules/backtrace_cleaner' + autoload :Formatter, 'exception_notifier/modules/formatter' autoload :Notifier, 'exception_notifier/notifier' autoload :EmailNotifier, 'exception_notifier/email_notifier' diff --git a/lib/exception_notifier/google_chat_notifier.rb b/lib/exception_notifier/google_chat_notifier.rb index a4d67d4f..8218df35 100644 --- a/lib/exception_notifier/google_chat_notifier.rb +++ b/lib/exception_notifier/google_chat_notifier.rb @@ -1,100 +1,32 @@ -require 'action_dispatch' -require 'active_support/core_ext/time' require 'httparty' module ExceptionNotifier class GoogleChatNotifier < BaseNotifier - include ExceptionNotifier::BacktraceCleaner - def call(exception, opts = {}) - @options = base_options.merge(opts) - @exception = exception + options = base_options.merge(opts) + formatter = Formatter.new(exception, options) HTTParty.post( options[:webhook_url], - body: { text: body }.to_json, + body: { text: body(exception, formatter) }.to_json, headers: { 'Content-Type' => 'application/json' } ) end private - attr_reader :options, :exception - - def body + def body(exception, formatter) text = [ - header, - '', - "⚠️ Error 500 in #{Rails.env} ⚠️", - "*#{exception.message.tr('`', "'")}*" - ] - - text += message_request - text += message_backtrace - - text.join("\n") - end - - def header - text = ["\nApplication: *#{app_name}*"] - - errors_text = errors_count > 1 ? errors_count : 'An' - text << "#{errors_text} *#{exception.class}* occured#{controller_text}." - - text - end - - def message_request - return [] unless (env = options[:env]) - - request = ActionDispatch::Request.new(env) - - [ + "\nApplication: *#{formatter.app_name}*", + formatter.subtitle, '', - '*Request:*', - '```', - "* url : #{request.original_url}", - "* http_method : #{request.method}", - "* ip_address : #{request.remote_ip}", - "* parameters : #{request.filtered_parameters}", - "* timestamp : #{Time.current}", - '```' + formatter.title, + "*#{exception.message.tr('`', "'")}*", + formatter.request_message, + formatter.backtrace_message ] - end - - def message_backtrace - backtrace = exception.backtrace ? clean_backtrace(exception) : nil - - return [] unless backtrace - - text = [] - - text << '' - text << '*Backtrace:*' - text << '```' - backtrace.first(3).each { |line| text << "* #{line}" } - text << '```' - - text - end - - def app_name - @app_name ||= options[:app_name] || rails_app_name || 'N/A' - end - - def errors_count - @errors_count ||= options[:accumulated_errors_count].to_i - end - - def rails_app_name - Rails.application.class.parent_name.underscore if defined?(Rails) - end - - def controller_text - env = options[:env] - controller = env ? env['action_controller.instance'] : nil - " in *#{controller.controller_name}##{controller.action_name}*" if controller + text.compact.join("\n") end end end diff --git a/lib/exception_notifier/modules/formatter.rb b/lib/exception_notifier/modules/formatter.rb new file mode 100644 index 00000000..d946b63c --- /dev/null +++ b/lib/exception_notifier/modules/formatter.rb @@ -0,0 +1,114 @@ +require 'active_support/core_ext/time' +require 'action_dispatch' + +module ExceptionNotifier + class Formatter + include ExceptionNotifier::BacktraceCleaner + + attr_reader :app_name + + def initialize(exception, opts = {}) + @exception = exception + + @env = opts[:env] + @errors_count = opts[:accumulated_errors_count].to_i + @app_name = opts[:app_name] || rails_app_name + end + + # + # :warning: Error occurred in production :warning: + # :warning: Error occurred :warning: + # + def title + env = Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env) + + if env + "⚠️ Error occurred in #{env} ⚠️" + else + '⚠️ Error occurred ⚠️' + end + end + + # + # A *NoMethodError* occurred. + # 3 *NoMethodError* occurred. + # A *NoMethodError* occurred in *home#index*. + # + def subtitle + errors_text = errors_count > 1 ? errors_count : 'A' + in_action = " in *#{controller_and_action}*" if controller + + "#{errors_text} *#{exception.class}* occurred#{in_action}." + end + + # + # + # *Request:* + # ``` + # * url : https://www.example.com/ + # * http_method : GET + # * ip_address : 127.0.0.1 + # * parameters : {"controller"=>"home", "action"=>"index"} + # * timestamp : 2019-01-01 00:00:00 UTC + # ``` + # + def request_message + request = ActionDispatch::Request.new(env) if env + return unless request + + [ + '', + '*Request:*', + '```', + "* url : #{request.original_url}", + "* http_method : #{request.method}", + "* ip_address : #{request.remote_ip}", + "* parameters : #{request.filtered_parameters}", + "* timestamp : #{Time.current}", + '```' + ].join("\n") + end + + # + # + # *Backtrace:* + # ``` + # * app/controllers/my_controller.rb:99:in `specific_function' + # * app/controllers/my_controller.rb:70:in `specific_param' + # * app/controllers/my_controller.rb:53:in `my_controller_params' + # ``` + # + def backtrace_message + backtrace = exception.backtrace ? clean_backtrace(exception) : nil + + return unless backtrace + + text = [] + + text << '' + text << '*Backtrace:*' + text << '```' + backtrace.first(3).each { |line| text << "* #{line}" } + text << '```' + + text.join("\n") + end + + private + + attr_reader :exception, :env, :errors_count + + def rails_app_name + return unless defined?(::Rails) && ::Rails.respond_to?(:application) + Rails.application.class.parent_name.underscore + end + + def controller + env['action_controller.instance'] if env + end + + def controller_and_action + "#{controller.controller_name}##{controller.action_name}" if controller + end + end +end diff --git a/test/exception_notifier/google_chat_notifier_test.rb b/test/exception_notifier/google_chat_notifier_test.rb index ad39304f..55a929a1 100644 --- a/test/exception_notifier/google_chat_notifier_test.rb +++ b/test/exception_notifier/google_chat_notifier_test.rb @@ -22,7 +22,7 @@ def teardown text = [ '', 'Application: *dummy*', - '5 *ArgumentError* occured.', + '5 *ArgumentError* occurred.', '', body ].join("\n") @@ -160,12 +160,12 @@ def header [ '', 'Application: *dummy*', - 'An *ArgumentError* occured.', + 'A *ArgumentError* occurred.', '' ].join("\n") end def body - "⚠️ Error 500 in test ⚠️\n*foo*" + "⚠️ Error occurred in test ⚠️\n*foo*" end end diff --git a/test/exception_notifier/modules/formatter_test.rb b/test/exception_notifier/modules/formatter_test.rb new file mode 100644 index 00000000..c2baf46c --- /dev/null +++ b/test/exception_notifier/modules/formatter_test.rb @@ -0,0 +1,122 @@ +require 'test_helper' +require 'timecop' + +class FormatterTest < ActiveSupport::TestCase + class HomeController < ActionController::Metal + def index; end + end + + setup do + @exception = RuntimeError.new('test') + Timecop.freeze('2018-12-09 12:07:16 UTC') + end + + teardown do + Timecop.return + end + + # + # #title + # + test 'title returns correct content' do + formatter = ExceptionNotifier::Formatter.new(@exception) + assert_equal '⚠️ Error occurred in test ⚠️', formatter.title + end + + # + # #subtitle + # + test 'subtitle without accumulated error' do + formatter = ExceptionNotifier::Formatter.new(@exception) + assert_equal 'A *RuntimeError* occurred.', formatter.subtitle + end + + test 'subtitle with accumulated error' do + formatter = ExceptionNotifier::Formatter.new(@exception, accumulated_errors_count: 3) + assert_equal '3 *RuntimeError* occurred.', formatter.subtitle + end + + test 'subtitle with controller' do + controller = HomeController.new + controller.process(:index) + + env = Rack::MockRequest.env_for( + '/', 'action_controller.instance' => controller + ) + + formatter = ExceptionNotifier::Formatter.new(@exception, env: env) + assert_equal 'A *RuntimeError* occurred in *home#index*.', formatter.subtitle + end + + # + # #app_name + # + test 'app_name defaults to Rails app name' do + formatter = ExceptionNotifier::Formatter.new(@exception) + assert_equal 'dummy', formatter.app_name + end + + test 'app_name can be overwritten using options' do + formatter = ExceptionNotifier::Formatter.new(@exception, app_name: 'test') + assert_equal 'test', formatter.app_name + end + + # + # #request_message + # + test 'request_message when env set' do + text = [ + '', + '*Request:*', + '```', + '* url : http://test.address/?id=foo', + '* http_method : GET', + '* ip_address : 127.0.0.1', + '* parameters : {"id"=>"foo"}', + '* timestamp : 2018-12-09 12:07:16 UTC', + '```' + ].join("\n") + + env = Rack::MockRequest.env_for( + '/', + 'HTTP_HOST' => 'test.address', + 'REMOTE_ADDR' => '127.0.0.1', + params: { id: 'foo' } + ) + + formatter = ExceptionNotifier::Formatter.new(@exception, env: env) + assert_equal text, formatter.request_message + end + + test 'request_message when env not set' do + formatter = ExceptionNotifier::Formatter.new(@exception) + assert_nil formatter.request_message + end + + # + # #backtrace_message + # + test 'backtrace_message when backtrace set' do + text = [ + '', + '*Backtrace:*', + '```', + "* app/controllers/my_controller.rb:53:in `my_controller_params'", + "* app/controllers/my_controller.rb:34:in `update'", + '```' + ].join("\n") + + @exception.set_backtrace([ + "app/controllers/my_controller.rb:53:in `my_controller_params'", + "app/controllers/my_controller.rb:34:in `update'" + ]) + + formatter = ExceptionNotifier::Formatter.new(@exception) + assert_equal text, formatter.backtrace_message + end + + test 'backtrace_message when no backtrace' do + formatter = ExceptionNotifier::Formatter.new(@exception) + assert_nil formatter.backtrace_message + end +end From 536f89bd8253ee6ef2369a8e68b329e589791543 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 2 Feb 2019 20:46:23 -0300 Subject: [PATCH 072/156] Refactor Mattermost tests --- .../mattermost_notifier_test.rb | 209 ++++++++++++------ 1 file changed, 146 insertions(+), 63 deletions(-) diff --git a/test/exception_notifier/mattermost_notifier_test.rb b/test/exception_notifier/mattermost_notifier_test.rb index f2404e23..953d30d8 100644 --- a/test/exception_notifier/mattermost_notifier_test.rb +++ b/test/exception_notifier/mattermost_notifier_test.rb @@ -1,102 +1,185 @@ require 'test_helper' require 'httparty' +require 'timecop' class MattermostNotifierTest < ActiveSupport::TestCase - test 'should send notification if properly configured' do - options = { - webhook_url: 'http://localhost:8000' - } - mattermost_notifier = ExceptionNotifier::MattermostNotifier.new - mattermost_notifier.httparty = FakeHTTParty.new - - options = mattermost_notifier.call ArgumentError.new('foo'), options + URL = 'http://localhost:8000'.freeze - body = ActiveSupport::JSON.decode options[:body] - assert body.key? 'text' - assert body.key? 'username' + def setup + Timecop.freeze('2018-12-09 12:07:16 UTC') + end - text = body['text'].split("\n") - assert_equal 4, text.size - assert_equal '@channel', text[0] - assert_equal 'An *ArgumentError* occured.', text[2] - assert_equal '*foo*', text[3] + def teardown + Timecop.return end - test 'should send notification with create issue link if specified' do - options = { - webhook_url: 'http://localhost:8000', - git_url: 'github.com/aschen' + test 'should send notification if properly configured' do + opts = { + body: default_body.to_json, + headers: defaul_headers } - mattermost_notifier = ExceptionNotifier::MattermostNotifier.new - mattermost_notifier.httparty = FakeHTTParty.new - options = mattermost_notifier.call ArgumentError.new('foo'), options + HTTParty.expects(:post).with(URL, opts) + notifier.call ArgumentError.new('foo') + end - body = ActiveSupport::JSON.decode options[:body] + test 'should send notification with create issue link if specified' do + body = default_body.merge( + text: [ + '@channel', + '### :warning: Error 500 in test :warning:', + 'An *ArgumentError* occured.', + '*foo*', + '[Create an issue](github.com/aschen/dummy/issues/new/?issue%5Btitle%5D=%5BBUG%5D+Error+500+%3A++%28ArgumentError%29+foo)' + ].join("\n") + ) + + opts = { + body: body.to_json, + headers: defaul_headers + } - text = body['text'].split("\n") - assert_equal 5, text.size - assert_equal '[Create an issue](github.com/aschen/dummy/issues/new/?issue%5Btitle%5D=%5BBUG%5D+Error+500+%3A++%28ArgumentError%29+foo)', text[4] + HTTParty.expects(:post).with(URL, opts) + notifier.call ArgumentError.new('foo'), git_url: 'github.com/aschen' end test 'should add username and icon_url params to the notification if specified' do - options = { - webhook_url: 'http://localhost:8000', + body = default_body.merge( username: 'Test Bot', - avatar: 'http://site.com/icon.png' - } - mattermost_notifier = ExceptionNotifier::MattermostNotifier.new - mattermost_notifier.httparty = FakeHTTParty.new - - options = mattermost_notifier.call ArgumentError.new('foo'), options + icon_url: 'http://site.com/icon.png' + ) - body = ActiveSupport::JSON.decode options[:body] + opts = { + body: body.to_json, + headers: defaul_headers + } - assert_equal 'Test Bot', body['username'] - assert_equal 'http://site.com/icon.png', body['icon_url'] + HTTParty.expects(:post).with(URL, opts) + notifier.call( + ArgumentError.new('foo'), + username: 'Test Bot', + avatar: 'http://site.com/icon.png' + ) end test 'should add other HTTParty options to params' do - options = { - webhook_url: 'http://localhost:8000', - username: 'Test Bot', - avatar: 'http://site.com/icon.png', + opts = { basic_auth: { username: 'clara', password: 'password' - } + }, + body: default_body.to_json, + headers: defaul_headers } - mattermost_notifier = ExceptionNotifier::MattermostNotifier.new - mattermost_notifier.httparty = FakeHTTParty.new - - options = mattermost_notifier.call ArgumentError.new('foo'), options - assert options.key? :basic_auth - assert 'clara', options[:basic_auth][:username] - assert 'password', options[:basic_auth][:password] + HTTParty.expects(:post).with(URL, opts) + notifier.call( + ArgumentError.new('foo'), + basic_auth: { + username: 'clara', + password: 'password' + } + ) end test "should use 'An' for exceptions count if :accumulated_errors_count option is nil" do - mattermost_notifier = ExceptionNotifier::MattermostNotifier.new - exception = ArgumentError.new('foo') - mattermost_notifier.instance_variable_set(:@exception, exception) - mattermost_notifier.instance_variable_set(:@options, {}) + opts = { + body: default_body.to_json, + headers: defaul_headers + } - assert_includes mattermost_notifier.send(:message_header), 'An *ArgumentError* occured.' + HTTParty.expects(:post).with(URL, opts) + notifier.call(ArgumentError.new('foo')) end test 'shoud use direct errors count if :accumulated_errors_count option is 5' do - mattermost_notifier = ExceptionNotifier::MattermostNotifier.new + body = default_body.merge( + text: [ + '@channel', + '### :warning: Error 500 in test :warning:', + '5 *ArgumentError* occured.', + '*foo*' + ].join("\n") + ) + + opts = { + body: body.to_json, + headers: defaul_headers, + accumulated_errors_count: 5 + } + + HTTParty.expects(:post).with(URL, opts) + notifier.call(ArgumentError.new('foo'), accumulated_errors_count: 5) + end + + test 'should include backtrace and request info' do + body = default_body.merge( + text: [ + '@channel', + '### :warning: Error 500 in test :warning:', + 'An *ArgumentError* occured in *#*.', + '*foo*', + '### Request', + '```', + '* url : http://test.address/?id=foo', + '* http_method : GET', + '* ip_address : 127.0.0.1', + '* parameters : {"id"=>"foo"}', + '* timestamp : 2018-12-09 12:07:16 UTC', + '```', + '### Backtrace', + '```', + "* app/controllers/my_controller.rb:53:in `my_controller_params'", + "* app/controllers/my_controller.rb:34:in `update'", + '```' + ].join("\n") + ) + + opts = { + body: body.to_json, + headers: defaul_headers + } + + HTTParty.expects(:post).with(URL, opts) + exception = ArgumentError.new('foo') - mattermost_notifier.instance_variable_set(:@exception, exception) - mattermost_notifier.instance_variable_set(:@options, accumulated_errors_count: 5) + exception.set_backtrace([ + "app/controllers/my_controller.rb:53:in `my_controller_params'", + "app/controllers/my_controller.rb:34:in `update'" + ]) - assert_includes mattermost_notifier.send(:message_header), '5 *ArgumentError* occured.' + notifier.call(exception, env: test_env) + end + + private + + def notifier + ExceptionNotifier::MattermostNotifier.new(webhook_url: URL) + end + + def default_body + { + text: [ + '@channel', + '### :warning: Error 500 in test :warning:', + 'An *ArgumentError* occured.', + '*foo*' + ].join("\n"), + username: 'Exception Notifier' + } + end + + def defaul_headers + { 'Content-Type' => 'application/json' } end -end -class FakeHTTParty - def post(_url, options) - options + def test_env + Rack::MockRequest.env_for( + '/', + 'HTTP_HOST' => 'test.address', + 'REMOTE_ADDR' => '127.0.0.1', + 'HTTP_USER_AGENT' => 'Rails Testing', + params: { id: 'foo' } + ) end end From 1d9db20b919b57817d63fac43e810905f4c28f62 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 2 Feb 2019 22:17:56 -0300 Subject: [PATCH 073/156] Use formatter in Mattermost notifier --- .rubocop_todo.yml | 1 - .../google_chat_notifier.rb | 16 +- lib/exception_notifier/mattermost_notifier.rb | 164 ++++-------------- lib/exception_notifier/modules/formatter.rb | 15 +- .../mattermost_notifier_test.rb | 16 +- .../modules/formatter_test.rb | 26 ++- 6 files changed, 88 insertions(+), 150 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1f0ed3ac..d447edba 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -56,5 +56,4 @@ Style/ClassVars: Style/MethodMissing: Exclude: - 'lib/exception_notifier/email_notifier.rb' - - 'lib/exception_notifier/mattermost_notifier.rb' - 'lib/exception_notifier/teams_notifier.rb' diff --git a/lib/exception_notifier/google_chat_notifier.rb b/lib/exception_notifier/google_chat_notifier.rb index 8218df35..94ce8480 100644 --- a/lib/exception_notifier/google_chat_notifier.rb +++ b/lib/exception_notifier/google_chat_notifier.rb @@ -21,11 +21,21 @@ def body(exception, formatter) formatter.subtitle, '', formatter.title, - "*#{exception.message.tr('`', "'")}*", - formatter.request_message, - formatter.backtrace_message + "*#{exception.message.tr('`', "'")}*" ] + if (request = formatter.request_message.presence) + text << '' + text << '*Request:*' + text << request + end + + if (backtrace = formatter.backtrace_message.presence) + text << '' + text << '*Backtrace:*' + text << backtrace + end + text.compact.join("\n") end end diff --git a/lib/exception_notifier/mattermost_notifier.rb b/lib/exception_notifier/mattermost_notifier.rb index 827b53b9..16554655 100644 --- a/lib/exception_notifier/mattermost_notifier.rb +++ b/lib/exception_notifier/mattermost_notifier.rb @@ -1,163 +1,75 @@ -require 'action_dispatch' -require 'active_support/core_ext/time' +require 'httparty' module ExceptionNotifier - class MattermostNotifier - include ExceptionNotifier::BacktraceCleaner - - class MissingController - def method_missing(*args, &block); end - end - - attr_accessor :httparty - - def initialize(options = {}) - super() - @default_options = options - @httparty = HTTParty - end - + class MattermostNotifier < BaseNotifier def call(exception, options = {}) - @options = options.merge(@default_options) + @options = options.merge(base_options) @exception = exception - @backtrace = exception.backtrace ? clean_backtrace(exception) : nil - @env = @options.delete(:env) + @formatter = Formatter.new( + exception, + env: @options.delete(:env), + app_name: @options.delete(:app_name), + accumulated_errors_count: @options[:accumulated_errors_count] + ) - @application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore + avatar = @options.delete(:avatar) + channel = @options.delete(:channel) @gitlab_url = @options.delete(:git_url) - @username = @options.delete(:username) || 'Exception Notifier' - @avatar = @options.delete(:avatar) - - @channel = @options.delete(:channel) @webhook_url = @options.delete(:webhook_url) raise ArgumentError, "You must provide 'webhook_url' parameter." unless @webhook_url - if @env.nil? - @controller = @request_items = nil - else - @controller = @env['action_controller.instance'] || MissingController.new - - request = ActionDispatch::Request.new(@env) - - @request_items = { url: request.original_url, - http_method: request.method, - ip_address: request.remote_ip, - parameters: request.filtered_parameters, - timestamp: Time.current } - - if request.session['warden.user.user.key'] - current_user = User.find(request.session['warden.user.user.key'][0][0]) - @request_items[:current_user] = { id: current_user.id, email: current_user.email } - end - end - - payload = message_text.merge(user_info).merge(channel_info) + payload = { + text: message_text.compact.join("\n") + } + payload[:username] = @options.delete(:username) || 'Exception Notifier' + payload[:icon_url] = avatar if avatar + payload[:channel] = channel if channel @options[:body] = payload.to_json @options[:headers] ||= {} @options[:headers]['Content-Type'] = 'application/json' - @httparty.post(@webhook_url, @options) + HTTParty.post(@webhook_url, @options) end private - def channel_info - if @channel - { channel: @channel } - else - {} - end - end - - def user_info - infos = {} - - infos[:username] = @username if @username - infos[:icon_url] = @avatar if @avatar - - infos - end + attr_reader :formatter def message_text - text = [] - - text += ['@channel'] - text += message_header - text += message_request if @request_items - text += message_backtrace if @backtrace - text += message_issue_link if @gitlab_url - - { text: text.join("\n") } - end - - def message_header - text = [] - - errors_count = @options[:accumulated_errors_count].to_i - text << "### :warning: Error 500 in #{Rails.env} :warning:" - text << "#{errors_count > 1 ? errors_count : 'An'} *#{@exception.class}* occured" + (@controller ? " in *#{controller_and_method}*." : '.') - text << "*#{@exception.message}*" - - text - end - - def message_request - text = [] - - text << '### Request' - text << '```' - text << hash_presentation(@request_items) - text << '```' - - text - end + text = [ + '@channel', + "### #{formatter.title}", + formatter.subtitle, + "*#{@exception.message}*" + ] + + if (request = formatter.request_message.presence) + text << '### Request' + text << request + end - def message_backtrace(size = 3) - text = [] + if (backtrace = formatter.backtrace_message.presence) + text << '### Backtrace' + text << backtrace + end - size = @backtrace.size < size ? @backtrace.size : size - text << '### Backtrace' - text << '```' - size.times { |i| text << '* ' + @backtrace[i] } - text << '```' + text << message_issue_link if @gitlab_url text end def message_issue_link - text = [] - - link = [@gitlab_url, @application_name, 'issues', 'new'].join('/') + link = [@gitlab_url, formatter.app_name, 'issues', 'new'].join('/') params = { 'issue[title]' => ['[BUG] Error 500 :', - controller_and_method, + formatter.controller_and_action || '', "(#{@exception.class})", @exception.message].compact.join(' ') }.to_query - text << "[Create an issue](#{link}/?#{params})" - - text - end - - def controller_and_method - if @controller - "#{@controller.controller_name}##{@controller.action_name}" - else - '' - end - end - - def hash_presentation(hash) - text = [] - - hash.each do |key, value| - text << "* #{key} : #{value}" - end - - text.join("\n") + "[Create an issue](#{link}/?#{params})" end end end diff --git a/lib/exception_notifier/modules/formatter.rb b/lib/exception_notifier/modules/formatter.rb index d946b63c..d62c7659 100644 --- a/lib/exception_notifier/modules/formatter.rb +++ b/lib/exception_notifier/modules/formatter.rb @@ -57,8 +57,6 @@ def request_message return unless request [ - '', - '*Request:*', '```', "* url : #{request.original_url}", "* http_method : #{request.method}", @@ -85,8 +83,6 @@ def backtrace_message text = [] - text << '' - text << '*Backtrace:*' text << '```' backtrace.first(3).each { |line| text << "* #{line}" } text << '```' @@ -94,6 +90,13 @@ def backtrace_message text.join("\n") end + # + # home#index + # + def controller_and_action + "#{controller.controller_name}##{controller.action_name}" if controller + end + private attr_reader :exception, :env, :errors_count @@ -106,9 +109,5 @@ def rails_app_name def controller env['action_controller.instance'] if env end - - def controller_and_action - "#{controller.controller_name}##{controller.action_name}" if controller - end end end diff --git a/test/exception_notifier/mattermost_notifier_test.rb b/test/exception_notifier/mattermost_notifier_test.rb index 953d30d8..3035b73f 100644 --- a/test/exception_notifier/mattermost_notifier_test.rb +++ b/test/exception_notifier/mattermost_notifier_test.rb @@ -27,8 +27,8 @@ def teardown body = default_body.merge( text: [ '@channel', - '### :warning: Error 500 in test :warning:', - 'An *ArgumentError* occured.', + '### ⚠️ Error occurred in test ⚠️', + 'A *ArgumentError* occurred.', '*foo*', '[Create an issue](github.com/aschen/dummy/issues/new/?issue%5Btitle%5D=%5BBUG%5D+Error+500+%3A++%28ArgumentError%29+foo)' ].join("\n") @@ -96,8 +96,8 @@ def teardown body = default_body.merge( text: [ '@channel', - '### :warning: Error 500 in test :warning:', - '5 *ArgumentError* occured.', + '### ⚠️ Error occurred in test ⚠️', + '5 *ArgumentError* occurred.', '*foo*' ].join("\n") ) @@ -116,8 +116,8 @@ def teardown body = default_body.merge( text: [ '@channel', - '### :warning: Error 500 in test :warning:', - 'An *ArgumentError* occured in *#*.', + '### ⚠️ Error occurred in test ⚠️', + 'A *ArgumentError* occurred.', '*foo*', '### Request', '```', @@ -161,8 +161,8 @@ def default_body { text: [ '@channel', - '### :warning: Error 500 in test :warning:', - 'An *ArgumentError* occured.', + '### ⚠️ Error occurred in test ⚠️', + 'A *ArgumentError* occurred.', '*foo*' ].join("\n"), username: 'Exception Notifier' diff --git a/test/exception_notifier/modules/formatter_test.rb b/test/exception_notifier/modules/formatter_test.rb index c2baf46c..2199e1ec 100644 --- a/test/exception_notifier/modules/formatter_test.rb +++ b/test/exception_notifier/modules/formatter_test.rb @@ -66,8 +66,6 @@ def index; end # test 'request_message when env set' do text = [ - '', - '*Request:*', '```', '* url : http://test.address/?id=foo', '* http_method : GET', @@ -98,8 +96,6 @@ def index; end # test 'backtrace_message when backtrace set' do text = [ - '', - '*Backtrace:*', '```', "* app/controllers/my_controller.rb:53:in `my_controller_params'", "* app/controllers/my_controller.rb:34:in `update'", @@ -119,4 +115,26 @@ def index; end formatter = ExceptionNotifier::Formatter.new(@exception) assert_nil formatter.backtrace_message end + + # + # #controller_and_action + # + test 'correct controller_and_action if controller is present' do + controller = HomeController.new + controller.process(:index) + + env = Rack::MockRequest.env_for( + '/', 'action_controller.instance' => controller + ) + + formatter = ExceptionNotifier::Formatter.new(@exception, env: env) + assert_equal 'home#index', formatter.controller_and_action + end + + test 'controller_and_action is nil if no controller' do + env = Rack::MockRequest.env_for('/') + + formatter = ExceptionNotifier::Formatter.new(@exception, env: env) + assert_nil formatter.controller_and_action + end end From 3870e1007b26eb18c36dc184087f6644c073b7b1 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 2 Feb 2019 22:43:49 -0300 Subject: [PATCH 074/156] Refactor Mattermost notifier --- lib/exception_notifier/mattermost_notifier.rb | 39 +++++++++---------- .../mattermost_notifier_test.rb | 3 +- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/lib/exception_notifier/mattermost_notifier.rb b/lib/exception_notifier/mattermost_notifier.rb index 16554655..63f9ef3c 100644 --- a/lib/exception_notifier/mattermost_notifier.rb +++ b/lib/exception_notifier/mattermost_notifier.rb @@ -2,35 +2,32 @@ module ExceptionNotifier class MattermostNotifier < BaseNotifier - def call(exception, options = {}) - @options = options.merge(base_options) + def call(exception, opts = {}) + options = opts.merge(base_options) @exception = exception - @formatter = Formatter.new( - exception, - env: @options.delete(:env), - app_name: @options.delete(:app_name), - accumulated_errors_count: @options[:accumulated_errors_count] - ) + @formatter = Formatter.new(exception, options) - avatar = @options.delete(:avatar) - channel = @options.delete(:channel) - @gitlab_url = @options.delete(:git_url) - @webhook_url = @options.delete(:webhook_url) - raise ArgumentError, "You must provide 'webhook_url' parameter." unless @webhook_url + @gitlab_url = options[:git_url] payload = { - text: message_text.compact.join("\n") + text: message_text.compact.join("\n"), + username: options[:username] || 'Exception Notifier' } - payload[:username] = @options.delete(:username) || 'Exception Notifier' - payload[:icon_url] = avatar if avatar - payload[:channel] = channel if channel - @options[:body] = payload.to_json - @options[:headers] ||= {} - @options[:headers]['Content-Type'] = 'application/json' + payload[:icon_url] = options[:avatar] if options[:avatar] + payload[:channel] = options[:channel] if options[:channel] + + httparty_options = options.except( + :avatar, :channel, :username, :git_url, :webhook_url, + :env, :accumulated_errors_count, :app_name + ) + + httparty_options[:body] = payload.to_json + httparty_options[:headers] ||= {} + httparty_options[:headers]['Content-Type'] = 'application/json' - HTTParty.post(@webhook_url, @options) + HTTParty.post(options[:webhook_url], httparty_options) end private diff --git a/test/exception_notifier/mattermost_notifier_test.rb b/test/exception_notifier/mattermost_notifier_test.rb index 3035b73f..5677d702 100644 --- a/test/exception_notifier/mattermost_notifier_test.rb +++ b/test/exception_notifier/mattermost_notifier_test.rb @@ -104,8 +104,7 @@ def teardown opts = { body: body.to_json, - headers: defaul_headers, - accumulated_errors_count: 5 + headers: defaul_headers } HTTParty.expects(:post).with(URL, opts) From df864ea68405a49a213fa0d0ca839983af31d0cc Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Tue, 19 Feb 2019 17:42:11 -0300 Subject: [PATCH 075/156] Use correct article --- lib/exception_notifier/modules/formatter.rb | 7 ++++++- test/exception_notifier/google_chat_notifier_test.rb | 2 +- test/exception_notifier/mattermost_notifier_test.rb | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/exception_notifier/modules/formatter.rb b/lib/exception_notifier/modules/formatter.rb index d62c7659..a583745b 100644 --- a/lib/exception_notifier/modules/formatter.rb +++ b/lib/exception_notifier/modules/formatter.rb @@ -35,7 +35,12 @@ def title # A *NoMethodError* occurred in *home#index*. # def subtitle - errors_text = errors_count > 1 ? errors_count : 'A' + errors_text = if errors_count > 1 + errors_count + else + exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' + end + in_action = " in *#{controller_and_action}*" if controller "#{errors_text} *#{exception.class}* occurred#{in_action}." diff --git a/test/exception_notifier/google_chat_notifier_test.rb b/test/exception_notifier/google_chat_notifier_test.rb index 55a929a1..30628194 100644 --- a/test/exception_notifier/google_chat_notifier_test.rb +++ b/test/exception_notifier/google_chat_notifier_test.rb @@ -160,7 +160,7 @@ def header [ '', 'Application: *dummy*', - 'A *ArgumentError* occurred.', + 'An *ArgumentError* occurred.', '' ].join("\n") end diff --git a/test/exception_notifier/mattermost_notifier_test.rb b/test/exception_notifier/mattermost_notifier_test.rb index 5677d702..c08a8b8b 100644 --- a/test/exception_notifier/mattermost_notifier_test.rb +++ b/test/exception_notifier/mattermost_notifier_test.rb @@ -28,7 +28,7 @@ def teardown text: [ '@channel', '### ⚠️ Error occurred in test ⚠️', - 'A *ArgumentError* occurred.', + 'An *ArgumentError* occurred.', '*foo*', '[Create an issue](github.com/aschen/dummy/issues/new/?issue%5Btitle%5D=%5BBUG%5D+Error+500+%3A++%28ArgumentError%29+foo)' ].join("\n") @@ -116,7 +116,7 @@ def teardown text: [ '@channel', '### ⚠️ Error occurred in test ⚠️', - 'A *ArgumentError* occurred.', + 'An *ArgumentError* occurred.', '*foo*', '### Request', '```', @@ -161,7 +161,7 @@ def default_body text: [ '@channel', '### ⚠️ Error occurred in test ⚠️', - 'A *ArgumentError* occurred.', + 'An *ArgumentError* occurred.', '*foo*' ].join("\n"), username: 'Exception Notifier' From 290d3f9d0a45741d4817ac2c68c55d62a07f8408 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Tue, 19 Feb 2019 17:46:39 -0300 Subject: [PATCH 076/156] Fix typo --- .../mattermost_notifier_test.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/exception_notifier/mattermost_notifier_test.rb b/test/exception_notifier/mattermost_notifier_test.rb index c08a8b8b..1a0b1496 100644 --- a/test/exception_notifier/mattermost_notifier_test.rb +++ b/test/exception_notifier/mattermost_notifier_test.rb @@ -16,7 +16,7 @@ def teardown test 'should send notification if properly configured' do opts = { body: default_body.to_json, - headers: defaul_headers + headers: default_headers } HTTParty.expects(:post).with(URL, opts) @@ -36,7 +36,7 @@ def teardown opts = { body: body.to_json, - headers: defaul_headers + headers: default_headers } HTTParty.expects(:post).with(URL, opts) @@ -51,7 +51,7 @@ def teardown opts = { body: body.to_json, - headers: defaul_headers + headers: default_headers } HTTParty.expects(:post).with(URL, opts) @@ -69,7 +69,7 @@ def teardown password: 'password' }, body: default_body.to_json, - headers: defaul_headers + headers: default_headers } HTTParty.expects(:post).with(URL, opts) @@ -85,7 +85,7 @@ def teardown test "should use 'An' for exceptions count if :accumulated_errors_count option is nil" do opts = { body: default_body.to_json, - headers: defaul_headers + headers: default_headers } HTTParty.expects(:post).with(URL, opts) @@ -104,7 +104,7 @@ def teardown opts = { body: body.to_json, - headers: defaul_headers + headers: default_headers } HTTParty.expects(:post).with(URL, opts) @@ -136,7 +136,7 @@ def teardown opts = { body: body.to_json, - headers: defaul_headers + headers: default_headers } HTTParty.expects(:post).with(URL, opts) @@ -168,7 +168,7 @@ def default_body } end - def defaul_headers + def default_headers { 'Content-Type' => 'application/json' } end From e63df5546b9007fc1292fe0da93d9e3232f999b6 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Tue, 19 Feb 2019 21:53:41 -0300 Subject: [PATCH 077/156] Use sqlite3 < 1.4 --- exception_notification.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 23afd287..50b1c571 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -34,7 +34,7 @@ Gem::Specification.new do |s| # Sidekiq 3.2.2 does not support Ruby 1.9. s.add_development_dependency 'sidekiq', '~> 3.0.0', '< 3.2.2' s.add_development_dependency 'slack-notifier', '>= 1.0.0' - s.add_development_dependency 'sqlite3', '>= 1.3.4' + s.add_development_dependency 'sqlite3', '~> 1.3.4' s.add_development_dependency 'timecop', '~>0.9.0' s.add_development_dependency 'tinder', '~> 1.8' end From e02a8e305339c0d4b245230dd10ca8b2a863f09d Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Tue, 19 Feb 2019 21:53:41 -0300 Subject: [PATCH 078/156] Use sqlite3 < 1.4 --- exception_notification.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 23afd287..50b1c571 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -34,7 +34,7 @@ Gem::Specification.new do |s| # Sidekiq 3.2.2 does not support Ruby 1.9. s.add_development_dependency 'sidekiq', '~> 3.0.0', '< 3.2.2' s.add_development_dependency 'slack-notifier', '>= 1.0.0' - s.add_development_dependency 'sqlite3', '>= 1.3.4' + s.add_development_dependency 'sqlite3', '~> 1.3.4' s.add_development_dependency 'timecop', '~>0.9.0' s.add_development_dependency 'tinder', '~> 1.8' end From 0a24ee8c5162f1f7838acee1e166f84ab7c21160 Mon Sep 17 00:00:00 2001 From: Victoria Madrid Date: Wed, 19 Dec 2018 00:05:24 -0300 Subject: [PATCH 079/156] add sample app for bugs reports --- sample_app.rb | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 sample_app.rb diff --git a/sample_app.rb b/sample_app.rb new file mode 100644 index 00000000..b031016a --- /dev/null +++ b/sample_app.rb @@ -0,0 +1,56 @@ +# To run the application: ruby sample_app.rb +require 'bundler/inline' + +gemfile do + source 'https://rubygems.org' + + gem 'rails', '5.0.0' + gem 'exception_notification', '4.3.0' + gem 'httparty', '0.15.7' +end + +class SampleApp < Rails::Application + config.middleware.use ExceptionNotification::Rack, + # ----------------------------------- + # Change this with the notifier you want to test + # https://github.com/smartinez87/exception_notification#notifiers + webhook: { + url: 'http://domain.com:5555/hubot/path' + } + # ----------------------------------- + + config.secret_key_base = 'my secret key base' + config.logger = Logger.new($stdout) + Rails.logger = config.logger + + routes.draw do + get 'raise_exception', to: 'exceptions#sample' + end +end + +require 'action_controller/railtie' +require 'active_support' + +class ExceptionsController < ActionController::Base + include Rails.application.routes.url_helpers + + def sample + raise 'Sample exception raised, you should receive a notification!' + end +end + +require 'minitest/autorun' + +class Test < Minitest::Test + include Rack::Test::Methods + + def test_raise_exception + get '/raise_exception' + end + + private + + def app + Rails.application + end +end From 82f841a33a6dc1bd4393f7426258566676d688c5 Mon Sep 17 00:00:00 2001 From: Ana Machado Date: Wed, 23 Jan 2019 19:49:57 -0300 Subject: [PATCH 080/156] Add sample_app.log to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 136bc30a..611f0a97 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.gemfile.lock /Gemfile.lock /.idea/ +sample_app.log From 5e67e6ba0dfe49716bc3ca413dddc41151017e40 Mon Sep 17 00:00:00 2001 From: Ana Machado Date: Wed, 23 Jan 2019 19:52:54 -0300 Subject: [PATCH 081/156] Change comments info --- sample_app.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sample_app.rb b/sample_app.rb index b031016a..70fdba4c 100644 --- a/sample_app.rb +++ b/sample_app.rb @@ -1,4 +1,7 @@ +# ------------------------------------------- # To run the application: ruby sample_app.rb +# ------------------------------------------- + require 'bundler/inline' gemfile do @@ -12,7 +15,7 @@ class SampleApp < Rails::Application config.middleware.use ExceptionNotification::Rack, # ----------------------------------- - # Change this with the notifier you want to test + # Change this with your configuration # https://github.com/smartinez87/exception_notification#notifiers webhook: { url: 'http://domain.com:5555/hubot/path' From 9fb2b3d5dcefcb422a27543e2c9d4a3f24d764c3 Mon Sep 17 00:00:00 2001 From: Ana Machado Date: Wed, 23 Jan 2019 19:53:39 -0300 Subject: [PATCH 082/156] Change method names and add info --- sample_app.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/sample_app.rb b/sample_app.rb index 70fdba4c..29b3491f 100644 --- a/sample_app.rb +++ b/sample_app.rb @@ -23,11 +23,12 @@ class SampleApp < Rails::Application # ----------------------------------- config.secret_key_base = 'my secret key base' - config.logger = Logger.new($stdout) - Rails.logger = config.logger + file = File.open('sample_app.log', 'w') + logger = Logger.new(file) + Rails.logger = logger routes.draw do - get 'raise_exception', to: 'exceptions#sample' + get 'raise_sample_exception', to: 'exceptions#raise_sample_exception' end end @@ -37,7 +38,8 @@ class SampleApp < Rails::Application class ExceptionsController < ActionController::Base include Rails.application.routes.url_helpers - def sample + def raise_sample_exception + puts 'Raising exception!' raise 'Sample exception raised, you should receive a notification!' end end @@ -48,7 +50,8 @@ class Test < Minitest::Test include Rack::Test::Methods def test_raise_exception - get '/raise_exception' + get '/raise_sample_exception' + puts "Working OK!" end private From 3988888008790b753d03ad8c1cd49207cd410472 Mon Sep 17 00:00:00 2001 From: Ana Machado Date: Wed, 23 Jan 2019 20:28:35 -0300 Subject: [PATCH 083/156] Moved sample app to examples folder --- sample_app.rb => examples/sample_app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename sample_app.rb => examples/sample_app.rb (96%) diff --git a/sample_app.rb b/examples/sample_app.rb similarity index 96% rename from sample_app.rb rename to examples/sample_app.rb index 29b3491f..f652c109 100644 --- a/sample_app.rb +++ b/examples/sample_app.rb @@ -1,5 +1,5 @@ # ------------------------------------------- -# To run the application: ruby sample_app.rb +# To run the application: ruby example/sample_app.rb # ------------------------------------------- require 'bundler/inline' From 6579e49a55ba2525422ab5438fe095a2816c7ec7 Mon Sep 17 00:00:00 2001 From: Ana Machado Date: Wed, 23 Jan 2019 20:48:53 -0300 Subject: [PATCH 084/156] Update run comment --- examples/sample_app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sample_app.rb b/examples/sample_app.rb index f652c109..899bac2e 100644 --- a/examples/sample_app.rb +++ b/examples/sample_app.rb @@ -1,5 +1,5 @@ # ------------------------------------------- -# To run the application: ruby example/sample_app.rb +# To run the application: ruby examples/sample_app.rb # ------------------------------------------- require 'bundler/inline' From 3bd190a5cb9c0b6c709c29338cce1f97654c7221 Mon Sep 17 00:00:00 2001 From: Ana Machado Date: Wed, 23 Jan 2019 20:49:55 -0300 Subject: [PATCH 085/156] Add guide to use sample app --- CONTRIBUTING.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c2ba7a1..c70e03d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,24 @@ need contributors to follow: like OS version, gem versions, etc... * Even better, provide a failing test case for it. +To help you add information to an issue, you can use the sample_app. +Steps to use sample_app: + +1) Add your configuration to (ex. with webhook): +```ruby +config.middleware.use ExceptionNotification::Rack, + # ----------------------------------- + # Change this with your configuration + # https://github.com/smartinez87/exception_notification#notifiers + webhook: { + url: 'http://domain.com:5555/hubot/path' + } + # ----------------------------------- +``` + +2) Run `ruby examples/sample_app.rb` +If exception notification is working OK, you'll see the message "'Raising exception!" and then "Working OK!" and should receive the notification as configured above. If it's not, you can copy the information printed on the terminal related to exception notification and report an issue with more info! + ## Pull Requests If you've gone the extra mile and have a patch that fixes the issue, you From 2d5405381289a1d625d7f4b9a1bad027026ed5fb Mon Sep 17 00:00:00 2001 From: Ana Machado Date: Tue, 19 Feb 2019 16:43:59 -0300 Subject: [PATCH 086/156] Fixed rubocop errors --- examples/sample_app.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/sample_app.rb b/examples/sample_app.rb index 899bac2e..f0a53a88 100644 --- a/examples/sample_app.rb +++ b/examples/sample_app.rb @@ -14,13 +14,9 @@ class SampleApp < Rails::Application config.middleware.use ExceptionNotification::Rack, - # ----------------------------------- - # Change this with your configuration - # https://github.com/smartinez87/exception_notification#notifiers webhook: { url: 'http://domain.com:5555/hubot/path' } - # ----------------------------------- config.secret_key_base = 'my secret key base' file = File.open('sample_app.log', 'w') @@ -51,7 +47,7 @@ class Test < Minitest::Test def test_raise_exception get '/raise_sample_exception' - puts "Working OK!" + puts 'Working OK!' end private From 664566595a289e0c5e11de83d57252d1bfa4d217 Mon Sep 17 00:00:00 2001 From: chaadow Date: Mon, 25 Feb 2019 15:03:47 +0100 Subject: [PATCH 087/156] Add slack channel name override - Update the `slacks.md` documentation to include: - The new `channel` option when callin `ExceptionNotifier.notify_exception` - the `:pre_callback` hook on the rack middleware, that allows to manipulate what's sent to the underlying slack notifier. In particular, this can append an option to the `#ping` slack notifier method. but you can pretty much do any manipulation right before sending the notification. To make the API easier, the slack notifier for `ExceptionNotification` now allows to override the default channel name, since it's an option that is possible in the `slack-notifier` gem (and documented in their README) --- docs/notifiers/slack.md | 60 ++++++++++++++++++++++++ lib/exception_notifier/slack_notifier.rb | 2 + 2 files changed, 62 insertions(+) diff --git a/docs/notifiers/slack.md b/docs/notifiers/slack.md index d6c3e774..3fa47be6 100644 --- a/docs/notifiers/slack.md +++ b/docs/notifiers/slack.md @@ -61,6 +61,66 @@ Rails.application.config.middleware.use ExceptionNotification::Rack, Any evaluation to `true` will cause the key / value pair not be be sent along to Slack. + +the `slack-notifier` gem allows to override the channel default value, if you ever +need to send a notification to a different slack channel. Simply add the +`channel` option when calling `.notify_exception` + +```ruby +ExceptionNotifier.notify_exception( + exception, + env: request.env, + channel: '#my-custom-channel', # Make sure the channel name starts with `#` + data: { + error: error_variable, + server: server_name + } +) +``` + +If you ever need to add more `slack-notifier` specific options, and +particularly to the `#ping` method of the slack notifier, you can use +the `pre_callback` option when defining the middleware. +```ruby + pre_callback: proc { |opts, _notifier, _backtrace, _message, message_opts| + message_opts[:channel] = opts[:channel] if opts.key?(:channel) + } + +``` +- `message_opts` is the hash you want to append to if you need to add an option. +- `options` is the hash containing the values when you call + `ExceptionNotification.notify_exception` + +An example implementation would be: +```ruby +config.middleware.use ExceptionNotification::Rack, + slack: { + webhook_url: '[Your webhook url]', + pre_callback: proc { |opts, _notifier, _backtrace, _message, message_opts| + message_opts[:ping_option] = opts[:ping_option] if + opts.key?(:ping_option) + } + }, + error_grouping: true +``` +Then when calling from within your application code: +```ruby +ExceptionNotifier.notify_exception( + exception, + env: request.env, + ping_option: 'value', + # this will be passed to the slack notifier's `#ping` + # method, as a parameter. The `:pre_callback` hook will catch it + # and do that for you. + # Helpful, if the API evolves, you only need to update + # the `slack-notifier` gem + data: { + error: error_variable, + server: server_name + } +) + +``` #### Options ##### webhook_url diff --git a/lib/exception_notifier/slack_notifier.rb b/lib/exception_notifier/slack_notifier.rb index 8e8568b7..8b03e512 100644 --- a/lib/exception_notifier/slack_notifier.rb +++ b/lib/exception_notifier/slack_notifier.rb @@ -28,6 +28,8 @@ def call(exception, options = {}) args = [exception, options, clean_message, @message_opts.merge(attachments: attchs)] send_notice(*args) do |_msg, message_opts| + message_opts[:channel] = options[:channel] if options.key?(:channel) + @notifier.ping '', message_opts end end From b6f33b5c1fe8a8f005f95de1f0c7834f64aaab23 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 4 Mar 2019 12:12:05 -0300 Subject: [PATCH 088/156] Avoid using ActiveRecord exception so tests don't depend on Rails --- test/exception_notifier/email_notifier_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index c7665e36..c698c52c 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -112,13 +112,13 @@ class EmailNotifierTest < ActiveSupport::TestCase test "mail should prefix exception class with 'an' instead of 'a' when it starts with a vowel" do begin - raise ActiveRecord::RecordNotFound + raise ArgumentError rescue StandardError => e @vowel_exception = e @vowel_mail = @email_notifier.create_email(@vowel_exception) end - assert_includes @vowel_mail.encoded, "An ActiveRecord::RecordNotFound occurred in background at #{Time.current}" + assert_includes @vowel_mail.encoded, "An ArgumentError occurred in background at #{Time.current}" end test 'mail should contain backtrace in body' do @@ -138,7 +138,7 @@ class EmailNotifierTest < ActiveSupport::TestCase test 'should not send notification if one of ignored exceptions' do begin - raise ActiveRecord::RecordNotFound + raise AbstractController::ActionNotFound rescue StandardError => e @ignored_exception = e unless ExceptionNotifier.ignored_exceptions.include?(@ignored_exception.class.name) @@ -146,7 +146,7 @@ class EmailNotifierTest < ActiveSupport::TestCase end end - assert_equal @ignored_exception.class.inspect, 'ActiveRecord::RecordNotFound' + assert_equal @ignored_exception.class.inspect, 'AbstractController::ActionNotFound' assert_nil ignored_mail end From af928eccb6a2b2deb3049ae89acb7339fa1d0079 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 4 Mar 2019 12:12:06 -0300 Subject: [PATCH 089/156] Simplify PostsController so it doens't need ActiveRecord --- .../dummy/app/controllers/posts_controller.rb | 23 ++----------------- .../test/functional/posts_controller_test.rb | 2 +- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/test/dummy/app/controllers/posts_controller.rb b/test/dummy/app/controllers/posts_controller.rb index 21d46b08..0c804d36 100644 --- a/test/dummy/app/controllers/posts_controller.rb +++ b/test/dummy/app/controllers/posts_controller.rb @@ -1,30 +1,11 @@ class PostsController < ApplicationController - # GET /posts/1 - # GET /posts/1.xml def show - @post = Post.find(params[:id]) - - respond_to do |format| - format.html # show.html.erb - format.xml { render xml: @post } - end + render :show end - # POST /posts - # POST /posts.xml def create @sections = Object.new # Have this line raise an exception - @post = Post.nw(params[:post]) - - respond_to do |format| - if @post.save - format.html { redirect_to(post_path(@post), notice: 'Post was successfully created.') } - format.xml { render xml: @post, status: :created, location: @post } - else - format.html { render action: 'new' } - format.xml { render xml: @post.errors, status: :unprocessable_entity } - end - end + Object.nw end end diff --git a/test/dummy/test/functional/posts_controller_test.rb b/test/dummy/test/functional/posts_controller_test.rb index 5ba75eb9..e3b15b0c 100644 --- a/test/dummy/test/functional/posts_controller_test.rb +++ b/test/dummy/test/functional/posts_controller_test.rb @@ -41,7 +41,7 @@ class PostsControllerTest < ActionController::TestCase end test 'mail should contain backtrace in body' do - assert_includes @mail.encoded, "`method_missing'\r\n app/controllers/posts_controller.rb:18:in `create'\r\n" + assert_includes @mail.encoded, "undefined method `nw' for Object:Class\r\n app/controllers/posts_controller.rb:9:in `create'\r\n" end test 'mail should contain timestamp of exception in body' do From 9e2c0ca01c8a62a570b4b23f15120fe0afa62f21 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 4 Mar 2019 12:12:07 -0300 Subject: [PATCH 090/156] Remove unused model --- test/dummy/app/models/post.rb | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 test/dummy/app/models/post.rb diff --git a/test/dummy/app/models/post.rb b/test/dummy/app/models/post.rb deleted file mode 100644 index 791dcb56..00000000 --- a/test/dummy/app/models/post.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Post < ActiveRecord::Base -end From b9fb80a8b15f1aa7121cf060a1c9662097d8862a Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 4 Mar 2019 12:12:08 -0300 Subject: [PATCH 091/156] Don't load ActiveRecord and other frameworks that are not used --- test/dummy/config/application.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index 16768057..8df9e781 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -1,6 +1,16 @@ require File.expand_path('boot', __dir__) -require 'rails/all' +require 'rails' +# Pick the frameworks you want: +# require 'active_model/railtie' +# require 'active_job/railtie' +# require 'active_record/railtie' +require 'action_controller/railtie' +require 'action_mailer/railtie' +require 'action_view/railtie' +# require 'action_cable/engine' +# require 'sprockets/railtie' +require 'rails/test_unit/railtie' # If you have a Gemfile, require the gems listed there, including any gems # you've limited to :test, :development, or :production. @@ -38,8 +48,5 @@ class Application < Rails::Application # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += %i[password secret] - - rails_version = Gem::Version.new(Rails.version) - config.active_record.sqlite3.represent_boolean_as_integer = true if rails_version >= Gem::Version.new('5.2.0') end end From 8a8074f1ed69e7896ff9bc862e605d70845a91f6 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 4 Mar 2019 12:12:09 -0300 Subject: [PATCH 092/156] Remove sqlite3 gem --- exception_notification.gemspec | 1 - 1 file changed, 1 deletion(-) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 50b1c571..2d680c55 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -34,7 +34,6 @@ Gem::Specification.new do |s| # Sidekiq 3.2.2 does not support Ruby 1.9. s.add_development_dependency 'sidekiq', '~> 3.0.0', '< 3.2.2' s.add_development_dependency 'slack-notifier', '>= 1.0.0' - s.add_development_dependency 'sqlite3', '~> 1.3.4' s.add_development_dependency 'timecop', '~>0.9.0' s.add_development_dependency 'tinder', '~> 1.8' end From 30aceb56e4da4731e9ad39f13af8381df1264290 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 4 Mar 2019 12:12:10 -0300 Subject: [PATCH 093/156] Remove db configuration --- test/dummy/config/database.yml | 22 ------------------- .../db/migrate/20110729022608_create_posts.rb | 15 ------------- test/dummy/db/schema.rb | 21 ------------------ test/dummy/db/seeds.rb | 7 ------ 4 files changed, 65 deletions(-) delete mode 100644 test/dummy/config/database.yml delete mode 100644 test/dummy/db/migrate/20110729022608_create_posts.rb delete mode 100644 test/dummy/db/schema.rb delete mode 100644 test/dummy/db/seeds.rb diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml deleted file mode 100644 index 90d87cc2..00000000 --- a/test/dummy/config/database.yml +++ /dev/null @@ -1,22 +0,0 @@ -# SQLite version 3.x -# gem install sqlite3 -development: - adapter: sqlite3 - database: db/development.sqlite3 - pool: 5 - timeout: 5000 - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - adapter: sqlite3 - database: db/test.sqlite3 - pool: 5 - timeout: 5000 - -production: - adapter: sqlite3 - database: db/production.sqlite3 - pool: 5 - timeout: 5000 diff --git a/test/dummy/db/migrate/20110729022608_create_posts.rb b/test/dummy/db/migrate/20110729022608_create_posts.rb deleted file mode 100644 index b55e917b..00000000 --- a/test/dummy/db/migrate/20110729022608_create_posts.rb +++ /dev/null @@ -1,15 +0,0 @@ -class CreatePosts < ActiveRecord::Migration - def self.up - create_table :posts do |t| - t.string :title - t.text :body - t.string :secret - - t.timestamps - end - end - - def self.down - drop_table :posts - end -end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb deleted file mode 100644 index da892226..00000000 --- a/test/dummy/db/schema.rb +++ /dev/null @@ -1,21 +0,0 @@ -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 20_110_729_022_608) do - create_table 'posts', force: true do |t| - t.string 'title' - t.text 'body' - t.string 'secret' - t.datetime 'created_at' - t.datetime 'updated_at' - end -end diff --git a/test/dummy/db/seeds.rb b/test/dummy/db/seeds.rb deleted file mode 100644 index 4b46ca3e..00000000 --- a/test/dummy/db/seeds.rb +++ /dev/null @@ -1,7 +0,0 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). -# -# Examples: -# -# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) -# Mayor.create(name: 'Daley', city: cities.first) From 2f80314b237facd0162ba570f5697bd1fe559623 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Sun, 17 Mar 2019 20:07:26 -0500 Subject: [PATCH 094/156] Fix typos and grammar --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index efe9def7..74861932 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,12 @@ ExceptionNotification relies on notifiers to deliver notifications when errors o But, you also can easily implement your own [custom notifier](docs/notifiers/custom.md). ## Error Grouping -In general, exception notification will send every notification when an error occured, which may result in a problem: if your site has a high throughput and an same error raised frequently, you will receive too many notifications during a short period time, your mail box may be full of thousands of exception mails or even your mail server will be slow. To prevent this, you can choose to error errors by using `:error_grouping` option and set it to `true`. -Error grouping has a default formula `log2(errors_count)` to determine if it is needed to send the notification based on the accumulated errors count for specified exception, this makes the notifier only send notification when count is: 1, 2, 4, 8, 16, 32, 64, 128, ... (2**n). You can use `:notification_trigger` to override this default formula. +In general, ExceptionNotification will send a notification when every error occurs, which may result in a problem: if your site has a high throughput and a particular error is raised frequently, you will receive too many notifications. During a short period of time, your mail box may be filled with thousands of exception mails, or your mail server may even become slow. To prevent this, you can choose to group errors by setting the `:error_grouping` option to `true`. -The below shows options used to enable error grouping: +Error grouping uses a default formula of `log2(errors_count)` to determine whether to send the notification, based on the accumulated error count for each specific exception. This makes the notifier only send a notification when the count is: 1, 2, 4, 8, 16, 32, 64, 128, ..., (2**n). You can use `:notification_trigger` to override this default formula. + +The following code shows the available options to configure error grouping: ```ruby Rails.application.config.middleware.use ExceptionNotification::Rack, From 76994c1620d5cc84f76807654581b29ce854ce3b Mon Sep 17 00:00:00 2001 From: mherold Date: Tue, 26 Mar 2019 11:56:23 +0100 Subject: [PATCH 095/156] Fix backtrace_based_key in test With this fix, it is actually tested that the error count for the backtrace_based_key is not incremented. --- test/exception_notifier/modules/error_grouping_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/exception_notifier/modules/error_grouping_test.rb b/test/exception_notifier/modules/error_grouping_test.rb index 1365cd0b..3624ac4f 100644 --- a/test/exception_notifier/modules/error_grouping_test.rb +++ b/test/exception_notifier/modules/error_grouping_test.rb @@ -134,7 +134,7 @@ module TestModule test 'should group error by that message have high priority' do message_based_key = "exception:#{Zlib.crc32("RuntimeError\nmessage:ERROR")}" - backtrace_based_key = "exception:#{Zlib.crc32("RuntimeError\n/path/where/error/raised:1")}" + backtrace_based_key = "exception:#{Zlib.crc32("RuntimeError\npath:/path/where/error/raised:1")}" TestModule.save_error_count(message_based_key, 1) TestModule.save_error_count(backtrace_based_key, 1) From 3d0cec01b7c4717320b8d8c171ba28a8b471a7a7 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Wed, 17 Apr 2019 00:21:58 -0300 Subject: [PATCH 096/156] Set up Email Notifier in the test itself Before it was relying on the one created by the Rails dummy app --- test/exception_notifier/email_notifier_test.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index c698c52c..3727d781 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -4,7 +4,18 @@ class EmailNotifierTest < ActiveSupport::TestCase setup do Time.stubs(:current).returns('Sat, 20 Apr 2013 20:58:55 UTC +00:00') - @email_notifier = ExceptionNotifier.registered_exception_notifier(:email) + + @email_notifier = ExceptionNotifier::EmailNotifier.new( + email_prefix: '[Dummy ERROR] ', + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], + email_headers: { 'X-Custom-Header' => 'foobar' }, + sections: %w[new_section request session environment backtrace], + background_sections: %w[new_bkg_section backtrace data], + pre_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:pre_callback_called] = 1 }, + post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } + ) + begin 1 / 0 rescue StandardError => e @@ -122,7 +133,7 @@ class EmailNotifierTest < ActiveSupport::TestCase end test 'mail should contain backtrace in body' do - assert @mail.encoded.include?('test/exception_notifier/email_notifier_test.rb:9'), "\n#{@mail.inspect}" + assert @mail.encoded.include?('test/exception_notifier/email_notifier_test.rb:20'), "\n#{@mail.inspect}" end test 'mail should contain data in body' do From 6d801e0fcde8bc53c80671011ef91f09dd12c0bd Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Wed, 17 Apr 2019 17:52:13 -0300 Subject: [PATCH 097/156] Refactor how test exception is generated --- test/exception_notifier/email_notifier_test.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index 3727d781..ea752b65 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -5,6 +5,9 @@ class EmailNotifierTest < ActiveSupport::TestCase setup do Time.stubs(:current).returns('Sat, 20 Apr 2013 20:58:55 UTC +00:00') + @exception = ZeroDivisionError.new('divided by 0') + @exception.set_backtrace(['test/exception_notifier/email_notifier_test.rb:20']) + @email_notifier = ExceptionNotifier::EmailNotifier.new( email_prefix: '[Dummy ERROR] ', sender_address: %("Dummy Notifier" ), @@ -16,15 +19,10 @@ class EmailNotifierTest < ActiveSupport::TestCase post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } ) - begin - 1 / 0 - rescue StandardError => e - @exception = e - @mail = @email_notifier.create_email( - @exception, - data: { job: 'DivideWorkerJob', payload: '1/0', message: 'My Custom Message' } - ) - end + @mail = @email_notifier.call( + @exception, + data: { job: 'DivideWorkerJob', payload: '1/0', message: 'My Custom Message' } + ) end test 'should call pre/post_callback if specified' do From eca29d5b60e21a69c521c43e5b8fde61a15e0e78 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Wed, 17 Apr 2019 17:54:38 -0300 Subject: [PATCH 098/156] Cleanup tests --- .../exception_notifier/email_notifier_test.rb | 123 +++++------------- 1 file changed, 35 insertions(+), 88 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index ea752b65..ca99deae 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -30,20 +30,42 @@ class EmailNotifierTest < ActiveSupport::TestCase assert_equal @email_notifier.options[:post_callback_called], 1 end - test 'should have default sender address overridden' do - assert_equal @email_notifier.sender_address, %("Dummy Notifier" ) - end + test 'sends mail with correct content' do + assert_equal %("Dummy Notifier" ), @mail[:from].value + assert_equal %w[dummyexceptions@example.com], @mail.to + assert_equal '[Dummy ERROR] (ZeroDivisionError) "divided by 0"', @mail.subject + assert_equal 'foobar', @mail['X-Custom-Header'].value + assert_equal 'text/plain; charset=UTF-8', @mail.content_type + assert_equal [], @mail.attachments - test 'should have default exception recipients overridden' do - assert_equal @email_notifier.exception_recipients, %w[dummyexceptions@example.com] - end + body = <<-BODY.strip_heredoc + A ZeroDivisionError occurred in background at Sat, 20 Apr 2013 20:58:55 UTC +00:00 : - test 'should have default email prefix overridden' do - assert_equal @email_notifier.email_prefix, '[Dummy ERROR] ' - end + divided by 0 + test/exception_notifier/email_notifier_test.rb:20 + + ------------------------------- + New bkg section: + ------------------------------- + + * New background section for testing + + ------------------------------- + Backtrace: + ------------------------------- + + test/exception_notifier/email_notifier_test.rb:20 - test 'should have default email headers overridden' do - assert_equal @email_notifier.email_headers, 'X-Custom-Header' => 'foobar' + ------------------------------- + Data: + ------------------------------- + + * data: {:job=>"DivideWorkerJob", :payload=>"1/0", :message=>"My Custom Message"} + + + BODY + + assert_equal body, @mail.decode_body end test 'should have default sections overridden' do @@ -58,26 +80,6 @@ class EmailNotifierTest < ActiveSupport::TestCase end end - test 'should have email format by default' do - assert_equal @email_notifier.email_format, :text - end - - test 'should have verbose subject by default' do - assert @email_notifier.verbose_subject - end - - test 'should have normalize_subject false by default' do - refute @email_notifier.normalize_subject - end - - test 'should have delivery_method nil by default' do - assert_nil @email_notifier.delivery_method - end - - test 'should have mailer_settings nil by default' do - assert_nil @email_notifier.mailer_settings - end - test 'should have mailer_parent by default' do assert_equal @email_notifier.mailer_parent, 'ActionMailer::Base' end @@ -91,34 +93,6 @@ class EmailNotifierTest < ActiveSupport::TestCase ExceptionNotifier::EmailNotifier.normalize_digits('1 foo 12 bar 123 baz 1234') end - test 'mail should be plain text and UTF-8 enconded by default' do - assert_equal @mail.content_type, 'text/plain; charset=UTF-8' - end - - test 'should have raised an exception' do - refute_nil @exception - end - - test 'should have generated a notification email' do - refute_nil @mail - end - - test 'mail should have a from address set' do - assert_equal @mail.from, ['dummynotifier@example.com'] - end - - test 'mail should have a to address set' do - assert_equal @mail.to, ['dummyexceptions@example.com'] - end - - test 'mail should have a descriptive subject' do - assert_match(/^\[Dummy ERROR\]\s+\(ZeroDivisionError\) "divided by 0"$/, @mail.subject) - end - - test 'mail should say exception was raised in background at show timestamp' do - assert_includes @mail.encoded, "A ZeroDivisionError occurred in background at #{Time.current}" - end - test "mail should prefix exception class with 'an' instead of 'a' when it starts with a vowel" do begin raise ArgumentError @@ -130,21 +104,6 @@ class EmailNotifierTest < ActiveSupport::TestCase assert_includes @vowel_mail.encoded, "An ArgumentError occurred in background at #{Time.current}" end - test 'mail should contain backtrace in body' do - assert @mail.encoded.include?('test/exception_notifier/email_notifier_test.rb:20'), "\n#{@mail.inspect}" - end - - test 'mail should contain data in body' do - assert_includes @mail.encoded, '* data:' - assert_includes @mail.encoded, ':payload=>"1/0"' - assert_includes @mail.encoded, ':job=>"DivideWorkerJob"' - assert_includes @mail.encoded, 'My Custom Message' - end - - test 'mail should not contain any attachments' do - assert_equal @mail.attachments, [] - end - test 'should not send notification if one of ignored exceptions' do begin raise AbstractController::ActionNotFound @@ -172,8 +131,7 @@ class EmailNotifierTest < ActiveSupport::TestCase 'REQUEST_METHOD' => 'GET', 'rack.input' => '', 'invalid_encoding' => "R\xC3\xA9sum\xC3\xA9".force_encoding(Encoding::ASCII) - }, - email_format: :text + } ) assert_match(/invalid_encoding\s+: R__sum__/, mail.encoded) @@ -181,16 +139,7 @@ class EmailNotifierTest < ActiveSupport::TestCase test 'should send email using ActionMailer' do ActionMailer::Base.deliveries.clear - - email_notifier = ExceptionNotifier::EmailNotifier.new( - email_prefix: '[Dummy ERROR] ', - sender_address: %("Dummy Notifier" ), - exception_recipients: %w[dummyexceptions@example.com], - delivery_method: :test - ) - - email_notifier.call(@exception) - + @email_notifier.call(@exception) assert_equal 1, ActionMailer::Base.deliveries.count end @@ -231,8 +180,6 @@ class EmailNotifierTest < ActiveSupport::TestCase end test 'should prepend accumulated_errors_count in email subject if accumulated_errors_count larger than 1' do - ActionMailer::Base.deliveries.clear - email_notifier = ExceptionNotifier::EmailNotifier.new( email_prefix: '[Dummy ERROR] ', sender_address: %("Dummy Notifier" ), From 7ddaedf7588900125a08f3e01ea1dbb467d39a3f Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Wed, 17 Apr 2019 19:37:00 -0300 Subject: [PATCH 099/156] Test SMTP settings --- test/exception_notifier/email_notifier_test.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index ca99deae..afa8df7b 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -16,7 +16,11 @@ class EmailNotifierTest < ActiveSupport::TestCase sections: %w[new_section request session environment backtrace], background_sections: %w[new_bkg_section backtrace data], pre_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:pre_callback_called] = 1 }, - post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } + post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 }, + smtp_settings: { + user_name: 'Dummy user_name', + password: 'Dummy password' + } ) @mail = @email_notifier.call( @@ -37,6 +41,8 @@ class EmailNotifierTest < ActiveSupport::TestCase assert_equal 'foobar', @mail['X-Custom-Header'].value assert_equal 'text/plain; charset=UTF-8', @mail.content_type assert_equal [], @mail.attachments + assert_equal 'Dummy user_name', @mail.delivery_method.settings[:user_name] + assert_equal 'Dummy password', @mail.delivery_method.settings[:password] body = <<-BODY.strip_heredoc A ZeroDivisionError occurred in background at Sat, 20 Apr 2013 20:58:55 UTC +00:00 : From 5b467c2c4e4f4547b5911f7fc816b8a24404f0e4 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Wed, 17 Apr 2019 19:37:51 -0300 Subject: [PATCH 100/156] Add tests for use case with env --- .../exception_notifier/_environment.text.erb | 2 +- .../exception_notifier/email_notifier_test.rb | 123 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/lib/exception_notifier/views/exception_notifier/_environment.text.erb b/lib/exception_notifier/views/exception_notifier/_environment.text.erb index 69b4a8e1..cf92a1b8 100644 --- a/lib/exception_notifier/views/exception_notifier/_environment.text.erb +++ b/lib/exception_notifier/views/exception_notifier/_environment.text.erb @@ -1,5 +1,5 @@ <% filtered_env = @request.filtered_env -%> <% max = filtered_env.keys.map(&:to_s).max { |a, b| a.length <=> b.length } -%> <% filtered_env.keys.map(&:to_s).sort.each do |key| -%> - * <%= raw safe_encode("%-*s: %s" % [max.length, key, inspect_object(filtered_env[key])]) %> + * <%= raw safe_encode("%-*s: %s" % [max.length, key, inspect_object(filtered_env[key])]).strip %> <% end -%> diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index afa8df7b..c743a4de 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -197,3 +197,126 @@ class EmailNotifierTest < ActiveSupport::TestCase assert mail.subject.start_with?('[Dummy ERROR] (3 times) (ZeroDivisionError)') end end + +class EmailNotifierWithEnvTest < ActiveSupport::TestCase + setup do + Time.stubs(:current).returns('Sat, 20 Apr 2013 20:58:55 UTC +00:00') + + @exception = ZeroDivisionError.new('divided by 0') + @exception.set_backtrace(['test/exception_notifier/email_notifier_test.rb:20']) + + @email_notifier = ExceptionNotifier::EmailNotifier.new( + email_prefix: '[Dummy ERROR] ', + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], + email_headers: { 'X-Custom-Header' => 'foobar' }, + sections: %w[new_section request session environment backtrace], + background_sections: %w[new_bkg_section backtrace data], + pre_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:pre_callback_called] = 1 }, + post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } + ) + + @test_env = Rack::MockRequest.env_for( + '/', + 'HTTP_HOST' => 'test.address', + 'REMOTE_ADDR' => '127.0.0.1', + 'HTTP_USER_AGENT' => 'Rails Testing', + 'action_dispatch.parameter_filter' => ['secret'], + 'HTTPS' => 'on', + params: { id: 'foo', secret: 'secret' } + ) + + @mail = @email_notifier.call(@exception, env: @test_env, data: { message: 'My Custom Message' }) + end + + test 'sends mail with correct content' do + assert_equal %("Dummy Notifier" ), @mail[:from].value + assert_equal %w[dummyexceptions@example.com], @mail.to + assert_equal '[Dummy ERROR] (ZeroDivisionError) "divided by 0"', @mail.subject + assert_equal 'foobar', @mail['X-Custom-Header'].value + assert_equal 'text/plain; charset=UTF-8', @mail.content_type + assert_equal [], @mail.attachments + + body = <<-BODY.strip_heredoc + A ZeroDivisionError occurred in #: + + divided by 0 + test/exception_notifier/email_notifier_test.rb:20 + + + ------------------------------- + New section: + ------------------------------- + + * New text section for testing + + ------------------------------- + Request: + ------------------------------- + + * URL : https://test.address/?id=foo&secret=secret + * HTTP Method: GET + * IP address : 127.0.0.1 + * Parameters : {\"id\"=>\"foo\", \"secret\"=>\"[FILTERED]\"} + * Timestamp : Sat, 20 Apr 2013 20:58:55 UTC +00:00 + * Server : #{Socket.gethostname} + * Rails root : #{Rails.root} + * Process: #{$PROCESS_ID} + + ------------------------------- + Session: + ------------------------------- + + * session id: [FILTERED] + * data: {} + + ------------------------------- + Environment: + ------------------------------- + + * CONTENT_LENGTH : 0 + * HTTPS : on + * HTTP_HOST : test.address + * HTTP_USER_AGENT : Rails Testing + * PATH_INFO : / + * QUERY_STRING : id=foo&secret=secret + * REMOTE_ADDR : 127.0.0.1 + * REQUEST_METHOD : GET + * SCRIPT_NAME : + * SERVER_NAME : example.org + * SERVER_PORT : 80 + * action_dispatch.parameter_filter : [\"secret\"] + * action_dispatch.request.content_type : + * action_dispatch.request.parameters : {"id"=>"foo", "secret"=>"[FILTERED]"} + * action_dispatch.request.path_parameters : {} + * action_dispatch.request.query_parameters : {"id"=>"foo", "secret"=>"[FILTERED]"} + * action_dispatch.request.request_parameters: {} + * rack.errors : #{@test_env['rack.errors']} + * rack.input : #{@test_env['rack.input']} + * rack.multiprocess : true + * rack.multithread : true + * rack.request.query_hash : {"id"=>"foo", "secret"=>"[FILTERED]"} + * rack.request.query_string : id=foo&secret=secret + * rack.run_once : false + * rack.session : {} + * rack.url_scheme : http + * rack.version : #{Rack::VERSION} + + ------------------------------- + Backtrace: + ------------------------------- + + test/exception_notifier/email_notifier_test.rb:20 + + ------------------------------- + Data: + ------------------------------- + + * data: {:message=>\"My Custom Message\"} + + + BODY + + assert_equal body, @mail.decode_body + end +end From 00dff57e5b5b1bd28b3b2ef0889514b0928df5b7 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Wed, 17 Apr 2019 19:39:42 -0300 Subject: [PATCH 101/156] Remove Rails tests that are already covered in EmailNotifier unit test --- .../test/functional/posts_controller_test.rb | 155 ------------------ 1 file changed, 155 deletions(-) diff --git a/test/dummy/test/functional/posts_controller_test.rb b/test/dummy/test/functional/posts_controller_test.rb index e3b15b0c..3d7efebe 100644 --- a/test/dummy/test/functional/posts_controller_test.rb +++ b/test/dummy/test/functional/posts_controller_test.rb @@ -12,88 +12,6 @@ class PostsControllerTest < ActionController::TestCase end end - test 'should have raised an exception' do - refute_nil @exception - end - - test 'should have generated a notification email' do - refute_nil @mail - end - - test 'mail should be plain text and UTF-8 enconded by default' do - assert_equal @mail.content_type, 'text/plain; charset=UTF-8' - end - - test 'mail should have a from address set' do - assert_equal @mail.from, ['dummynotifier@example.com'] - end - - test 'mail should have a to address set' do - assert_equal @mail.to, ['dummyexceptions@example.com'] - end - - test 'mail subject should have the proper prefix' do - assert_includes @mail.subject, '[Dummy ERROR]' - end - - test 'mail subject should include descriptive error message' do - assert_includes @mail.subject, "(NoMethodError) \"undefined method `nw'" - end - - test 'mail should contain backtrace in body' do - assert_includes @mail.encoded, "undefined method `nw' for Object:Class\r\n app/controllers/posts_controller.rb:9:in `create'\r\n" - end - - test 'mail should contain timestamp of exception in body' do - assert_includes @mail.encoded, "Timestamp : #{Time.current}" - end - - test 'mail should contain the newly defined section' do - assert_includes @mail.encoded, '* New text section for testing' - end - - test 'mail should contain the custom message' do - assert_includes @mail.encoded, 'My Custom Message' - end - - test 'should filter sensible data' do - assert_includes @mail.encoded, 'secret"=>"[FILTERED]' - end - - test 'mail should contain the custom header' do - assert_includes @mail.encoded, 'X-Custom-Header: foobar' - end - - test 'mail should not contain any attachments' do - assert_equal @mail.attachments, [] - end - - test 'should not send notification if one of ignored exceptions' do - begin - get :invalid - rescue StandardError => e - @ignored_exception = e - unless ExceptionNotifier.ignored_exceptions.include?(@ignored_exception.class.name) - ignored_mail = @email_notifier.create_email(@ignored_exception, env: request.env) - end - end - - assert_equal @ignored_exception.class.inspect, 'ActionController::UrlGenerationError' - assert_nil ignored_mail - end - - test 'should filter session_id on secure requests' do - request.env['HTTPS'] = 'on' - begin - post :create, method: :post - rescue StandardError => e - @secured_mail = @email_notifier.create_email(e, env: request.env) - end - - assert request.ssl? - assert_includes @secured_mail.encoded, "* session id: [FILTERED]\r\n *" - end - test 'should ignore exception if from unwanted crawler' do request.env['HTTP_USER_AGENT'] = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' begin @@ -164,76 +82,3 @@ class PostsControllerTestWithoutControllerAndActionNames < ActionController::Tes refute_includes @mail.subject, 'posts#create' end end - -class PostsControllerTestWithSmtpSettings < ActionController::TestCase - tests PostsController - setup do - @email_notifier = ExceptionNotifier::EmailNotifier.new( - smtp_settings: { - user_name: 'Dummy user_name', - password: 'Dummy password' - } - ) - - begin - post :create, method: :post - rescue StandardError => e - @exception = e - @mail = @email_notifier.create_email(@exception, env: request.env) - end - end - - test 'should have overridden smtp settings' do - assert_equal 'Dummy user_name', @mail.delivery_method.settings[:user_name] - assert_equal 'Dummy password', @mail.delivery_method.settings[:password] - end - - test 'should have overridden smtp settings with background notification' do - @mail = @email_notifier.create_email(@exception) - assert_equal 'Dummy user_name', @mail.delivery_method.settings[:user_name] - assert_equal 'Dummy password', @mail.delivery_method.settings[:password] - end -end - -class PostsControllerTestBackgroundNotification < ActionController::TestCase - tests PostsController - setup do - @email_notifier = ExceptionNotifier.registered_exception_notifier(:email) - begin - post :create, method: :post - rescue StandardError => exception - @mail = @email_notifier.create_email(exception) - end - end - - test 'mail should contain the specified section' do - assert_includes @mail.encoded, '* New background section for testing' - end -end - -class PostsControllerTestWithExceptionRecipientsAsProc < ActionController::TestCase - tests PostsController - setup do - exception_recipients = %w[first@example.com second@example.com] - - @email_notifier = ExceptionNotifier::EmailNotifier.new( - exception_recipients: -> { [exception_recipients.shift] } - ) - - @action = proc do - begin - post :create, method: :post - rescue StandardError => e - @exception = e - @mail = @email_notifier.create_email(@exception, env: request.env) - end - end - end - - test 'should lazily evaluate exception_recipients' do - @action.call - assert_equal ['first@example.com'], @mail.to - @action.call - assert_equal ['second@example.com'], @mail.to - end -end From eb173627daaf29c703f14233f1b73b2b183c79bb Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Wed, 17 Apr 2019 21:16:44 -0300 Subject: [PATCH 102/156] Add controller instance --- test/exception_notifier/email_notifier_test.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index c743a4de..5b352fc1 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -199,6 +199,10 @@ class EmailNotifierTest < ActiveSupport::TestCase end class EmailNotifierWithEnvTest < ActiveSupport::TestCase + class HomeController < ActionController::Metal + def index; end + end + setup do Time.stubs(:current).returns('Sat, 20 Apr 2013 20:58:55 UTC +00:00') @@ -216,6 +220,9 @@ class EmailNotifierWithEnvTest < ActiveSupport::TestCase post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } ) + @controller = HomeController.new + @controller.process(:index) + @test_env = Rack::MockRequest.env_for( '/', 'HTTP_HOST' => 'test.address', @@ -223,6 +230,7 @@ class EmailNotifierWithEnvTest < ActiveSupport::TestCase 'HTTP_USER_AGENT' => 'Rails Testing', 'action_dispatch.parameter_filter' => ['secret'], 'HTTPS' => 'on', + 'action_controller.instance' => @controller, params: { id: 'foo', secret: 'secret' } ) @@ -232,13 +240,13 @@ class EmailNotifierWithEnvTest < ActiveSupport::TestCase test 'sends mail with correct content' do assert_equal %("Dummy Notifier" ), @mail[:from].value assert_equal %w[dummyexceptions@example.com], @mail.to - assert_equal '[Dummy ERROR] (ZeroDivisionError) "divided by 0"', @mail.subject + assert_equal '[Dummy ERROR] home index (ZeroDivisionError) "divided by 0"', @mail.subject assert_equal 'foobar', @mail['X-Custom-Header'].value assert_equal 'text/plain; charset=UTF-8', @mail.content_type assert_equal [], @mail.attachments body = <<-BODY.strip_heredoc - A ZeroDivisionError occurred in #: + A ZeroDivisionError occurred in home#index: divided by 0 test/exception_notifier/email_notifier_test.rb:20 @@ -285,6 +293,7 @@ class EmailNotifierWithEnvTest < ActiveSupport::TestCase * SCRIPT_NAME : * SERVER_NAME : example.org * SERVER_PORT : 80 + * action_controller.instance : #{@controller} * action_dispatch.parameter_filter : [\"secret\"] * action_dispatch.request.content_type : * action_dispatch.request.parameters : {"id"=>"foo", "secret"=>"[FILTERED]"} From 4d28d7302f88540d2b99bcd376c444ee9918e922 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 18 Apr 2019 00:26:17 -0300 Subject: [PATCH 103/156] Move more tests out of Rails --- .../test/functional/posts_controller_test.rb | 52 ------------------- .../exception_notifier/email_notifier_test.rb | 36 +++++++++++++ 2 files changed, 36 insertions(+), 52 deletions(-) diff --git a/test/dummy/test/functional/posts_controller_test.rb b/test/dummy/test/functional/posts_controller_test.rb index 3d7efebe..c4a84fbe 100644 --- a/test/dummy/test/functional/posts_controller_test.rb +++ b/test/dummy/test/functional/posts_controller_test.rb @@ -29,56 +29,4 @@ class PostsControllerTest < ActionController::TestCase assert_nil ignored_mail end - - test 'should send html email when selected html format' do - begin - post :create, method: :post - rescue StandardError => e - @exception = e - custom_env = request.env - custom_env['exception_notifier.options'] ||= {} - custom_env['exception_notifier.options'][:email_format] = :html - @mail = @email_notifier.create_email(@exception, env: custom_env) - end - - assert_includes @mail.content_type, 'multipart/alternative' - end -end - -class PostsControllerTestWithoutVerboseSubject < ActionController::TestCase - tests PostsController - setup do - @email_notifier = ExceptionNotifier::EmailNotifier.new(verbose_subject: false) - begin - post :create, method: :post - rescue StandardError => e - @exception = e - @mail = @email_notifier.create_email(@exception, env: request.env) - end - end - - test 'should not include exception message in subject' do - assert_includes @mail.subject, '[ERROR]' - assert_includes @mail.subject, '(NoMethodError)' - refute_includes @mail.subject, 'undefined method' - end -end - -class PostsControllerTestWithoutControllerAndActionNames < ActionController::TestCase - tests PostsController - setup do - @email_notifier = ExceptionNotifier::EmailNotifier.new(include_controller_and_action_names_in_subject: false) - begin - post :create, method: :post - rescue StandardError => e - @exception = e - @mail = @email_notifier.create_email(@exception, env: request.env) - end - end - - test 'should include controller and action names in subject' do - assert_includes @mail.subject, '[ERROR]' - assert_includes @mail.subject, '(NoMethodError)' - refute_includes @mail.subject, 'posts#create' - end end diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index 5b352fc1..a6d16e4a 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -196,6 +196,30 @@ class EmailNotifierTest < ActiveSupport::TestCase mail = email_notifier.call(@exception, accumulated_errors_count: 3) assert mail.subject.start_with?('[Dummy ERROR] (3 times) (ZeroDivisionError)') end + + test 'should not include exception message in subject when verbose_subject: false' do + email_notifier = ExceptionNotifier::EmailNotifier.new( + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], + verbose_subject: false + ) + + mail = email_notifier.call(@exception) + + assert_equal '[ERROR] (ZeroDivisionError)', mail.subject + end + + test 'should send html email when selected html format' do + email_notifier = ExceptionNotifier::EmailNotifier.new( + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], + email_format: :html + ) + + mail = email_notifier.call(@exception) + + assert mail.multipart? + end end class EmailNotifierWithEnvTest < ActiveSupport::TestCase @@ -328,4 +352,16 @@ def index; end assert_equal body, @mail.decode_body end + + test 'should not include controller and action names in subject' do + email_notifier = ExceptionNotifier::EmailNotifier.new( + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], + include_controller_and_action_names_in_subject: false + ) + + mail = email_notifier.call(@exception, env: @test_env) + + assert_equal '[ERROR] (ZeroDivisionError) "divided by 0"', mail.subject + end end From 081bcd7ad1358ba90d9bfc0ed887e314a606a6a2 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 18 Apr 2019 00:29:10 -0300 Subject: [PATCH 104/156] Test ignore_crawlers in ExceptionNotification::Rack unit tests --- .../test/functional/posts_controller_test.rb | 18 ------------------ test/exception_notification/rack_test.rb | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/test/dummy/test/functional/posts_controller_test.rb b/test/dummy/test/functional/posts_controller_test.rb index c4a84fbe..cc3affa7 100644 --- a/test/dummy/test/functional/posts_controller_test.rb +++ b/test/dummy/test/functional/posts_controller_test.rb @@ -11,22 +11,4 @@ class PostsControllerTest < ActionController::TestCase @mail = @email_notifier.create_email(@exception, env: request.env, data: { message: 'My Custom Message' }) end end - - test 'should ignore exception if from unwanted crawler' do - request.env['HTTP_USER_AGENT'] = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' - begin - post :create, method: :post - rescue StandardError => e - @exception = e - custom_env = request.env - custom_env['exception_notifier.options'] ||= {} - custom_env['exception_notifier.options'][:ignore_crawlers] = %w[Googlebot] - ignore_array = custom_env['exception_notifier.options'][:ignore_crawlers] - unless ExceptionNotification::Rack.new(Dummy::Application, custom_env['exception_notifier.options']).send(:from_crawler, custom_env, ignore_array) - ignored_mail = @email_notifier.create_email(@exception, env: custom_env) - end - end - - assert_nil ignored_mail - end end diff --git a/test/exception_notification/rack_test.rb b/test/exception_notification/rack_test.rb index 7c7653c9..4e8ca285 100644 --- a/test/exception_notification/rack_test.rb +++ b/test/exception_notification/rack_test.rb @@ -40,4 +40,19 @@ class RackTest < ActiveSupport::TestCase ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({}) assert_equal Rails.cache, ExceptionNotifier.error_grouping_cache end + + test 'should ignore exceptions with Usar Agent in ignore_crawlers' do + exception_app = Object.new + exception_app.stubs(:call).raises(RuntimeError) + + env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; Crawlerbot/2.1;)' } + + begin + ExceptionNotification::Rack.new(exception_app, ignore_crawlers: %w[Crawlerbot]).call(env) + + flunk + rescue StandardError + refute env['exception_notifier.delivered'] + end + end end From 2c6d45f8baa4e4c6b1accb5469ea5cf049deab04 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 18 Apr 2019 00:30:35 -0300 Subject: [PATCH 105/156] Remove dummy app tests --- .../dummy/test/functional/posts_controller_test.rb | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 test/dummy/test/functional/posts_controller_test.rb diff --git a/test/dummy/test/functional/posts_controller_test.rb b/test/dummy/test/functional/posts_controller_test.rb deleted file mode 100644 index cc3affa7..00000000 --- a/test/dummy/test/functional/posts_controller_test.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'test_helper' - -class PostsControllerTest < ActionController::TestCase - setup do - Time.stubs(:current).returns('Sat, 20 Apr 2013 20:58:55 UTC +00:00') - @email_notifier = ExceptionNotifier.registered_exception_notifier(:email) - begin - post :create, method: :post, params: { secret: 'secret' } - rescue StandardError => e - @exception = e - @mail = @email_notifier.create_email(@exception, env: request.env, data: { message: 'My Custom Message' }) - end - end -end From 54f72a001ddfcf3b7d8c3410dae6bde599d0c7e1 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 18 Apr 2019 11:48:42 -0300 Subject: [PATCH 106/156] Use constant to specify gem version --- exception_notification.gemspec | 4 +++- lib/exception_notification.rb | 1 + lib/exception_notification/version.rb | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 lib/exception_notification/version.rb diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 2d680c55..3857641e 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -1,6 +1,8 @@ +require File.expand_path('../lib/exception_notification/version', __FILE__) + Gem::Specification.new do |s| s.name = 'exception_notification' - s.version = '4.3.0' + s.version = ExceptionNotification::VERSION s.authors = ['Jamis Buck', 'Josh Peek'] s.date = '2018-11-22' s.summary = 'Exception notification for Rails apps' diff --git a/lib/exception_notification.rb b/lib/exception_notification.rb index fb83d710..4bbdc29b 100644 --- a/lib/exception_notification.rb +++ b/lib/exception_notification.rb @@ -1,5 +1,6 @@ require 'exception_notifier' require 'exception_notification/rack' +require 'exception_notification/version' module ExceptionNotification # Alternative way to setup ExceptionNotification. diff --git a/lib/exception_notification/version.rb b/lib/exception_notification/version.rb new file mode 100644 index 00000000..f43a8fdf --- /dev/null +++ b/lib/exception_notification/version.rb @@ -0,0 +1,3 @@ +module ExceptionNotification + VERSION = '4.3.0'.freeze +end From b40cf8159f37b8c028f4be11fab718330747527f Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 18 Apr 2019 12:23:29 -0300 Subject: [PATCH 107/156] Stop loading dummy app test_helper --- test/dummy/test/test_helper.rb | 9 --------- test/test_helper.rb | 1 - 2 files changed, 10 deletions(-) delete mode 100644 test/dummy/test/test_helper.rb diff --git a/test/dummy/test/test_helper.rb b/test/dummy/test/test_helper.rb deleted file mode 100644 index 3e30f622..00000000 --- a/test/dummy/test/test_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -ENV['RAILS_ENV'] = 'test' -require File.expand_path('../config/environment', __dir__) -require 'rails/test_help' - -module ActiveSupport - class TestCase - # Add more helper methods to be used by all tests here... - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb index 7e24f042..92ae49b8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,7 +10,6 @@ require File.expand_path('dummy/config/environment.rb', __dir__) require 'rails/test_help' -require File.expand_path('dummy/test/test_helper.rb', __dir__) require 'mocha/setup' From 10a71107a0a7a7df8255e4111b77df70709f30a0 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 18 Apr 2019 12:32:56 -0300 Subject: [PATCH 108/156] Remove unnecessary dummy app code Keep only the code needed to keep tests passing --- test/dummy/Rakefile | 7 - .../app/controllers/application_controller.rb | 3 - .../dummy/app/controllers/posts_controller.rb | 11 - test/dummy/app/helpers/application_helper.rb | 2 - test/dummy/app/helpers/posts_helper.rb | 2 - .../app/views/layouts/application.html.erb | 14 - test/dummy/app/views/posts/_form.html.erb | 0 test/dummy/app/views/posts/new.html.erb | 0 test/dummy/app/views/posts/show.html.erb | 0 test/dummy/config.ru | 4 - test/dummy/config/application.rb | 52 +- test/dummy/config/boot.rb | 6 - test/dummy/config/environment.rb | 17 - test/dummy/config/environments/development.rb | 25 - test/dummy/config/environments/production.rb | 50 - test/dummy/config/environments/test.rb | 35 - .../initializers/backtrace_silencers.rb | 7 - test/dummy/config/initializers/inflections.rb | 10 - test/dummy/config/initializers/mime_types.rb | 5 - .../dummy/config/initializers/secret_token.rb | 7 - .../config/initializers/session_store.rb | 8 - test/dummy/config/locales/en.yml | 5 - test/dummy/config/routes.rb | 3 - test/dummy/lib/tasks/.gitkeep | 0 test/dummy/public/404.html | 26 - test/dummy/public/422.html | 26 - test/dummy/public/500.html | 26 - test/dummy/public/favicon.ico | 0 test/dummy/public/images/rails.png | Bin 6646 -> 0 bytes test/dummy/public/index.html | 239 - test/dummy/public/javascripts/application.js | 2 - test/dummy/public/javascripts/controls.js | 965 --- test/dummy/public/javascripts/dragdrop.js | 974 --- test/dummy/public/javascripts/effects.js | 1123 --- test/dummy/public/javascripts/prototype.js | 6001 ----------------- test/dummy/public/javascripts/rails.js | 191 - test/dummy/public/robots.txt | 5 - test/dummy/public/stylesheets/.gitkeep | 0 test/dummy/public/stylesheets/scaffold.css | 56 - test/dummy/script/rails | 6 - test/test_helper.rb | 5 +- 41 files changed, 20 insertions(+), 9898 deletions(-) delete mode 100644 test/dummy/Rakefile delete mode 100644 test/dummy/app/controllers/application_controller.rb delete mode 100644 test/dummy/app/controllers/posts_controller.rb delete mode 100644 test/dummy/app/helpers/application_helper.rb delete mode 100644 test/dummy/app/helpers/posts_helper.rb delete mode 100644 test/dummy/app/views/layouts/application.html.erb delete mode 100644 test/dummy/app/views/posts/_form.html.erb delete mode 100644 test/dummy/app/views/posts/new.html.erb delete mode 100644 test/dummy/app/views/posts/show.html.erb delete mode 100644 test/dummy/config/boot.rb delete mode 100644 test/dummy/config/environment.rb delete mode 100644 test/dummy/config/environments/development.rb delete mode 100644 test/dummy/config/environments/production.rb delete mode 100644 test/dummy/config/environments/test.rb delete mode 100644 test/dummy/config/initializers/backtrace_silencers.rb delete mode 100644 test/dummy/config/initializers/inflections.rb delete mode 100644 test/dummy/config/initializers/mime_types.rb delete mode 100644 test/dummy/config/initializers/secret_token.rb delete mode 100644 test/dummy/config/initializers/session_store.rb delete mode 100644 test/dummy/config/locales/en.yml delete mode 100644 test/dummy/config/routes.rb delete mode 100644 test/dummy/lib/tasks/.gitkeep delete mode 100644 test/dummy/public/404.html delete mode 100644 test/dummy/public/422.html delete mode 100644 test/dummy/public/500.html delete mode 100644 test/dummy/public/favicon.ico delete mode 100644 test/dummy/public/images/rails.png delete mode 100644 test/dummy/public/index.html delete mode 100644 test/dummy/public/javascripts/application.js delete mode 100644 test/dummy/public/javascripts/controls.js delete mode 100644 test/dummy/public/javascripts/dragdrop.js delete mode 100644 test/dummy/public/javascripts/effects.js delete mode 100644 test/dummy/public/javascripts/prototype.js delete mode 100644 test/dummy/public/javascripts/rails.js delete mode 100644 test/dummy/public/robots.txt delete mode 100644 test/dummy/public/stylesheets/.gitkeep delete mode 100644 test/dummy/public/stylesheets/scaffold.css delete mode 100755 test/dummy/script/rails diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile deleted file mode 100644 index 20ddc49e..00000000 --- a/test/dummy/Rakefile +++ /dev/null @@ -1,7 +0,0 @@ -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path('config/application', __dir__) -require 'rake' - -Dummy::Application.load_tasks diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb deleted file mode 100644 index e8065d95..00000000 --- a/test/dummy/app/controllers/application_controller.rb +++ /dev/null @@ -1,3 +0,0 @@ -class ApplicationController < ActionController::Base - protect_from_forgery -end diff --git a/test/dummy/app/controllers/posts_controller.rb b/test/dummy/app/controllers/posts_controller.rb deleted file mode 100644 index 0c804d36..00000000 --- a/test/dummy/app/controllers/posts_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -class PostsController < ApplicationController - def show - render :show - end - - def create - @sections = Object.new - # Have this line raise an exception - Object.nw - end -end diff --git a/test/dummy/app/helpers/application_helper.rb b/test/dummy/app/helpers/application_helper.rb deleted file mode 100644 index de6be794..00000000 --- a/test/dummy/app/helpers/application_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ApplicationHelper -end diff --git a/test/dummy/app/helpers/posts_helper.rb b/test/dummy/app/helpers/posts_helper.rb deleted file mode 100644 index a7b8cec8..00000000 --- a/test/dummy/app/helpers/posts_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module PostsHelper -end diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb deleted file mode 100644 index a3b5a225..00000000 --- a/test/dummy/app/views/layouts/application.html.erb +++ /dev/null @@ -1,14 +0,0 @@ - - - - Dummy - <%= stylesheet_link_tag :all %> - <%= javascript_include_tag :defaults %> - <%= csrf_meta_tag %> - - - -<%= yield %> - - - diff --git a/test/dummy/app/views/posts/_form.html.erb b/test/dummy/app/views/posts/_form.html.erb deleted file mode 100644 index e69de29b..00000000 diff --git a/test/dummy/app/views/posts/new.html.erb b/test/dummy/app/views/posts/new.html.erb deleted file mode 100644 index e69de29b..00000000 diff --git a/test/dummy/app/views/posts/show.html.erb b/test/dummy/app/views/posts/show.html.erb deleted file mode 100644 index e69de29b..00000000 diff --git a/test/dummy/config.ru b/test/dummy/config.ru index cbd74159..e69de29b 100644 --- a/test/dummy/config.ru +++ b/test/dummy/config.ru @@ -1,4 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require ::File.expand_path('../config/environment', __FILE__) -run Dummy::Application diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index 8df9e781..c935c2d0 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -1,5 +1,3 @@ -require File.expand_path('boot', __dir__) - require 'rails' # Pick the frameworks you want: # require 'active_model/railtie' @@ -12,41 +10,23 @@ # require 'sprockets/railtie' require 'rails/test_unit/railtie' -# If you have a Gemfile, require the gems listed there, including any gems -# you've limited to :test, :development, or :production. -Bundler.require(:default, Rails.env) if defined?(Bundler) - module Dummy class Application < Rails::Application - # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. - - # Custom directories with classes and modules you want to be autoloadable. - # config.autoload_paths += %W(#{config.root}/extras) - - # Only load the plugins named here, in the order given (default is alphabetical). - # :all can be used as a placeholder for all plugins not explicitly named. - # config.plugins = [ :exception_notification, :ssl_requirement, :all ] - - # Activate observers that should always be running. - # config.active_record.observers = :cacher, :garbage_collector, :forum_observer - - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' - - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de - - # JavaScript files you want as :defaults (application.js is always included). - # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) - - # Configure the default encoding used in templates for Ruby 1.9. - config.encoding = 'utf-8' - - # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += %i[password secret] + config.eager_load = false + config.action_mailer.delivery_method = :test + + config.middleware.use ExceptionNotification::Rack, + email: { + email_prefix: '[Dummy ERROR] ', + sender_address: %("Dummy Notifier" ), + exception_recipients: %w[dummyexceptions@example.com], + email_headers: { 'X-Custom-Header' => 'foobar' }, + sections: %w[new_section request session environment backtrace], + background_sections: %w[new_bkg_section backtrace data], + pre_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:pre_callback_called] = 1 }, + post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } + } end end + +Dummy::Application.initialize! diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb deleted file mode 100644 index 24efefdb..00000000 --- a/test/dummy/config/boot.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'rubygems' - -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) - -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb deleted file mode 100644 index 00d98427..00000000 --- a/test/dummy/config/environment.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Load the rails application -require File.expand_path('application', __dir__) - -Dummy::Application.config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[Dummy ERROR] ', - sender_address: %("Dummy Notifier" ), - exception_recipients: %w[dummyexceptions@example.com], - email_headers: { 'X-Custom-Header' => 'foobar' }, - sections: %w[new_section request session environment backtrace], - background_sections: %w[new_bkg_section backtrace data], - pre_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:pre_callback_called] = 1 }, - post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } - } - -# Initialize the rails application -Dummy::Application.initialize! diff --git a/test/dummy/config/environments/development.rb b/test/dummy/config/environments/development.rb deleted file mode 100644 index afc8d99d..00000000 --- a/test/dummy/config/environments/development.rb +++ /dev/null @@ -1,25 +0,0 @@ -Dummy::Application.configure do - # Settings specified here will take precedence over those in config/application.rb - - config.eager_load = false - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development - # since you don't have to restart the webserver when you make code changes. - config.cache_classes = false - - # Log error messages when you accidentally call methods on nil. - config.whiny_nils = true - - # Show full error reports and disable caching - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - # Don't care if the mailer can't send - config.action_mailer.raise_delivery_errors = false - - # Print deprecation notices to the Rails logger - config.active_support.deprecation = :log - - # Only use best-standards-support built into browsers - config.action_dispatch.best_standards_support = :builtin -end diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb deleted file mode 100644 index d276322f..00000000 --- a/test/dummy/config/environments/production.rb +++ /dev/null @@ -1,50 +0,0 @@ -Dummy::Application.configure do - # Settings specified here will take precedence over those in config/application.rb - - config.eager_load = true - # The production environment is meant for finished, "live" apps. - # Code is not reloaded between requests - config.cache_classes = true - - # Full error reports are disabled and caching is turned on - config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Specifies the header that your server uses for sending files - config.action_dispatch.x_sendfile_header = 'X-Sendfile' - - # For nginx: - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' - - # If you have no front-end server that supports something like X-Sendfile, - # just comment this out and Rails will serve the files - - # See everything in the log (default is :info) - # config.log_level = :debug - - # Use a different logger for distributed setups - # config.logger = SyslogLogger.new - - # Use a different cache store in production - # config.cache_store = :mem_cache_store - - # Disable Rails's static asset server - # In production, Apache or nginx will already do this - config.serve_static_assets = false - - # Enable serving of images, stylesheets, and javascripts from an asset server - # config.action_controller.asset_host = "http://assets.example.com" - - # Disable delivery errors, bad email addresses will be ignored - # config.action_mailer.raise_delivery_errors = false - - # Enable threaded mode - # config.threadsafe! - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found) - config.i18n.fallbacks = true - - # Send deprecation notices to registered listeners - config.active_support.deprecation = :notify -end diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb deleted file mode 100644 index 822f64a8..00000000 --- a/test/dummy/config/environments/test.rb +++ /dev/null @@ -1,35 +0,0 @@ -Dummy::Application.configure do - # Settings specified here will take precedence over those in config/application.rb - - config.eager_load = false - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true - - # Show full error reports and disable caching - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - # Raise exceptions instead of rendering exception templates - config.action_dispatch.show_exceptions = false - - # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false - - # Tell Action Mailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - config.action_mailer.delivery_method = :test - - # Use SQL instead of Active Record's schema dumper when creating the test database. - # This is necessary if your schema can't be completely dumped by the schema dumper, - # like if you have constraints or database-specific column types - # config.active_record.schema_format = :sql - - # Print deprecation notices to the stderr - config.active_support.deprecation = :stderr - - config.active_support.test_order = :random -end diff --git a/test/dummy/config/initializers/backtrace_silencers.rb b/test/dummy/config/initializers/backtrace_silencers.rb deleted file mode 100644 index 59385cdf..00000000 --- a/test/dummy/config/initializers/backtrace_silencers.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. -# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } - -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -# Rails.backtrace_cleaner.remove_silencers! diff --git a/test/dummy/config/initializers/inflections.rb b/test/dummy/config/initializers/inflections.rb deleted file mode 100644 index 9e8b0131..00000000 --- a/test/dummy/config/initializers/inflections.rb +++ /dev/null @@ -1,10 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new inflection rules using the following format -# (all these examples are active by default): -# ActiveSupport::Inflector.inflections do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' -# inflect.uncountable %w( fish sheep ) -# end diff --git a/test/dummy/config/initializers/mime_types.rb b/test/dummy/config/initializers/mime_types.rb deleted file mode 100644 index 72aca7e4..00000000 --- a/test/dummy/config/initializers/mime_types.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new mime types for use in respond_to blocks: -# Mime::Type.register "text/richtext", :rtf -# Mime::Type.register_alias "text/html", :iphone diff --git a/test/dummy/config/initializers/secret_token.rb b/test/dummy/config/initializers/secret_token.rb deleted file mode 100644 index 1de52661..00000000 --- a/test/dummy/config/initializers/secret_token.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -Dummy::Application.config.secret_key_base = 'my new secret' diff --git a/test/dummy/config/initializers/session_store.rb b/test/dummy/config/initializers/session_store.rb deleted file mode 100644 index 952473ff..00000000 --- a/test/dummy/config/initializers/session_store.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Be sure to restart your server when you modify this file. - -Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' - -# Use the database for sessions instead of the cookie-based default, -# which shouldn't be used to store highly confidential information -# (create the session table with "rails generate session_migration") -# Dummy::Application.config.session_store :active_record_store diff --git a/test/dummy/config/locales/en.yml b/test/dummy/config/locales/en.yml deleted file mode 100644 index a747bfa6..00000000 --- a/test/dummy/config/locales/en.yml +++ /dev/null @@ -1,5 +0,0 @@ -# Sample localization file for English. Add more files in this directory for other locales. -# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. - -en: - hello: "Hello world" diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb deleted file mode 100644 index 0ba13a15..00000000 --- a/test/dummy/config/routes.rb +++ /dev/null @@ -1,3 +0,0 @@ -Dummy::Application.routes.draw do - resources :posts, only: %i[create show] -end diff --git a/test/dummy/lib/tasks/.gitkeep b/test/dummy/lib/tasks/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/test/dummy/public/404.html b/test/dummy/public/404.html deleted file mode 100644 index 9a48320a..00000000 --- a/test/dummy/public/404.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - The page you were looking for doesn't exist (404) - - - - - -
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
- - diff --git a/test/dummy/public/422.html b/test/dummy/public/422.html deleted file mode 100644 index 83660ab1..00000000 --- a/test/dummy/public/422.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - The change you wanted was rejected (422) - - - - - -
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
- - diff --git a/test/dummy/public/500.html b/test/dummy/public/500.html deleted file mode 100644 index b80307fc..00000000 --- a/test/dummy/public/500.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - We're sorry, but something went wrong (500) - - - - - -
-

We're sorry, but something went wrong.

-

We've been notified about this issue and we'll take a look at it shortly.

-
- - diff --git a/test/dummy/public/favicon.ico b/test/dummy/public/favicon.ico deleted file mode 100644 index e69de29b..00000000 diff --git a/test/dummy/public/images/rails.png b/test/dummy/public/images/rails.png deleted file mode 100644 index d5edc04e65f555e3ba4dcdaad39dc352e75b575e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6646 zcmVpVcQya!6@Dsmj@#jv7C*qh zIhOJ6_K0n?*d`*T7TDuW-}m`9Kz3~>+7`DUkbAraU%yi+R{N~~XA2B%zt-4=tLimUer9!2M~N{G5bftFij_O&)a zsHnOppFIzebQ`RA0$!yUM-lg#*o@_O2wf422iLnM6cU(ktYU8#;*G!QGhIy9+ZfzKjLuZo%@a z-i@9A`X%J{^;2q&ZHY3C(B%gqCPW!8{9C0PMcNZccefK){s|V5-xxtHQc@uf>XqhD z7#N^siWqetgq29aX>G^olMf=bbRF6@Y(}zYxw6o!9WBdG1unP}<(V;zKlcR2p86fq zYjaqB^;Ycq>Wy@5T1xOzG3tucG3e%nPvajaN{CrFbnzv^9&K3$NrDm*eQe4`BGQ2bI;dFEwyt>hK%X!L6)82aOZp zsrGcJ#7PoX7)s|~t6is?FfX*7vWdREi58tiY4S)t6u*|kv?J)d_$r+CH#eZ?Ef+I_ z(eVlX8dh~4QP?o*E`_MgaNFIKj*rtN(0Raj3ECjSXcWfd#27NYs&~?t`QZFT}!Zaf=ldZIhi}LhQlqLo+o5(Pvui&{7PD__^53f9j>HW`Q z_V8X5j~$|GP9qXu0C#!@RX2}lXD35@3N5{BkUi%jtaPQ*H6OX2zIz4QPuqmTv3`vG{zc>l3t0B9E75h< z8&twGh%dp7WPNI+tRl%#gf2}Epg8st+~O4GjtwJsXfN;EjAmyr6z5dnaFU(;IV~QK zW62fogF~zA``(Q>_SmD!izc6Y4zq*97|NAPHp1j5X7Op2%;GLYm>^HEMyObo6s7l) zE3n|aOHi5~B84!}b^b*-aL2E)>OEJX_tJ~t<#VJ?bT?lDwyDB&5SZ$_1aUhmAY}#* zs@V1I+c5md9%R-o#_DUfqVtRk>59{+Opd5Yu%dAU#VQW}^m}x-30ftBx#527{^pI4 z6l2C6C7QBG$~NLYb3rVdLD#Z{+SleOp`(Lg5J}`kxdTHe(nV5BdpLrD=l|)e$gEqA zwI6vuX-PFCtcDIH>bGY2dwq&^tf+&R?)nY-@7_j%4CMRAF}C9w%p86W<2!aSY$p+k zrkFtG=cGo38RnrG28;?PNk%7a@faaXq&MS*&?1Z`7Ojw7(#>}ZG4nMAs3VXxfdW>i zY4VX02c5;f7jDPY_7@Oa)CHH}cH<3y#}_!nng^W+h1e-RL*YFYOteC@h?BtJZ+?sE zy)P5^8Mregx{nQaw1NY-|3>{Z)|0`?zc?G2-acYiSU`tj#sSGfm7k86ZQ0SQgPevcklHxM9<~4yW zR796sisf1|!#{Z=e^)0;_8iUhL8g(;j$l=02FTPZ(dZV@s#aQ`DHkLM6=YsbE4iQ!b#*374l0Jw5;jD%J;vQayq=nD8-kHI~f9Ux|32SJUM`> zGp2UGK*4t?cRKi!2he`zI#j0f${I#f-jeT?u_C7S4WsA0)ryi-1L0(@%pa^&g5x=e z=KW9+Nn(=)1T&S8g_ug%dgk*~l2O-$r9#zEGBdQsweO%t*6F4c8JC36JtTizCyy+E4h%G(+ z5>y$%0txMuQ$e~wjFgN(xrAndHQo`Za+K*?gUVDTBV&Ap^}|{w#CIq{DRe}+l@(Ec zCCV6f_?dY_{+f{}6XGn!pL_up?}@>KijT^$w#Lb6iHW&^8RP~g6y=vZBXx~B9nI^i zGexaPjcd(%)zGw!DG_dDwh-7x6+ST#R^${iz_M$uM!da8SxgB_;Z0G%Y*HpvLjKw; zX=ir7i1O$-T|*TBoH$dlW+TLf5j5sep^DlDtkox;Kg{Q%EXWedJq@J@%VAcK)j3y1 zShM!CS#qax;D@RND%2t3W6kv+#Ky0F9<3YKDbV^XJ=^$s(Vtza8V72YY)577nnldI zHMA0PUo!F3j(ubV*CM@PiK<^|RM2(DuCbG7`W}Rg(xdYC>C~ z;1KJGLN&$cRxSZunjXcntykmpFJ7;dk>shY(DdK&3K_JDJ6R%D`e~6Qv67@Rwu+q9 z*|NG{r}4F8f{Dfzt0+cZMd$fvlX3Q`dzM46@r?ISxr;9gBTG2rmfiGOD*#c*3f)cc zF+PFZobY$-^}J8 z%n=h4;x2}cP!@SiVd!v;^Wwo0(N??-ygDr7gG^NKxDjSo{5T{?$|Qo5;8V!~D6O;F*I zuY!gd@+2j_8Rn=UWDa#*4E2auWoGYDddMW7t0=yuC(xLWky?vLimM~!$3fgu!dR>p z?L?!8z>6v$|MsLb&dU?ob)Zd!B)!a*Z2eTE7 zKCzP&e}XO>CT%=o(v+WUY`Az*`9inbTG& z_9_*oQKw;sc8{ipoBC`S4Tb7a%tUE)1fE+~ib$;|(`|4QbXc2>VzFi%1nX%ti;^s3~NIL0R}!!a{0A zyCRp0F7Y&vcP&3`&Dzv5!&#h}F2R-h&QhIfq*ts&qO13{_CP}1*sLz!hI9VoTSzTu zok5pV0+~jrGymE~{TgbS#nN5+*rF7ij)cnSLQw0Ltc70zmk|O!O(kM<3zw-sUvkx~ z2`y+{xAwKSa-0}n7{$I@Zop7CWy%_xIeN1e-7&OjQ6vZZPbZ^3_ z(~=;ZSP98S2oB#35b1~_x`2gWiPdIVddEf`AD9<@c_s)TM;3J$T_l?pr{<7PTgdiy zBc5IGx)g~n=s+Z$RzYCmv8PlJu%gkh^;%mTGMc)UwRINVD~K;`Rl!5@hhGg;y>5qj zq|u-Yf0q_~Y+Mbivkkfa0nAOzB1acnytogsj_m7FB(-FjihMek#GAU4M!iXCgdK8a zjoKm?*|iz7;dHm4$^hh(`Ufl>yb>$hjIA-;>{>C}G0Di%bGvUsJkfLAV|xq32c>RqJqTBJ3Dx zYC;*Dt|S$b6)aCJFnK(Eey$M1DpVV~_MIhwK> zygo(jWC|_IRw|456`roEyXtkNLWNAt-4N1qyN$I@DvBzt;e|?g<*HK1%~cq|^u*}C zmMrwh>{QAq?Ar~4l^DqT%SQ)w)FA(#7#u+N;>E975rYML>)LgE`2<7nN=C1pC{IkV zVw}_&v6j&S?QVh*)wF3#XmE@0($^BVl1969csLKUBNer{suVd!a~B!0MxWY?=(GD6 zy$G&ERFR#i6G4=2F?R4}Mz3B?3tnpoX3)qFF2sh9-Jn*e%9F>i{WG7$_~XyOO2!+@ z6k+38KyD@-0=uee54D0!Z1@B^ilj~StchdOn(*qvg~s5QJpWGc!6U^Aj!xt-HZn_V zS%|fyQ5YS@EP2lBIodXCLjG_+a)%En+7jzngk@J>6D~^xbxKkvf-R0-c%mX+o{?&j zZZ%RxFeav8Y0gkwtdtrwUb-i0Egd2C=ADu%w5VV-hNJvl)GZ?M;y$!?b=S+wKRK7Q zcOjPT!p<*#8m;TsBih=@Xc&c)?Vy`Ys>IvK@|1%N+M6J-^RCRaZcPP2eQh9DEGZr+ z?8B~wF14mk4Xkuen{wY^CWwS1PI<8gikY*)3?RSo5l8es4*J z43k_BIwc}of=6Pfs%xIxlMDGOJN zvl!a>G)52XMqA%fbgkZi%)%bN*ZzZw2!rn4@+J)2eK#kWuEW{)W~-`y1vhA5-7p%R z&f5N!a9f8cK1Xa=O}=9{wg%}Ur^+8Y(!UCeqw>%wj@|bYHD-bZO~mk3L$9_^MmF3G zvCiK^e@q6G?tHkM8%GqsBMZaB20W$UEt_5r~jc#WlR>Bv{6W>A=!#InoY zLOd04@Rz?*7PpW8u|+}bt`?+Z(GsX{Br4A2$ZZ(26Degmr9`O=t2KgHTL*==R3xcP z&Y(J7hC@6_x8zVz!CX3l4Xtss6i7r#E6kXMNN1~>9KTRzewfp))ij%)SBBl0fZdYP zd!zzQD5u8yk-u|41|Rqz7_tCFUMThZJVj)yQf6^Cwtn|Ew6cm5J|u1Bq>MWX-AfB&NE;C z62@=-0le`E6-CurMKjoIy)BuUmhMGJb}pPx!@GLWMT+wH2R?wA=MEy)o57~feFp8P zY@YXAyt4<1FD<|iw{FGQu~GEI<4C64)V*QiVk+VzOV^9GWf4ir#oYgHJz!wq>iZV#_6@_{)&lum)4x z_Of*CLVQ7wdT#XT-(h0qH%mcIF7yzMIvvTN3bPceK>PpJi(=3Nny zbSn}p$dGKQUlX&-t~RR)#F7I<8NCD^yke(vdf#4^aAh}M-{tS9-&^tC4`KU_pToXy z+|K8sx}a)Kh{h{;*V1#hs1xB%(?j>)g~`Wv(9F)f=Qn)(daVB7hZtcp^#LrEr1T1J zZSJ*lVyVVjhy)mkex9Whn=EinKDHe@KlfQI-Fl7M?-c~HnW0;C;+MbUY8?FToy;A+ zs&Nc7VZ=Of+e!G6s#+S5WBU)kgQq_I1@!uH74GJ-+O|%0HXm9Mqlvp|j%0`T>fr9^ zK;qo>XdwZW<>%tTA+<(1^6(>=-2N;hRgBnjvEjN;VbKMbFg--WrGy|XESoH1p|M4` z86(gC^vB4qScASZ&cdpT{~QDN-jC|GJ(RYoW1VW4!SSn- zhQds9&RBKn6M&GVK_Aayt(Hekbnw=tr>f z^o@v9_*iQO1*zeOrts9Q-$pc@!StS&kz$cF`s@pM`rmJXTP&h5G)A74!0e%ZJbl}( zssI|_!%~_hZFypv*S^JE5N&Kvmx7KiG<|fGMO=WrH+@Yhuj+KwiS#l4>@%2nl zS)mDikfmokO4q2A)hRVZBq2-5q&XC>%HOLkOYxZ66(s86?=0s4z5xbiOV)}L-&6b)h6(~CIaR#JNw~46+WBiU7IhB zq!NuR4!TsYnyBg>@G=Ib*cMq^k<}AMpCeYEf&dzfiGI-wOQ7hb+nA zkN7_){y&c3xC0 AQ~&?~ diff --git a/test/dummy/public/index.html b/test/dummy/public/index.html deleted file mode 100644 index 75d5edd0..00000000 --- a/test/dummy/public/index.html +++ /dev/null @@ -1,239 +0,0 @@ - - - - Ruby on Rails: Welcome aboard - - - - -
- - -
- - - - -
-

Getting started

-

Here’s how to get rolling:

- -
    -
  1. -

    Use rails generate to create your models and controllers

    -

    To see all available options, run it without parameters.

    -
  2. - -
  3. -

    Set up a default route and remove or rename this file

    -

    Routes are set up in config/routes.rb.

    -
  4. - -
  5. -

    Create your database

    -

    Run rake db:migrate to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

    -
  6. -
-
-
- - -
- - diff --git a/test/dummy/public/javascripts/application.js b/test/dummy/public/javascripts/application.js deleted file mode 100644 index fe457769..00000000 --- a/test/dummy/public/javascripts/application.js +++ /dev/null @@ -1,2 +0,0 @@ -// Place your application-specific JavaScript functions and classes here -// This file is automatically included by javascript_include_tag :defaults diff --git a/test/dummy/public/javascripts/controls.js b/test/dummy/public/javascripts/controls.js deleted file mode 100644 index 7392fb66..00000000 --- a/test/dummy/public/javascripts/controls.js +++ /dev/null @@ -1,965 +0,0 @@ -// script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 - -// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan) -// (c) 2005-2009 Jon Tirsen (http://www.tirsen.com) -// Contributors: -// Richard Livsey -// Rahul Bhargava -// Rob Wills -// -// script.aculo.us is freely distributable under the terms of an MIT-style license. -// For details, see the script.aculo.us web site: http://script.aculo.us/ - -// Autocompleter.Base handles all the autocompletion functionality -// that's independent of the data source for autocompletion. This -// includes drawing the autocompletion menu, observing keyboard -// and mouse events, and similar. -// -// Specific autocompleters need to provide, at the very least, -// a getUpdatedChoices function that will be invoked every time -// the text inside the monitored textbox changes. This method -// should get the text for which to provide autocompletion by -// invoking this.getToken(), NOT by directly accessing -// this.element.value. This is to allow incremental tokenized -// autocompletion. Specific auto-completion logic (AJAX, etc) -// belongs in getUpdatedChoices. -// -// Tokenized incremental autocompletion is enabled automatically -// when an autocompleter is instantiated with the 'tokens' option -// in the options parameter, e.g.: -// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); -// will incrementally autocomplete with a comma as the token. -// Additionally, ',' in the above example can be replaced with -// a token array, e.g. { tokens: [',', '\n'] } which -// enables autocompletion on multiple tokens. This is most -// useful when one of the tokens is \n (a newline), as it -// allows smart autocompletion after linebreaks. - -if(typeof Effect == 'undefined') - throw("controls.js requires including script.aculo.us' effects.js library"); - -var Autocompleter = { }; -Autocompleter.Base = Class.create({ - baseInitialize: function(element, update, options) { - element = $(element); - this.element = element; - this.update = $(update); - this.hasFocus = false; - this.changed = false; - this.active = false; - this.index = 0; - this.entryCount = 0; - this.oldElementValue = this.element.value; - - if(this.setOptions) - this.setOptions(options); - else - this.options = options || { }; - - this.options.paramName = this.options.paramName || this.element.name; - this.options.tokens = this.options.tokens || []; - this.options.frequency = this.options.frequency || 0.4; - this.options.minChars = this.options.minChars || 1; - this.options.onShow = this.options.onShow || - function(element, update){ - if(!update.style.position || update.style.position=='absolute') { - update.style.position = 'absolute'; - Position.clone(element, update, { - setHeight: false, - offsetTop: element.offsetHeight - }); - } - Effect.Appear(update,{duration:0.15}); - }; - this.options.onHide = this.options.onHide || - function(element, update){ new Effect.Fade(update,{duration:0.15}) }; - - if(typeof(this.options.tokens) == 'string') - this.options.tokens = new Array(this.options.tokens); - // Force carriage returns as token delimiters anyway - if (!this.options.tokens.include('\n')) - this.options.tokens.push('\n'); - - this.observer = null; - - this.element.setAttribute('autocomplete','off'); - - Element.hide(this.update); - - Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); - Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); - }, - - show: function() { - if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); - if(!this.iefix && - (Prototype.Browser.IE) && - (Element.getStyle(this.update, 'position')=='absolute')) { - new Insertion.After(this.update, - ''); - this.iefix = $(this.update.id+'_iefix'); - } - if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); - }, - - fixIEOverlapping: function() { - Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); - this.iefix.style.zIndex = 1; - this.update.style.zIndex = 2; - Element.show(this.iefix); - }, - - hide: function() { - this.stopIndicator(); - if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); - if(this.iefix) Element.hide(this.iefix); - }, - - startIndicator: function() { - if(this.options.indicator) Element.show(this.options.indicator); - }, - - stopIndicator: function() { - if(this.options.indicator) Element.hide(this.options.indicator); - }, - - onKeyPress: function(event) { - if(this.active) - switch(event.keyCode) { - case Event.KEY_TAB: - case Event.KEY_RETURN: - this.selectEntry(); - Event.stop(event); - case Event.KEY_ESC: - this.hide(); - this.active = false; - Event.stop(event); - return; - case Event.KEY_LEFT: - case Event.KEY_RIGHT: - return; - case Event.KEY_UP: - this.markPrevious(); - this.render(); - Event.stop(event); - return; - case Event.KEY_DOWN: - this.markNext(); - this.render(); - Event.stop(event); - return; - } - else - if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || - (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; - - this.changed = true; - this.hasFocus = true; - - if(this.observer) clearTimeout(this.observer); - this.observer = - setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); - }, - - activate: function() { - this.changed = false; - this.hasFocus = true; - this.getUpdatedChoices(); - }, - - onHover: function(event) { - var element = Event.findElement(event, 'LI'); - if(this.index != element.autocompleteIndex) - { - this.index = element.autocompleteIndex; - this.render(); - } - Event.stop(event); - }, - - onClick: function(event) { - var element = Event.findElement(event, 'LI'); - this.index = element.autocompleteIndex; - this.selectEntry(); - this.hide(); - }, - - onBlur: function(event) { - // needed to make click events working - setTimeout(this.hide.bind(this), 250); - this.hasFocus = false; - this.active = false; - }, - - render: function() { - if(this.entryCount > 0) { - for (var i = 0; i < this.entryCount; i++) - this.index==i ? - Element.addClassName(this.getEntry(i),"selected") : - Element.removeClassName(this.getEntry(i),"selected"); - if(this.hasFocus) { - this.show(); - this.active = true; - } - } else { - this.active = false; - this.hide(); - } - }, - - markPrevious: function() { - if(this.index > 0) this.index--; - else this.index = this.entryCount-1; - this.getEntry(this.index).scrollIntoView(true); - }, - - markNext: function() { - if(this.index < this.entryCount-1) this.index++; - else this.index = 0; - this.getEntry(this.index).scrollIntoView(false); - }, - - getEntry: function(index) { - return this.update.firstChild.childNodes[index]; - }, - - getCurrentEntry: function() { - return this.getEntry(this.index); - }, - - selectEntry: function() { - this.active = false; - this.updateElement(this.getCurrentEntry()); - }, - - updateElement: function(selectedElement) { - if (this.options.updateElement) { - this.options.updateElement(selectedElement); - return; - } - var value = ''; - if (this.options.select) { - var nodes = $(selectedElement).select('.' + this.options.select) || []; - if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); - } else - value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); - - var bounds = this.getTokenBounds(); - if (bounds[0] != -1) { - var newValue = this.element.value.substr(0, bounds[0]); - var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); - if (whitespace) - newValue += whitespace[0]; - this.element.value = newValue + value + this.element.value.substr(bounds[1]); - } else { - this.element.value = value; - } - this.oldElementValue = this.element.value; - this.element.focus(); - - if (this.options.afterUpdateElement) - this.options.afterUpdateElement(this.element, selectedElement); - }, - - updateChoices: function(choices) { - if(!this.changed && this.hasFocus) { - this.update.innerHTML = choices; - Element.cleanWhitespace(this.update); - Element.cleanWhitespace(this.update.down()); - - if(this.update.firstChild && this.update.down().childNodes) { - this.entryCount = - this.update.down().childNodes.length; - for (var i = 0; i < this.entryCount; i++) { - var entry = this.getEntry(i); - entry.autocompleteIndex = i; - this.addObservers(entry); - } - } else { - this.entryCount = 0; - } - - this.stopIndicator(); - this.index = 0; - - if(this.entryCount==1 && this.options.autoSelect) { - this.selectEntry(); - this.hide(); - } else { - this.render(); - } - } - }, - - addObservers: function(element) { - Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); - Event.observe(element, "click", this.onClick.bindAsEventListener(this)); - }, - - onObserverEvent: function() { - this.changed = false; - this.tokenBounds = null; - if(this.getToken().length>=this.options.minChars) { - this.getUpdatedChoices(); - } else { - this.active = false; - this.hide(); - } - this.oldElementValue = this.element.value; - }, - - getToken: function() { - var bounds = this.getTokenBounds(); - return this.element.value.substring(bounds[0], bounds[1]).strip(); - }, - - getTokenBounds: function() { - if (null != this.tokenBounds) return this.tokenBounds; - var value = this.element.value; - if (value.strip().empty()) return [-1, 0]; - var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); - var offset = (diff == this.oldElementValue.length ? 1 : 0); - var prevTokenPos = -1, nextTokenPos = value.length; - var tp; - for (var index = 0, l = this.options.tokens.length; index < l; ++index) { - tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); - if (tp > prevTokenPos) prevTokenPos = tp; - tp = value.indexOf(this.options.tokens[index], diff + offset); - if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; - } - return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); - } -}); - -Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { - var boundary = Math.min(newS.length, oldS.length); - for (var index = 0; index < boundary; ++index) - if (newS[index] != oldS[index]) - return index; - return boundary; -}; - -Ajax.Autocompleter = Class.create(Autocompleter.Base, { - initialize: function(element, update, url, options) { - this.baseInitialize(element, update, options); - this.options.asynchronous = true; - this.options.onComplete = this.onComplete.bind(this); - this.options.defaultParams = this.options.parameters || null; - this.url = url; - }, - - getUpdatedChoices: function() { - this.startIndicator(); - - var entry = encodeURIComponent(this.options.paramName) + '=' + - encodeURIComponent(this.getToken()); - - this.options.parameters = this.options.callback ? - this.options.callback(this.element, entry) : entry; - - if(this.options.defaultParams) - this.options.parameters += '&' + this.options.defaultParams; - - new Ajax.Request(this.url, this.options); - }, - - onComplete: function(request) { - this.updateChoices(request.responseText); - } -}); - -// The local array autocompleter. Used when you'd prefer to -// inject an array of autocompletion options into the page, rather -// than sending out Ajax queries, which can be quite slow sometimes. -// -// The constructor takes four parameters. The first two are, as usual, -// the id of the monitored textbox, and id of the autocompletion menu. -// The third is the array you want to autocomplete from, and the fourth -// is the options block. -// -// Extra local autocompletion options: -// - choices - How many autocompletion choices to offer -// -// - partialSearch - If false, the autocompleter will match entered -// text only at the beginning of strings in the -// autocomplete array. Defaults to true, which will -// match text at the beginning of any *word* in the -// strings in the autocomplete array. If you want to -// search anywhere in the string, additionally set -// the option fullSearch to true (default: off). -// -// - fullSsearch - Search anywhere in autocomplete array strings. -// -// - partialChars - How many characters to enter before triggering -// a partial match (unlike minChars, which defines -// how many characters are required to do any match -// at all). Defaults to 2. -// -// - ignoreCase - Whether to ignore case when autocompleting. -// Defaults to true. -// -// It's possible to pass in a custom function as the 'selector' -// option, if you prefer to write your own autocompletion logic. -// In that case, the other options above will not apply unless -// you support them. - -Autocompleter.Local = Class.create(Autocompleter.Base, { - initialize: function(element, update, array, options) { - this.baseInitialize(element, update, options); - this.options.array = array; - }, - - getUpdatedChoices: function() { - this.updateChoices(this.options.selector(this)); - }, - - setOptions: function(options) { - this.options = Object.extend({ - choices: 10, - partialSearch: true, - partialChars: 2, - ignoreCase: true, - fullSearch: false, - selector: function(instance) { - var ret = []; // Beginning matches - var partial = []; // Inside matches - var entry = instance.getToken(); - var count = 0; - - for (var i = 0; i < instance.options.array.length && - ret.length < instance.options.choices ; i++) { - - var elem = instance.options.array[i]; - var foundPos = instance.options.ignoreCase ? - elem.toLowerCase().indexOf(entry.toLowerCase()) : - elem.indexOf(entry); - - while (foundPos != -1) { - if (foundPos == 0 && elem.length != entry.length) { - ret.push("
  • " + elem.substr(0, entry.length) + "" + - elem.substr(entry.length) + "
  • "); - break; - } else if (entry.length >= instance.options.partialChars && - instance.options.partialSearch && foundPos != -1) { - if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { - partial.push("
  • " + elem.substr(0, foundPos) + "" + - elem.substr(foundPos, entry.length) + "" + elem.substr( - foundPos + entry.length) + "
  • "); - break; - } - } - - foundPos = instance.options.ignoreCase ? - elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : - elem.indexOf(entry, foundPos + 1); - - } - } - if (partial.length) - ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); - return "
      " + ret.join('') + "
    "; - } - }, options || { }); - } -}); - -// AJAX in-place editor and collection editor -// Full rewrite by Christophe Porteneuve (April 2007). - -// Use this if you notice weird scrolling problems on some browsers, -// the DOM might be a bit confused when this gets called so do this -// waits 1 ms (with setTimeout) until it does the activation -Field.scrollFreeActivate = function(field) { - setTimeout(function() { - Field.activate(field); - }, 1); -}; - -Ajax.InPlaceEditor = Class.create({ - initialize: function(element, url, options) { - this.url = url; - this.element = element = $(element); - this.prepareOptions(); - this._controls = { }; - arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! - Object.extend(this.options, options || { }); - if (!this.options.formId && this.element.id) { - this.options.formId = this.element.id + '-inplaceeditor'; - if ($(this.options.formId)) - this.options.formId = ''; - } - if (this.options.externalControl) - this.options.externalControl = $(this.options.externalControl); - if (!this.options.externalControl) - this.options.externalControlOnly = false; - this._originalBackground = this.element.getStyle('background-color') || 'transparent'; - this.element.title = this.options.clickToEditText; - this._boundCancelHandler = this.handleFormCancellation.bind(this); - this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); - this._boundFailureHandler = this.handleAJAXFailure.bind(this); - this._boundSubmitHandler = this.handleFormSubmission.bind(this); - this._boundWrapperHandler = this.wrapUp.bind(this); - this.registerListeners(); - }, - checkForEscapeOrReturn: function(e) { - if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; - if (Event.KEY_ESC == e.keyCode) - this.handleFormCancellation(e); - else if (Event.KEY_RETURN == e.keyCode) - this.handleFormSubmission(e); - }, - createControl: function(mode, handler, extraClasses) { - var control = this.options[mode + 'Control']; - var text = this.options[mode + 'Text']; - if ('button' == control) { - var btn = document.createElement('input'); - btn.type = 'submit'; - btn.value = text; - btn.className = 'editor_' + mode + '_button'; - if ('cancel' == mode) - btn.onclick = this._boundCancelHandler; - this._form.appendChild(btn); - this._controls[mode] = btn; - } else if ('link' == control) { - var link = document.createElement('a'); - link.href = '#'; - link.appendChild(document.createTextNode(text)); - link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; - link.className = 'editor_' + mode + '_link'; - if (extraClasses) - link.className += ' ' + extraClasses; - this._form.appendChild(link); - this._controls[mode] = link; - } - }, - createEditField: function() { - var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); - var fld; - if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { - fld = document.createElement('input'); - fld.type = 'text'; - var size = this.options.size || this.options.cols || 0; - if (0 < size) fld.size = size; - } else { - fld = document.createElement('textarea'); - fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); - fld.cols = this.options.cols || 40; - } - fld.name = this.options.paramName; - fld.value = text; // No HTML breaks conversion anymore - fld.className = 'editor_field'; - if (this.options.submitOnBlur) - fld.onblur = this._boundSubmitHandler; - this._controls.editor = fld; - if (this.options.loadTextURL) - this.loadExternalText(); - this._form.appendChild(this._controls.editor); - }, - createForm: function() { - var ipe = this; - function addText(mode, condition) { - var text = ipe.options['text' + mode + 'Controls']; - if (!text || condition === false) return; - ipe._form.appendChild(document.createTextNode(text)); - }; - this._form = $(document.createElement('form')); - this._form.id = this.options.formId; - this._form.addClassName(this.options.formClassName); - this._form.onsubmit = this._boundSubmitHandler; - this.createEditField(); - if ('textarea' == this._controls.editor.tagName.toLowerCase()) - this._form.appendChild(document.createElement('br')); - if (this.options.onFormCustomization) - this.options.onFormCustomization(this, this._form); - addText('Before', this.options.okControl || this.options.cancelControl); - this.createControl('ok', this._boundSubmitHandler); - addText('Between', this.options.okControl && this.options.cancelControl); - this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); - addText('After', this.options.okControl || this.options.cancelControl); - }, - destroy: function() { - if (this._oldInnerHTML) - this.element.innerHTML = this._oldInnerHTML; - this.leaveEditMode(); - this.unregisterListeners(); - }, - enterEditMode: function(e) { - if (this._saving || this._editing) return; - this._editing = true; - this.triggerCallback('onEnterEditMode'); - if (this.options.externalControl) - this.options.externalControl.hide(); - this.element.hide(); - this.createForm(); - this.element.parentNode.insertBefore(this._form, this.element); - if (!this.options.loadTextURL) - this.postProcessEditField(); - if (e) Event.stop(e); - }, - enterHover: function(e) { - if (this.options.hoverClassName) - this.element.addClassName(this.options.hoverClassName); - if (this._saving) return; - this.triggerCallback('onEnterHover'); - }, - getText: function() { - return this.element.innerHTML.unescapeHTML(); - }, - handleAJAXFailure: function(transport) { - this.triggerCallback('onFailure', transport); - if (this._oldInnerHTML) { - this.element.innerHTML = this._oldInnerHTML; - this._oldInnerHTML = null; - } - }, - handleFormCancellation: function(e) { - this.wrapUp(); - if (e) Event.stop(e); - }, - handleFormSubmission: function(e) { - var form = this._form; - var value = $F(this._controls.editor); - this.prepareSubmission(); - var params = this.options.callback(form, value) || ''; - if (Object.isString(params)) - params = params.toQueryParams(); - params.editorId = this.element.id; - if (this.options.htmlResponse) { - var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); - Object.extend(options, { - parameters: params, - onComplete: this._boundWrapperHandler, - onFailure: this._boundFailureHandler - }); - new Ajax.Updater({ success: this.element }, this.url, options); - } else { - var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); - Object.extend(options, { - parameters: params, - onComplete: this._boundWrapperHandler, - onFailure: this._boundFailureHandler - }); - new Ajax.Request(this.url, options); - } - if (e) Event.stop(e); - }, - leaveEditMode: function() { - this.element.removeClassName(this.options.savingClassName); - this.removeForm(); - this.leaveHover(); - this.element.style.backgroundColor = this._originalBackground; - this.element.show(); - if (this.options.externalControl) - this.options.externalControl.show(); - this._saving = false; - this._editing = false; - this._oldInnerHTML = null; - this.triggerCallback('onLeaveEditMode'); - }, - leaveHover: function(e) { - if (this.options.hoverClassName) - this.element.removeClassName(this.options.hoverClassName); - if (this._saving) return; - this.triggerCallback('onLeaveHover'); - }, - loadExternalText: function() { - this._form.addClassName(this.options.loadingClassName); - this._controls.editor.disabled = true; - var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); - Object.extend(options, { - parameters: 'editorId=' + encodeURIComponent(this.element.id), - onComplete: Prototype.emptyFunction, - onSuccess: function(transport) { - this._form.removeClassName(this.options.loadingClassName); - var text = transport.responseText; - if (this.options.stripLoadedTextTags) - text = text.stripTags(); - this._controls.editor.value = text; - this._controls.editor.disabled = false; - this.postProcessEditField(); - }.bind(this), - onFailure: this._boundFailureHandler - }); - new Ajax.Request(this.options.loadTextURL, options); - }, - postProcessEditField: function() { - var fpc = this.options.fieldPostCreation; - if (fpc) - $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); - }, - prepareOptions: function() { - this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); - Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); - [this._extraDefaultOptions].flatten().compact().each(function(defs) { - Object.extend(this.options, defs); - }.bind(this)); - }, - prepareSubmission: function() { - this._saving = true; - this.removeForm(); - this.leaveHover(); - this.showSaving(); - }, - registerListeners: function() { - this._listeners = { }; - var listener; - $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { - listener = this[pair.value].bind(this); - this._listeners[pair.key] = listener; - if (!this.options.externalControlOnly) - this.element.observe(pair.key, listener); - if (this.options.externalControl) - this.options.externalControl.observe(pair.key, listener); - }.bind(this)); - }, - removeForm: function() { - if (!this._form) return; - this._form.remove(); - this._form = null; - this._controls = { }; - }, - showSaving: function() { - this._oldInnerHTML = this.element.innerHTML; - this.element.innerHTML = this.options.savingText; - this.element.addClassName(this.options.savingClassName); - this.element.style.backgroundColor = this._originalBackground; - this.element.show(); - }, - triggerCallback: function(cbName, arg) { - if ('function' == typeof this.options[cbName]) { - this.options[cbName](this, arg); - } - }, - unregisterListeners: function() { - $H(this._listeners).each(function(pair) { - if (!this.options.externalControlOnly) - this.element.stopObserving(pair.key, pair.value); - if (this.options.externalControl) - this.options.externalControl.stopObserving(pair.key, pair.value); - }.bind(this)); - }, - wrapUp: function(transport) { - this.leaveEditMode(); - // Can't use triggerCallback due to backward compatibility: requires - // binding + direct element - this._boundComplete(transport, this.element); - } -}); - -Object.extend(Ajax.InPlaceEditor.prototype, { - dispose: Ajax.InPlaceEditor.prototype.destroy -}); - -Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { - initialize: function($super, element, url, options) { - this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; - $super(element, url, options); - }, - - createEditField: function() { - var list = document.createElement('select'); - list.name = this.options.paramName; - list.size = 1; - this._controls.editor = list; - this._collection = this.options.collection || []; - if (this.options.loadCollectionURL) - this.loadCollection(); - else - this.checkForExternalText(); - this._form.appendChild(this._controls.editor); - }, - - loadCollection: function() { - this._form.addClassName(this.options.loadingClassName); - this.showLoadingText(this.options.loadingCollectionText); - var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); - Object.extend(options, { - parameters: 'editorId=' + encodeURIComponent(this.element.id), - onComplete: Prototype.emptyFunction, - onSuccess: function(transport) { - var js = transport.responseText.strip(); - if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check - throw('Server returned an invalid collection representation.'); - this._collection = eval(js); - this.checkForExternalText(); - }.bind(this), - onFailure: this.onFailure - }); - new Ajax.Request(this.options.loadCollectionURL, options); - }, - - showLoadingText: function(text) { - this._controls.editor.disabled = true; - var tempOption = this._controls.editor.firstChild; - if (!tempOption) { - tempOption = document.createElement('option'); - tempOption.value = ''; - this._controls.editor.appendChild(tempOption); - tempOption.selected = true; - } - tempOption.update((text || '').stripScripts().stripTags()); - }, - - checkForExternalText: function() { - this._text = this.getText(); - if (this.options.loadTextURL) - this.loadExternalText(); - else - this.buildOptionList(); - }, - - loadExternalText: function() { - this.showLoadingText(this.options.loadingText); - var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); - Object.extend(options, { - parameters: 'editorId=' + encodeURIComponent(this.element.id), - onComplete: Prototype.emptyFunction, - onSuccess: function(transport) { - this._text = transport.responseText.strip(); - this.buildOptionList(); - }.bind(this), - onFailure: this.onFailure - }); - new Ajax.Request(this.options.loadTextURL, options); - }, - - buildOptionList: function() { - this._form.removeClassName(this.options.loadingClassName); - this._collection = this._collection.map(function(entry) { - return 2 === entry.length ? entry : [entry, entry].flatten(); - }); - var marker = ('value' in this.options) ? this.options.value : this._text; - var textFound = this._collection.any(function(entry) { - return entry[0] == marker; - }.bind(this)); - this._controls.editor.update(''); - var option; - this._collection.each(function(entry, index) { - option = document.createElement('option'); - option.value = entry[0]; - option.selected = textFound ? entry[0] == marker : 0 == index; - option.appendChild(document.createTextNode(entry[1])); - this._controls.editor.appendChild(option); - }.bind(this)); - this._controls.editor.disabled = false; - Field.scrollFreeActivate(this._controls.editor); - } -}); - -//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** -//**** This only exists for a while, in order to let **** -//**** users adapt to the new API. Read up on the new **** -//**** API and convert your code to it ASAP! **** - -Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { - if (!options) return; - function fallback(name, expr) { - if (name in options || expr === undefined) return; - options[name] = expr; - }; - fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : - options.cancelLink == options.cancelButton == false ? false : undefined))); - fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : - options.okLink == options.okButton == false ? false : undefined))); - fallback('highlightColor', options.highlightcolor); - fallback('highlightEndColor', options.highlightendcolor); -}; - -Object.extend(Ajax.InPlaceEditor, { - DefaultOptions: { - ajaxOptions: { }, - autoRows: 3, // Use when multi-line w/ rows == 1 - cancelControl: 'link', // 'link'|'button'|false - cancelText: 'cancel', - clickToEditText: 'Click to edit', - externalControl: null, // id|elt - externalControlOnly: false, - fieldPostCreation: 'activate', // 'activate'|'focus'|false - formClassName: 'inplaceeditor-form', - formId: null, // id|elt - highlightColor: '#ffff99', - highlightEndColor: '#ffffff', - hoverClassName: '', - htmlResponse: true, - loadingClassName: 'inplaceeditor-loading', - loadingText: 'Loading...', - okControl: 'button', // 'link'|'button'|false - okText: 'ok', - paramName: 'value', - rows: 1, // If 1 and multi-line, uses autoRows - savingClassName: 'inplaceeditor-saving', - savingText: 'Saving...', - size: 0, - stripLoadedTextTags: false, - submitOnBlur: false, - textAfterControls: '', - textBeforeControls: '', - textBetweenControls: '' - }, - DefaultCallbacks: { - callback: function(form) { - return Form.serialize(form); - }, - onComplete: function(transport, element) { - // For backward compatibility, this one is bound to the IPE, and passes - // the element directly. It was too often customized, so we don't break it. - new Effect.Highlight(element, { - startcolor: this.options.highlightColor, keepBackgroundImage: true }); - }, - onEnterEditMode: null, - onEnterHover: function(ipe) { - ipe.element.style.backgroundColor = ipe.options.highlightColor; - if (ipe._effect) - ipe._effect.cancel(); - }, - onFailure: function(transport, ipe) { - alert('Error communication with the server: ' + transport.responseText.stripTags()); - }, - onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. - onLeaveEditMode: null, - onLeaveHover: function(ipe) { - ipe._effect = new Effect.Highlight(ipe.element, { - startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, - restorecolor: ipe._originalBackground, keepBackgroundImage: true - }); - } - }, - Listeners: { - click: 'enterEditMode', - keydown: 'checkForEscapeOrReturn', - mouseover: 'enterHover', - mouseout: 'leaveHover' - } -}); - -Ajax.InPlaceCollectionEditor.DefaultOptions = { - loadingCollectionText: 'Loading options...' -}; - -// Delayed observer, like Form.Element.Observer, -// but waits for delay after last key input -// Ideal for live-search fields - -Form.Element.DelayedObserver = Class.create({ - initialize: function(element, delay, callback) { - this.delay = delay || 0.5; - this.element = $(element); - this.callback = callback; - this.timer = null; - this.lastValue = $F(this.element); - Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); - }, - delayedListener: function(event) { - if(this.lastValue == $F(this.element)) return; - if(this.timer) clearTimeout(this.timer); - this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); - this.lastValue = $F(this.element); - }, - onTimerEvent: function() { - this.timer = null; - this.callback(this.element, $F(this.element)); - } -}); \ No newline at end of file diff --git a/test/dummy/public/javascripts/dragdrop.js b/test/dummy/public/javascripts/dragdrop.js deleted file mode 100644 index 15c6dbca..00000000 --- a/test/dummy/public/javascripts/dragdrop.js +++ /dev/null @@ -1,974 +0,0 @@ -// script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 - -// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// -// script.aculo.us is freely distributable under the terms of an MIT-style license. -// For details, see the script.aculo.us web site: http://script.aculo.us/ - -if(Object.isUndefined(Effect)) - throw("dragdrop.js requires including script.aculo.us' effects.js library"); - -var Droppables = { - drops: [], - - remove: function(element) { - this.drops = this.drops.reject(function(d) { return d.element==$(element) }); - }, - - add: function(element) { - element = $(element); - var options = Object.extend({ - greedy: true, - hoverclass: null, - tree: false - }, arguments[1] || { }); - - // cache containers - if(options.containment) { - options._containers = []; - var containment = options.containment; - if(Object.isArray(containment)) { - containment.each( function(c) { options._containers.push($(c)) }); - } else { - options._containers.push($(containment)); - } - } - - if(options.accept) options.accept = [options.accept].flatten(); - - Element.makePositioned(element); // fix IE - options.element = element; - - this.drops.push(options); - }, - - findDeepestChild: function(drops) { - deepest = drops[0]; - - for (i = 1; i < drops.length; ++i) - if (Element.isParent(drops[i].element, deepest.element)) - deepest = drops[i]; - - return deepest; - }, - - isContained: function(element, drop) { - var containmentNode; - if(drop.tree) { - containmentNode = element.treeNode; - } else { - containmentNode = element.parentNode; - } - return drop._containers.detect(function(c) { return containmentNode == c }); - }, - - isAffected: function(point, element, drop) { - return ( - (drop.element!=element) && - ((!drop._containers) || - this.isContained(element, drop)) && - ((!drop.accept) || - (Element.classNames(element).detect( - function(v) { return drop.accept.include(v) } ) )) && - Position.within(drop.element, point[0], point[1]) ); - }, - - deactivate: function(drop) { - if(drop.hoverclass) - Element.removeClassName(drop.element, drop.hoverclass); - this.last_active = null; - }, - - activate: function(drop) { - if(drop.hoverclass) - Element.addClassName(drop.element, drop.hoverclass); - this.last_active = drop; - }, - - show: function(point, element) { - if(!this.drops.length) return; - var drop, affected = []; - - this.drops.each( function(drop) { - if(Droppables.isAffected(point, element, drop)) - affected.push(drop); - }); - - if(affected.length>0) - drop = Droppables.findDeepestChild(affected); - - if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); - if (drop) { - Position.within(drop.element, point[0], point[1]); - if(drop.onHover) - drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); - - if (drop != this.last_active) Droppables.activate(drop); - } - }, - - fire: function(event, element) { - if(!this.last_active) return; - Position.prepare(); - - if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) - if (this.last_active.onDrop) { - this.last_active.onDrop(element, this.last_active.element, event); - return true; - } - }, - - reset: function() { - if(this.last_active) - this.deactivate(this.last_active); - } -}; - -var Draggables = { - drags: [], - observers: [], - - register: function(draggable) { - if(this.drags.length == 0) { - this.eventMouseUp = this.endDrag.bindAsEventListener(this); - this.eventMouseMove = this.updateDrag.bindAsEventListener(this); - this.eventKeypress = this.keyPress.bindAsEventListener(this); - - Event.observe(document, "mouseup", this.eventMouseUp); - Event.observe(document, "mousemove", this.eventMouseMove); - Event.observe(document, "keypress", this.eventKeypress); - } - this.drags.push(draggable); - }, - - unregister: function(draggable) { - this.drags = this.drags.reject(function(d) { return d==draggable }); - if(this.drags.length == 0) { - Event.stopObserving(document, "mouseup", this.eventMouseUp); - Event.stopObserving(document, "mousemove", this.eventMouseMove); - Event.stopObserving(document, "keypress", this.eventKeypress); - } - }, - - activate: function(draggable) { - if(draggable.options.delay) { - this._timeout = setTimeout(function() { - Draggables._timeout = null; - window.focus(); - Draggables.activeDraggable = draggable; - }.bind(this), draggable.options.delay); - } else { - window.focus(); // allows keypress events if window isn't currently focused, fails for Safari - this.activeDraggable = draggable; - } - }, - - deactivate: function() { - this.activeDraggable = null; - }, - - updateDrag: function(event) { - if(!this.activeDraggable) return; - var pointer = [Event.pointerX(event), Event.pointerY(event)]; - // Mozilla-based browsers fire successive mousemove events with - // the same coordinates, prevent needless redrawing (moz bug?) - if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; - this._lastPointer = pointer; - - this.activeDraggable.updateDrag(event, pointer); - }, - - endDrag: function(event) { - if(this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - } - if(!this.activeDraggable) return; - this._lastPointer = null; - this.activeDraggable.endDrag(event); - this.activeDraggable = null; - }, - - keyPress: function(event) { - if(this.activeDraggable) - this.activeDraggable.keyPress(event); - }, - - addObserver: function(observer) { - this.observers.push(observer); - this._cacheObserverCallbacks(); - }, - - removeObserver: function(element) { // element instead of observer fixes mem leaks - this.observers = this.observers.reject( function(o) { return o.element==element }); - this._cacheObserverCallbacks(); - }, - - notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' - if(this[eventName+'Count'] > 0) - this.observers.each( function(o) { - if(o[eventName]) o[eventName](eventName, draggable, event); - }); - if(draggable.options[eventName]) draggable.options[eventName](draggable, event); - }, - - _cacheObserverCallbacks: function() { - ['onStart','onEnd','onDrag'].each( function(eventName) { - Draggables[eventName+'Count'] = Draggables.observers.select( - function(o) { return o[eventName]; } - ).length; - }); - } -}; - -/*--------------------------------------------------------------------------*/ - -var Draggable = Class.create({ - initialize: function(element) { - var defaults = { - handle: false, - reverteffect: function(element, top_offset, left_offset) { - var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; - new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, - queue: {scope:'_draggable', position:'end'} - }); - }, - endeffect: function(element) { - var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; - new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, - queue: {scope:'_draggable', position:'end'}, - afterFinish: function(){ - Draggable._dragging[element] = false - } - }); - }, - zindex: 1000, - revert: false, - quiet: false, - scroll: false, - scrollSensitivity: 20, - scrollSpeed: 15, - snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } - delay: 0 - }; - - if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) - Object.extend(defaults, { - starteffect: function(element) { - element._opacity = Element.getOpacity(element); - Draggable._dragging[element] = true; - new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); - } - }); - - var options = Object.extend(defaults, arguments[1] || { }); - - this.element = $(element); - - if(options.handle && Object.isString(options.handle)) - this.handle = this.element.down('.'+options.handle, 0); - - if(!this.handle) this.handle = $(options.handle); - if(!this.handle) this.handle = this.element; - - if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { - options.scroll = $(options.scroll); - this._isScrollChild = Element.childOf(this.element, options.scroll); - } - - Element.makePositioned(this.element); // fix IE - - this.options = options; - this.dragging = false; - - this.eventMouseDown = this.initDrag.bindAsEventListener(this); - Event.observe(this.handle, "mousedown", this.eventMouseDown); - - Draggables.register(this); - }, - - destroy: function() { - Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); - Draggables.unregister(this); - }, - - currentDelta: function() { - return([ - parseInt(Element.getStyle(this.element,'left') || '0'), - parseInt(Element.getStyle(this.element,'top') || '0')]); - }, - - initDrag: function(event) { - if(!Object.isUndefined(Draggable._dragging[this.element]) && - Draggable._dragging[this.element]) return; - if(Event.isLeftClick(event)) { - // abort on form elements, fixes a Firefox issue - var src = Event.element(event); - if((tag_name = src.tagName.toUpperCase()) && ( - tag_name=='INPUT' || - tag_name=='SELECT' || - tag_name=='OPTION' || - tag_name=='BUTTON' || - tag_name=='TEXTAREA')) return; - - var pointer = [Event.pointerX(event), Event.pointerY(event)]; - var pos = this.element.cumulativeOffset(); - this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); - - Draggables.activate(this); - Event.stop(event); - } - }, - - startDrag: function(event) { - this.dragging = true; - if(!this.delta) - this.delta = this.currentDelta(); - - if(this.options.zindex) { - this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); - this.element.style.zIndex = this.options.zindex; - } - - if(this.options.ghosting) { - this._clone = this.element.cloneNode(true); - this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); - if (!this._originallyAbsolute) - Position.absolutize(this.element); - this.element.parentNode.insertBefore(this._clone, this.element); - } - - if(this.options.scroll) { - if (this.options.scroll == window) { - var where = this._getWindowScroll(this.options.scroll); - this.originalScrollLeft = where.left; - this.originalScrollTop = where.top; - } else { - this.originalScrollLeft = this.options.scroll.scrollLeft; - this.originalScrollTop = this.options.scroll.scrollTop; - } - } - - Draggables.notify('onStart', this, event); - - if(this.options.starteffect) this.options.starteffect(this.element); - }, - - updateDrag: function(event, pointer) { - if(!this.dragging) this.startDrag(event); - - if(!this.options.quiet){ - Position.prepare(); - Droppables.show(pointer, this.element); - } - - Draggables.notify('onDrag', this, event); - - this.draw(pointer); - if(this.options.change) this.options.change(this); - - if(this.options.scroll) { - this.stopScrolling(); - - var p; - if (this.options.scroll == window) { - with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } - } else { - p = Position.page(this.options.scroll); - p[0] += this.options.scroll.scrollLeft + Position.deltaX; - p[1] += this.options.scroll.scrollTop + Position.deltaY; - p.push(p[0]+this.options.scroll.offsetWidth); - p.push(p[1]+this.options.scroll.offsetHeight); - } - var speed = [0,0]; - if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); - if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); - if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); - if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); - this.startScrolling(speed); - } - - // fix AppleWebKit rendering - if(Prototype.Browser.WebKit) window.scrollBy(0,0); - - Event.stop(event); - }, - - finishDrag: function(event, success) { - this.dragging = false; - - if(this.options.quiet){ - Position.prepare(); - var pointer = [Event.pointerX(event), Event.pointerY(event)]; - Droppables.show(pointer, this.element); - } - - if(this.options.ghosting) { - if (!this._originallyAbsolute) - Position.relativize(this.element); - delete this._originallyAbsolute; - Element.remove(this._clone); - this._clone = null; - } - - var dropped = false; - if(success) { - dropped = Droppables.fire(event, this.element); - if (!dropped) dropped = false; - } - if(dropped && this.options.onDropped) this.options.onDropped(this.element); - Draggables.notify('onEnd', this, event); - - var revert = this.options.revert; - if(revert && Object.isFunction(revert)) revert = revert(this.element); - - var d = this.currentDelta(); - if(revert && this.options.reverteffect) { - if (dropped == 0 || revert != 'failure') - this.options.reverteffect(this.element, - d[1]-this.delta[1], d[0]-this.delta[0]); - } else { - this.delta = d; - } - - if(this.options.zindex) - this.element.style.zIndex = this.originalZ; - - if(this.options.endeffect) - this.options.endeffect(this.element); - - Draggables.deactivate(this); - Droppables.reset(); - }, - - keyPress: function(event) { - if(event.keyCode!=Event.KEY_ESC) return; - this.finishDrag(event, false); - Event.stop(event); - }, - - endDrag: function(event) { - if(!this.dragging) return; - this.stopScrolling(); - this.finishDrag(event, true); - Event.stop(event); - }, - - draw: function(point) { - var pos = this.element.cumulativeOffset(); - if(this.options.ghosting) { - var r = Position.realOffset(this.element); - pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; - } - - var d = this.currentDelta(); - pos[0] -= d[0]; pos[1] -= d[1]; - - if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { - pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; - pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; - } - - var p = [0,1].map(function(i){ - return (point[i]-pos[i]-this.offset[i]) - }.bind(this)); - - if(this.options.snap) { - if(Object.isFunction(this.options.snap)) { - p = this.options.snap(p[0],p[1],this); - } else { - if(Object.isArray(this.options.snap)) { - p = p.map( function(v, i) { - return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); - } else { - p = p.map( function(v) { - return (v/this.options.snap).round()*this.options.snap }.bind(this)); - } - }} - - var style = this.element.style; - if((!this.options.constraint) || (this.options.constraint=='horizontal')) - style.left = p[0] + "px"; - if((!this.options.constraint) || (this.options.constraint=='vertical')) - style.top = p[1] + "px"; - - if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering - }, - - stopScrolling: function() { - if(this.scrollInterval) { - clearInterval(this.scrollInterval); - this.scrollInterval = null; - Draggables._lastScrollPointer = null; - } - }, - - startScrolling: function(speed) { - if(!(speed[0] || speed[1])) return; - this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; - this.lastScrolled = new Date(); - this.scrollInterval = setInterval(this.scroll.bind(this), 10); - }, - - scroll: function() { - var current = new Date(); - var delta = current - this.lastScrolled; - this.lastScrolled = current; - if(this.options.scroll == window) { - with (this._getWindowScroll(this.options.scroll)) { - if (this.scrollSpeed[0] || this.scrollSpeed[1]) { - var d = delta / 1000; - this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); - } - } - } else { - this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; - this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; - } - - Position.prepare(); - Droppables.show(Draggables._lastPointer, this.element); - Draggables.notify('onDrag', this); - if (this._isScrollChild) { - Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); - Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; - Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; - if (Draggables._lastScrollPointer[0] < 0) - Draggables._lastScrollPointer[0] = 0; - if (Draggables._lastScrollPointer[1] < 0) - Draggables._lastScrollPointer[1] = 0; - this.draw(Draggables._lastScrollPointer); - } - - if(this.options.change) this.options.change(this); - }, - - _getWindowScroll: function(w) { - var T, L, W, H; - with (w.document) { - if (w.document.documentElement && documentElement.scrollTop) { - T = documentElement.scrollTop; - L = documentElement.scrollLeft; - } else if (w.document.body) { - T = body.scrollTop; - L = body.scrollLeft; - } - if (w.innerWidth) { - W = w.innerWidth; - H = w.innerHeight; - } else if (w.document.documentElement && documentElement.clientWidth) { - W = documentElement.clientWidth; - H = documentElement.clientHeight; - } else { - W = body.offsetWidth; - H = body.offsetHeight; - } - } - return { top: T, left: L, width: W, height: H }; - } -}); - -Draggable._dragging = { }; - -/*--------------------------------------------------------------------------*/ - -var SortableObserver = Class.create({ - initialize: function(element, observer) { - this.element = $(element); - this.observer = observer; - this.lastValue = Sortable.serialize(this.element); - }, - - onStart: function() { - this.lastValue = Sortable.serialize(this.element); - }, - - onEnd: function() { - Sortable.unmark(); - if(this.lastValue != Sortable.serialize(this.element)) - this.observer(this.element) - } -}); - -var Sortable = { - SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, - - sortables: { }, - - _findRootElement: function(element) { - while (element.tagName.toUpperCase() != "BODY") { - if(element.id && Sortable.sortables[element.id]) return element; - element = element.parentNode; - } - }, - - options: function(element) { - element = Sortable._findRootElement($(element)); - if(!element) return; - return Sortable.sortables[element.id]; - }, - - destroy: function(element){ - element = $(element); - var s = Sortable.sortables[element.id]; - - if(s) { - Draggables.removeObserver(s.element); - s.droppables.each(function(d){ Droppables.remove(d) }); - s.draggables.invoke('destroy'); - - delete Sortable.sortables[s.element.id]; - } - }, - - create: function(element) { - element = $(element); - var options = Object.extend({ - element: element, - tag: 'li', // assumes li children, override with tag: 'tagname' - dropOnEmpty: false, - tree: false, - treeTag: 'ul', - overlap: 'vertical', // one of 'vertical', 'horizontal' - constraint: 'vertical', // one of 'vertical', 'horizontal', false - containment: element, // also takes array of elements (or id's); or false - handle: false, // or a CSS class - only: false, - delay: 0, - hoverclass: null, - ghosting: false, - quiet: false, - scroll: false, - scrollSensitivity: 20, - scrollSpeed: 15, - format: this.SERIALIZE_RULE, - - // these take arrays of elements or ids and can be - // used for better initialization performance - elements: false, - handles: false, - - onChange: Prototype.emptyFunction, - onUpdate: Prototype.emptyFunction - }, arguments[1] || { }); - - // clear any old sortable with same element - this.destroy(element); - - // build options for the draggables - var options_for_draggable = { - revert: true, - quiet: options.quiet, - scroll: options.scroll, - scrollSpeed: options.scrollSpeed, - scrollSensitivity: options.scrollSensitivity, - delay: options.delay, - ghosting: options.ghosting, - constraint: options.constraint, - handle: options.handle }; - - if(options.starteffect) - options_for_draggable.starteffect = options.starteffect; - - if(options.reverteffect) - options_for_draggable.reverteffect = options.reverteffect; - else - if(options.ghosting) options_for_draggable.reverteffect = function(element) { - element.style.top = 0; - element.style.left = 0; - }; - - if(options.endeffect) - options_for_draggable.endeffect = options.endeffect; - - if(options.zindex) - options_for_draggable.zindex = options.zindex; - - // build options for the droppables - var options_for_droppable = { - overlap: options.overlap, - containment: options.containment, - tree: options.tree, - hoverclass: options.hoverclass, - onHover: Sortable.onHover - }; - - var options_for_tree = { - onHover: Sortable.onEmptyHover, - overlap: options.overlap, - containment: options.containment, - hoverclass: options.hoverclass - }; - - // fix for gecko engine - Element.cleanWhitespace(element); - - options.draggables = []; - options.droppables = []; - - // drop on empty handling - if(options.dropOnEmpty || options.tree) { - Droppables.add(element, options_for_tree); - options.droppables.push(element); - } - - (options.elements || this.findElements(element, options) || []).each( function(e,i) { - var handle = options.handles ? $(options.handles[i]) : - (options.handle ? $(e).select('.' + options.handle)[0] : e); - options.draggables.push( - new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); - Droppables.add(e, options_for_droppable); - if(options.tree) e.treeNode = element; - options.droppables.push(e); - }); - - if(options.tree) { - (Sortable.findTreeElements(element, options) || []).each( function(e) { - Droppables.add(e, options_for_tree); - e.treeNode = element; - options.droppables.push(e); - }); - } - - // keep reference - this.sortables[element.identify()] = options; - - // for onupdate - Draggables.addObserver(new SortableObserver(element, options.onUpdate)); - - }, - - // return all suitable-for-sortable elements in a guaranteed order - findElements: function(element, options) { - return Element.findChildren( - element, options.only, options.tree ? true : false, options.tag); - }, - - findTreeElements: function(element, options) { - return Element.findChildren( - element, options.only, options.tree ? true : false, options.treeTag); - }, - - onHover: function(element, dropon, overlap) { - if(Element.isParent(dropon, element)) return; - - if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { - return; - } else if(overlap>0.5) { - Sortable.mark(dropon, 'before'); - if(dropon.previousSibling != element) { - var oldParentNode = element.parentNode; - element.style.visibility = "hidden"; // fix gecko rendering - dropon.parentNode.insertBefore(element, dropon); - if(dropon.parentNode!=oldParentNode) - Sortable.options(oldParentNode).onChange(element); - Sortable.options(dropon.parentNode).onChange(element); - } - } else { - Sortable.mark(dropon, 'after'); - var nextElement = dropon.nextSibling || null; - if(nextElement != element) { - var oldParentNode = element.parentNode; - element.style.visibility = "hidden"; // fix gecko rendering - dropon.parentNode.insertBefore(element, nextElement); - if(dropon.parentNode!=oldParentNode) - Sortable.options(oldParentNode).onChange(element); - Sortable.options(dropon.parentNode).onChange(element); - } - } - }, - - onEmptyHover: function(element, dropon, overlap) { - var oldParentNode = element.parentNode; - var droponOptions = Sortable.options(dropon); - - if(!Element.isParent(dropon, element)) { - var index; - - var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); - var child = null; - - if(children) { - var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); - - for (index = 0; index < children.length; index += 1) { - if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { - offset -= Element.offsetSize (children[index], droponOptions.overlap); - } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { - child = index + 1 < children.length ? children[index + 1] : null; - break; - } else { - child = children[index]; - break; - } - } - } - - dropon.insertBefore(element, child); - - Sortable.options(oldParentNode).onChange(element); - droponOptions.onChange(element); - } - }, - - unmark: function() { - if(Sortable._marker) Sortable._marker.hide(); - }, - - mark: function(dropon, position) { - // mark on ghosting only - var sortable = Sortable.options(dropon.parentNode); - if(sortable && !sortable.ghosting) return; - - if(!Sortable._marker) { - Sortable._marker = - ($('dropmarker') || Element.extend(document.createElement('DIV'))). - hide().addClassName('dropmarker').setStyle({position:'absolute'}); - document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); - } - var offsets = dropon.cumulativeOffset(); - Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); - - if(position=='after') - if(sortable.overlap == 'horizontal') - Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); - else - Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); - - Sortable._marker.show(); - }, - - _tree: function(element, options, parent) { - var children = Sortable.findElements(element, options) || []; - - for (var i = 0; i < children.length; ++i) { - var match = children[i].id.match(options.format); - - if (!match) continue; - - var child = { - id: encodeURIComponent(match ? match[1] : null), - element: element, - parent: parent, - children: [], - position: parent.children.length, - container: $(children[i]).down(options.treeTag) - }; - - /* Get the element containing the children and recurse over it */ - if (child.container) - this._tree(child.container, options, child); - - parent.children.push (child); - } - - return parent; - }, - - tree: function(element) { - element = $(element); - var sortableOptions = this.options(element); - var options = Object.extend({ - tag: sortableOptions.tag, - treeTag: sortableOptions.treeTag, - only: sortableOptions.only, - name: element.id, - format: sortableOptions.format - }, arguments[1] || { }); - - var root = { - id: null, - parent: null, - children: [], - container: element, - position: 0 - }; - - return Sortable._tree(element, options, root); - }, - - /* Construct a [i] index for a particular node */ - _constructIndex: function(node) { - var index = ''; - do { - if (node.id) index = '[' + node.position + ']' + index; - } while ((node = node.parent) != null); - return index; - }, - - sequence: function(element) { - element = $(element); - var options = Object.extend(this.options(element), arguments[1] || { }); - - return $(this.findElements(element, options) || []).map( function(item) { - return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; - }); - }, - - setSequence: function(element, new_sequence) { - element = $(element); - var options = Object.extend(this.options(element), arguments[2] || { }); - - var nodeMap = { }; - this.findElements(element, options).each( function(n) { - if (n.id.match(options.format)) - nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; - n.parentNode.removeChild(n); - }); - - new_sequence.each(function(ident) { - var n = nodeMap[ident]; - if (n) { - n[1].appendChild(n[0]); - delete nodeMap[ident]; - } - }); - }, - - serialize: function(element) { - element = $(element); - var options = Object.extend(Sortable.options(element), arguments[1] || { }); - var name = encodeURIComponent( - (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); - - if (options.tree) { - return Sortable.tree(element, arguments[1]).children.map( function (item) { - return [name + Sortable._constructIndex(item) + "[id]=" + - encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); - }).flatten().join('&'); - } else { - return Sortable.sequence(element, arguments[1]).map( function(item) { - return name + "[]=" + encodeURIComponent(item); - }).join('&'); - } - } -}; - -// Returns true if child is contained within element -Element.isParent = function(child, element) { - if (!child.parentNode || child == element) return false; - if (child.parentNode == element) return true; - return Element.isParent(child.parentNode, element); -}; - -Element.findChildren = function(element, only, recursive, tagName) { - if(!element.hasChildNodes()) return null; - tagName = tagName.toUpperCase(); - if(only) only = [only].flatten(); - var elements = []; - $A(element.childNodes).each( function(e) { - if(e.tagName && e.tagName.toUpperCase()==tagName && - (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) - elements.push(e); - if(recursive) { - var grandchildren = Element.findChildren(e, only, recursive, tagName); - if(grandchildren) elements.push(grandchildren); - } - }); - - return (elements.length>0 ? elements.flatten() : []); -}; - -Element.offsetSize = function (element, type) { - return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; -}; \ No newline at end of file diff --git a/test/dummy/public/javascripts/effects.js b/test/dummy/public/javascripts/effects.js deleted file mode 100644 index c81e6c7d..00000000 --- a/test/dummy/public/javascripts/effects.js +++ /dev/null @@ -1,1123 +0,0 @@ -// script.aculo.us effects.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 - -// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// Contributors: -// Justin Palmer (http://encytemedia.com/) -// Mark Pilgrim (http://diveintomark.org/) -// Martin Bialasinki -// -// script.aculo.us is freely distributable under the terms of an MIT-style license. -// For details, see the script.aculo.us web site: http://script.aculo.us/ - -// converts rgb() and #xxx to #xxxxxx format, -// returns self (or first argument) if not convertable -String.prototype.parseColor = function() { - var color = '#'; - if (this.slice(0,4) == 'rgb(') { - var cols = this.slice(4,this.length-1).split(','); - var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); - } else { - if (this.slice(0,1) == '#') { - if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); - if (this.length==7) color = this.toLowerCase(); - } - } - return (color.length==7 ? color : (arguments[0] || this)); -}; - -/*--------------------------------------------------------------------------*/ - -Element.collectTextNodes = function(element) { - return $A($(element).childNodes).collect( function(node) { - return (node.nodeType==3 ? node.nodeValue : - (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); - }).flatten().join(''); -}; - -Element.collectTextNodesIgnoreClass = function(element, className) { - return $A($(element).childNodes).collect( function(node) { - return (node.nodeType==3 ? node.nodeValue : - ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? - Element.collectTextNodesIgnoreClass(node, className) : '')); - }).flatten().join(''); -}; - -Element.setContentZoom = function(element, percent) { - element = $(element); - element.setStyle({fontSize: (percent/100) + 'em'}); - if (Prototype.Browser.WebKit) window.scrollBy(0,0); - return element; -}; - -Element.getInlineOpacity = function(element){ - return $(element).style.opacity || ''; -}; - -Element.forceRerendering = function(element) { - try { - element = $(element); - var n = document.createTextNode(' '); - element.appendChild(n); - element.removeChild(n); - } catch(e) { } -}; - -/*--------------------------------------------------------------------------*/ - -var Effect = { - _elementDoesNotExistError: { - name: 'ElementDoesNotExistError', - message: 'The specified DOM element does not exist, but is required for this effect to operate' - }, - Transitions: { - linear: Prototype.K, - sinoidal: function(pos) { - return (-Math.cos(pos*Math.PI)/2) + .5; - }, - reverse: function(pos) { - return 1-pos; - }, - flicker: function(pos) { - var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; - return pos > 1 ? 1 : pos; - }, - wobble: function(pos) { - return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; - }, - pulse: function(pos, pulses) { - return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; - }, - spring: function(pos) { - return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); - }, - none: function(pos) { - return 0; - }, - full: function(pos) { - return 1; - } - }, - DefaultOptions: { - duration: 1.0, // seconds - fps: 100, // 100= assume 66fps max. - sync: false, // true for combining - from: 0.0, - to: 1.0, - delay: 0.0, - queue: 'parallel' - }, - tagifyText: function(element) { - var tagifyStyle = 'position:relative'; - if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; - - element = $(element); - $A(element.childNodes).each( function(child) { - if (child.nodeType==3) { - child.nodeValue.toArray().each( function(character) { - element.insertBefore( - new Element('span', {style: tagifyStyle}).update( - character == ' ' ? String.fromCharCode(160) : character), - child); - }); - Element.remove(child); - } - }); - }, - multiple: function(element, effect) { - var elements; - if (((typeof element == 'object') || - Object.isFunction(element)) && - (element.length)) - elements = element; - else - elements = $(element).childNodes; - - var options = Object.extend({ - speed: 0.1, - delay: 0.0 - }, arguments[2] || { }); - var masterDelay = options.delay; - - $A(elements).each( function(element, index) { - new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); - }); - }, - PAIRS: { - 'slide': ['SlideDown','SlideUp'], - 'blind': ['BlindDown','BlindUp'], - 'appear': ['Appear','Fade'] - }, - toggle: function(element, effect, options) { - element = $(element); - effect = (effect || 'appear').toLowerCase(); - - return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({ - queue: { position:'end', scope:(element.id || 'global'), limit: 1 } - }, options || {})); - } -}; - -Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; - -/* ------------- core effects ------------- */ - -Effect.ScopedQueue = Class.create(Enumerable, { - initialize: function() { - this.effects = []; - this.interval = null; - }, - _each: function(iterator) { - this.effects._each(iterator); - }, - add: function(effect) { - var timestamp = new Date().getTime(); - - var position = Object.isString(effect.options.queue) ? - effect.options.queue : effect.options.queue.position; - - switch(position) { - case 'front': - // move unstarted effects after this effect - this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { - e.startOn += effect.finishOn; - e.finishOn += effect.finishOn; - }); - break; - case 'with-last': - timestamp = this.effects.pluck('startOn').max() || timestamp; - break; - case 'end': - // start effect after last queued effect has finished - timestamp = this.effects.pluck('finishOn').max() || timestamp; - break; - } - - effect.startOn += timestamp; - effect.finishOn += timestamp; - - if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) - this.effects.push(effect); - - if (!this.interval) - this.interval = setInterval(this.loop.bind(this), 15); - }, - remove: function(effect) { - this.effects = this.effects.reject(function(e) { return e==effect }); - if (this.effects.length == 0) { - clearInterval(this.interval); - this.interval = null; - } - }, - loop: function() { - var timePos = new Date().getTime(); - for(var i=0, len=this.effects.length;i= this.startOn) { - if (timePos >= this.finishOn) { - this.render(1.0); - this.cancel(); - this.event('beforeFinish'); - if (this.finish) this.finish(); - this.event('afterFinish'); - return; - } - var pos = (timePos - this.startOn) / this.totalTime, - frame = (pos * this.totalFrames).round(); - if (frame > this.currentFrame) { - this.render(pos); - this.currentFrame = frame; - } - } - }, - cancel: function() { - if (!this.options.sync) - Effect.Queues.get(Object.isString(this.options.queue) ? - 'global' : this.options.queue.scope).remove(this); - this.state = 'finished'; - }, - event: function(eventName) { - if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); - if (this.options[eventName]) this.options[eventName](this); - }, - inspect: function() { - var data = $H(); - for(property in this) - if (!Object.isFunction(this[property])) data.set(property, this[property]); - return '#'; - } -}); - -Effect.Parallel = Class.create(Effect.Base, { - initialize: function(effects) { - this.effects = effects || []; - this.start(arguments[1]); - }, - update: function(position) { - this.effects.invoke('render', position); - }, - finish: function(position) { - this.effects.each( function(effect) { - effect.render(1.0); - effect.cancel(); - effect.event('beforeFinish'); - if (effect.finish) effect.finish(position); - effect.event('afterFinish'); - }); - } -}); - -Effect.Tween = Class.create(Effect.Base, { - initialize: function(object, from, to) { - object = Object.isString(object) ? $(object) : object; - var args = $A(arguments), method = args.last(), - options = args.length == 5 ? args[3] : null; - this.method = Object.isFunction(method) ? method.bind(object) : - Object.isFunction(object[method]) ? object[method].bind(object) : - function(value) { object[method] = value }; - this.start(Object.extend({ from: from, to: to }, options || { })); - }, - update: function(position) { - this.method(position); - } -}); - -Effect.Event = Class.create(Effect.Base, { - initialize: function() { - this.start(Object.extend({ duration: 0 }, arguments[0] || { })); - }, - update: Prototype.emptyFunction -}); - -Effect.Opacity = Class.create(Effect.Base, { - initialize: function(element) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - // make this work on IE on elements without 'layout' - if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) - this.element.setStyle({zoom: 1}); - var options = Object.extend({ - from: this.element.getOpacity() || 0.0, - to: 1.0 - }, arguments[1] || { }); - this.start(options); - }, - update: function(position) { - this.element.setOpacity(position); - } -}); - -Effect.Move = Class.create(Effect.Base, { - initialize: function(element) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - var options = Object.extend({ - x: 0, - y: 0, - mode: 'relative' - }, arguments[1] || { }); - this.start(options); - }, - setup: function() { - this.element.makePositioned(); - this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); - this.originalTop = parseFloat(this.element.getStyle('top') || '0'); - if (this.options.mode == 'absolute') { - this.options.x = this.options.x - this.originalLeft; - this.options.y = this.options.y - this.originalTop; - } - }, - update: function(position) { - this.element.setStyle({ - left: (this.options.x * position + this.originalLeft).round() + 'px', - top: (this.options.y * position + this.originalTop).round() + 'px' - }); - } -}); - -// for backwards compatibility -Effect.MoveBy = function(element, toTop, toLeft) { - return new Effect.Move(element, - Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); -}; - -Effect.Scale = Class.create(Effect.Base, { - initialize: function(element, percent) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - var options = Object.extend({ - scaleX: true, - scaleY: true, - scaleContent: true, - scaleFromCenter: false, - scaleMode: 'box', // 'box' or 'contents' or { } with provided values - scaleFrom: 100.0, - scaleTo: percent - }, arguments[2] || { }); - this.start(options); - }, - setup: function() { - this.restoreAfterFinish = this.options.restoreAfterFinish || false; - this.elementPositioning = this.element.getStyle('position'); - - this.originalStyle = { }; - ['top','left','width','height','fontSize'].each( function(k) { - this.originalStyle[k] = this.element.style[k]; - }.bind(this)); - - this.originalTop = this.element.offsetTop; - this.originalLeft = this.element.offsetLeft; - - var fontSize = this.element.getStyle('font-size') || '100%'; - ['em','px','%','pt'].each( function(fontSizeType) { - if (fontSize.indexOf(fontSizeType)>0) { - this.fontSize = parseFloat(fontSize); - this.fontSizeType = fontSizeType; - } - }.bind(this)); - - this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; - - this.dims = null; - if (this.options.scaleMode=='box') - this.dims = [this.element.offsetHeight, this.element.offsetWidth]; - if (/^content/.test(this.options.scaleMode)) - this.dims = [this.element.scrollHeight, this.element.scrollWidth]; - if (!this.dims) - this.dims = [this.options.scaleMode.originalHeight, - this.options.scaleMode.originalWidth]; - }, - update: function(position) { - var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); - if (this.options.scaleContent && this.fontSize) - this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); - this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); - }, - finish: function(position) { - if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); - }, - setDimensions: function(height, width) { - var d = { }; - if (this.options.scaleX) d.width = width.round() + 'px'; - if (this.options.scaleY) d.height = height.round() + 'px'; - if (this.options.scaleFromCenter) { - var topd = (height - this.dims[0])/2; - var leftd = (width - this.dims[1])/2; - if (this.elementPositioning == 'absolute') { - if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; - if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; - } else { - if (this.options.scaleY) d.top = -topd + 'px'; - if (this.options.scaleX) d.left = -leftd + 'px'; - } - } - this.element.setStyle(d); - } -}); - -Effect.Highlight = Class.create(Effect.Base, { - initialize: function(element) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); - this.start(options); - }, - setup: function() { - // Prevent executing on elements not in the layout flow - if (this.element.getStyle('display')=='none') { this.cancel(); return; } - // Disable background image during the effect - this.oldStyle = { }; - if (!this.options.keepBackgroundImage) { - this.oldStyle.backgroundImage = this.element.getStyle('background-image'); - this.element.setStyle({backgroundImage: 'none'}); - } - if (!this.options.endcolor) - this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); - if (!this.options.restorecolor) - this.options.restorecolor = this.element.getStyle('background-color'); - // init color calculations - this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); - this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); - }, - update: function(position) { - this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ - return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); - }, - finish: function() { - this.element.setStyle(Object.extend(this.oldStyle, { - backgroundColor: this.options.restorecolor - })); - } -}); - -Effect.ScrollTo = function(element) { - var options = arguments[1] || { }, - scrollOffsets = document.viewport.getScrollOffsets(), - elementOffsets = $(element).cumulativeOffset(); - - if (options.offset) elementOffsets[1] += options.offset; - - return new Effect.Tween(null, - scrollOffsets.top, - elementOffsets[1], - options, - function(p){ scrollTo(scrollOffsets.left, p.round()); } - ); -}; - -/* ------------- combination effects ------------- */ - -Effect.Fade = function(element) { - element = $(element); - var oldOpacity = element.getInlineOpacity(); - var options = Object.extend({ - from: element.getOpacity() || 1.0, - to: 0.0, - afterFinishInternal: function(effect) { - if (effect.options.to!=0) return; - effect.element.hide().setStyle({opacity: oldOpacity}); - } - }, arguments[1] || { }); - return new Effect.Opacity(element,options); -}; - -Effect.Appear = function(element) { - element = $(element); - var options = Object.extend({ - from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), - to: 1.0, - // force Safari to render floated elements properly - afterFinishInternal: function(effect) { - effect.element.forceRerendering(); - }, - beforeSetup: function(effect) { - effect.element.setOpacity(effect.options.from).show(); - }}, arguments[1] || { }); - return new Effect.Opacity(element,options); -}; - -Effect.Puff = function(element) { - element = $(element); - var oldStyle = { - opacity: element.getInlineOpacity(), - position: element.getStyle('position'), - top: element.style.top, - left: element.style.left, - width: element.style.width, - height: element.style.height - }; - return new Effect.Parallel( - [ new Effect.Scale(element, 200, - { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), - new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], - Object.extend({ duration: 1.0, - beforeSetupInternal: function(effect) { - Position.absolutize(effect.effects[0].element); - }, - afterFinishInternal: function(effect) { - effect.effects[0].element.hide().setStyle(oldStyle); } - }, arguments[1] || { }) - ); -}; - -Effect.BlindUp = function(element) { - element = $(element); - element.makeClipping(); - return new Effect.Scale(element, 0, - Object.extend({ scaleContent: false, - scaleX: false, - restoreAfterFinish: true, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping(); - } - }, arguments[1] || { }) - ); -}; - -Effect.BlindDown = function(element) { - element = $(element); - var elementDimensions = element.getDimensions(); - return new Effect.Scale(element, 100, Object.extend({ - scaleContent: false, - scaleX: false, - scaleFrom: 0, - scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, - restoreAfterFinish: true, - afterSetup: function(effect) { - effect.element.makeClipping().setStyle({height: '0px'}).show(); - }, - afterFinishInternal: function(effect) { - effect.element.undoClipping(); - } - }, arguments[1] || { })); -}; - -Effect.SwitchOff = function(element) { - element = $(element); - var oldOpacity = element.getInlineOpacity(); - return new Effect.Appear(element, Object.extend({ - duration: 0.4, - from: 0, - transition: Effect.Transitions.flicker, - afterFinishInternal: function(effect) { - new Effect.Scale(effect.element, 1, { - duration: 0.3, scaleFromCenter: true, - scaleX: false, scaleContent: false, restoreAfterFinish: true, - beforeSetup: function(effect) { - effect.element.makePositioned().makeClipping(); - }, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); - } - }); - } - }, arguments[1] || { })); -}; - -Effect.DropOut = function(element) { - element = $(element); - var oldStyle = { - top: element.getStyle('top'), - left: element.getStyle('left'), - opacity: element.getInlineOpacity() }; - return new Effect.Parallel( - [ new Effect.Move(element, {x: 0, y: 100, sync: true }), - new Effect.Opacity(element, { sync: true, to: 0.0 }) ], - Object.extend( - { duration: 0.5, - beforeSetup: function(effect) { - effect.effects[0].element.makePositioned(); - }, - afterFinishInternal: function(effect) { - effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); - } - }, arguments[1] || { })); -}; - -Effect.Shake = function(element) { - element = $(element); - var options = Object.extend({ - distance: 20, - duration: 0.5 - }, arguments[1] || {}); - var distance = parseFloat(options.distance); - var split = parseFloat(options.duration) / 10.0; - var oldStyle = { - top: element.getStyle('top'), - left: element.getStyle('left') }; - return new Effect.Move(element, - { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { - new Effect.Move(effect.element, - { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { - effect.element.undoPositioned().setStyle(oldStyle); - }}); }}); }}); }}); }}); }}); -}; - -Effect.SlideDown = function(element) { - element = $(element).cleanWhitespace(); - // SlideDown need to have the content of the element wrapped in a container element with fixed height! - var oldInnerBottom = element.down().getStyle('bottom'); - var elementDimensions = element.getDimensions(); - return new Effect.Scale(element, 100, Object.extend({ - scaleContent: false, - scaleX: false, - scaleFrom: window.opera ? 0 : 1, - scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, - restoreAfterFinish: true, - afterSetup: function(effect) { - effect.element.makePositioned(); - effect.element.down().makePositioned(); - if (window.opera) effect.element.setStyle({top: ''}); - effect.element.makeClipping().setStyle({height: '0px'}).show(); - }, - afterUpdateInternal: function(effect) { - effect.element.down().setStyle({bottom: - (effect.dims[0] - effect.element.clientHeight) + 'px' }); - }, - afterFinishInternal: function(effect) { - effect.element.undoClipping().undoPositioned(); - effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } - }, arguments[1] || { }) - ); -}; - -Effect.SlideUp = function(element) { - element = $(element).cleanWhitespace(); - var oldInnerBottom = element.down().getStyle('bottom'); - var elementDimensions = element.getDimensions(); - return new Effect.Scale(element, window.opera ? 0 : 1, - Object.extend({ scaleContent: false, - scaleX: false, - scaleMode: 'box', - scaleFrom: 100, - scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, - restoreAfterFinish: true, - afterSetup: function(effect) { - effect.element.makePositioned(); - effect.element.down().makePositioned(); - if (window.opera) effect.element.setStyle({top: ''}); - effect.element.makeClipping().show(); - }, - afterUpdateInternal: function(effect) { - effect.element.down().setStyle({bottom: - (effect.dims[0] - effect.element.clientHeight) + 'px' }); - }, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping().undoPositioned(); - effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); - } - }, arguments[1] || { }) - ); -}; - -// Bug in opera makes the TD containing this element expand for a instance after finish -Effect.Squish = function(element) { - return new Effect.Scale(element, window.opera ? 1 : 0, { - restoreAfterFinish: true, - beforeSetup: function(effect) { - effect.element.makeClipping(); - }, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping(); - } - }); -}; - -Effect.Grow = function(element) { - element = $(element); - var options = Object.extend({ - direction: 'center', - moveTransition: Effect.Transitions.sinoidal, - scaleTransition: Effect.Transitions.sinoidal, - opacityTransition: Effect.Transitions.full - }, arguments[1] || { }); - var oldStyle = { - top: element.style.top, - left: element.style.left, - height: element.style.height, - width: element.style.width, - opacity: element.getInlineOpacity() }; - - var dims = element.getDimensions(); - var initialMoveX, initialMoveY; - var moveX, moveY; - - switch (options.direction) { - case 'top-left': - initialMoveX = initialMoveY = moveX = moveY = 0; - break; - case 'top-right': - initialMoveX = dims.width; - initialMoveY = moveY = 0; - moveX = -dims.width; - break; - case 'bottom-left': - initialMoveX = moveX = 0; - initialMoveY = dims.height; - moveY = -dims.height; - break; - case 'bottom-right': - initialMoveX = dims.width; - initialMoveY = dims.height; - moveX = -dims.width; - moveY = -dims.height; - break; - case 'center': - initialMoveX = dims.width / 2; - initialMoveY = dims.height / 2; - moveX = -dims.width / 2; - moveY = -dims.height / 2; - break; - } - - return new Effect.Move(element, { - x: initialMoveX, - y: initialMoveY, - duration: 0.01, - beforeSetup: function(effect) { - effect.element.hide().makeClipping().makePositioned(); - }, - afterFinishInternal: function(effect) { - new Effect.Parallel( - [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), - new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), - new Effect.Scale(effect.element, 100, { - scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, - sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) - ], Object.extend({ - beforeSetup: function(effect) { - effect.effects[0].element.setStyle({height: '0px'}).show(); - }, - afterFinishInternal: function(effect) { - effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); - } - }, options) - ); - } - }); -}; - -Effect.Shrink = function(element) { - element = $(element); - var options = Object.extend({ - direction: 'center', - moveTransition: Effect.Transitions.sinoidal, - scaleTransition: Effect.Transitions.sinoidal, - opacityTransition: Effect.Transitions.none - }, arguments[1] || { }); - var oldStyle = { - top: element.style.top, - left: element.style.left, - height: element.style.height, - width: element.style.width, - opacity: element.getInlineOpacity() }; - - var dims = element.getDimensions(); - var moveX, moveY; - - switch (options.direction) { - case 'top-left': - moveX = moveY = 0; - break; - case 'top-right': - moveX = dims.width; - moveY = 0; - break; - case 'bottom-left': - moveX = 0; - moveY = dims.height; - break; - case 'bottom-right': - moveX = dims.width; - moveY = dims.height; - break; - case 'center': - moveX = dims.width / 2; - moveY = dims.height / 2; - break; - } - - return new Effect.Parallel( - [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), - new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), - new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) - ], Object.extend({ - beforeStartInternal: function(effect) { - effect.effects[0].element.makePositioned().makeClipping(); - }, - afterFinishInternal: function(effect) { - effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } - }, options) - ); -}; - -Effect.Pulsate = function(element) { - element = $(element); - var options = arguments[1] || { }, - oldOpacity = element.getInlineOpacity(), - transition = options.transition || Effect.Transitions.linear, - reverser = function(pos){ - return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); - }; - - return new Effect.Opacity(element, - Object.extend(Object.extend({ duration: 2.0, from: 0, - afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } - }, options), {transition: reverser})); -}; - -Effect.Fold = function(element) { - element = $(element); - var oldStyle = { - top: element.style.top, - left: element.style.left, - width: element.style.width, - height: element.style.height }; - element.makeClipping(); - return new Effect.Scale(element, 5, Object.extend({ - scaleContent: false, - scaleX: false, - afterFinishInternal: function(effect) { - new Effect.Scale(element, 1, { - scaleContent: false, - scaleY: false, - afterFinishInternal: function(effect) { - effect.element.hide().undoClipping().setStyle(oldStyle); - } }); - }}, arguments[1] || { })); -}; - -Effect.Morph = Class.create(Effect.Base, { - initialize: function(element) { - this.element = $(element); - if (!this.element) throw(Effect._elementDoesNotExistError); - var options = Object.extend({ - style: { } - }, arguments[1] || { }); - - if (!Object.isString(options.style)) this.style = $H(options.style); - else { - if (options.style.include(':')) - this.style = options.style.parseStyle(); - else { - this.element.addClassName(options.style); - this.style = $H(this.element.getStyles()); - this.element.removeClassName(options.style); - var css = this.element.getStyles(); - this.style = this.style.reject(function(style) { - return style.value == css[style.key]; - }); - options.afterFinishInternal = function(effect) { - effect.element.addClassName(effect.options.style); - effect.transforms.each(function(transform) { - effect.element.style[transform.style] = ''; - }); - }; - } - } - this.start(options); - }, - - setup: function(){ - function parseColor(color){ - if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; - color = color.parseColor(); - return $R(0,2).map(function(i){ - return parseInt( color.slice(i*2+1,i*2+3), 16 ); - }); - } - this.transforms = this.style.map(function(pair){ - var property = pair[0], value = pair[1], unit = null; - - if (value.parseColor('#zzzzzz') != '#zzzzzz') { - value = value.parseColor(); - unit = 'color'; - } else if (property == 'opacity') { - value = parseFloat(value); - if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) - this.element.setStyle({zoom: 1}); - } else if (Element.CSS_LENGTH.test(value)) { - var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); - value = parseFloat(components[1]); - unit = (components.length == 3) ? components[2] : null; - } - - var originalValue = this.element.getStyle(property); - return { - style: property.camelize(), - originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), - targetValue: unit=='color' ? parseColor(value) : value, - unit: unit - }; - }.bind(this)).reject(function(transform){ - return ( - (transform.originalValue == transform.targetValue) || - ( - transform.unit != 'color' && - (isNaN(transform.originalValue) || isNaN(transform.targetValue)) - ) - ); - }); - }, - update: function(position) { - var style = { }, transform, i = this.transforms.length; - while(i--) - style[(transform = this.transforms[i]).style] = - transform.unit=='color' ? '#'+ - (Math.round(transform.originalValue[0]+ - (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + - (Math.round(transform.originalValue[1]+ - (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + - (Math.round(transform.originalValue[2]+ - (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : - (transform.originalValue + - (transform.targetValue - transform.originalValue) * position).toFixed(3) + - (transform.unit === null ? '' : transform.unit); - this.element.setStyle(style, true); - } -}); - -Effect.Transform = Class.create({ - initialize: function(tracks){ - this.tracks = []; - this.options = arguments[1] || { }; - this.addTracks(tracks); - }, - addTracks: function(tracks){ - tracks.each(function(track){ - track = $H(track); - var data = track.values().first(); - this.tracks.push($H({ - ids: track.keys().first(), - effect: Effect.Morph, - options: { style: data } - })); - }.bind(this)); - return this; - }, - play: function(){ - return new Effect.Parallel( - this.tracks.map(function(track){ - var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); - var elements = [$(ids) || $$(ids)].flatten(); - return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); - }).flatten(), - this.options - ); - } -}); - -Element.CSS_PROPERTIES = $w( - 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + - 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + - 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + - 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + - 'fontSize fontWeight height left letterSpacing lineHeight ' + - 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ - 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + - 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + - 'right textIndent top width wordSpacing zIndex'); - -Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; - -String.__parseStyleElement = document.createElement('div'); -String.prototype.parseStyle = function(){ - var style, styleRules = $H(); - if (Prototype.Browser.WebKit) - style = new Element('div',{style:this}).style; - else { - String.__parseStyleElement.innerHTML = '
    '; - style = String.__parseStyleElement.childNodes[0].style; - } - - Element.CSS_PROPERTIES.each(function(property){ - if (style[property]) styleRules.set(property, style[property]); - }); - - if (Prototype.Browser.IE && this.include('opacity')) - styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); - - return styleRules; -}; - -if (document.defaultView && document.defaultView.getComputedStyle) { - Element.getStyles = function(element) { - var css = document.defaultView.getComputedStyle($(element), null); - return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { - styles[property] = css[property]; - return styles; - }); - }; -} else { - Element.getStyles = function(element) { - element = $(element); - var css = element.currentStyle, styles; - styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { - results[property] = css[property]; - return results; - }); - if (!styles.opacity) styles.opacity = element.getOpacity(); - return styles; - }; -} - -Effect.Methods = { - morph: function(element, style) { - element = $(element); - new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); - return element; - }, - visualEffect: function(element, effect, options) { - element = $(element); - var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); - new Effect[klass](element, options); - return element; - }, - highlight: function(element, options) { - element = $(element); - new Effect.Highlight(element, options); - return element; - } -}; - -$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ - 'pulsate shake puff squish switchOff dropOut').each( - function(effect) { - Effect.Methods[effect] = function(element, options){ - element = $(element); - Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); - return element; - }; - } -); - -$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( - function(f) { Effect.Methods[f] = Element[f]; } -); - -Element.addMethods(Effect.Methods); \ No newline at end of file diff --git a/test/dummy/public/javascripts/prototype.js b/test/dummy/public/javascripts/prototype.js deleted file mode 100644 index 06249a6a..00000000 --- a/test/dummy/public/javascripts/prototype.js +++ /dev/null @@ -1,6001 +0,0 @@ -/* Prototype JavaScript framework, version 1.7_rc2 - * (c) 2005-2010 Sam Stephenson - * - * Prototype is freely distributable under the terms of an MIT-style license. - * For details, see the Prototype web site: http://www.prototypejs.org/ - * - *--------------------------------------------------------------------------*/ - -var Prototype = { - - Version: '1.7_rc2', - - Browser: (function(){ - var ua = navigator.userAgent; - var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; - return { - IE: !!window.attachEvent && !isOpera, - Opera: isOpera, - WebKit: ua.indexOf('AppleWebKit/') > -1, - Gecko: ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1, - MobileSafari: /Apple.*Mobile/.test(ua) - } - })(), - - BrowserFeatures: { - XPath: !!document.evaluate, - - SelectorsAPI: !!document.querySelector, - - ElementExtensions: (function() { - var constructor = window.Element || window.HTMLElement; - return !!(constructor && constructor.prototype); - })(), - SpecificElementExtensions: (function() { - if (typeof window.HTMLDivElement !== 'undefined') - return true; - - var div = document.createElement('div'), - form = document.createElement('form'), - isSupported = false; - - if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) { - isSupported = true; - } - - div = form = null; - - return isSupported; - })() - }, - - ScriptFragment: ']*>([\\S\\s]*?)<\/script>', - JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, - - emptyFunction: function() { }, - - K: function(x) { return x } -}; - -if (Prototype.Browser.MobileSafari) - Prototype.BrowserFeatures.SpecificElementExtensions = false; - - -var Abstract = { }; - - -var Try = { - these: function() { - var returnValue; - - for (var i = 0, length = arguments.length; i < length; i++) { - var lambda = arguments[i]; - try { - returnValue = lambda(); - break; - } catch (e) { } - } - - return returnValue; - } -}; - -/* Based on Alex Arnell's inheritance implementation. */ - -var Class = (function() { - - var IS_DONTENUM_BUGGY = (function(){ - for (var p in { toString: 1 }) { - if (p === 'toString') return false; - } - return true; - })(); - - function subclass() {}; - function create() { - var parent = null, properties = $A(arguments); - if (Object.isFunction(properties[0])) - parent = properties.shift(); - - function klass() { - this.initialize.apply(this, arguments); - } - - Object.extend(klass, Class.Methods); - klass.superclass = parent; - klass.subclasses = []; - - if (parent) { - subclass.prototype = parent.prototype; - klass.prototype = new subclass; - parent.subclasses.push(klass); - } - - for (var i = 0, length = properties.length; i < length; i++) - klass.addMethods(properties[i]); - - if (!klass.prototype.initialize) - klass.prototype.initialize = Prototype.emptyFunction; - - klass.prototype.constructor = klass; - return klass; - } - - function addMethods(source) { - var ancestor = this.superclass && this.superclass.prototype, - properties = Object.keys(source); - - if (IS_DONTENUM_BUGGY) { - if (source.toString != Object.prototype.toString) - properties.push("toString"); - if (source.valueOf != Object.prototype.valueOf) - properties.push("valueOf"); - } - - for (var i = 0, length = properties.length; i < length; i++) { - var property = properties[i], value = source[property]; - if (ancestor && Object.isFunction(value) && - value.argumentNames()[0] == "$super") { - var method = value; - value = (function(m) { - return function() { return ancestor[m].apply(this, arguments); }; - })(property).wrap(method); - - value.valueOf = method.valueOf.bind(method); - value.toString = method.toString.bind(method); - } - this.prototype[property] = value; - } - - return this; - } - - return { - create: create, - Methods: { - addMethods: addMethods - } - }; -})(); -(function() { - - var _toString = Object.prototype.toString, - NULL_TYPE = 'Null', - UNDEFINED_TYPE = 'Undefined', - BOOLEAN_TYPE = 'Boolean', - NUMBER_TYPE = 'Number', - STRING_TYPE = 'String', - OBJECT_TYPE = 'Object', - BOOLEAN_CLASS = '[object Boolean]', - NUMBER_CLASS = '[object Number]', - STRING_CLASS = '[object String]', - ARRAY_CLASS = '[object Array]', - NATIVE_JSON_STRINGIFY_SUPPORT = window.JSON && - typeof JSON.stringify === 'function' && - JSON.stringify(0) === '0' && - typeof JSON.stringify(Prototype.K) === 'undefined'; - - function Type(o) { - switch(o) { - case null: return NULL_TYPE; - case (void 0): return UNDEFINED_TYPE; - } - var type = typeof o; - switch(type) { - case 'boolean': return BOOLEAN_TYPE; - case 'number': return NUMBER_TYPE; - case 'string': return STRING_TYPE; - } - return OBJECT_TYPE; - } - - function extend(destination, source) { - for (var property in source) - destination[property] = source[property]; - return destination; - } - - function inspect(object) { - try { - if (isUndefined(object)) return 'undefined'; - if (object === null) return 'null'; - return object.inspect ? object.inspect() : String(object); - } catch (e) { - if (e instanceof RangeError) return '...'; - throw e; - } - } - - function toJSON(value) { - return Str('', { '': value }, []); - } - - function Str(key, holder, stack) { - var value = holder[key], - type = typeof value; - - if (Type(value) === OBJECT_TYPE && typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - - var _class = _toString.call(value); - - switch (_class) { - case NUMBER_CLASS: - case BOOLEAN_CLASS: - case STRING_CLASS: - value = value.valueOf(); - } - - switch (value) { - case null: return 'null'; - case true: return 'true'; - case false: return 'false'; - } - - type = typeof value; - switch (type) { - case 'string': - return value.inspect(true); - case 'number': - return isFinite(value) ? String(value) : 'null'; - case 'object': - - for (var i = 0, length = stack.length; i < length; i++) { - if (stack[i] === value) { throw new TypeError(); } - } - stack.push(value); - - var partial = []; - if (_class === ARRAY_CLASS) { - for (var i = 0, length = value.length; i < length; i++) { - var str = Str(i, value, stack); - partial.push(typeof str === 'undefined' ? 'null' : str); - } - partial = '[' + partial.join(',') + ']'; - } else { - var keys = Object.keys(value); - for (var i = 0, length = keys.length; i < length; i++) { - var key = keys[i], str = Str(key, value, stack); - if (typeof str !== "undefined") { - partial.push(key.inspect(true)+ ':' + str); - } - } - partial = '{' + partial.join(',') + '}'; - } - stack.pop(); - return partial; - } - } - - function stringify(object) { - return JSON.stringify(object); - } - - function toQueryString(object) { - return $H(object).toQueryString(); - } - - function toHTML(object) { - return object && object.toHTML ? object.toHTML() : String.interpret(object); - } - - function keys(object) { - if (Type(object) !== OBJECT_TYPE) { throw new TypeError(); } - var results = []; - for (var property in object) { - if (object.hasOwnProperty(property)) { - results.push(property); - } - } - return results; - } - - function values(object) { - var results = []; - for (var property in object) - results.push(object[property]); - return results; - } - - function clone(object) { - return extend({ }, object); - } - - function isElement(object) { - return !!(object && object.nodeType == 1); - } - - function isArray(object) { - return _toString.call(object) === ARRAY_CLASS; - } - - var hasNativeIsArray = (typeof Array.isArray == 'function') - && Array.isArray([]) && !Array.isArray({}); - - if (hasNativeIsArray) { - isArray = Array.isArray; - } - - function isHash(object) { - return object instanceof Hash; - } - - function isFunction(object) { - return typeof object === "function"; - } - - function isString(object) { - return _toString.call(object) === STRING_CLASS; - } - - function isNumber(object) { - return _toString.call(object) === NUMBER_CLASS; - } - - function isUndefined(object) { - return typeof object === "undefined"; - } - - extend(Object, { - extend: extend, - inspect: inspect, - toJSON: NATIVE_JSON_STRINGIFY_SUPPORT ? stringify : toJSON, - toQueryString: toQueryString, - toHTML: toHTML, - keys: Object.keys || keys, - values: values, - clone: clone, - isElement: isElement, - isArray: isArray, - isHash: isHash, - isFunction: isFunction, - isString: isString, - isNumber: isNumber, - isUndefined: isUndefined - }); -})(); -Object.extend(Function.prototype, (function() { - var slice = Array.prototype.slice; - - function update(array, args) { - var arrayLength = array.length, length = args.length; - while (length--) array[arrayLength + length] = args[length]; - return array; - } - - function merge(array, args) { - array = slice.call(array, 0); - return update(array, args); - } - - function argumentNames() { - var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] - .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') - .replace(/\s+/g, '').split(','); - return names.length == 1 && !names[0] ? [] : names; - } - - function bind(context) { - if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; - var __method = this, args = slice.call(arguments, 1); - return function() { - var a = merge(args, arguments); - return __method.apply(context, a); - } - } - - function bindAsEventListener(context) { - var __method = this, args = slice.call(arguments, 1); - return function(event) { - var a = update([event || window.event], args); - return __method.apply(context, a); - } - } - - function curry() { - if (!arguments.length) return this; - var __method = this, args = slice.call(arguments, 0); - return function() { - var a = merge(args, arguments); - return __method.apply(this, a); - } - } - - function delay(timeout) { - var __method = this, args = slice.call(arguments, 1); - timeout = timeout * 1000; - return window.setTimeout(function() { - return __method.apply(__method, args); - }, timeout); - } - - function defer() { - var args = update([0.01], arguments); - return this.delay.apply(this, args); - } - - function wrap(wrapper) { - var __method = this; - return function() { - var a = update([__method.bind(this)], arguments); - return wrapper.apply(this, a); - } - } - - function methodize() { - if (this._methodized) return this._methodized; - var __method = this; - return this._methodized = function() { - var a = update([this], arguments); - return __method.apply(null, a); - }; - } - - return { - argumentNames: argumentNames, - bind: bind, - bindAsEventListener: bindAsEventListener, - curry: curry, - delay: delay, - defer: defer, - wrap: wrap, - methodize: methodize - } -})()); - - - -(function(proto) { - - - function toISOString() { - return this.getUTCFullYear() + '-' + - (this.getUTCMonth() + 1).toPaddedString(2) + '-' + - this.getUTCDate().toPaddedString(2) + 'T' + - this.getUTCHours().toPaddedString(2) + ':' + - this.getUTCMinutes().toPaddedString(2) + ':' + - this.getUTCSeconds().toPaddedString(2) + 'Z'; - } - - - function toJSON() { - return this.toISOString(); - } - - if (!proto.toISOString) proto.toISOString = toISOString; - if (!proto.toJSON) proto.toJSON = toJSON; - -})(Date.prototype); - - -RegExp.prototype.match = RegExp.prototype.test; - -RegExp.escape = function(str) { - return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); -}; -var PeriodicalExecuter = Class.create({ - initialize: function(callback, frequency) { - this.callback = callback; - this.frequency = frequency; - this.currentlyExecuting = false; - - this.registerCallback(); - }, - - registerCallback: function() { - this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); - }, - - execute: function() { - this.callback(this); - }, - - stop: function() { - if (!this.timer) return; - clearInterval(this.timer); - this.timer = null; - }, - - onTimerEvent: function() { - if (!this.currentlyExecuting) { - try { - this.currentlyExecuting = true; - this.execute(); - this.currentlyExecuting = false; - } catch(e) { - this.currentlyExecuting = false; - throw e; - } - } - } -}); -Object.extend(String, { - interpret: function(value) { - return value == null ? '' : String(value); - }, - specialChar: { - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '\\': '\\\\' - } -}); - -Object.extend(String.prototype, (function() { - var NATIVE_JSON_PARSE_SUPPORT = window.JSON && - typeof JSON.parse === 'function' && - JSON.parse('{"test": true}').test; - - function prepareReplacement(replacement) { - if (Object.isFunction(replacement)) return replacement; - var template = new Template(replacement); - return function(match) { return template.evaluate(match) }; - } - - function gsub(pattern, replacement) { - var result = '', source = this, match; - replacement = prepareReplacement(replacement); - - if (Object.isString(pattern)) - pattern = RegExp.escape(pattern); - - if (!(pattern.length || pattern.source)) { - replacement = replacement(''); - return replacement + source.split('').join(replacement) + replacement; - } - - while (source.length > 0) { - if (match = source.match(pattern)) { - result += source.slice(0, match.index); - result += String.interpret(replacement(match)); - source = source.slice(match.index + match[0].length); - } else { - result += source, source = ''; - } - } - return result; - } - - function sub(pattern, replacement, count) { - replacement = prepareReplacement(replacement); - count = Object.isUndefined(count) ? 1 : count; - - return this.gsub(pattern, function(match) { - if (--count < 0) return match[0]; - return replacement(match); - }); - } - - function scan(pattern, iterator) { - this.gsub(pattern, iterator); - return String(this); - } - - function truncate(length, truncation) { - length = length || 30; - truncation = Object.isUndefined(truncation) ? '...' : truncation; - return this.length > length ? - this.slice(0, length - truncation.length) + truncation : String(this); - } - - function strip() { - return this.replace(/^\s+/, '').replace(/\s+$/, ''); - } - - function stripTags() { - return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, ''); - } - - function stripScripts() { - return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); - } - - function extractScripts() { - var matchAll = new RegExp(Prototype.ScriptFragment, 'img'), - matchOne = new RegExp(Prototype.ScriptFragment, 'im'); - return (this.match(matchAll) || []).map(function(scriptTag) { - return (scriptTag.match(matchOne) || ['', ''])[1]; - }); - } - - function evalScripts() { - return this.extractScripts().map(function(script) { return eval(script) }); - } - - function escapeHTML() { - return this.replace(/&/g,'&').replace(//g,'>'); - } - - function unescapeHTML() { - return this.stripTags().replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&'); - } - - - function toQueryParams(separator) { - var match = this.strip().match(/([^?#]*)(#.*)?$/); - if (!match) return { }; - - return match[1].split(separator || '&').inject({ }, function(hash, pair) { - if ((pair = pair.split('='))[0]) { - var key = decodeURIComponent(pair.shift()), - value = pair.length > 1 ? pair.join('=') : pair[0]; - - if (value != undefined) value = decodeURIComponent(value); - - if (key in hash) { - if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; - hash[key].push(value); - } - else hash[key] = value; - } - return hash; - }); - } - - function toArray() { - return this.split(''); - } - - function succ() { - return this.slice(0, this.length - 1) + - String.fromCharCode(this.charCodeAt(this.length - 1) + 1); - } - - function times(count) { - return count < 1 ? '' : new Array(count + 1).join(this); - } - - function camelize() { - return this.replace(/-+(.)?/g, function(match, chr) { - return chr ? chr.toUpperCase() : ''; - }); - } - - function capitalize() { - return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); - } - - function underscore() { - return this.replace(/::/g, '/') - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') - .replace(/([a-z\d])([A-Z])/g, '$1_$2') - .replace(/-/g, '_') - .toLowerCase(); - } - - function dasherize() { - return this.replace(/_/g, '-'); - } - - function inspect(useDoubleQuotes) { - var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) { - if (character in String.specialChar) { - return String.specialChar[character]; - } - return '\\u00' + character.charCodeAt().toPaddedString(2, 16); - }); - if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; - return "'" + escapedString.replace(/'/g, '\\\'') + "'"; - } - - function unfilterJSON(filter) { - return this.replace(filter || Prototype.JSONFilter, '$1'); - } - - function isJSON() { - var str = this; - if (str.blank()) return false; - str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); - str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); - str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); - return (/^[\],:{}\s]*$/).test(str); - } - - function evalJSON(sanitize) { - var json = this.unfilterJSON(), - cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; - if (cx.test(json)) { - json = json.replace(cx, function (a) { - return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } - try { - if (!sanitize || json.isJSON()) return eval('(' + json + ')'); - } catch (e) { } - throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); - } - - function parseJSON() { - var json = this.unfilterJSON(); - return JSON.parse(json); - } - - function include(pattern) { - return this.indexOf(pattern) > -1; - } - - function startsWith(pattern) { - return this.lastIndexOf(pattern, 0) === 0; - } - - function endsWith(pattern) { - var d = this.length - pattern.length; - return d >= 0 && this.indexOf(pattern, d) === d; - } - - function empty() { - return this == ''; - } - - function blank() { - return /^\s*$/.test(this); - } - - function interpolate(object, pattern) { - return new Template(this, pattern).evaluate(object); - } - - return { - gsub: gsub, - sub: sub, - scan: scan, - truncate: truncate, - strip: String.prototype.trim || strip, - stripTags: stripTags, - stripScripts: stripScripts, - extractScripts: extractScripts, - evalScripts: evalScripts, - escapeHTML: escapeHTML, - unescapeHTML: unescapeHTML, - toQueryParams: toQueryParams, - parseQuery: toQueryParams, - toArray: toArray, - succ: succ, - times: times, - camelize: camelize, - capitalize: capitalize, - underscore: underscore, - dasherize: dasherize, - inspect: inspect, - unfilterJSON: unfilterJSON, - isJSON: isJSON, - evalJSON: NATIVE_JSON_PARSE_SUPPORT ? parseJSON : evalJSON, - include: include, - startsWith: startsWith, - endsWith: endsWith, - empty: empty, - blank: blank, - interpolate: interpolate - }; -})()); - -var Template = Class.create({ - initialize: function(template, pattern) { - this.template = template.toString(); - this.pattern = pattern || Template.Pattern; - }, - - evaluate: function(object) { - if (object && Object.isFunction(object.toTemplateReplacements)) - object = object.toTemplateReplacements(); - - return this.template.gsub(this.pattern, function(match) { - if (object == null) return (match[1] + ''); - - var before = match[1] || ''; - if (before == '\\') return match[2]; - - var ctx = object, expr = match[3], - pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; - - match = pattern.exec(expr); - if (match == null) return before; - - while (match != null) { - var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : match[1]; - ctx = ctx[comp]; - if (null == ctx || '' == match[3]) break; - expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); - match = pattern.exec(expr); - } - - return before + String.interpret(ctx); - }); - } -}); -Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; - -var $break = { }; - -var Enumerable = (function() { - function each(iterator, context) { - var index = 0; - try { - this._each(function(value) { - iterator.call(context, value, index++); - }); - } catch (e) { - if (e != $break) throw e; - } - return this; - } - - function eachSlice(number, iterator, context) { - var index = -number, slices = [], array = this.toArray(); - if (number < 1) return array; - while ((index += number) < array.length) - slices.push(array.slice(index, index+number)); - return slices.collect(iterator, context); - } - - function all(iterator, context) { - iterator = iterator || Prototype.K; - var result = true; - this.each(function(value, index) { - result = result && !!iterator.call(context, value, index); - if (!result) throw $break; - }); - return result; - } - - function any(iterator, context) { - iterator = iterator || Prototype.K; - var result = false; - this.each(function(value, index) { - if (result = !!iterator.call(context, value, index)) - throw $break; - }); - return result; - } - - function collect(iterator, context) { - iterator = iterator || Prototype.K; - var results = []; - this.each(function(value, index) { - results.push(iterator.call(context, value, index)); - }); - return results; - } - - function detect(iterator, context) { - var result; - this.each(function(value, index) { - if (iterator.call(context, value, index)) { - result = value; - throw $break; - } - }); - return result; - } - - function findAll(iterator, context) { - var results = []; - this.each(function(value, index) { - if (iterator.call(context, value, index)) - results.push(value); - }); - return results; - } - - function grep(filter, iterator, context) { - iterator = iterator || Prototype.K; - var results = []; - - if (Object.isString(filter)) - filter = new RegExp(RegExp.escape(filter)); - - this.each(function(value, index) { - if (filter.match(value)) - results.push(iterator.call(context, value, index)); - }); - return results; - } - - function include(object) { - if (Object.isFunction(this.indexOf)) - if (this.indexOf(object) != -1) return true; - - var found = false; - this.each(function(value) { - if (value == object) { - found = true; - throw $break; - } - }); - return found; - } - - function inGroupsOf(number, fillWith) { - fillWith = Object.isUndefined(fillWith) ? null : fillWith; - return this.eachSlice(number, function(slice) { - while(slice.length < number) slice.push(fillWith); - return slice; - }); - } - - function inject(memo, iterator, context) { - this.each(function(value, index) { - memo = iterator.call(context, memo, value, index); - }); - return memo; - } - - function invoke(method) { - var args = $A(arguments).slice(1); - return this.map(function(value) { - return value[method].apply(value, args); - }); - } - - function max(iterator, context) { - iterator = iterator || Prototype.K; - var result; - this.each(function(value, index) { - value = iterator.call(context, value, index); - if (result == null || value >= result) - result = value; - }); - return result; - } - - function min(iterator, context) { - iterator = iterator || Prototype.K; - var result; - this.each(function(value, index) { - value = iterator.call(context, value, index); - if (result == null || value < result) - result = value; - }); - return result; - } - - function partition(iterator, context) { - iterator = iterator || Prototype.K; - var trues = [], falses = []; - this.each(function(value, index) { - (iterator.call(context, value, index) ? - trues : falses).push(value); - }); - return [trues, falses]; - } - - function pluck(property) { - var results = []; - this.each(function(value) { - results.push(value[property]); - }); - return results; - } - - function reject(iterator, context) { - var results = []; - this.each(function(value, index) { - if (!iterator.call(context, value, index)) - results.push(value); - }); - return results; - } - - function sortBy(iterator, context) { - return this.map(function(value, index) { - return { - value: value, - criteria: iterator.call(context, value, index) - }; - }).sort(function(left, right) { - var a = left.criteria, b = right.criteria; - return a < b ? -1 : a > b ? 1 : 0; - }).pluck('value'); - } - - function toArray() { - return this.map(); - } - - function zip() { - var iterator = Prototype.K, args = $A(arguments); - if (Object.isFunction(args.last())) - iterator = args.pop(); - - var collections = [this].concat(args).map($A); - return this.map(function(value, index) { - return iterator(collections.pluck(index)); - }); - } - - function size() { - return this.toArray().length; - } - - function inspect() { - return '#'; - } - - - - - - - - - - return { - each: each, - eachSlice: eachSlice, - all: all, - every: all, - any: any, - some: any, - collect: collect, - map: collect, - detect: detect, - findAll: findAll, - select: findAll, - filter: findAll, - grep: grep, - include: include, - member: include, - inGroupsOf: inGroupsOf, - inject: inject, - invoke: invoke, - max: max, - min: min, - partition: partition, - pluck: pluck, - reject: reject, - sortBy: sortBy, - toArray: toArray, - entries: toArray, - zip: zip, - size: size, - inspect: inspect, - find: detect - }; -})(); - -function $A(iterable) { - if (!iterable) return []; - if ('toArray' in Object(iterable)) return iterable.toArray(); - var length = iterable.length || 0, results = new Array(length); - while (length--) results[length] = iterable[length]; - return results; -} - - -function $w(string) { - if (!Object.isString(string)) return []; - string = string.strip(); - return string ? string.split(/\s+/) : []; -} - -Array.from = $A; - - -(function() { - var arrayProto = Array.prototype, - slice = arrayProto.slice, - _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available - - function each(iterator) { - for (var i = 0, length = this.length; i < length; i++) - iterator(this[i]); - } - if (!_each) _each = each; - - function clear() { - this.length = 0; - return this; - } - - function first() { - return this[0]; - } - - function last() { - return this[this.length - 1]; - } - - function compact() { - return this.select(function(value) { - return value != null; - }); - } - - function flatten() { - return this.inject([], function(array, value) { - if (Object.isArray(value)) - return array.concat(value.flatten()); - array.push(value); - return array; - }); - } - - function without() { - var values = slice.call(arguments, 0); - return this.select(function(value) { - return !values.include(value); - }); - } - - function reverse(inline) { - return (inline === false ? this.toArray() : this)._reverse(); - } - - function uniq(sorted) { - return this.inject([], function(array, value, index) { - if (0 == index || (sorted ? array.last() != value : !array.include(value))) - array.push(value); - return array; - }); - } - - function intersect(array) { - return this.uniq().findAll(function(item) { - return array.detect(function(value) { return item === value }); - }); - } - - - function clone() { - return slice.call(this, 0); - } - - function size() { - return this.length; - } - - function inspect() { - return '[' + this.map(Object.inspect).join(', ') + ']'; - } - - function indexOf(item, i) { - i || (i = 0); - var length = this.length; - if (i < 0) i = length + i; - for (; i < length; i++) - if (this[i] === item) return i; - return -1; - } - - function lastIndexOf(item, i) { - i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; - var n = this.slice(0, i).reverse().indexOf(item); - return (n < 0) ? n : i - n - 1; - } - - function concat() { - var array = slice.call(this, 0), item; - for (var i = 0, length = arguments.length; i < length; i++) { - item = arguments[i]; - if (Object.isArray(item) && !('callee' in item)) { - for (var j = 0, arrayLength = item.length; j < arrayLength; j++) - array.push(item[j]); - } else { - array.push(item); - } - } - return array; - } - - Object.extend(arrayProto, Enumerable); - - if (!arrayProto._reverse) - arrayProto._reverse = arrayProto.reverse; - - Object.extend(arrayProto, { - _each: _each, - clear: clear, - first: first, - last: last, - compact: compact, - flatten: flatten, - without: without, - reverse: reverse, - uniq: uniq, - intersect: intersect, - clone: clone, - toArray: clone, - size: size, - inspect: inspect - }); - - var CONCAT_ARGUMENTS_BUGGY = (function() { - return [].concat(arguments)[0][0] !== 1; - })(1,2) - - if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat; - - if (!arrayProto.indexOf) arrayProto.indexOf = indexOf; - if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf; -})(); -function $H(object) { - return new Hash(object); -}; - -var Hash = Class.create(Enumerable, (function() { - function initialize(object) { - this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); - } - - - function _each(iterator) { - for (var key in this._object) { - var value = this._object[key], pair = [key, value]; - pair.key = key; - pair.value = value; - iterator(pair); - } - } - - function set(key, value) { - return this._object[key] = value; - } - - function get(key) { - if (this._object[key] !== Object.prototype[key]) - return this._object[key]; - } - - function unset(key) { - var value = this._object[key]; - delete this._object[key]; - return value; - } - - function toObject() { - return Object.clone(this._object); - } - - - - function keys() { - return this.pluck('key'); - } - - function values() { - return this.pluck('value'); - } - - function index(value) { - var match = this.detect(function(pair) { - return pair.value === value; - }); - return match && match.key; - } - - function merge(object) { - return this.clone().update(object); - } - - function update(object) { - return new Hash(object).inject(this, function(result, pair) { - result.set(pair.key, pair.value); - return result; - }); - } - - function toQueryPair(key, value) { - if (Object.isUndefined(value)) return key; - return key + '=' + encodeURIComponent(String.interpret(value)); - } - - function toQueryString() { - return this.inject([], function(results, pair) { - var key = encodeURIComponent(pair.key), values = pair.value; - - if (values && typeof values == 'object') { - if (Object.isArray(values)) - return results.concat(values.map(toQueryPair.curry(key))); - } else results.push(toQueryPair(key, values)); - return results; - }).join('&'); - } - - function inspect() { - return '#'; - } - - function clone() { - return new Hash(this); - } - - return { - initialize: initialize, - _each: _each, - set: set, - get: get, - unset: unset, - toObject: toObject, - toTemplateReplacements: toObject, - keys: keys, - values: values, - index: index, - merge: merge, - update: update, - toQueryString: toQueryString, - inspect: inspect, - toJSON: toObject, - clone: clone - }; -})()); - -Hash.from = $H; -Object.extend(Number.prototype, (function() { - function toColorPart() { - return this.toPaddedString(2, 16); - } - - function succ() { - return this + 1; - } - - function times(iterator, context) { - $R(0, this, true).each(iterator, context); - return this; - } - - function toPaddedString(length, radix) { - var string = this.toString(radix || 10); - return '0'.times(length - string.length) + string; - } - - function abs() { - return Math.abs(this); - } - - function round() { - return Math.round(this); - } - - function ceil() { - return Math.ceil(this); - } - - function floor() { - return Math.floor(this); - } - - return { - toColorPart: toColorPart, - succ: succ, - times: times, - toPaddedString: toPaddedString, - abs: abs, - round: round, - ceil: ceil, - floor: floor - }; -})()); - -function $R(start, end, exclusive) { - return new ObjectRange(start, end, exclusive); -} - -var ObjectRange = Class.create(Enumerable, (function() { - function initialize(start, end, exclusive) { - this.start = start; - this.end = end; - this.exclusive = exclusive; - } - - function _each(iterator) { - var value = this.start; - while (this.include(value)) { - iterator(value); - value = value.succ(); - } - } - - function include(value) { - if (value < this.start) - return false; - if (this.exclusive) - return value < this.end; - return value <= this.end; - } - - return { - initialize: initialize, - _each: _each, - include: include - }; -})()); - - - -var Ajax = { - getTransport: function() { - return Try.these( - function() {return new XMLHttpRequest()}, - function() {return new ActiveXObject('Msxml2.XMLHTTP')}, - function() {return new ActiveXObject('Microsoft.XMLHTTP')} - ) || false; - }, - - activeRequestCount: 0 -}; - -Ajax.Responders = { - responders: [], - - _each: function(iterator) { - this.responders._each(iterator); - }, - - register: function(responder) { - if (!this.include(responder)) - this.responders.push(responder); - }, - - unregister: function(responder) { - this.responders = this.responders.without(responder); - }, - - dispatch: function(callback, request, transport, json) { - this.each(function(responder) { - if (Object.isFunction(responder[callback])) { - try { - responder[callback].apply(responder, [request, transport, json]); - } catch (e) { } - } - }); - } -}; - -Object.extend(Ajax.Responders, Enumerable); - -Ajax.Responders.register({ - onCreate: function() { Ajax.activeRequestCount++ }, - onComplete: function() { Ajax.activeRequestCount-- } -}); -Ajax.Base = Class.create({ - initialize: function(options) { - this.options = { - method: 'post', - asynchronous: true, - contentType: 'application/x-www-form-urlencoded', - encoding: 'UTF-8', - parameters: '', - evalJSON: true, - evalJS: true - }; - Object.extend(this.options, options || { }); - - this.options.method = this.options.method.toLowerCase(); - - if (Object.isString(this.options.parameters)) - this.options.parameters = this.options.parameters.toQueryParams(); - else if (Object.isHash(this.options.parameters)) - this.options.parameters = this.options.parameters.toObject(); - } -}); -Ajax.Request = Class.create(Ajax.Base, { - _complete: false, - - initialize: function($super, url, options) { - $super(options); - this.transport = Ajax.getTransport(); - this.request(url); - }, - - request: function(url) { - this.url = url; - this.method = this.options.method; - var params = Object.clone(this.options.parameters); - - if (!['get', 'post'].include(this.method)) { - params['_method'] = this.method; - this.method = 'post'; - } - - this.parameters = params; - - if (params = Object.toQueryString(params)) { - if (this.method == 'get') - this.url += (this.url.include('?') ? '&' : '?') + params; - else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) - params += '&_='; - } - - try { - var response = new Ajax.Response(this); - if (this.options.onCreate) this.options.onCreate(response); - Ajax.Responders.dispatch('onCreate', this, response); - - this.transport.open(this.method.toUpperCase(), this.url, - this.options.asynchronous); - - if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); - - this.transport.onreadystatechange = this.onStateChange.bind(this); - this.setRequestHeaders(); - - this.body = this.method == 'post' ? (this.options.postBody || params) : null; - this.transport.send(this.body); - - /* Force Firefox to handle ready state 4 for synchronous requests */ - if (!this.options.asynchronous && this.transport.overrideMimeType) - this.onStateChange(); - - } - catch (e) { - this.dispatchException(e); - } - }, - - onStateChange: function() { - var readyState = this.transport.readyState; - if (readyState > 1 && !((readyState == 4) && this._complete)) - this.respondToReadyState(this.transport.readyState); - }, - - setRequestHeaders: function() { - var headers = { - 'X-Requested-With': 'XMLHttpRequest', - 'X-Prototype-Version': Prototype.Version, - 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' - }; - - if (this.method == 'post') { - headers['Content-type'] = this.options.contentType + - (this.options.encoding ? '; charset=' + this.options.encoding : ''); - - /* Force "Connection: close" for older Mozilla browsers to work - * around a bug where XMLHttpRequest sends an incorrect - * Content-length header. See Mozilla Bugzilla #246651. - */ - if (this.transport.overrideMimeType && - (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) - headers['Connection'] = 'close'; - } - - if (typeof this.options.requestHeaders == 'object') { - var extras = this.options.requestHeaders; - - if (Object.isFunction(extras.push)) - for (var i = 0, length = extras.length; i < length; i += 2) - headers[extras[i]] = extras[i+1]; - else - $H(extras).each(function(pair) { headers[pair.key] = pair.value }); - } - - for (var name in headers) - this.transport.setRequestHeader(name, headers[name]); - }, - - success: function() { - var status = this.getStatus(); - return !status || (status >= 200 && status < 300); - }, - - getStatus: function() { - try { - return this.transport.status || 0; - } catch (e) { return 0 } - }, - - respondToReadyState: function(readyState) { - var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); - - if (state == 'Complete') { - try { - this._complete = true; - (this.options['on' + response.status] - || this.options['on' + (this.success() ? 'Success' : 'Failure')] - || Prototype.emptyFunction)(response, response.headerJSON); - } catch (e) { - this.dispatchException(e); - } - - var contentType = response.getHeader('Content-type'); - if (this.options.evalJS == 'force' - || (this.options.evalJS && this.isSameOrigin() && contentType - && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) - this.evalResponse(); - } - - try { - (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); - Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); - } catch (e) { - this.dispatchException(e); - } - - if (state == 'Complete') { - this.transport.onreadystatechange = Prototype.emptyFunction; - } - }, - - isSameOrigin: function() { - var m = this.url.match(/^\s*https?:\/\/[^\/]*/); - return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ - protocol: location.protocol, - domain: document.domain, - port: location.port ? ':' + location.port : '' - })); - }, - - getHeader: function(name) { - try { - return this.transport.getResponseHeader(name) || null; - } catch (e) { return null; } - }, - - evalResponse: function() { - try { - return eval((this.transport.responseText || '').unfilterJSON()); - } catch (e) { - this.dispatchException(e); - } - }, - - dispatchException: function(exception) { - (this.options.onException || Prototype.emptyFunction)(this, exception); - Ajax.Responders.dispatch('onException', this, exception); - } -}); - -Ajax.Request.Events = - ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; - - - - - - - - -Ajax.Response = Class.create({ - initialize: function(request){ - this.request = request; - var transport = this.transport = request.transport, - readyState = this.readyState = transport.readyState; - - if ((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { - this.status = this.getStatus(); - this.statusText = this.getStatusText(); - this.responseText = String.interpret(transport.responseText); - this.headerJSON = this._getHeaderJSON(); - } - - if (readyState == 4) { - var xml = transport.responseXML; - this.responseXML = Object.isUndefined(xml) ? null : xml; - this.responseJSON = this._getResponseJSON(); - } - }, - - status: 0, - - statusText: '', - - getStatus: Ajax.Request.prototype.getStatus, - - getStatusText: function() { - try { - return this.transport.statusText || ''; - } catch (e) { return '' } - }, - - getHeader: Ajax.Request.prototype.getHeader, - - getAllHeaders: function() { - try { - return this.getAllResponseHeaders(); - } catch (e) { return null } - }, - - getResponseHeader: function(name) { - return this.transport.getResponseHeader(name); - }, - - getAllResponseHeaders: function() { - return this.transport.getAllResponseHeaders(); - }, - - _getHeaderJSON: function() { - var json = this.getHeader('X-JSON'); - if (!json) return null; - json = decodeURIComponent(escape(json)); - try { - return json.evalJSON(this.request.options.sanitizeJSON || - !this.request.isSameOrigin()); - } catch (e) { - this.request.dispatchException(e); - } - }, - - _getResponseJSON: function() { - var options = this.request.options; - if (!options.evalJSON || (options.evalJSON != 'force' && - !(this.getHeader('Content-type') || '').include('application/json')) || - this.responseText.blank()) - return null; - try { - return this.responseText.evalJSON(options.sanitizeJSON || - !this.request.isSameOrigin()); - } catch (e) { - this.request.dispatchException(e); - } - } -}); - -Ajax.Updater = Class.create(Ajax.Request, { - initialize: function($super, container, url, options) { - this.container = { - success: (container.success || container), - failure: (container.failure || (container.success ? null : container)) - }; - - options = Object.clone(options); - var onComplete = options.onComplete; - options.onComplete = (function(response, json) { - this.updateContent(response.responseText); - if (Object.isFunction(onComplete)) onComplete(response, json); - }).bind(this); - - $super(url, options); - }, - - updateContent: function(responseText) { - var receiver = this.container[this.success() ? 'success' : 'failure'], - options = this.options; - - if (!options.evalScripts) responseText = responseText.stripScripts(); - - if (receiver = $(receiver)) { - if (options.insertion) { - if (Object.isString(options.insertion)) { - var insertion = { }; insertion[options.insertion] = responseText; - receiver.insert(insertion); - } - else options.insertion(receiver, responseText); - } - else receiver.update(responseText); - } - } -}); - -Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { - initialize: function($super, container, url, options) { - $super(options); - this.onComplete = this.options.onComplete; - - this.frequency = (this.options.frequency || 2); - this.decay = (this.options.decay || 1); - - this.updater = { }; - this.container = container; - this.url = url; - - this.start(); - }, - - start: function() { - this.options.onComplete = this.updateComplete.bind(this); - this.onTimerEvent(); - }, - - stop: function() { - this.updater.options.onComplete = undefined; - clearTimeout(this.timer); - (this.onComplete || Prototype.emptyFunction).apply(this, arguments); - }, - - updateComplete: function(response) { - if (this.options.decay) { - this.decay = (response.responseText == this.lastText ? - this.decay * this.options.decay : 1); - - this.lastText = response.responseText; - } - this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); - }, - - onTimerEvent: function() { - this.updater = new Ajax.Updater(this.container, this.url, this.options); - } -}); - - -function $(element) { - if (arguments.length > 1) { - for (var i = 0, elements = [], length = arguments.length; i < length; i++) - elements.push($(arguments[i])); - return elements; - } - if (Object.isString(element)) - element = document.getElementById(element); - return Element.extend(element); -} - -if (Prototype.BrowserFeatures.XPath) { - document._getElementsByXPath = function(expression, parentElement) { - var results = []; - var query = document.evaluate(expression, $(parentElement) || document, - null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); - for (var i = 0, length = query.snapshotLength; i < length; i++) - results.push(Element.extend(query.snapshotItem(i))); - return results; - }; -} - -/*--------------------------------------------------------------------------*/ - -if (!Node) var Node = { }; - -if (!Node.ELEMENT_NODE) { - Object.extend(Node, { - ELEMENT_NODE: 1, - ATTRIBUTE_NODE: 2, - TEXT_NODE: 3, - CDATA_SECTION_NODE: 4, - ENTITY_REFERENCE_NODE: 5, - ENTITY_NODE: 6, - PROCESSING_INSTRUCTION_NODE: 7, - COMMENT_NODE: 8, - DOCUMENT_NODE: 9, - DOCUMENT_TYPE_NODE: 10, - DOCUMENT_FRAGMENT_NODE: 11, - NOTATION_NODE: 12 - }); -} - - - -(function(global) { - - var HAS_EXTENDED_CREATE_ELEMENT_SYNTAX = (function(){ - try { - var el = document.createElement(''); - return el.tagName.toLowerCase() === 'input' && el.name === 'x'; - } - catch(err) { - return false; - } - })(); - - var element = global.Element; - - global.Element = function(tagName, attributes) { - attributes = attributes || { }; - tagName = tagName.toLowerCase(); - var cache = Element.cache; - if (HAS_EXTENDED_CREATE_ELEMENT_SYNTAX && attributes.name) { - tagName = '<' + tagName + ' name="' + attributes.name + '">'; - delete attributes.name; - return Element.writeAttribute(document.createElement(tagName), attributes); - } - if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); - return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); - }; - - Object.extend(global.Element, element || { }); - if (element) global.Element.prototype = element.prototype; - -})(this); - -Element.idCounter = 1; -Element.cache = { }; - -function purgeElement(element) { - var uid = element._prototypeUID; - if (uid) { - Element.stopObserving(element); - element._prototypeUID = void 0; - delete Element.Storage[uid]; - } -} - -Element.Methods = { - visible: function(element) { - return $(element).style.display != 'none'; - }, - - toggle: function(element) { - element = $(element); - Element[Element.visible(element) ? 'hide' : 'show'](element); - return element; - }, - - hide: function(element) { - element = $(element); - element.style.display = 'none'; - return element; - }, - - show: function(element) { - element = $(element); - element.style.display = ''; - return element; - }, - - remove: function(element) { - element = $(element); - element.parentNode.removeChild(element); - return element; - }, - - update: (function(){ - - var SELECT_ELEMENT_INNERHTML_BUGGY = (function(){ - var el = document.createElement("select"), - isBuggy = true; - el.innerHTML = ""; - if (el.options && el.options[0]) { - isBuggy = el.options[0].nodeName.toUpperCase() !== "OPTION"; - } - el = null; - return isBuggy; - })(); - - var TABLE_ELEMENT_INNERHTML_BUGGY = (function(){ - try { - var el = document.createElement("table"); - if (el && el.tBodies) { - el.innerHTML = "test"; - var isBuggy = typeof el.tBodies[0] == "undefined"; - el = null; - return isBuggy; - } - } catch (e) { - return true; - } - })(); - - var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () { - var s = document.createElement("script"), - isBuggy = false; - try { - s.appendChild(document.createTextNode("")); - isBuggy = !s.firstChild || - s.firstChild && s.firstChild.nodeType !== 3; - } catch (e) { - isBuggy = true; - } - s = null; - return isBuggy; - })(); - - function update(element, content) { - element = $(element); - - var descendants = element.getElementsByTagName('*'), - i = descendants.length; - while (i--) purgeElement(descendants[i]); - - if (content && content.toElement) - content = content.toElement(); - - if (Object.isElement(content)) - return element.update().insert(content); - - content = Object.toHTML(content); - - var tagName = element.tagName.toUpperCase(); - - if (tagName === 'SCRIPT' && SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING) { - element.text = content; - return element; - } - - if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) { - if (tagName in Element._insertionTranslations.tags) { - while (element.firstChild) { - element.removeChild(element.firstChild); - } - Element._getContentFromAnonymousElement(tagName, content.stripScripts()) - .each(function(node) { - element.appendChild(node) - }); - } - else { - element.innerHTML = content.stripScripts(); - } - } - else { - element.innerHTML = content.stripScripts(); - } - - content.evalScripts.bind(content).defer(); - return element; - } - - return update; - })(), - - replace: function(element, content) { - element = $(element); - if (content && content.toElement) content = content.toElement(); - else if (!Object.isElement(content)) { - content = Object.toHTML(content); - var range = element.ownerDocument.createRange(); - range.selectNode(element); - content.evalScripts.bind(content).defer(); - content = range.createContextualFragment(content.stripScripts()); - } - element.parentNode.replaceChild(content, element); - return element; - }, - - insert: function(element, insertions) { - element = $(element); - - if (Object.isString(insertions) || Object.isNumber(insertions) || - Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) - insertions = {bottom:insertions}; - - var content, insert, tagName, childNodes; - - for (var position in insertions) { - content = insertions[position]; - position = position.toLowerCase(); - insert = Element._insertionTranslations[position]; - - if (content && content.toElement) content = content.toElement(); - if (Object.isElement(content)) { - insert(element, content); - continue; - } - - content = Object.toHTML(content); - - tagName = ((position == 'before' || position == 'after') - ? element.parentNode : element).tagName.toUpperCase(); - - childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); - - if (position == 'top' || position == 'after') childNodes.reverse(); - childNodes.each(insert.curry(element)); - - content.evalScripts.bind(content).defer(); - } - - return element; - }, - - wrap: function(element, wrapper, attributes) { - element = $(element); - if (Object.isElement(wrapper)) - $(wrapper).writeAttribute(attributes || { }); - else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); - else wrapper = new Element('div', wrapper); - if (element.parentNode) - element.parentNode.replaceChild(wrapper, element); - wrapper.appendChild(element); - return wrapper; - }, - - inspect: function(element) { - element = $(element); - var result = '<' + element.tagName.toLowerCase(); - $H({'id': 'id', 'className': 'class'}).each(function(pair) { - var property = pair.first(), - attribute = pair.last(), - value = (element[property] || '').toString(); - if (value) result += ' ' + attribute + '=' + value.inspect(true); - }); - return result + '>'; - }, - - recursivelyCollect: function(element, property, maximumLength) { - element = $(element); - maximumLength = maximumLength || -1; - var elements = []; - - while (element = element[property]) { - if (element.nodeType == 1) - elements.push(Element.extend(element)); - if (elements.length == maximumLength) - break; - } - - return elements; - }, - - ancestors: function(element) { - return Element.recursivelyCollect(element, 'parentNode'); - }, - - descendants: function(element) { - return Element.select(element, "*"); - }, - - firstDescendant: function(element) { - element = $(element).firstChild; - while (element && element.nodeType != 1) element = element.nextSibling; - return $(element); - }, - - immediateDescendants: function(element) { - var results = [], child = $(element).firstChild; - while (child) { - if (child.nodeType === 1) { - results.push(Element.extend(child)); - } - child = child.nextSibling; - } - return results; - }, - - previousSiblings: function(element, maximumLength) { - return Element.recursivelyCollect(element, 'previousSibling'); - }, - - nextSiblings: function(element) { - return Element.recursivelyCollect(element, 'nextSibling'); - }, - - siblings: function(element) { - element = $(element); - return Element.previousSiblings(element).reverse() - .concat(Element.nextSiblings(element)); - }, - - match: function(element, selector) { - element = $(element); - if (Object.isString(selector)) - return Prototype.Selector.match(element, selector); - return selector.match(element); - }, - - up: function(element, expression, index) { - element = $(element); - if (arguments.length == 1) return $(element.parentNode); - var ancestors = Element.ancestors(element); - return Object.isNumber(expression) ? ancestors[expression] : - Prototype.Selector.find(ancestors, expression, index); - }, - - down: function(element, expression, index) { - element = $(element); - if (arguments.length == 1) return Element.firstDescendant(element); - return Object.isNumber(expression) ? Element.descendants(element)[expression] : - Element.select(element, expression)[index || 0]; - }, - - previous: function(element, expression, index) { - element = $(element); - if (Object.isNumber(expression)) index = expression, expression = false; - if (!Object.isNumber(index)) index = 0; - - if (expression) { - return Prototype.Selector.find(element.previousSiblings(), expression, index); - } else { - return element.recursivelyCollect("previousSibling", index + 1)[index]; - } - }, - - next: function(element, expression, index) { - element = $(element); - if (Object.isNumber(expression)) index = expression, expression = false; - if (!Object.isNumber(index)) index = 0; - - if (expression) { - return Prototype.Selector.find(element.nextSiblings(), expression, index); - } else { - var maximumLength = Object.isNumber(index) ? index + 1 : 1; - return element.recursivelyCollect("nextSibling", index + 1)[index]; - } - }, - - - select: function(element) { - element = $(element); - var expressions = Array.prototype.slice.call(arguments, 1).join(', '); - return Prototype.Selector.select(expressions, element); - }, - - adjacent: function(element) { - element = $(element); - var expressions = Array.prototype.slice.call(arguments, 1).join(', '); - return Prototype.Selector.select(expressions, element.parentNode).without(element); - }, - - identify: function(element) { - element = $(element); - var id = Element.readAttribute(element, 'id'); - if (id) return id; - do { id = 'anonymous_element_' + Element.idCounter++ } while ($(id)); - Element.writeAttribute(element, 'id', id); - return id; - }, - - readAttribute: function(element, name) { - element = $(element); - if (Prototype.Browser.IE) { - var t = Element._attributeTranslations.read; - if (t.values[name]) return t.values[name](element, name); - if (t.names[name]) name = t.names[name]; - if (name.include(':')) { - return (!element.attributes || !element.attributes[name]) ? null : - element.attributes[name].value; - } - } - return element.getAttribute(name); - }, - - writeAttribute: function(element, name, value) { - element = $(element); - var attributes = { }, t = Element._attributeTranslations.write; - - if (typeof name == 'object') attributes = name; - else attributes[name] = Object.isUndefined(value) ? true : value; - - for (var attr in attributes) { - name = t.names[attr] || attr; - value = attributes[attr]; - if (t.values[attr]) name = t.values[attr](element, value); - if (value === false || value === null) - element.removeAttribute(name); - else if (value === true) - element.setAttribute(name, name); - else element.setAttribute(name, value); - } - return element; - }, - - getHeight: function(element) { - return Element.getDimensions(element).height; - }, - - getWidth: function(element) { - return Element.getDimensions(element).width; - }, - - classNames: function(element) { - return new Element.ClassNames(element); - }, - - hasClassName: function(element, className) { - if (!(element = $(element))) return; - var elementClassName = element.className; - return (elementClassName.length > 0 && (elementClassName == className || - new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); - }, - - addClassName: function(element, className) { - if (!(element = $(element))) return; - if (!Element.hasClassName(element, className)) - element.className += (element.className ? ' ' : '') + className; - return element; - }, - - removeClassName: function(element, className) { - if (!(element = $(element))) return; - element.className = element.className.replace( - new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); - return element; - }, - - toggleClassName: function(element, className) { - if (!(element = $(element))) return; - return Element[Element.hasClassName(element, className) ? - 'removeClassName' : 'addClassName'](element, className); - }, - - cleanWhitespace: function(element) { - element = $(element); - var node = element.firstChild; - while (node) { - var nextNode = node.nextSibling; - if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) - element.removeChild(node); - node = nextNode; - } - return element; - }, - - empty: function(element) { - return $(element).innerHTML.blank(); - }, - - descendantOf: function(element, ancestor) { - element = $(element), ancestor = $(ancestor); - - if (element.compareDocumentPosition) - return (element.compareDocumentPosition(ancestor) & 8) === 8; - - if (ancestor.contains) - return ancestor.contains(element) && ancestor !== element; - - while (element = element.parentNode) - if (element == ancestor) return true; - - return false; - }, - - scrollTo: function(element) { - element = $(element); - var pos = Element.cumulativeOffset(element); - window.scrollTo(pos[0], pos[1]); - return element; - }, - - getStyle: function(element, style) { - element = $(element); - style = style == 'float' ? 'cssFloat' : style.camelize(); - var value = element.style[style]; - if (!value || value == 'auto') { - var css = document.defaultView.getComputedStyle(element, null); - value = css ? css[style] : null; - } - if (style == 'opacity') return value ? parseFloat(value) : 1.0; - return value == 'auto' ? null : value; - }, - - getOpacity: function(element) { - return $(element).getStyle('opacity'); - }, - - setStyle: function(element, styles) { - element = $(element); - var elementStyle = element.style, match; - if (Object.isString(styles)) { - element.style.cssText += ';' + styles; - return styles.include('opacity') ? - element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; - } - for (var property in styles) - if (property == 'opacity') element.setOpacity(styles[property]); - else - elementStyle[(property == 'float' || property == 'cssFloat') ? - (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : - property] = styles[property]; - - return element; - }, - - setOpacity: function(element, value) { - element = $(element); - element.style.opacity = (value == 1 || value === '') ? '' : - (value < 0.00001) ? 0 : value; - return element; - }, - - makePositioned: function(element) { - element = $(element); - var pos = Element.getStyle(element, 'position'); - if (pos == 'static' || !pos) { - element._madePositioned = true; - element.style.position = 'relative'; - if (Prototype.Browser.Opera) { - element.style.top = 0; - element.style.left = 0; - } - } - return element; - }, - - undoPositioned: function(element) { - element = $(element); - if (element._madePositioned) { - element._madePositioned = undefined; - element.style.position = - element.style.top = - element.style.left = - element.style.bottom = - element.style.right = ''; - } - return element; - }, - - makeClipping: function(element) { - element = $(element); - if (element._overflow) return element; - element._overflow = Element.getStyle(element, 'overflow') || 'auto'; - if (element._overflow !== 'hidden') - element.style.overflow = 'hidden'; - return element; - }, - - undoClipping: function(element) { - element = $(element); - if (!element._overflow) return element; - element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; - element._overflow = null; - return element; - }, - - cumulativeOffset: function(element) { - var valueT = 0, valueL = 0; - if (element.parentNode) { - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - } while (element); - } - return Element._returnOffset(valueL, valueT); - }, - - positionedOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - if (element) { - if (element.tagName.toUpperCase() == 'BODY') break; - var p = Element.getStyle(element, 'position'); - if (p !== 'static') break; - } - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - absolutize: function(element) { - element = $(element); - if (Element.getStyle(element, 'position') == 'absolute') return element; - - var offsets = Element.positionedOffset(element), - top = offsets[1], - left = offsets[0], - width = element.clientWidth, - height = element.clientHeight; - - element._originalLeft = left - parseFloat(element.style.left || 0); - element._originalTop = top - parseFloat(element.style.top || 0); - element._originalWidth = element.style.width; - element._originalHeight = element.style.height; - - element.style.position = 'absolute'; - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.width = width + 'px'; - element.style.height = height + 'px'; - return element; - }, - - relativize: function(element) { - element = $(element); - if (Element.getStyle(element, 'position') == 'relative') return element; - - element.style.position = 'relative'; - var top = parseFloat(element.style.top || 0) - (element._originalTop || 0), - left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); - - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.height = element._originalHeight; - element.style.width = element._originalWidth; - return element; - }, - - cumulativeScrollOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.scrollTop || 0; - valueL += element.scrollLeft || 0; - element = element.parentNode; - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - getOffsetParent: function(element) { - if (element.offsetParent) return $(element.offsetParent); - if (element == document.body) return $(element); - - while ((element = element.parentNode) && element != document.body) - if (Element.getStyle(element, 'position') != 'static') - return $(element); - - return $(document.body); - }, - - viewportOffset: function(forElement) { - var valueT = 0, - valueL = 0, - element = forElement; - - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - - if (element.offsetParent == document.body && - Element.getStyle(element, 'position') == 'absolute') break; - - } while (element = element.offsetParent); - - element = forElement; - do { - if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { - valueT -= element.scrollTop || 0; - valueL -= element.scrollLeft || 0; - } - } while (element = element.parentNode); - - return Element._returnOffset(valueL, valueT); - }, - - clonePosition: function(element, source) { - var options = Object.extend({ - setLeft: true, - setTop: true, - setWidth: true, - setHeight: true, - offsetTop: 0, - offsetLeft: 0 - }, arguments[2] || { }); - - source = $(source); - var p = Element.viewportOffset(source), delta = [0, 0], parent = null; - - element = $(element); - - if (Element.getStyle(element, 'position') == 'absolute') { - parent = Element.getOffsetParent(element); - delta = Element.viewportOffset(parent); - } - - if (parent == document.body) { - delta[0] -= document.body.offsetLeft; - delta[1] -= document.body.offsetTop; - } - - if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; - if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; - if (options.setWidth) element.style.width = source.offsetWidth + 'px'; - if (options.setHeight) element.style.height = source.offsetHeight + 'px'; - return element; - } -}; - -Object.extend(Element.Methods, { - getElementsBySelector: Element.Methods.select, - - childElements: Element.Methods.immediateDescendants -}); - -Element._attributeTranslations = { - write: { - names: { - className: 'class', - htmlFor: 'for' - }, - values: { } - } -}; - -if (Prototype.Browser.Opera) { - Element.Methods.getStyle = Element.Methods.getStyle.wrap( - function(proceed, element, style) { - switch (style) { - case 'left': case 'top': case 'right': case 'bottom': - if (proceed(element, 'position') === 'static') return null; - case 'height': case 'width': - if (!Element.visible(element)) return null; - - var dim = parseInt(proceed(element, style), 10); - - if (dim !== element['offset' + style.capitalize()]) - return dim + 'px'; - - var properties; - if (style === 'height') { - properties = ['border-top-width', 'padding-top', - 'padding-bottom', 'border-bottom-width']; - } - else { - properties = ['border-left-width', 'padding-left', - 'padding-right', 'border-right-width']; - } - return properties.inject(dim, function(memo, property) { - var val = proceed(element, property); - return val === null ? memo : memo - parseInt(val, 10); - }) + 'px'; - default: return proceed(element, style); - } - } - ); - - Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( - function(proceed, element, attribute) { - if (attribute === 'title') return element.title; - return proceed(element, attribute); - } - ); -} - -else if (Prototype.Browser.IE) { - Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( - function(proceed, element) { - element = $(element); - if (!element.parentNode) return $(document.body); - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - } - ); - - $w('positionedOffset viewportOffset').each(function(method) { - Element.Methods[method] = Element.Methods[method].wrap( - function(proceed, element) { - element = $(element); - if (!element.parentNode) return Element._returnOffset(0, 0); - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - var offsetParent = element.getOffsetParent(); - if (offsetParent && offsetParent.getStyle('position') === 'fixed') - offsetParent.setStyle({ zoom: 1 }); - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - } - ); - }); - - Element.Methods.getStyle = function(element, style) { - element = $(element); - style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); - var value = element.style[style]; - if (!value && element.currentStyle) value = element.currentStyle[style]; - - if (style == 'opacity') { - if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) - if (value[1]) return parseFloat(value[1]) / 100; - return 1.0; - } - - if (value == 'auto') { - if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) - return element['offset' + style.capitalize()] + 'px'; - return null; - } - return value; - }; - - Element.Methods.setOpacity = function(element, value) { - function stripAlpha(filter){ - return filter.replace(/alpha\([^\)]*\)/gi,''); - } - element = $(element); - var currentStyle = element.currentStyle; - if ((currentStyle && !currentStyle.hasLayout) || - (!currentStyle && element.style.zoom == 'normal')) - element.style.zoom = 1; - - var filter = element.getStyle('filter'), style = element.style; - if (value == 1 || value === '') { - (filter = stripAlpha(filter)) ? - style.filter = filter : style.removeAttribute('filter'); - return element; - } else if (value < 0.00001) value = 0; - style.filter = stripAlpha(filter) + - 'alpha(opacity=' + (value * 100) + ')'; - return element; - }; - - Element._attributeTranslations = (function(){ - - var classProp = 'className', - forProp = 'for', - el = document.createElement('div'); - - el.setAttribute(classProp, 'x'); - - if (el.className !== 'x') { - el.setAttribute('class', 'x'); - if (el.className === 'x') { - classProp = 'class'; - } - } - el = null; - - el = document.createElement('label'); - el.setAttribute(forProp, 'x'); - if (el.htmlFor !== 'x') { - el.setAttribute('htmlFor', 'x'); - if (el.htmlFor === 'x') { - forProp = 'htmlFor'; - } - } - el = null; - - return { - read: { - names: { - 'class': classProp, - 'className': classProp, - 'for': forProp, - 'htmlFor': forProp - }, - values: { - _getAttr: function(element, attribute) { - return element.getAttribute(attribute); - }, - _getAttr2: function(element, attribute) { - return element.getAttribute(attribute, 2); - }, - _getAttrNode: function(element, attribute) { - var node = element.getAttributeNode(attribute); - return node ? node.value : ""; - }, - _getEv: (function(){ - - var el = document.createElement('div'), f; - el.onclick = Prototype.emptyFunction; - var value = el.getAttribute('onclick'); - - if (String(value).indexOf('{') > -1) { - f = function(element, attribute) { - attribute = element.getAttribute(attribute); - if (!attribute) return null; - attribute = attribute.toString(); - attribute = attribute.split('{')[1]; - attribute = attribute.split('}')[0]; - return attribute.strip(); - }; - } - else if (value === '') { - f = function(element, attribute) { - attribute = element.getAttribute(attribute); - if (!attribute) return null; - return attribute.strip(); - }; - } - el = null; - return f; - })(), - _flag: function(element, attribute) { - return $(element).hasAttribute(attribute) ? attribute : null; - }, - style: function(element) { - return element.style.cssText.toLowerCase(); - }, - title: function(element) { - return element.title; - } - } - } - } - })(); - - Element._attributeTranslations.write = { - names: Object.extend({ - cellpadding: 'cellPadding', - cellspacing: 'cellSpacing' - }, Element._attributeTranslations.read.names), - values: { - checked: function(element, value) { - element.checked = !!value; - }, - - style: function(element, value) { - element.style.cssText = value ? value : ''; - } - } - }; - - Element._attributeTranslations.has = {}; - - $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + - 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { - Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; - Element._attributeTranslations.has[attr.toLowerCase()] = attr; - }); - - (function(v) { - Object.extend(v, { - href: v._getAttr2, - src: v._getAttr2, - type: v._getAttr, - action: v._getAttrNode, - disabled: v._flag, - checked: v._flag, - readonly: v._flag, - multiple: v._flag, - onload: v._getEv, - onunload: v._getEv, - onclick: v._getEv, - ondblclick: v._getEv, - onmousedown: v._getEv, - onmouseup: v._getEv, - onmouseover: v._getEv, - onmousemove: v._getEv, - onmouseout: v._getEv, - onfocus: v._getEv, - onblur: v._getEv, - onkeypress: v._getEv, - onkeydown: v._getEv, - onkeyup: v._getEv, - onsubmit: v._getEv, - onreset: v._getEv, - onselect: v._getEv, - onchange: v._getEv - }); - })(Element._attributeTranslations.read.values); - - if (Prototype.BrowserFeatures.ElementExtensions) { - (function() { - function _descendants(element) { - var nodes = element.getElementsByTagName('*'), results = []; - for (var i = 0, node; node = nodes[i]; i++) - if (node.tagName !== "!") // Filter out comment nodes. - results.push(node); - return results; - } - - Element.Methods.down = function(element, expression, index) { - element = $(element); - if (arguments.length == 1) return element.firstDescendant(); - return Object.isNumber(expression) ? _descendants(element)[expression] : - Element.select(element, expression)[index || 0]; - } - })(); - } - -} - -else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { - Element.Methods.setOpacity = function(element, value) { - element = $(element); - element.style.opacity = (value == 1) ? 0.999999 : - (value === '') ? '' : (value < 0.00001) ? 0 : value; - return element; - }; -} - -else if (Prototype.Browser.WebKit) { - Element.Methods.setOpacity = function(element, value) { - element = $(element); - element.style.opacity = (value == 1 || value === '') ? '' : - (value < 0.00001) ? 0 : value; - - if (value == 1) - if (element.tagName.toUpperCase() == 'IMG' && element.width) { - element.width++; element.width--; - } else try { - var n = document.createTextNode(' '); - element.appendChild(n); - element.removeChild(n); - } catch (e) { } - - return element; - }; - - Element.Methods.cumulativeOffset = function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - if (element.offsetParent == document.body) - if (Element.getStyle(element, 'position') == 'absolute') break; - - element = element.offsetParent; - } while (element); - - return Element._returnOffset(valueL, valueT); - }; -} - -if ('outerHTML' in document.documentElement) { - Element.Methods.replace = function(element, content) { - element = $(element); - - if (content && content.toElement) content = content.toElement(); - if (Object.isElement(content)) { - element.parentNode.replaceChild(content, element); - return element; - } - - content = Object.toHTML(content); - var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); - - if (Element._insertionTranslations.tags[tagName]) { - var nextSibling = element.next(), - fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); - parent.removeChild(element); - if (nextSibling) - fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); - else - fragments.each(function(node) { parent.appendChild(node) }); - } - else element.outerHTML = content.stripScripts(); - - content.evalScripts.bind(content).defer(); - return element; - }; -} - -Element._returnOffset = function(l, t) { - var result = [l, t]; - result.left = l; - result.top = t; - return result; -}; - -Element._getContentFromAnonymousElement = function(tagName, html) { - var div = new Element('div'), - t = Element._insertionTranslations.tags[tagName]; - if (t) { - div.innerHTML = t[0] + html + t[1]; - for (var i = t[2]; i--; ) { - div = div.firstChild; - } - } - else { - div.innerHTML = html; - } - return $A(div.childNodes); -}; - -Element._insertionTranslations = { - before: function(element, node) { - element.parentNode.insertBefore(node, element); - }, - top: function(element, node) { - element.insertBefore(node, element.firstChild); - }, - bottom: function(element, node) { - element.appendChild(node); - }, - after: function(element, node) { - element.parentNode.insertBefore(node, element.nextSibling); - }, - tags: { - TABLE: ['', '
    ', 1], - TBODY: ['', '
    ', 2], - TR: ['', '
    ', 3], - TD: ['
    ', '
    ', 4], - SELECT: ['', 1] - } -}; - -(function() { - var tags = Element._insertionTranslations.tags; - Object.extend(tags, { - THEAD: tags.TBODY, - TFOOT: tags.TBODY, - TH: tags.TD - }); -})(); - -Element.Methods.Simulated = { - hasAttribute: function(element, attribute) { - attribute = Element._attributeTranslations.has[attribute] || attribute; - var node = $(element).getAttributeNode(attribute); - return !!(node && node.specified); - } -}; - -Element.Methods.ByTag = { }; - -Object.extend(Element, Element.Methods); - -(function(div) { - - if (!Prototype.BrowserFeatures.ElementExtensions && div['__proto__']) { - window.HTMLElement = { }; - window.HTMLElement.prototype = div['__proto__']; - Prototype.BrowserFeatures.ElementExtensions = true; - } - - div = null; - -})(document.createElement('div')); - -Element.extend = (function() { - - function checkDeficiency(tagName) { - if (typeof window.Element != 'undefined') { - var proto = window.Element.prototype; - if (proto) { - var id = '_' + (Math.random()+'').slice(2), - el = document.createElement(tagName); - proto[id] = 'x'; - var isBuggy = (el[id] !== 'x'); - delete proto[id]; - el = null; - return isBuggy; - } - } - return false; - } - - function extendElementWith(element, methods) { - for (var property in methods) { - var value = methods[property]; - if (Object.isFunction(value) && !(property in element)) - element[property] = value.methodize(); - } - } - - var HTMLOBJECTELEMENT_PROTOTYPE_BUGGY = checkDeficiency('object'); - - if (Prototype.BrowserFeatures.SpecificElementExtensions) { - if (HTMLOBJECTELEMENT_PROTOTYPE_BUGGY) { - return function(element) { - if (element && typeof element._extendedByPrototype == 'undefined') { - var t = element.tagName; - if (t && (/^(?:object|applet|embed)$/i.test(t))) { - extendElementWith(element, Element.Methods); - extendElementWith(element, Element.Methods.Simulated); - extendElementWith(element, Element.Methods.ByTag[t.toUpperCase()]); - } - } - return element; - } - } - return Prototype.K; - } - - var Methods = { }, ByTag = Element.Methods.ByTag; - - var extend = Object.extend(function(element) { - if (!element || typeof element._extendedByPrototype != 'undefined' || - element.nodeType != 1 || element == window) return element; - - var methods = Object.clone(Methods), - tagName = element.tagName.toUpperCase(); - - if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); - - extendElementWith(element, methods); - - element._extendedByPrototype = Prototype.emptyFunction; - return element; - - }, { - refresh: function() { - if (!Prototype.BrowserFeatures.ElementExtensions) { - Object.extend(Methods, Element.Methods); - Object.extend(Methods, Element.Methods.Simulated); - } - } - }); - - extend.refresh(); - return extend; -})(); - -if (document.documentElement.hasAttribute) { - Element.hasAttribute = function(element, attribute) { - return element.hasAttribute(attribute); - }; -} -else { - Element.hasAttribute = Element.Methods.Simulated.hasAttribute; -} - -Element.addMethods = function(methods) { - var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; - - if (!methods) { - Object.extend(Form, Form.Methods); - Object.extend(Form.Element, Form.Element.Methods); - Object.extend(Element.Methods.ByTag, { - "FORM": Object.clone(Form.Methods), - "INPUT": Object.clone(Form.Element.Methods), - "SELECT": Object.clone(Form.Element.Methods), - "TEXTAREA": Object.clone(Form.Element.Methods) - }); - } - - if (arguments.length == 2) { - var tagName = methods; - methods = arguments[1]; - } - - if (!tagName) Object.extend(Element.Methods, methods || { }); - else { - if (Object.isArray(tagName)) tagName.each(extend); - else extend(tagName); - } - - function extend(tagName) { - tagName = tagName.toUpperCase(); - if (!Element.Methods.ByTag[tagName]) - Element.Methods.ByTag[tagName] = { }; - Object.extend(Element.Methods.ByTag[tagName], methods); - } - - function copy(methods, destination, onlyIfAbsent) { - onlyIfAbsent = onlyIfAbsent || false; - for (var property in methods) { - var value = methods[property]; - if (!Object.isFunction(value)) continue; - if (!onlyIfAbsent || !(property in destination)) - destination[property] = value.methodize(); - } - } - - function findDOMClass(tagName) { - var klass; - var trans = { - "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", - "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", - "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", - "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", - "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": - "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": - "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": - "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": - "FrameSet", "IFRAME": "IFrame" - }; - if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; - if (window[klass]) return window[klass]; - klass = 'HTML' + tagName + 'Element'; - if (window[klass]) return window[klass]; - klass = 'HTML' + tagName.capitalize() + 'Element'; - if (window[klass]) return window[klass]; - - var element = document.createElement(tagName), - proto = element['__proto__'] || element.constructor.prototype; - - element = null; - return proto; - } - - var elementPrototype = window.HTMLElement ? HTMLElement.prototype : - Element.prototype; - - if (F.ElementExtensions) { - copy(Element.Methods, elementPrototype); - copy(Element.Methods.Simulated, elementPrototype, true); - } - - if (F.SpecificElementExtensions) { - for (var tag in Element.Methods.ByTag) { - var klass = findDOMClass(tag); - if (Object.isUndefined(klass)) continue; - copy(T[tag], klass.prototype); - } - } - - Object.extend(Element, Element.Methods); - delete Element.ByTag; - - if (Element.extend.refresh) Element.extend.refresh(); - Element.cache = { }; -}; - - -document.viewport = { - - getDimensions: function() { - return { width: this.getWidth(), height: this.getHeight() }; - }, - - getScrollOffsets: function() { - return Element._returnOffset( - window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, - window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); - } -}; - -(function(viewport) { - var B = Prototype.Browser, doc = document, element, property = {}; - - function getRootElement() { - if (B.WebKit && !doc.evaluate) - return document; - - if (B.Opera && window.parseFloat(window.opera.version()) < 9.5) - return document.body; - - return document.documentElement; - } - - function define(D) { - if (!element) element = getRootElement(); - - property[D] = 'client' + D; - - viewport['get' + D] = function() { return element[property[D]] }; - return viewport['get' + D](); - } - - viewport.getWidth = define.curry('Width'); - - viewport.getHeight = define.curry('Height'); -})(document.viewport); - - -Element.Storage = { - UID: 1 -}; - -Element.addMethods({ - getStorage: function(element) { - if (!(element = $(element))) return; - - var uid; - if (element === window) { - uid = 0; - } else { - if (typeof element._prototypeUID === "undefined") - element._prototypeUID = Element.Storage.UID++; - uid = element._prototypeUID; - } - - if (!Element.Storage[uid]) - Element.Storage[uid] = $H(); - - return Element.Storage[uid]; - }, - - store: function(element, key, value) { - if (!(element = $(element))) return; - - if (arguments.length === 2) { - Element.getStorage(element).update(key); - } else { - Element.getStorage(element).set(key, value); - } - - return element; - }, - - retrieve: function(element, key, defaultValue) { - if (!(element = $(element))) return; - var hash = Element.getStorage(element), value = hash.get(key); - - if (Object.isUndefined(value)) { - hash.set(key, defaultValue); - value = defaultValue; - } - - return value; - }, - - clone: function(element, deep) { - if (!(element = $(element))) return; - var clone = element.cloneNode(deep); - clone._prototypeUID = void 0; - if (deep) { - var descendants = Element.select(clone, '*'), - i = descendants.length; - while (i--) { - descendants[i]._prototypeUID = void 0; - } - } - return Element.extend(clone); - }, - - purge: function(element) { - if (!(element = $(element))) return; - purgeElement(element); - - var descendants = element.getElementsByTagName('*'), - i = descendants.length; - - while (i--) purgeElement(descendants[i]); - - return null; - } -}); - -(function() { - - function toDecimal(pctString) { - var match = pctString.match(/^(\d+)%?$/i); - if (!match) return null; - return (Number(match[1]) / 100); - } - - function getPixelValue(value, property) { - if (Object.isElement(value)) { - element = value; - value = element.getStyle(property); - } - if (value === null) { - return null; - } - - if ((/^(?:-)?\d+(\.\d+)?(px)?$/i).test(value)) { - return window.parseFloat(value); - } - - if (/\d/.test(value) && element.runtimeStyle) { - var style = element.style.left, rStyle = element.runtimeStyle.left; - element.runtimeStyle.left = element.currentStyle.left; - element.style.left = value || 0; - value = element.style.pixelLeft; - element.style.left = style; - element.runtimeStyle.left = rStyle; - - return value; - } - - if (value.include('%')) { - var decimal = toDecimal(value); - var whole; - if (property.include('left') || property.include('right') || - property.include('width')) { - whole = $(element.parentNode).measure('width'); - } else if (property.include('top') || property.include('bottom') || - property.include('height')) { - whole = $(element.parentNode).measure('height'); - } - - return whole * decimal; - } - - return 0; - } - - function toCSSPixels(number) { - if (Object.isString(number) && number.endsWith('px')) { - return number; - } - return number + 'px'; - } - - function isDisplayed(element) { - var originalElement = element; - while (element && element.parentNode) { - var display = element.getStyle('display'); - if (display === 'none') { - return false; - } - element = $(element.parentNode); - } - return true; - } - - var hasLayout = Prototype.K; - if ('currentStyle' in document.documentElement) { - hasLayout = function(element) { - if (!element.currentStyle.hasLayout) { - element.style.zoom = 1; - } - return element; - }; - } - - function cssNameFor(key) { - if (key.include('border')) key = key + '-width'; - return key.camelize(); - } - - Element.Layout = Class.create(Hash, { - initialize: function($super, element, preCompute) { - $super(); - this.element = $(element); - - Element.Layout.PROPERTIES.each( function(property) { - this._set(property, null); - }, this); - - if (preCompute) { - this._preComputing = true; - this._begin(); - Element.Layout.PROPERTIES.each( this._compute, this ); - this._end(); - this._preComputing = false; - } - }, - - _set: function(property, value) { - return Hash.prototype.set.call(this, property, value); - }, - - set: function(property, value) { - throw "Properties of Element.Layout are read-only."; - }, - - get: function($super, property) { - var value = $super(property); - return value === null ? this._compute(property) : value; - }, - - _begin: function() { - if (this._prepared) return; - - var element = this.element; - if (isDisplayed(element)) { - this._prepared = true; - return; - } - - var originalStyles = { - position: element.style.position || '', - width: element.style.width || '', - visibility: element.style.visibility || '', - display: element.style.display || '' - }; - - element.store('prototype_original_styles', originalStyles); - - var position = element.getStyle('position'), - width = element.getStyle('width'); - - element.setStyle({ - position: 'absolute', - visibility: 'hidden', - display: 'block' - }); - - var positionedWidth = element.getStyle('width'); - - var newWidth; - if (width && (positionedWidth === width)) { - newWidth = getPixelValue(width); - } else if (width && (position === 'absolute' || position === 'fixed')) { - newWidth = getPixelValue(width); - } else { - var parent = element.parentNode, pLayout = $(parent).getLayout(); - - newWidth = pLayout.get('width') - - this.get('margin-left') - - this.get('border-left') - - this.get('padding-left') - - this.get('padding-right') - - this.get('border-right') - - this.get('margin-right'); - } - - element.setStyle({ width: newWidth + 'px' }); - - this._prepared = true; - }, - - _end: function() { - var element = this.element; - var originalStyles = element.retrieve('prototype_original_styles'); - element.store('prototype_original_styles', null); - element.setStyle(originalStyles); - this._prepared = false; - }, - - _compute: function(property) { - var COMPUTATIONS = Element.Layout.COMPUTATIONS; - if (!(property in COMPUTATIONS)) { - throw "Property not found."; - } - return this._set(property, COMPUTATIONS[property].call(this, this.element)); - }, - - toObject: function() { - var args = $A(arguments); - var keys = (args.length === 0) ? Element.Layout.PROPERTIES : - args.join(' ').split(' '); - var obj = {}; - keys.each( function(key) { - if (!Element.Layout.PROPERTIES.include(key)) return; - var value = this.get(key); - if (value != null) obj[key] = value; - }, this); - return obj; - }, - - toHash: function() { - var obj = this.toObject.apply(this, arguments); - return new Hash(obj); - }, - - toCSS: function() { - var args = $A(arguments); - var keys = (args.length === 0) ? Element.Layout.PROPERTIES : - args.join(' ').split(' '); - var css = {}; - - keys.each( function(key) { - if (!Element.Layout.PROPERTIES.include(key)) return; - if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return; - - var value = this.get(key); - if (value != null) css[cssNameFor(key)] = value + 'px'; - }, this); - return css; - }, - - inspect: function() { - return "#"; - } - }); - - Object.extend(Element.Layout, { - PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'), - - COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'), - - COMPUTATIONS: { - 'height': function(element) { - if (!this._preComputing) this._begin(); - - var bHeight = this.get('border-box-height'); - if (bHeight <= 0) return 0; - - var bTop = this.get('border-top'), - bBottom = this.get('border-bottom'); - - var pTop = this.get('padding-top'), - pBottom = this.get('padding-bottom'); - - if (!this._preComputing) this._end(); - - return bHeight - bTop - bBottom - pTop - pBottom; - }, - - 'width': function(element) { - if (!this._preComputing) this._begin(); - - var bWidth = this.get('border-box-width'); - if (bWidth <= 0) return 0; - - var bLeft = this.get('border-left'), - bRight = this.get('border-right'); - - var pLeft = this.get('padding-left'), - pRight = this.get('padding-right'); - - if (!this._preComputing) this._end(); - - return bWidth - bLeft - bRight - pLeft - pRight; - }, - - 'padding-box-height': function(element) { - var height = this.get('height'), - pTop = this.get('padding-top'), - pBottom = this.get('padding-bottom'); - - return height + pTop + pBottom; - }, - - 'padding-box-width': function(element) { - var width = this.get('width'), - pLeft = this.get('padding-left'), - pRight = this.get('padding-right'); - - return width + pLeft + pRight; - }, - - 'border-box-height': function(element) { - return element.offsetHeight; - }, - - 'border-box-width': function(element) { - return element.offsetWidth; - }, - - 'margin-box-height': function(element) { - var bHeight = this.get('border-box-height'), - mTop = this.get('margin-top'), - mBottom = this.get('margin-bottom'); - - if (bHeight <= 0) return 0; - - return bHeight + mTop + mBottom; - }, - - 'margin-box-width': function(element) { - var bWidth = this.get('border-box-width'), - mLeft = this.get('margin-left'), - mRight = this.get('margin-right'); - - if (bWidth <= 0) return 0; - - return bWidth + mLeft + mRight; - }, - - 'top': function(element) { - var offset = element.positionedOffset(); - return offset.top; - }, - - 'bottom': function(element) { - var offset = element.positionedOffset(), - parent = element.getOffsetParent(), - pHeight = parent.measure('height'); - - var mHeight = this.get('border-box-height'); - - return pHeight - mHeight - offset.top; - }, - - 'left': function(element) { - var offset = element.positionedOffset(); - return offset.left; - }, - - 'right': function(element) { - var offset = element.positionedOffset(), - parent = element.getOffsetParent(), - pWidth = parent.measure('width'); - - var mWidth = this.get('border-box-width'); - - return pWidth - mWidth - offset.left; - }, - - 'padding-top': function(element) { - return getPixelValue(element, 'paddingTop'); - }, - - 'padding-bottom': function(element) { - return getPixelValue(element, 'paddingBottom'); - }, - - 'padding-left': function(element) { - return getPixelValue(element, 'paddingLeft'); - }, - - 'padding-right': function(element) { - return getPixelValue(element, 'paddingRight'); - }, - - 'border-top': function(element) { - return Object.isNumber(element.clientTop) ? element.clientTop : - getPixelValue(element, 'borderTopWidth'); - }, - - 'border-bottom': function(element) { - return Object.isNumber(element.clientBottom) ? element.clientBottom : - getPixelValue(element, 'borderBottomWidth'); - }, - - 'border-left': function(element) { - return Object.isNumber(element.clientLeft) ? element.clientLeft : - getPixelValue(element, 'borderLeftWidth'); - }, - - 'border-right': function(element) { - return Object.isNumber(element.clientRight) ? element.clientRight : - getPixelValue(element, 'borderRightWidth'); - }, - - 'margin-top': function(element) { - return getPixelValue(element, 'marginTop'); - }, - - 'margin-bottom': function(element) { - return getPixelValue(element, 'marginBottom'); - }, - - 'margin-left': function(element) { - return getPixelValue(element, 'marginLeft'); - }, - - 'margin-right': function(element) { - return getPixelValue(element, 'marginRight'); - } - } - }); - - if ('getBoundingClientRect' in document.documentElement) { - Object.extend(Element.Layout.COMPUTATIONS, { - 'right': function(element) { - var parent = hasLayout(element.getOffsetParent()); - var rect = element.getBoundingClientRect(), - pRect = parent.getBoundingClientRect(); - - return (pRect.right - rect.right).round(); - }, - - 'bottom': function(element) { - var parent = hasLayout(element.getOffsetParent()); - var rect = element.getBoundingClientRect(), - pRect = parent.getBoundingClientRect(); - - return (pRect.bottom - rect.bottom).round(); - } - }); - } - - Element.Offset = Class.create({ - initialize: function(left, top) { - this.left = left.round(); - this.top = top.round(); - - this[0] = this.left; - this[1] = this.top; - }, - - relativeTo: function(offset) { - return new Element.Offset( - this.left - offset.left, - this.top - offset.top - ); - }, - - inspect: function() { - return "#".interpolate(this); - }, - - toString: function() { - return "[#{left}, #{top}]".interpolate(this); - }, - - toArray: function() { - return [this.left, this.top]; - } - }); - - function getLayout(element, preCompute) { - return new Element.Layout(element, preCompute); - } - - function measure(element, property) { - return $(element).getLayout().get(property); - } - - function getDimensions(element) { - var layout = $(element).getLayout(); - return { - width: layout.get('width'), - height: layout.get('height') - }; - } - - function getOffsetParent(element) { - if (isDetached(element)) return $(document.body); - - var isInline = (Element.getStyle(element, 'display') === 'inline'); - if (!isInline && element.offsetParent) return $(element.offsetParent); - if (element === document.body) return $(element); - - while ((element = element.parentNode) && element !== document.body) { - if (Element.getStyle(element, 'position') !== 'static') { - return (element.nodeName === 'HTML') ? $(document.body) : $(element); - } - } - - return $(document.body); - } - - - function cumulativeOffset(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - } while (element); - return new Element.Offset(valueL, valueT); - } - - function positionedOffset(element) { - var layout = element.getLayout(); - - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - if (element) { - if (isBody(element)) break; - var p = Element.getStyle(element, 'position'); - if (p !== 'static') break; - } - } while (element); - - valueL -= layout.get('margin-top'); - valueT -= layout.get('margin-left'); - - return new Element.Offset(valueL, valueT); - } - - function cumulativeScrollOffset(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.scrollTop || 0; - valueL += element.scrollLeft || 0; - element = element.parentNode; - } while (element); - return new Element.Offset(valueL, valueT); - } - - function viewportOffset(forElement) { - var valueT = 0, valueL = 0, docBody = document.body; - - var element = forElement; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - if (element.offsetParent == docBody && - Element.getStyle(element, 'position') == 'absolute') break; - } while (element = element.offsetParent); - - element = forElement; - do { - if (element != docBody) { - valueT -= element.scrollTop || 0; - valueL -= element.scrollLeft || 0; - } - } while (element = element.parentNode); - return new Element.Offset(valueL, valueT); - } - - function absolutize(element) { - element = $(element); - - if (Element.getStyle(element, 'position') === 'absolute') { - return element; - } - - var offsetParent = getOffsetParent(element); - var eOffset = element.viewportOffset(), - pOffset = offsetParent.viewportOffset(); - - var offset = eOffset.relativeTo(pOffset); - var layout = element.getLayout(); - - element.store('prototype_absolutize_original_styles', { - left: element.getStyle('left'), - top: element.getStyle('top'), - width: element.getStyle('width'), - height: element.getStyle('height') - }); - - element.setStyle({ - position: 'absolute', - top: offset.top + 'px', - left: offset.left + 'px', - width: layout.get('width') + 'px', - height: layout.get('height') + 'px' - }); - - return element; - } - - function relativize(element) { - element = $(element); - if (Element.getStyle(element, 'position') === 'relative') { - return element; - } - - var originalStyles = - element.retrieve('prototype_absolutize_original_styles'); - - if (originalStyles) element.setStyle(originalStyles); - return element; - } - - Element.addMethods({ - getLayout: getLayout, - measure: measure, - getDimensions: getDimensions, - getOffsetParent: getOffsetParent, - cumulativeOffset: cumulativeOffset, - positionedOffset: positionedOffset, - cumulativeScrollOffset: cumulativeScrollOffset, - viewportOffset: viewportOffset, - absolutize: absolutize, - relativize: relativize - }); - - function isBody(element) { - return element.nodeName.toUpperCase() === 'BODY'; - } - - function isDetached(element) { - return element !== document.body && - !Element.descendantOf(element, document.body); - } - - if ('getBoundingClientRect' in document.documentElement) { - Element.addMethods({ - viewportOffset: function(element) { - element = $(element); - if (isDetached(element)) return new Element.Offset(0, 0); - - var rect = element.getBoundingClientRect(), - docEl = document.documentElement; - return new Element.Offset(rect.left - docEl.clientLeft, - rect.top - docEl.clientTop); - }, - - positionedOffset: function(element) { - element = $(element); - var parent = element.getOffsetParent(); - if (isDetached(element)) return new Element.Offset(0, 0); - - if (element.offsetParent && - element.offsetParent.nodeName.toUpperCase() === 'HTML') { - return positionedOffset(element); - } - - var eOffset = element.viewportOffset(), - pOffset = isBody(parent) ? viewportOffset(parent) : - parent.viewportOffset(); - var retOffset = eOffset.relativeTo(pOffset); - - var layout = element.getLayout(); - var top = retOffset.top - layout.get('margin-top'); - var left = retOffset.left - layout.get('margin-left'); - - return new Element.Offset(left, top); - } - }); - } -})(); -window.$$ = function() { - var expression = $A(arguments).join(', '); - return Prototype.Selector.select(expression, document); -}; - -Prototype.Selector = (function() { - - function select() { - throw new Error('Method "Prototype.Selector.select" must be defined.'); - } - - function match() { - throw new Error('Method "Prototype.Selector.match" must be defined.'); - } - - function find(elements, expression, index) { - index = index || 0; - var match = Prototype.Selector.match, length = elements.length, matchIndex = 0, i; - - for (i = 0; i < length; i++) { - if (match(elements[i], expression) && index == matchIndex++) { - return Element.extend(elements[i]); - } - } - } - - function extendElements(elements) { - for (var i = 0, length = elements.length; i < length; i++) { - Element.extend(elements[i]); - } - return elements; - } - - - var K = Prototype.K; - - return { - select: select, - match: match, - find: find, - extendElements: (Element.extend === K) ? K : extendElements, - extendElement: Element.extend - }; -})(); -Prototype._original_property = window.Sizzle; -/*! - * Sizzle CSS Selector Engine - v1.0 - * Copyright 2009, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * More information: http://sizzlejs.com/ - */ -(function(){ - -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true; - -[0, 0].sort(function(){ - baseHasDuplicate = false; - return 0; -}); - -var Sizzle = function(selector, context, results, seed) { - results = results || []; - var origContext = context = context || document; - - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; - } - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - var parts = [], m, set, checkSet, check, mode, extra, prune = true, contextXML = isXML(context), - soFar = selector; - - while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; - break; - } - } - - if ( parts.length > 1 && origPOS.exec( selector ) ) { - if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); - } else { - set = Expr.relative[ parts[0] ] ? - [ context ] : - Sizzle( parts.shift(), context ); - - while ( parts.length ) { - selector = parts.shift(); - - if ( Expr.relative[ selector ] ) - selector += parts.shift(); - - set = posProcess( selector, set ); - } - } - } else { - if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && - Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { - var ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; - } - - if ( context ) { - var ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; - - if ( parts.length > 0 ) { - checkSet = makeArray(set); - } else { - prune = false; - } - - while ( parts.length ) { - var cur = parts.pop(), pop = cur; - - if ( !Expr.relative[ cur ] ) { - cur = ""; - } else { - pop = parts.pop(); - } - - if ( pop == null ) { - pop = context; - } - - Expr.relative[ cur ]( checkSet, pop, contextXML ); - } - } else { - checkSet = parts = []; - } - } - - if ( !checkSet ) { - checkSet = set; - } - - if ( !checkSet ) { - throw "Syntax error, unrecognized expression: " + (cur || selector); - } - - if ( toString.call(checkSet) === "[object Array]" ) { - if ( !prune ) { - results.push.apply( results, checkSet ); - } else if ( context && context.nodeType === 1 ) { - for ( var i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) { - results.push( set[i] ); - } - } - } else { - for ( var i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && checkSet[i].nodeType === 1 ) { - results.push( set[i] ); - } - } - } - } else { - makeArray( checkSet, results ); - } - - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } - - return results; -}; - -Sizzle.uniqueSort = function(results){ - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort(sortOrder); - - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[i-1] ) { - results.splice(i--, 1); - } - } - } - } - - return results; -}; - -Sizzle.matches = function(expr, set){ - return Sizzle(expr, null, null, set); -}; - -Sizzle.find = function(expr, context, isXML){ - var set, match; - - if ( !expr ) { - return []; - } - - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var type = Expr.order[i], match; - - if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; - match.splice(1,1); - - if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace(/\\/g, ""); - set = Expr.find[ type ]( match, context, isXML ); - if ( set != null ) { - expr = expr.replace( Expr.match[ type ], "" ); - break; - } - } - } - } - - if ( !set ) { - set = context.getElementsByTagName("*"); - } - - return {set: set, expr: expr}; -}; - -Sizzle.filter = function(expr, set, inplace, not){ - var old = expr, result = [], curLoop = set, match, anyFound, - isXMLFilter = set && set[0] && isXML(set[0]); - - while ( expr && set.length ) { - for ( var type in Expr.filter ) { - if ( (match = Expr.match[ type ].exec( expr )) != null ) { - var filter = Expr.filter[ type ], found, item; - anyFound = false; - - if ( curLoop == result ) { - result = []; - } - - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); - - if ( !match ) { - anyFound = found = true; - } else if ( match === true ) { - continue; - } - } - - if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; - - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - } else { - curLoop[i] = false; - } - } else if ( pass ) { - result.push( item ); - anyFound = true; - } - } - } - } - - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } - - expr = expr.replace( Expr.match[ type ], "" ); - - if ( !anyFound ) { - return []; - } - - break; - } - } - } - - if ( expr == old ) { - if ( anyFound == null ) { - throw "Syntax error, unrecognized expression: " + expr; - } else { - break; - } - } - - old = expr; - } - - return curLoop; -}; - -var Expr = Sizzle.selectors = { - order: [ "ID", "NAME", "TAG" ], - match: { - ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/, - POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/ - }, - leftMatch: {}, - attrMap: { - "class": "className", - "for": "htmlFor" - }, - attrHandle: { - href: function(elem){ - return elem.getAttribute("href"); - } - }, - relative: { - "+": function(checkSet, part, isXML){ - var isPartStr = typeof part === "string", - isTag = isPartStr && !/\W/.test(part), - isPartStrNotTag = isPartStr && !isTag; - - if ( isTag && !isXML ) { - part = part.toUpperCase(); - } - - for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { - if ( (elem = checkSet[i]) ) { - while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} - - checkSet[i] = isPartStrNotTag || elem && elem.nodeName === part ? - elem || false : - elem === part; - } - } - - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } - }, - ">": function(checkSet, part, isXML){ - var isPartStr = typeof part === "string"; - - if ( isPartStr && !/\W/.test(part) ) { - part = isXML ? part : part.toUpperCase(); - - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - var parent = elem.parentNode; - checkSet[i] = parent.nodeName === part ? parent : false; - } - } - } else { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - checkSet[i] = isPartStr ? - elem.parentNode : - elem.parentNode === part; - } - } - - if ( isPartStr ) { - Sizzle.filter( part, checkSet, true ); - } - } - }, - "": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck; - - if ( !/\W/.test(part) ) { - var nodeCheck = part = isXML ? part : part.toUpperCase(); - checkFn = dirNodeCheck; - } - - checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); - }, - "~": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck; - - if ( typeof part === "string" && !/\W/.test(part) ) { - var nodeCheck = part = isXML ? part : part.toUpperCase(); - checkFn = dirNodeCheck; - } - - checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); - } - }, - find: { - ID: function(match, context, isXML){ - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - return m ? [m] : []; - } - }, - NAME: function(match, context, isXML){ - if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], results = context.getElementsByName(match[1]); - - for ( var i = 0, l = results.length; i < l; i++ ) { - if ( results[i].getAttribute("name") === match[1] ) { - ret.push( results[i] ); - } - } - - return ret.length === 0 ? null : ret; - } - }, - TAG: function(match, context){ - return context.getElementsByTagName(match[1]); - } - }, - preFilter: { - CLASS: function(match, curLoop, inplace, result, not, isXML){ - match = " " + match[1].replace(/\\/g, "") + " "; - - if ( isXML ) { - return match; - } - - for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { - if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").indexOf(match) >= 0) ) { - if ( !inplace ) - result.push( elem ); - } else if ( inplace ) { - curLoop[i] = false; - } - } - } - - return false; - }, - ID: function(match){ - return match[1].replace(/\\/g, ""); - }, - TAG: function(match, curLoop){ - for ( var i = 0; curLoop[i] === false; i++ ){} - return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase(); - }, - CHILD: function(match){ - if ( match[1] == "nth" ) { - var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( - match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" || - !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - - match[2] = (test[1] + (test[2] || 1)) - 0; - match[3] = test[3] - 0; - } - - match[0] = done++; - - return match; - }, - ATTR: function(match, curLoop, inplace, result, not, isXML){ - var name = match[1].replace(/\\/g, ""); - - if ( !isXML && Expr.attrMap[name] ) { - match[1] = Expr.attrMap[name]; - } - - if ( match[2] === "~=" ) { - match[4] = " " + match[4] + " "; - } - - return match; - }, - PSEUDO: function(match, curLoop, inplace, result, not){ - if ( match[1] === "not" ) { - if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { - match[3] = Sizzle(match[3], null, null, curLoop); - } else { - var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); - if ( !inplace ) { - result.push.apply( result, ret ); - } - return false; - } - } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { - return true; - } - - return match; - }, - POS: function(match){ - match.unshift( true ); - return match; - } - }, - filters: { - enabled: function(elem){ - return elem.disabled === false && elem.type !== "hidden"; - }, - disabled: function(elem){ - return elem.disabled === true; - }, - checked: function(elem){ - return elem.checked === true; - }, - selected: function(elem){ - elem.parentNode.selectedIndex; - return elem.selected === true; - }, - parent: function(elem){ - return !!elem.firstChild; - }, - empty: function(elem){ - return !elem.firstChild; - }, - has: function(elem, i, match){ - return !!Sizzle( match[3], elem ).length; - }, - header: function(elem){ - return /h\d/i.test( elem.nodeName ); - }, - text: function(elem){ - return "text" === elem.type; - }, - radio: function(elem){ - return "radio" === elem.type; - }, - checkbox: function(elem){ - return "checkbox" === elem.type; - }, - file: function(elem){ - return "file" === elem.type; - }, - password: function(elem){ - return "password" === elem.type; - }, - submit: function(elem){ - return "submit" === elem.type; - }, - image: function(elem){ - return "image" === elem.type; - }, - reset: function(elem){ - return "reset" === elem.type; - }, - button: function(elem){ - return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON"; - }, - input: function(elem){ - return /input|select|textarea|button/i.test(elem.nodeName); - } - }, - setFilters: { - first: function(elem, i){ - return i === 0; - }, - last: function(elem, i, match, array){ - return i === array.length - 1; - }, - even: function(elem, i){ - return i % 2 === 0; - }, - odd: function(elem, i){ - return i % 2 === 1; - }, - lt: function(elem, i, match){ - return i < match[3] - 0; - }, - gt: function(elem, i, match){ - return i > match[3] - 0; - }, - nth: function(elem, i, match){ - return match[3] - 0 == i; - }, - eq: function(elem, i, match){ - return match[3] - 0 == i; - } - }, - filter: { - PSEUDO: function(elem, match, i, array){ - var name = match[1], filter = Expr.filters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0; - } else if ( name === "not" ) { - var not = match[3]; - - for ( var i = 0, l = not.length; i < l; i++ ) { - if ( not[i] === elem ) { - return false; - } - } - - return true; - } - }, - CHILD: function(elem, match){ - var type = match[1], node = elem; - switch (type) { - case 'only': - case 'first': - while ( (node = node.previousSibling) ) { - if ( node.nodeType === 1 ) return false; - } - if ( type == 'first') return true; - node = elem; - case 'last': - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) return false; - } - return true; - case 'nth': - var first = match[2], last = match[3]; - - if ( first == 1 && last == 0 ) { - return true; - } - - var doneName = match[0], - parent = elem.parentNode; - - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; - for ( node = parent.firstChild; node; node = node.nextSibling ) { - if ( node.nodeType === 1 ) { - node.nodeIndex = ++count; - } - } - parent.sizcache = doneName; - } - - var diff = elem.nodeIndex - last; - if ( first == 0 ) { - return diff == 0; - } else { - return ( diff % first == 0 && diff / first >= 0 ); - } - } - }, - ID: function(elem, match){ - return elem.nodeType === 1 && elem.getAttribute("id") === match; - }, - TAG: function(elem, match){ - return (match === "*" && elem.nodeType === 1) || elem.nodeName === match; - }, - CLASS: function(elem, match){ - return (" " + (elem.className || elem.getAttribute("class")) + " ") - .indexOf( match ) > -1; - }, - ATTR: function(elem, match){ - var name = match[1], - result = Expr.attrHandle[ name ] ? - Expr.attrHandle[ name ]( elem ) : - elem[ name ] != null ? - elem[ name ] : - elem.getAttribute( name ), - value = result + "", - type = match[2], - check = match[4]; - - return result == null ? - type === "!=" : - type === "=" ? - value === check : - type === "*=" ? - value.indexOf(check) >= 0 : - type === "~=" ? - (" " + value + " ").indexOf(check) >= 0 : - !check ? - value && result !== false : - type === "!=" ? - value != check : - type === "^=" ? - value.indexOf(check) === 0 : - type === "$=" ? - value.substr(value.length - check.length) === check : - type === "|=" ? - value === check || value.substr(0, check.length + 1) === check + "-" : - false; - }, - POS: function(elem, match, i, array){ - var name = match[2], filter = Expr.setFilters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } - } - } -}; - -var origPOS = Expr.match.POS; - -for ( var type in Expr.match ) { - Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source ); - Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source ); -} - -var makeArray = function(array, results) { - array = Array.prototype.slice.call( array, 0 ); - - if ( results ) { - results.push.apply( results, array ); - return results; - } - - return array; -}; - -try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 ); - -} catch(e){ - makeArray = function(array, results) { - var ret = results || []; - - if ( toString.call(array) === "[object Array]" ) { - Array.prototype.push.apply( ret, array ); - } else { - if ( typeof array.length === "number" ) { - for ( var i = 0, l = array.length; i < l; i++ ) { - ret.push( array[i] ); - } - } else { - for ( var i = 0; array[i]; i++ ) { - ret.push( array[i] ); - } - } - } - - return ret; - }; -} - -var sortOrder; - -if ( document.documentElement.compareDocumentPosition ) { - sortOrder = function( a, b ) { - if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - if ( a == b ) { - hasDuplicate = true; - } - return 0; - } - - var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} else if ( "sourceIndex" in document.documentElement ) { - sortOrder = function( a, b ) { - if ( !a.sourceIndex || !b.sourceIndex ) { - if ( a == b ) { - hasDuplicate = true; - } - return 0; - } - - var ret = a.sourceIndex - b.sourceIndex; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} else if ( document.createRange ) { - sortOrder = function( a, b ) { - if ( !a.ownerDocument || !b.ownerDocument ) { - if ( a == b ) { - hasDuplicate = true; - } - return 0; - } - - var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); - aRange.setStart(a, 0); - aRange.setEnd(a, 0); - bRange.setStart(b, 0); - bRange.setEnd(b, 0); - var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} - -(function(){ - var form = document.createElement("div"), - id = "script" + (new Date).getTime(); - form.innerHTML = ""; - - var root = document.documentElement; - root.insertBefore( form, root.firstChild ); - - if ( !!document.getElementById( id ) ) { - Expr.find.ID = function(match, context, isXML){ - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; - } - }; - - Expr.filter.ID = function(elem, match){ - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - return elem.nodeType === 1 && node && node.nodeValue === match; - }; - } - - root.removeChild( form ); - root = form = null; // release memory in IE -})(); - -(function(){ - - var div = document.createElement("div"); - div.appendChild( document.createComment("") ); - - if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function(match, context){ - var results = context.getElementsByTagName(match[1]); - - if ( match[1] === "*" ) { - var tmp = []; - - for ( var i = 0; results[i]; i++ ) { - if ( results[i].nodeType === 1 ) { - tmp.push( results[i] ); - } - } - - results = tmp; - } - - return results; - }; - } - - div.innerHTML = ""; - if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && - div.firstChild.getAttribute("href") !== "#" ) { - Expr.attrHandle.href = function(elem){ - return elem.getAttribute("href", 2); - }; - } - - div = null; // release memory in IE -})(); - -if ( document.querySelectorAll ) (function(){ - var oldSizzle = Sizzle, div = document.createElement("div"); - div.innerHTML = "

    "; - - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } - - Sizzle = function(query, context, extra, seed){ - context = context || document; - - if ( !seed && context.nodeType === 9 && !isXML(context) ) { - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(e){} - } - - return oldSizzle(query, context, extra, seed); - }; - - for ( var prop in oldSizzle ) { - Sizzle[ prop ] = oldSizzle[ prop ]; - } - - div = null; // release memory in IE -})(); - -if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) (function(){ - var div = document.createElement("div"); - div.innerHTML = "
    "; - - if ( div.getElementsByClassName("e").length === 0 ) - return; - - div.lastChild.className = "e"; - - if ( div.getElementsByClassName("e").length === 1 ) - return; - - Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function(match, context, isXML) { - if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { - return context.getElementsByClassName(match[1]); - } - }; - - div = null; // release memory in IE -})(); - -function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - var sibDir = dir == "previousSibling" && !isXML; - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - if ( sibDir && elem.nodeType === 1 ){ - elem.sizcache = doneName; - elem.sizset = i; - } - elem = elem[dir]; - var match = false; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; - elem.sizset = i; - } - - if ( elem.nodeName === cur ) { - match = elem; - break; - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - var sibDir = dir == "previousSibling" && !isXML; - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - if ( sibDir && elem.nodeType === 1 ) { - elem.sizcache = doneName; - elem.sizset = i; - } - elem = elem[dir]; - var match = false; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem.sizcache = doneName; - elem.sizset = i; - } - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; - break; - } - - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; - break; - } - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -var contains = document.compareDocumentPosition ? function(a, b){ - return a.compareDocumentPosition(b) & 16; -} : function(a, b){ - return a !== b && (a.contains ? a.contains(b) : true); -}; - -var isXML = function(elem){ - return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || - !!elem.ownerDocument && elem.ownerDocument.documentElement.nodeName !== "HTML"; -}; - -var posProcess = function(selector, context){ - var tmpSet = [], later = "", match, - root = context.nodeType ? [context] : context; - - while ( (match = Expr.match.PSEUDO.exec( selector )) ) { - later += match[0]; - selector = selector.replace( Expr.match.PSEUDO, "" ); - } - - selector = Expr.relative[selector] ? selector + "*" : selector; - - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); - } - - return Sizzle.filter( later, tmpSet ); -}; - - -window.Sizzle = Sizzle; - -})(); - -;(function(engine) { - var extendElements = Prototype.Selector.extendElements; - - function select(selector, scope) { - return extendElements(engine(selector, scope || document)); - } - - function match(element, selector) { - return engine.matches(selector, [element]).length == 1; - } - - Prototype.Selector.engine = engine; - Prototype.Selector.select = select; - Prototype.Selector.match = match; -})(Sizzle); - -window.Sizzle = Prototype._original_property; -delete Prototype._original_property; - -var Form = { - reset: function(form) { - form = $(form); - form.reset(); - return form; - }, - - serializeElements: function(elements, options) { - if (typeof options != 'object') options = { hash: !!options }; - else if (Object.isUndefined(options.hash)) options.hash = true; - var key, value, submitted = false, submit = options.submit; - - var data = elements.inject({ }, function(result, element) { - if (!element.disabled && element.name) { - key = element.name; value = $(element).getValue(); - if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && - submit !== false && (!submit || key == submit) && (submitted = true)))) { - if (key in result) { - if (!Object.isArray(result[key])) result[key] = [result[key]]; - result[key].push(value); - } - else result[key] = value; - } - } - return result; - }); - - return options.hash ? data : Object.toQueryString(data); - } -}; - -Form.Methods = { - serialize: function(form, options) { - return Form.serializeElements(Form.getElements(form), options); - }, - - getElements: function(form) { - var elements = $(form).getElementsByTagName('*'), - element, - arr = [ ], - serializers = Form.Element.Serializers; - for (var i = 0; element = elements[i]; i++) { - arr.push(element); - } - return arr.inject([], function(elements, child) { - if (serializers[child.tagName.toLowerCase()]) - elements.push(Element.extend(child)); - return elements; - }) - }, - - getInputs: function(form, typeName, name) { - form = $(form); - var inputs = form.getElementsByTagName('input'); - - if (!typeName && !name) return $A(inputs).map(Element.extend); - - for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { - var input = inputs[i]; - if ((typeName && input.type != typeName) || (name && input.name != name)) - continue; - matchingInputs.push(Element.extend(input)); - } - - return matchingInputs; - }, - - disable: function(form) { - form = $(form); - Form.getElements(form).invoke('disable'); - return form; - }, - - enable: function(form) { - form = $(form); - Form.getElements(form).invoke('enable'); - return form; - }, - - findFirstElement: function(form) { - var elements = $(form).getElements().findAll(function(element) { - return 'hidden' != element.type && !element.disabled; - }); - var firstByIndex = elements.findAll(function(element) { - return element.hasAttribute('tabIndex') && element.tabIndex >= 0; - }).sortBy(function(element) { return element.tabIndex }).first(); - - return firstByIndex ? firstByIndex : elements.find(function(element) { - return /^(?:input|select|textarea)$/i.test(element.tagName); - }); - }, - - focusFirstElement: function(form) { - form = $(form); - form.findFirstElement().activate(); - return form; - }, - - request: function(form, options) { - form = $(form), options = Object.clone(options || { }); - - var params = options.parameters, action = form.readAttribute('action') || ''; - if (action.blank()) action = window.location.href; - options.parameters = form.serialize(true); - - if (params) { - if (Object.isString(params)) params = params.toQueryParams(); - Object.extend(options.parameters, params); - } - - if (form.hasAttribute('method') && !options.method) - options.method = form.method; - - return new Ajax.Request(action, options); - } -}; - -/*--------------------------------------------------------------------------*/ - - -Form.Element = { - focus: function(element) { - $(element).focus(); - return element; - }, - - select: function(element) { - $(element).select(); - return element; - } -}; - -Form.Element.Methods = { - - serialize: function(element) { - element = $(element); - if (!element.disabled && element.name) { - var value = element.getValue(); - if (value != undefined) { - var pair = { }; - pair[element.name] = value; - return Object.toQueryString(pair); - } - } - return ''; - }, - - getValue: function(element) { - element = $(element); - var method = element.tagName.toLowerCase(); - return Form.Element.Serializers[method](element); - }, - - setValue: function(element, value) { - element = $(element); - var method = element.tagName.toLowerCase(); - Form.Element.Serializers[method](element, value); - return element; - }, - - clear: function(element) { - $(element).value = ''; - return element; - }, - - present: function(element) { - return $(element).value != ''; - }, - - activate: function(element) { - element = $(element); - try { - element.focus(); - if (element.select && (element.tagName.toLowerCase() != 'input' || - !(/^(?:button|reset|submit)$/i.test(element.type)))) - element.select(); - } catch (e) { } - return element; - }, - - disable: function(element) { - element = $(element); - element.disabled = true; - return element; - }, - - enable: function(element) { - element = $(element); - element.disabled = false; - return element; - } -}; - -/*--------------------------------------------------------------------------*/ - -var Field = Form.Element; - -var $F = Form.Element.Methods.getValue; - -/*--------------------------------------------------------------------------*/ - -Form.Element.Serializers = { - input: function(element, value) { - switch (element.type.toLowerCase()) { - case 'checkbox': - case 'radio': - return Form.Element.Serializers.inputSelector(element, value); - default: - return Form.Element.Serializers.textarea(element, value); - } - }, - - inputSelector: function(element, value) { - if (Object.isUndefined(value)) return element.checked ? element.value : null; - else element.checked = !!value; - }, - - textarea: function(element, value) { - if (Object.isUndefined(value)) return element.value; - else element.value = value; - }, - - select: function(element, value) { - if (Object.isUndefined(value)) - return this[element.type == 'select-one' ? - 'selectOne' : 'selectMany'](element); - else { - var opt, currentValue, single = !Object.isArray(value); - for (var i = 0, length = element.length; i < length; i++) { - opt = element.options[i]; - currentValue = this.optionValue(opt); - if (single) { - if (currentValue == value) { - opt.selected = true; - return; - } - } - else opt.selected = value.include(currentValue); - } - } - }, - - selectOne: function(element) { - var index = element.selectedIndex; - return index >= 0 ? this.optionValue(element.options[index]) : null; - }, - - selectMany: function(element) { - var values, length = element.length; - if (!length) return null; - - for (var i = 0, values = []; i < length; i++) { - var opt = element.options[i]; - if (opt.selected) values.push(this.optionValue(opt)); - } - return values; - }, - - optionValue: function(opt) { - return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; - } -}; - -/*--------------------------------------------------------------------------*/ - - -Abstract.TimedObserver = Class.create(PeriodicalExecuter, { - initialize: function($super, element, frequency, callback) { - $super(callback, frequency); - this.element = $(element); - this.lastValue = this.getValue(); - }, - - execute: function() { - var value = this.getValue(); - if (Object.isString(this.lastValue) && Object.isString(value) ? - this.lastValue != value : String(this.lastValue) != String(value)) { - this.callback(this.element, value); - this.lastValue = value; - } - } -}); - -Form.Element.Observer = Class.create(Abstract.TimedObserver, { - getValue: function() { - return Form.Element.getValue(this.element); - } -}); - -Form.Observer = Class.create(Abstract.TimedObserver, { - getValue: function() { - return Form.serialize(this.element); - } -}); - -/*--------------------------------------------------------------------------*/ - -Abstract.EventObserver = Class.create({ - initialize: function(element, callback) { - this.element = $(element); - this.callback = callback; - - this.lastValue = this.getValue(); - if (this.element.tagName.toLowerCase() == 'form') - this.registerFormCallbacks(); - else - this.registerCallback(this.element); - }, - - onElementEvent: function() { - var value = this.getValue(); - if (this.lastValue != value) { - this.callback(this.element, value); - this.lastValue = value; - } - }, - - registerFormCallbacks: function() { - Form.getElements(this.element).each(this.registerCallback, this); - }, - - registerCallback: function(element) { - if (element.type) { - switch (element.type.toLowerCase()) { - case 'checkbox': - case 'radio': - Event.observe(element, 'click', this.onElementEvent.bind(this)); - break; - default: - Event.observe(element, 'change', this.onElementEvent.bind(this)); - break; - } - } - } -}); - -Form.Element.EventObserver = Class.create(Abstract.EventObserver, { - getValue: function() { - return Form.Element.getValue(this.element); - } -}); - -Form.EventObserver = Class.create(Abstract.EventObserver, { - getValue: function() { - return Form.serialize(this.element); - } -}); -(function() { - - var Event = { - KEY_BACKSPACE: 8, - KEY_TAB: 9, - KEY_RETURN: 13, - KEY_ESC: 27, - KEY_LEFT: 37, - KEY_UP: 38, - KEY_RIGHT: 39, - KEY_DOWN: 40, - KEY_DELETE: 46, - KEY_HOME: 36, - KEY_END: 35, - KEY_PAGEUP: 33, - KEY_PAGEDOWN: 34, - KEY_INSERT: 45, - - cache: {} - }; - - var docEl = document.documentElement; - var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl - && 'onmouseleave' in docEl; - - var _isButton; - if (Prototype.Browser.IE) { - var buttonMap = { 0: 1, 1: 4, 2: 2 }; - _isButton = function(event, code) { - return event.button === buttonMap[code]; - }; - } else if (Prototype.Browser.WebKit) { - _isButton = function(event, code) { - switch (code) { - case 0: return event.which == 1 && !event.metaKey; - case 1: return event.which == 1 && event.metaKey; - default: return false; - } - }; - } else { - _isButton = function(event, code) { - return event.which ? (event.which === code + 1) : (event.button === code); - }; - } - - function isLeftClick(event) { return _isButton(event, 0) } - - function isMiddleClick(event) { return _isButton(event, 1) } - - function isRightClick(event) { return _isButton(event, 2) } - - function element(event) { - event = Event.extend(event); - - var node = event.target, type = event.type, - currentTarget = event.currentTarget; - - if (currentTarget && currentTarget.tagName) { - if (type === 'load' || type === 'error' || - (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' - && currentTarget.type === 'radio')) - node = currentTarget; - } - - if (node.nodeType == Node.TEXT_NODE) - node = node.parentNode; - - return Element.extend(node); - } - - function findElement(event, expression) { - var element = Event.element(event); - if (!expression) return element; - while (element) { - if (Object.isElement(element) && Prototype.Selector.match(element, expression)) { - return Element.extend(element); - } - element = element.parentNode; - } - } - - function pointer(event) { - return { x: pointerX(event), y: pointerY(event) }; - } - - function pointerX(event) { - var docElement = document.documentElement, - body = document.body || { scrollLeft: 0 }; - - return event.pageX || (event.clientX + - (docElement.scrollLeft || body.scrollLeft) - - (docElement.clientLeft || 0)); - } - - function pointerY(event) { - var docElement = document.documentElement, - body = document.body || { scrollTop: 0 }; - - return event.pageY || (event.clientY + - (docElement.scrollTop || body.scrollTop) - - (docElement.clientTop || 0)); - } - - - function stop(event) { - Event.extend(event); - event.preventDefault(); - event.stopPropagation(); - - event.stopped = true; - } - - Event.Methods = { - isLeftClick: isLeftClick, - isMiddleClick: isMiddleClick, - isRightClick: isRightClick, - - element: element, - findElement: findElement, - - pointer: pointer, - pointerX: pointerX, - pointerY: pointerY, - - stop: stop - }; - - - var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { - m[name] = Event.Methods[name].methodize(); - return m; - }); - - if (Prototype.Browser.IE) { - function _relatedTarget(event) { - var element; - switch (event.type) { - case 'mouseover': element = event.fromElement; break; - case 'mouseout': element = event.toElement; break; - default: return null; - } - return Element.extend(element); - } - - Object.extend(methods, { - stopPropagation: function() { this.cancelBubble = true }, - preventDefault: function() { this.returnValue = false }, - inspect: function() { return '[object Event]' } - }); - - Event.extend = function(event, element) { - if (!event) return false; - if (event._extendedByPrototype) return event; - - event._extendedByPrototype = Prototype.emptyFunction; - var pointer = Event.pointer(event); - - Object.extend(event, { - target: event.srcElement || element, - relatedTarget: _relatedTarget(event), - pageX: pointer.x, - pageY: pointer.y - }); - - return Object.extend(event, methods); - }; - } else { - Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__; - Object.extend(Event.prototype, methods); - Event.extend = Prototype.K; - } - - function _createResponder(element, eventName, handler) { - var registry = Element.retrieve(element, 'prototype_event_registry'); - - if (Object.isUndefined(registry)) { - CACHE.push(element); - registry = Element.retrieve(element, 'prototype_event_registry', $H()); - } - - var respondersForEvent = registry.get(eventName); - if (Object.isUndefined(respondersForEvent)) { - respondersForEvent = []; - registry.set(eventName, respondersForEvent); - } - - if (respondersForEvent.pluck('handler').include(handler)) return false; - - var responder; - if (eventName.include(":")) { - responder = function(event) { - if (Object.isUndefined(event.eventName)) - return false; - - if (event.eventName !== eventName) - return false; - - Event.extend(event, element); - handler.call(element, event); - }; - } else { - if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED && - (eventName === "mouseenter" || eventName === "mouseleave")) { - if (eventName === "mouseenter" || eventName === "mouseleave") { - responder = function(event) { - Event.extend(event, element); - - var parent = event.relatedTarget; - while (parent && parent !== element) { - try { parent = parent.parentNode; } - catch(e) { parent = element; } - } - - if (parent === element) return; - - handler.call(element, event); - }; - } - } else { - responder = function(event) { - Event.extend(event, element); - handler.call(element, event); - }; - } - } - - responder.handler = handler; - respondersForEvent.push(responder); - return responder; - } - - function _destroyCache() { - for (var i = 0, length = CACHE.length; i < length; i++) { - Event.stopObserving(CACHE[i]); - CACHE[i] = null; - } - } - - var CACHE = []; - - if (Prototype.Browser.IE) - window.attachEvent('onunload', _destroyCache); - - if (Prototype.Browser.WebKit) - window.addEventListener('unload', Prototype.emptyFunction, false); - - - var _getDOMEventName = Prototype.K, - translations = { mouseenter: "mouseover", mouseleave: "mouseout" }; - - if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) { - _getDOMEventName = function(eventName) { - return (translations[eventName] || eventName); - }; - } - - function observe(element, eventName, handler) { - element = $(element); - - var responder = _createResponder(element, eventName, handler); - - if (!responder) return element; - - if (eventName.include(':')) { - if (element.addEventListener) - element.addEventListener("dataavailable", responder, false); - else { - element.attachEvent("ondataavailable", responder); - element.attachEvent("onfilterchange", responder); - } - } else { - var actualEventName = _getDOMEventName(eventName); - - if (element.addEventListener) - element.addEventListener(actualEventName, responder, false); - else - element.attachEvent("on" + actualEventName, responder); - } - - return element; - } - - function stopObserving(element, eventName, handler) { - element = $(element); - - var registry = Element.retrieve(element, 'prototype_event_registry'); - if (!registry) return element; - - if (!eventName) { - registry.each( function(pair) { - var eventName = pair.key; - stopObserving(element, eventName); - }); - return element; - } - - var responders = registry.get(eventName); - if (!responders) return element; - - if (!handler) { - responders.each(function(r) { - stopObserving(element, eventName, r.handler); - }); - return element; - } - - var responder = responders.find( function(r) { return r.handler === handler; }); - if (!responder) return element; - - if (eventName.include(':')) { - if (element.removeEventListener) - element.removeEventListener("dataavailable", responder, false); - else { - element.detachEvent("ondataavailable", responder); - element.detachEvent("onfilterchange", responder); - } - } else { - var actualEventName = _getDOMEventName(eventName); - if (element.removeEventListener) - element.removeEventListener(actualEventName, responder, false); - else - element.detachEvent('on' + actualEventName, responder); - } - - registry.set(eventName, responders.without(responder)); - - return element; - } - - function fire(element, eventName, memo, bubble) { - element = $(element); - - if (Object.isUndefined(bubble)) - bubble = true; - - if (element == document && document.createEvent && !element.dispatchEvent) - element = document.documentElement; - - var event; - if (document.createEvent) { - event = document.createEvent('HTMLEvents'); - event.initEvent('dataavailable', true, true); - } else { - event = document.createEventObject(); - event.eventType = bubble ? 'ondataavailable' : 'onfilterchange'; - } - - event.eventName = eventName; - event.memo = memo || { }; - - if (document.createEvent) - element.dispatchEvent(event); - else - element.fireEvent(event.eventType, event); - - return Event.extend(event); - } - - Event.Handler = Class.create({ - initialize: function(element, eventName, selector, callback) { - this.element = $(element); - this.eventName = eventName; - this.selector = selector; - this.callback = callback; - this.handler = this.handleEvent.bind(this); - }, - - start: function() { - Event.observe(this.element, this.eventName, this.handler); - return this; - }, - - stop: function() { - Event.stopObserving(this.element, this.eventName, this.handler); - return this; - }, - - handleEvent: function(event) { - var element = event.findElement(this.selector); - if (element) this.callback.call(this.element, event, element); - } - }); - - function on(element, eventName, selector, callback) { - element = $(element); - if (Object.isFunction(selector) && Object.isUndefined(callback)) { - callback = selector, selector = null; - } - - return new Event.Handler(element, eventName, selector, callback).start(); - } - - Object.extend(Event, Event.Methods); - - Object.extend(Event, { - fire: fire, - observe: observe, - stopObserving: stopObserving, - on: on - }); - - Element.addMethods({ - fire: fire, - - observe: observe, - - stopObserving: stopObserving, - - on: on - }); - - Object.extend(document, { - fire: fire.methodize(), - - observe: observe.methodize(), - - stopObserving: stopObserving.methodize(), - - on: on.methodize(), - - loaded: false - }); - - if (window.Event) Object.extend(window.Event, Event); - else window.Event = Event; -})(); - -(function() { - /* Support for the DOMContentLoaded event is based on work by Dan Webb, - Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */ - - var timer; - - function fireContentLoadedEvent() { - if (document.loaded) return; - if (timer) window.clearTimeout(timer); - document.loaded = true; - document.fire('dom:loaded'); - } - - function checkReadyState() { - if (document.readyState === 'complete') { - document.stopObserving('readystatechange', checkReadyState); - fireContentLoadedEvent(); - } - } - - function pollDoScroll() { - try { document.documentElement.doScroll('left'); } - catch(e) { - timer = pollDoScroll.defer(); - return; - } - fireContentLoadedEvent(); - } - - if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false); - } else { - document.observe('readystatechange', checkReadyState); - if (window == top) - timer = pollDoScroll.defer(); - } - - Event.observe(window, 'load', fireContentLoadedEvent); -})(); - -Element.addMethods(); - -/*------------------------------- DEPRECATED -------------------------------*/ - -Hash.toQueryString = Object.toQueryString; - -var Toggle = { display: Element.toggle }; - -Element.Methods.childOf = Element.Methods.descendantOf; - -var Insertion = { - Before: function(element, content) { - return Element.insert(element, {before:content}); - }, - - Top: function(element, content) { - return Element.insert(element, {top:content}); - }, - - Bottom: function(element, content) { - return Element.insert(element, {bottom:content}); - }, - - After: function(element, content) { - return Element.insert(element, {after:content}); - } -}; - -var $continue = new Error('"throw $continue" is deprecated, use "return" instead'); - -var Position = { - includeScrollOffsets: false, - - prepare: function() { - this.deltaX = window.pageXOffset - || document.documentElement.scrollLeft - || document.body.scrollLeft - || 0; - this.deltaY = window.pageYOffset - || document.documentElement.scrollTop - || document.body.scrollTop - || 0; - }, - - within: function(element, x, y) { - if (this.includeScrollOffsets) - return this.withinIncludingScrolloffsets(element, x, y); - this.xcomp = x; - this.ycomp = y; - this.offset = Element.cumulativeOffset(element); - - return (y >= this.offset[1] && - y < this.offset[1] + element.offsetHeight && - x >= this.offset[0] && - x < this.offset[0] + element.offsetWidth); - }, - - withinIncludingScrolloffsets: function(element, x, y) { - var offsetcache = Element.cumulativeScrollOffset(element); - - this.xcomp = x + offsetcache[0] - this.deltaX; - this.ycomp = y + offsetcache[1] - this.deltaY; - this.offset = Element.cumulativeOffset(element); - - return (this.ycomp >= this.offset[1] && - this.ycomp < this.offset[1] + element.offsetHeight && - this.xcomp >= this.offset[0] && - this.xcomp < this.offset[0] + element.offsetWidth); - }, - - overlap: function(mode, element) { - if (!mode) return 0; - if (mode == 'vertical') - return ((this.offset[1] + element.offsetHeight) - this.ycomp) / - element.offsetHeight; - if (mode == 'horizontal') - return ((this.offset[0] + element.offsetWidth) - this.xcomp) / - element.offsetWidth; - }, - - - cumulativeOffset: Element.Methods.cumulativeOffset, - - positionedOffset: Element.Methods.positionedOffset, - - absolutize: function(element) { - Position.prepare(); - return Element.absolutize(element); - }, - - relativize: function(element) { - Position.prepare(); - return Element.relativize(element); - }, - - realOffset: Element.Methods.cumulativeScrollOffset, - - offsetParent: Element.Methods.getOffsetParent, - - page: Element.Methods.viewportOffset, - - clone: function(source, target, options) { - options = options || { }; - return Element.clonePosition(target, source, options); - } -}; - -/*--------------------------------------------------------------------------*/ - -if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){ - function iter(name) { - return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]"; - } - - instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ? - function(element, className) { - className = className.toString().strip(); - var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className); - return cond ? document._getElementsByXPath('.//*' + cond, element) : []; - } : function(element, className) { - className = className.toString().strip(); - var elements = [], classNames = (/\s/.test(className) ? $w(className) : null); - if (!classNames && !className) return elements; - - var nodes = $(element).getElementsByTagName('*'); - className = ' ' + className + ' '; - - for (var i = 0, child, cn; child = nodes[i]; i++) { - if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) || - (classNames && classNames.all(function(name) { - return !name.toString().blank() && cn.include(' ' + name + ' '); - })))) - elements.push(Element.extend(child)); - } - return elements; - }; - - return function(className, parentElement) { - return $(parentElement || document.body).getElementsByClassName(className); - }; -}(Element.Methods); - -/*--------------------------------------------------------------------------*/ - -Element.ClassNames = Class.create(); -Element.ClassNames.prototype = { - initialize: function(element) { - this.element = $(element); - }, - - _each: function(iterator) { - this.element.className.split(/\s+/).select(function(name) { - return name.length > 0; - })._each(iterator); - }, - - set: function(className) { - this.element.className = className; - }, - - add: function(classNameToAdd) { - if (this.include(classNameToAdd)) return; - this.set($A(this).concat(classNameToAdd).join(' ')); - }, - - remove: function(classNameToRemove) { - if (!this.include(classNameToRemove)) return; - this.set($A(this).without(classNameToRemove).join(' ')); - }, - - toString: function() { - return $A(this).join(' '); - } -}; - -Object.extend(Element.ClassNames.prototype, Enumerable); - -/*--------------------------------------------------------------------------*/ - -(function() { - window.Selector = Class.create({ - initialize: function(expression) { - this.expression = expression.strip(); - }, - - findElements: function(rootElement) { - return Prototype.Selector.select(this.expression, rootElement); - }, - - match: function(element) { - return Prototype.Selector.match(element, this.expression); - }, - - toString: function() { - return this.expression; - }, - - inspect: function() { - return "#"; - } - }); - - Object.extend(Selector, { - matchElements: function(elements, expression) { - var match = Prototype.Selector.match, - results = []; - - for (var i = 0, length = elements.length; i < length; i++) { - var element = elements[i]; - if (match(element, expression)) { - results.push(Element.extend(element)); - } - } - return results; - }, - - findElement: function(elements, expression, index) { - index = index || 0; - var matchIndex = 0, element; - for (var i = 0, length = elements.length; i < length; i++) { - element = elements[i]; - if (Prototype.Selector.match(element, expression) && index === matchIndex++) { - return Element.extend(element); - } - } - }, - - findChildElements: function(element, expressions) { - var selector = expressions.toArray().join(', '); - return Prototype.Selector.select(selector, element || document); - } - }); -})(); diff --git a/test/dummy/public/javascripts/rails.js b/test/dummy/public/javascripts/rails.js deleted file mode 100644 index aed6aed3..00000000 --- a/test/dummy/public/javascripts/rails.js +++ /dev/null @@ -1,191 +0,0 @@ -(function() { - // Technique from Juriy Zaytsev - // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ - function isEventSupported(eventName) { - var el = document.createElement('div'); - eventName = 'on' + eventName; - var isSupported = (eventName in el); - if (!isSupported) { - el.setAttribute(eventName, 'return;'); - isSupported = typeof el[eventName] == 'function'; - } - el = null; - return isSupported; - } - - function isForm(element) { - return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM' - } - - function isInput(element) { - if (Object.isElement(element)) { - var name = element.nodeName.toUpperCase() - return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA' - } - else return false - } - - var submitBubbles = isEventSupported('submit'), - changeBubbles = isEventSupported('change') - - if (!submitBubbles || !changeBubbles) { - // augment the Event.Handler class to observe custom events when needed - Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap( - function(init, element, eventName, selector, callback) { - init(element, eventName, selector, callback) - // is the handler being attached to an element that doesn't support this event? - if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) || - (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) { - // "submit" => "emulated:submit" - this.eventName = 'emulated:' + this.eventName - } - } - ) - } - - if (!submitBubbles) { - // discover forms on the page by observing focus events which always bubble - document.on('focusin', 'form', function(focusEvent, form) { - // special handler for the real "submit" event (one-time operation) - if (!form.retrieve('emulated:submit')) { - form.on('submit', function(submitEvent) { - var emulated = form.fire('emulated:submit', submitEvent, true) - // if custom event received preventDefault, cancel the real one too - if (emulated.returnValue === false) submitEvent.preventDefault() - }) - form.store('emulated:submit', true) - } - }) - } - - if (!changeBubbles) { - // discover form inputs on the page - document.on('focusin', 'input, select, texarea', function(focusEvent, input) { - // special handler for real "change" events - if (!input.retrieve('emulated:change')) { - input.on('change', function(changeEvent) { - input.fire('emulated:change', changeEvent, true) - }) - input.store('emulated:change', true) - } - }) - } - - function handleRemote(element) { - var method, url, params; - - var event = element.fire("ajax:before"); - if (event.stopped) return false; - - if (element.tagName.toLowerCase() === 'form') { - method = element.readAttribute('method') || 'post'; - url = element.readAttribute('action'); - params = element.serialize(); - } else { - method = element.readAttribute('data-method') || 'get'; - url = element.readAttribute('href'); - params = {}; - } - - new Ajax.Request(url, { - method: method, - parameters: params, - evalScripts: true, - - onComplete: function(request) { element.fire("ajax:complete", request); }, - onSuccess: function(request) { element.fire("ajax:success", request); }, - onFailure: function(request) { element.fire("ajax:failure", request); } - }); - - element.fire("ajax:after"); - } - - function handleMethod(element) { - var method = element.readAttribute('data-method'), - url = element.readAttribute('href'), - csrf_param = $$('meta[name=csrf-param]')[0], - csrf_token = $$('meta[name=csrf-token]')[0]; - - var form = new Element('form', { method: "POST", action: url, style: "display: none;" }); - element.parentNode.insert(form); - - if (method !== 'post') { - var field = new Element('input', { type: 'hidden', name: '_method', value: method }); - form.insert(field); - } - - if (csrf_param) { - var param = csrf_param.readAttribute('content'), - token = csrf_token.readAttribute('content'), - field = new Element('input', { type: 'hidden', name: param, value: token }); - form.insert(field); - } - - form.submit(); - } - - - document.on("click", "*[data-confirm]", function(event, element) { - var message = element.readAttribute('data-confirm'); - if (!confirm(message)) event.stop(); - }); - - document.on("click", "a[data-remote]", function(event, element) { - if (event.stopped) return; - handleRemote(element); - event.stop(); - }); - - document.on("click", "a[data-method]", function(event, element) { - if (event.stopped) return; - handleMethod(element); - event.stop(); - }); - - document.on("submit", function(event) { - var element = event.findElement(), - message = element.readAttribute('data-confirm'); - if (message && !confirm(message)) { - event.stop(); - return false; - } - - var inputs = element.select("input[type=submit][data-disable-with]"); - inputs.each(function(input) { - input.disabled = true; - input.writeAttribute('data-original-value', input.value); - input.value = input.readAttribute('data-disable-with'); - }); - - var element = event.findElement("form[data-remote]"); - if (element) { - handleRemote(element); - event.stop(); - } - }); - - document.on("ajax:after", "form", function(event, element) { - var inputs = element.select("input[type=submit][disabled=true][data-disable-with]"); - inputs.each(function(input) { - input.value = input.readAttribute('data-original-value'); - input.removeAttribute('data-original-value'); - input.disabled = false; - }); - }); - - Ajax.Responders.register({ - onCreate: function(request) { - var csrf_meta_tag = $$('meta[name=csrf-token]')[0]; - - if (csrf_meta_tag) { - var header = 'X-CSRF-Token', - token = csrf_meta_tag.readAttribute('content'); - - if (!request.options.requestHeaders) { - request.options.requestHeaders = {}; - } - request.options.requestHeaders[header] = token; - } - } - }); -})(); diff --git a/test/dummy/public/robots.txt b/test/dummy/public/robots.txt deleted file mode 100644 index 085187fa..00000000 --- a/test/dummy/public/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file -# -# To ban all spiders from the entire site uncomment the next two lines: -# User-Agent: * -# Disallow: / diff --git a/test/dummy/public/stylesheets/.gitkeep b/test/dummy/public/stylesheets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/test/dummy/public/stylesheets/scaffold.css b/test/dummy/public/stylesheets/scaffold.css deleted file mode 100644 index 1ae70002..00000000 --- a/test/dummy/public/stylesheets/scaffold.css +++ /dev/null @@ -1,56 +0,0 @@ -body { background-color: #fff; color: #333; } - -body, p, ol, ul, td { - font-family: verdana, arial, helvetica, sans-serif; - font-size: 13px; - line-height: 18px; -} - -pre { - background-color: #eee; - padding: 10px; - font-size: 11px; -} - -a { color: #000; } -a:visited { color: #666; } -a:hover { color: #fff; background-color:#000; } - -div.field, div.actions { - margin-bottom: 10px; -} - -#notice { - color: green; -} - -.field_with_errors { - padding: 2px; - background-color: red; - display: table; -} - -#error_explanation { - width: 450px; - border: 2px solid red; - padding: 7px; - padding-bottom: 0; - margin-bottom: 20px; - background-color: #f0f0f0; -} - -#error_explanation h2 { - text-align: left; - font-weight: bold; - padding: 5px 5px 5px 15px; - font-size: 12px; - margin: -7px; - margin-bottom: 0px; - background-color: #c00; - color: #fff; -} - -#error_explanation ul li { - font-size: 12px; - list-style: square; -} diff --git a/test/dummy/script/rails b/test/dummy/script/rails deleted file mode 100755 index 3c234a25..00000000 --- a/test/dummy/script/rails +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -APP_PATH = File.expand_path('../config/application', __dir__) -require File.expand_path('../config/boot', __dir__) -require 'rails/commands' diff --git a/test/test_helper.rb b/test/test_helper.rb index 92ae49b8..ca750a23 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -8,7 +8,10 @@ warn 'warning: coveralls gem not found; skipping Coveralls' end -require File.expand_path('dummy/config/environment.rb', __dir__) +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +require 'exception_notification' + +require 'dummy/config/application.rb' require 'rails/test_help' require 'mocha/setup' From 97be0696fbe143069af633cd3495ae26b71d6df1 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 18 Apr 2019 21:47:50 -0300 Subject: [PATCH 109/156] Make webhook notifier test work even if Rails is not defined --- test/exception_notifier/webhook_notifier_test.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/exception_notifier/webhook_notifier_test.rb b/test/exception_notifier/webhook_notifier_test.rb index 26c6903f..6aa0e69b 100644 --- a/test/exception_notifier/webhook_notifier_test.rb +++ b/test/exception_notifier/webhook_notifier_test.rb @@ -68,11 +68,10 @@ def fake_response end def fake_params - { + params = { body: { server: Socket.gethostname, process: $PROCESS_ID, - rails_root: Rails.root, exception: { error_class: 'ZeroDivisionError', message: 'divided by 0'.inspect, @@ -81,6 +80,10 @@ def fake_params data: {} } } + + params[:body][:rails_root] = Rails.root if defined?(::Rails) && Rails.respond_to?(:root) + + params end def fake_exception From 8416dbaafe0a6e57b928fbb473b4b0938feb9c83 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 18 Apr 2019 22:06:07 -0300 Subject: [PATCH 110/156] Make ExceptionNotifierTest work without Rails --- test/exception_notifier_test.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/exception_notifier_test.rb b/test/exception_notifier_test.rb index 55c9f3df..551ad38d 100644 --- a/test/exception_notifier_test.rb +++ b/test/exception_notifier_test.rb @@ -5,6 +5,8 @@ class ExceptionTwo < StandardError; end class ExceptionNotifierTest < ActiveSupport::TestCase setup do + ExceptionNotifier.register_exception_notifier(:email, exception_recipients: %w[dummyexceptions@example.com]) + @notifier_calls = 0 @test_notifier = ->(_exception, _options) { @notifier_calls += 1 } end @@ -13,7 +15,8 @@ class ExceptionNotifierTest < ActiveSupport::TestCase ExceptionNotifier.error_grouping = false ExceptionNotifier.notification_trigger = nil ExceptionNotifier.class_eval('@@notifiers.delete_if { |k, _| k.to_s != "email"}') # reset notifiers - Rails.cache.clear + + Rails.cache.clear if defined?(Rails) && Rails.respond_to?(:cache) end test 'should have default ignored exceptions' do From f6da8aae7152774e9bbf8aa7327da3f55309556a Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Thu, 18 Apr 2019 22:20:25 -0300 Subject: [PATCH 111/156] Make RackTest work without Rails --- lib/exception_notification/rack.rb | 2 +- test/exception_notification/rack_test.rb | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/exception_notification/rack.rb b/lib/exception_notification/rack.rb index e8eb4fd6..34c838c2 100644 --- a/lib/exception_notification/rack.rb +++ b/lib/exception_notification/rack.rb @@ -12,7 +12,7 @@ def initialize(app, options = {}) if options.key?(:error_grouping_cache) ExceptionNotifier.error_grouping_cache = options.delete(:error_grouping_cache) - elsif defined?(Rails) + elsif defined?(Rails) && Rails.respond_to?(:cache) ExceptionNotifier.error_grouping_cache = Rails.cache end diff --git a/test/exception_notification/rack_test.rb b/test/exception_notification/rack_test.rb index 4e8ca285..3a107b61 100644 --- a/test/exception_notification/rack_test.rb +++ b/test/exception_notification/rack_test.rb @@ -36,9 +36,11 @@ class RackTest < ActiveSupport::TestCase assert_respond_to ExceptionNotifier.notification_trigger, :call end - test 'should set default cache to Rails cache' do - ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({}) - assert_equal Rails.cache, ExceptionNotifier.error_grouping_cache + if defined?(Rails) && Rails.respond_to?(:cache) + test 'should set default cache to Rails cache' do + ExceptionNotification::Rack.new(@normal_app, error_grouping: true).call({}) + assert_equal Rails.cache, ExceptionNotifier.error_grouping_cache + end end test 'should ignore exceptions with Usar Agent in ignore_crawlers' do From 9ae7d22edc1f8753971ef05d8522b86d37de652c Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 01:06:31 -0300 Subject: [PATCH 112/156] Fix TeamsNotifier to work without Rails --- lib/exception_notifier/teams_notifier.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/exception_notifier/teams_notifier.rb b/lib/exception_notifier/teams_notifier.rb index d419f924..26992e45 100644 --- a/lib/exception_notifier/teams_notifier.rb +++ b/lib/exception_notifier/teams_notifier.rb @@ -1,5 +1,6 @@ require 'action_dispatch' require 'active_support/core_ext/time' +require 'active_support/core_ext/object/json' module ExceptionNotifier class TeamsNotifier < BaseNotifier @@ -24,7 +25,7 @@ def call(exception, options = {}) @env = @options.delete(:env) - @application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore + @application_name = @options.delete(:app_name) || rails_app_name @gitlab_url = @options.delete(:git_url) @jira_url = @options.delete(:jira_url) @@ -69,7 +70,7 @@ def message_text '@type' => 'MessageCard', '@context' => 'http://schema.org/extensions', 'summary' => "#{@application_name} Exception Alert", - 'title' => "⚠️ Exception Occurred in #{Rails.env} ⚠️", + 'title' => "⚠️ Exception Occurred in #{env_name} ⚠️", 'sections' => [ { 'activityTitle' => "#{errors_count > 1 ? errors_count : 'A'} *#{@exception.class}* occurred" + (@controller ? " in *#{controller_and_method}*." : '.'), @@ -174,5 +175,13 @@ def hash_presentation(hash) text.join(" \n") end + + def rails_app_name + Rails.application.class.parent_name.underscore if defined?(Rails) && Rails.respond_to?(:application) + end + + def env_name + Rails.env if defined?(Rails) && Rails.respond_to?(:env) + end end end From 34b446da648a4fc60fbf72f99a2b7000af4cff9e Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 01:17:58 -0300 Subject: [PATCH 113/156] Make SnsNotifierTest work without Rails --- test/exception_notifier/sns_notifier_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/exception_notifier/sns_notifier_test.rb b/test/exception_notifier/sns_notifier_test.rb index 8d3ae40a..82abed8f 100644 --- a/test/exception_notifier/sns_notifier_test.rb +++ b/test/exception_notifier/sns_notifier_test.rb @@ -73,7 +73,9 @@ def setup end test 'should send a sns notification with controller#action information' do - ExamplesController.any_instance.stubs(:action_name).returns('index') + controller = mock('controller') + controller.stubs(:action_name).returns('index') + controller.stubs(:controller_name).returns('examples') Aws::SNS::Client.any_instance.expects(:publish).with( topic_arn: 'topicARN', @@ -90,14 +92,12 @@ def setup env: { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/examples', - 'action_controller.instance' => ExamplesController.new + 'action_controller.instance' => controller }) end private - class ExamplesController < ActionController::Base; end - def fake_exception 1 / 0 rescue StandardError => e From dde02877bdf86f700897f3ea5932c016dc41b181 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 11:01:40 -0300 Subject: [PATCH 114/156] Make GoogleChatNotifierTest work without Rails --- .../google_chat_notifier_test.rb | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/exception_notifier/google_chat_notifier_test.rb b/test/exception_notifier/google_chat_notifier_test.rb index 30628194..80e47fad 100644 --- a/test/exception_notifier/google_chat_notifier_test.rb +++ b/test/exception_notifier/google_chat_notifier_test.rb @@ -1,6 +1,8 @@ require 'test_helper' +require 'rack' require 'httparty' require 'timecop' +require 'active_support/core_ext/object/json' class GoogleChatNotifierTest < ActiveSupport::TestCase URL = 'http://localhost:8000'.freeze @@ -21,7 +23,7 @@ def teardown test 'shoud use errors count if accumulated_errors_count is provided' do text = [ '', - 'Application: *dummy*', + "Application: *#{app_name}*", '5 *ArgumentError* occurred.', '', body @@ -159,13 +161,21 @@ def test_env def header [ '', - 'Application: *dummy*', + "Application: *#{app_name}*", 'An *ArgumentError* occurred.', '' ].join("\n") end def body - "⚠️ Error occurred in test ⚠️\n*foo*" + if defined?(::Rails) && ::Rails.respond_to?(:env) + "⚠️ Error occurred in test ⚠️\n*foo*" + else + "⚠️ Error occurred ⚠️\n*foo*" + end + end + + def app_name + 'dummy' if defined?(::Rails) && ::Rails.respond_to?(:application) end end From 19d21cd45502377934bdb4ff2e0590d7ab090d9e Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 11:03:00 -0300 Subject: [PATCH 115/156] Make HipchatNotifierTest work without Rails --- test/exception_notifier/hipchat_notifier_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/exception_notifier/hipchat_notifier_test.rb b/test/exception_notifier/hipchat_notifier_test.rb index b5e944bf..f9709cce 100644 --- a/test/exception_notifier/hipchat_notifier_test.rb +++ b/test/exception_notifier/hipchat_notifier_test.rb @@ -1,4 +1,5 @@ require 'test_helper' +require 'rack' # silence_warnings trick around require can be removed once # https://github.com/hipchat/hipchat-rb/pull/174 From dd845f8b08fb96516f4ccdddb5a3a5214857e3d7 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 11:03:36 -0300 Subject: [PATCH 116/156] Make MattermostNotifierTest work without Rails --- .../mattermost_notifier_test.rb | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/test/exception_notifier/mattermost_notifier_test.rb b/test/exception_notifier/mattermost_notifier_test.rb index 1a0b1496..e4a233aa 100644 --- a/test/exception_notifier/mattermost_notifier_test.rb +++ b/test/exception_notifier/mattermost_notifier_test.rb @@ -1,6 +1,7 @@ require 'test_helper' require 'httparty' require 'timecop' +require 'active_support/core_ext/object/json' class MattermostNotifierTest < ActiveSupport::TestCase URL = 'http://localhost:8000'.freeze @@ -27,10 +28,10 @@ def teardown body = default_body.merge( text: [ '@channel', - '### ⚠️ Error occurred in test ⚠️', + error_occurred_in, 'An *ArgumentError* occurred.', '*foo*', - '[Create an issue](github.com/aschen/dummy/issues/new/?issue%5Btitle%5D=%5BBUG%5D+Error+500+%3A++%28ArgumentError%29+foo)' + github_link ].join("\n") ) @@ -96,7 +97,7 @@ def teardown body = default_body.merge( text: [ '@channel', - '### ⚠️ Error occurred in test ⚠️', + error_occurred_in, '5 *ArgumentError* occurred.', '*foo*' ].join("\n") @@ -115,7 +116,7 @@ def teardown body = default_body.merge( text: [ '@channel', - '### ⚠️ Error occurred in test ⚠️', + error_occurred_in, 'An *ArgumentError* occurred.', '*foo*', '### Request', @@ -160,7 +161,7 @@ def default_body { text: [ '@channel', - '### ⚠️ Error occurred in test ⚠️', + error_occurred_in, 'An *ArgumentError* occurred.', '*foo*' ].join("\n"), @@ -181,4 +182,21 @@ def test_env params: { id: 'foo' } ) end + + def error_occurred_in + if defined?(::Rails) && ::Rails.respond_to?(:env) + '### ⚠️ Error occurred in test ⚠️' + else + '### ⚠️ Error occurred ⚠️' + end + end + + def github_link + if defined?(::Rails) && ::Rails.respond_to?(:application) + '[Create an issue](github.com/aschen/dummy/issues/new/?issue%5Btitle%5D=%5BBUG%5D+Error+500+%3A++%28ArgumentError%29+foo)' + else + # TODO: fix missing app name + '[Create an issue](github.com/aschen//issues/new/?issue%5Btitle%5D=%5BBUG%5D+Error+500+%3A++%28ArgumentError%29+foo)' + end + end end From f27bb22d5f69d7a6abce52af1fea86100d80abd1 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 11:30:23 -0300 Subject: [PATCH 117/156] Make FormatterTest work without Rails --- .../modules/formatter_test.rb | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/test/exception_notifier/modules/formatter_test.rb b/test/exception_notifier/modules/formatter_test.rb index 2199e1ec..fef51c0a 100644 --- a/test/exception_notifier/modules/formatter_test.rb +++ b/test/exception_notifier/modules/formatter_test.rb @@ -2,10 +2,6 @@ require 'timecop' class FormatterTest < ActiveSupport::TestCase - class HomeController < ActionController::Metal - def index; end - end - setup do @exception = RuntimeError.new('test') Timecop.freeze('2018-12-09 12:07:16 UTC') @@ -20,7 +16,14 @@ def index; end # test 'title returns correct content' do formatter = ExceptionNotifier::Formatter.new(@exception) - assert_equal '⚠️ Error occurred in test ⚠️', formatter.title + + title = if defined?(::Rails) && ::Rails.respond_to?(:env) + '⚠️ Error occurred in test ⚠️' + else + '⚠️ Error occurred ⚠️' + end + + assert_equal title, formatter.title end # @@ -37,11 +40,8 @@ def index; end end test 'subtitle with controller' do - controller = HomeController.new - controller.process(:index) - env = Rack::MockRequest.env_for( - '/', 'action_controller.instance' => controller + '/', 'action_controller.instance' => test_controller ) formatter = ExceptionNotifier::Formatter.new(@exception, env: env) @@ -53,7 +53,12 @@ def index; end # test 'app_name defaults to Rails app name' do formatter = ExceptionNotifier::Formatter.new(@exception) - assert_equal 'dummy', formatter.app_name + + if defined?(::Rails) && ::Rails.respond_to?(:application) + assert_equal 'dummy', formatter.app_name + else + assert_nil formatter.app_name + end end test 'app_name can be overwritten using options' do @@ -120,11 +125,8 @@ def index; end # #controller_and_action # test 'correct controller_and_action if controller is present' do - controller = HomeController.new - controller.process(:index) - env = Rack::MockRequest.env_for( - '/', 'action_controller.instance' => controller + '/', 'action_controller.instance' => test_controller ) formatter = ExceptionNotifier::Formatter.new(@exception, env: env) @@ -137,4 +139,12 @@ def index; end formatter = ExceptionNotifier::Formatter.new(@exception, env: env) assert_nil formatter.controller_and_action end + + def test_controller + controller = mock('controller') + controller.stubs(:action_name).returns('index') + controller.stubs(:controller_name).returns('home') + + controller + end end From 7915b24c61fd7ba673cd28d800b44aebef1dc06f Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 11:48:09 -0300 Subject: [PATCH 118/156] Fix DatadogNotifier to work without Rails --- lib/exception_notifier/datadog_notifier.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/exception_notifier/datadog_notifier.rb b/lib/exception_notifier/datadog_notifier.rb index e29603d6..d8b810a5 100644 --- a/lib/exception_notifier/datadog_notifier.rb +++ b/lib/exception_notifier/datadog_notifier.rb @@ -1,3 +1,5 @@ +require 'action_dispatch' + module ExceptionNotifier class DatadogNotifier < BaseNotifier attr_reader :client, From 6012d3dbd04ced44a4223d55c584aca19ab8ff87 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 12:21:26 -0300 Subject: [PATCH 119/156] Make EmailNotifierTest work without Rails --- test/exception_notifier/email_notifier_test.rb | 13 +++++++++++-- .../exception_notifier/_new_bkg_section.html.erb | 1 + .../exception_notifier/_new_bkg_section.text.erb | 1 + .../views/exception_notifier/_new_section.html.erb | 1 + .../views/exception_notifier/_new_section.text.erb | 1 + 5 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 test/support/views/exception_notifier/_new_bkg_section.html.erb create mode 100644 test/support/views/exception_notifier/_new_bkg_section.text.erb create mode 100644 test/support/views/exception_notifier/_new_section.html.erb create mode 100644 test/support/views/exception_notifier/_new_section.text.erb diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index a6d16e4a..cb9528d4 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -1,5 +1,6 @@ require 'test_helper' require 'action_mailer' +require 'action_controller' class EmailNotifierTest < ActiveSupport::TestCase setup do @@ -23,6 +24,8 @@ class EmailNotifierTest < ActiveSupport::TestCase } ) + @email_notifier.mailer.append_view_path "#{File.dirname(__FILE__)}/../support/views" + @mail = @email_notifier.call( @exception, data: { job: 'DivideWorkerJob', payload: '1/0', message: 'My Custom Message' } @@ -244,6 +247,8 @@ def index; end post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } ) + @email_notifier.mailer.append_view_path "#{File.dirname(__FILE__)}/../support/views" + @controller = HomeController.new @controller.process(:index) @@ -292,8 +297,12 @@ def index; end * Parameters : {\"id\"=>\"foo\", \"secret\"=>\"[FILTERED]\"} * Timestamp : Sat, 20 Apr 2013 20:58:55 UTC +00:00 * Server : #{Socket.gethostname} - * Rails root : #{Rails.root} - * Process: #{$PROCESS_ID} + BODY + + body << " * Rails root : #{Rails.root}\n" if defined?(Rails) && Rails.respond_to?(:root) + + body << <<-BODY.strip_heredoc + * Process: #{Process.pid} ------------------------------- Session: diff --git a/test/support/views/exception_notifier/_new_bkg_section.html.erb b/test/support/views/exception_notifier/_new_bkg_section.html.erb new file mode 100644 index 00000000..96363fef --- /dev/null +++ b/test/support/views/exception_notifier/_new_bkg_section.html.erb @@ -0,0 +1 @@ +* New background section for testing diff --git a/test/support/views/exception_notifier/_new_bkg_section.text.erb b/test/support/views/exception_notifier/_new_bkg_section.text.erb new file mode 100644 index 00000000..96363fef --- /dev/null +++ b/test/support/views/exception_notifier/_new_bkg_section.text.erb @@ -0,0 +1 @@ +* New background section for testing diff --git a/test/support/views/exception_notifier/_new_section.html.erb b/test/support/views/exception_notifier/_new_section.html.erb new file mode 100644 index 00000000..d5953bee --- /dev/null +++ b/test/support/views/exception_notifier/_new_section.html.erb @@ -0,0 +1 @@ +* New html section for testing diff --git a/test/support/views/exception_notifier/_new_section.text.erb b/test/support/views/exception_notifier/_new_section.text.erb new file mode 100644 index 00000000..a2f31e22 --- /dev/null +++ b/test/support/views/exception_notifier/_new_section.text.erb @@ -0,0 +1 @@ +* New text section for testing From 9d79472f4241e706a7867170853ff89bbd997e38 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 12:30:43 -0300 Subject: [PATCH 120/156] Stop loading dummy Rails app --- test/test_helper.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index ca750a23..9d113e9f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,6 +1,3 @@ -# Configure Rails Environment -ENV['RAILS_ENV'] = 'test' - begin require 'coveralls' Coveralls.wear! @@ -11,10 +8,11 @@ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'exception_notification' -require 'dummy/config/application.rb' -require 'rails/test_help' - -require 'mocha/setup' +require 'minitest/autorun' +require 'mocha/minitest' +require 'active_support/test_case' +require 'action_mailer' -Rails.backtrace_cleaner.remove_silencers! ExceptionNotifier.testing_mode! +Time.zone = 'UTC' +ActionMailer::Base.delivery_method = :test From 5ba0dee8cd0777a486be6da156372c9a704dada6 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 12:31:02 -0300 Subject: [PATCH 121/156] Remove dummy Rails app --- test/dummy/.gitignore | 4 --- .../_new_bkg_section.html.erb | 1 - .../_new_bkg_section.text.erb | 1 - .../exception_notifier/_new_section.html.erb | 1 - .../exception_notifier/_new_section.text.erb | 1 - test/dummy/config.ru | 0 test/dummy/config/application.rb | 32 ------------------- 7 files changed, 40 deletions(-) delete mode 100644 test/dummy/.gitignore delete mode 100644 test/dummy/app/views/exception_notifier/_new_bkg_section.html.erb delete mode 100644 test/dummy/app/views/exception_notifier/_new_bkg_section.text.erb delete mode 100644 test/dummy/app/views/exception_notifier/_new_section.html.erb delete mode 100644 test/dummy/app/views/exception_notifier/_new_section.text.erb delete mode 100644 test/dummy/config.ru delete mode 100644 test/dummy/config/application.rb diff --git a/test/dummy/.gitignore b/test/dummy/.gitignore deleted file mode 100644 index f0fa30c5..00000000 --- a/test/dummy/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.bundle -db/*.sqlite3 -log/*.log -tmp/ diff --git a/test/dummy/app/views/exception_notifier/_new_bkg_section.html.erb b/test/dummy/app/views/exception_notifier/_new_bkg_section.html.erb deleted file mode 100644 index 96363fef..00000000 --- a/test/dummy/app/views/exception_notifier/_new_bkg_section.html.erb +++ /dev/null @@ -1 +0,0 @@ -* New background section for testing diff --git a/test/dummy/app/views/exception_notifier/_new_bkg_section.text.erb b/test/dummy/app/views/exception_notifier/_new_bkg_section.text.erb deleted file mode 100644 index 96363fef..00000000 --- a/test/dummy/app/views/exception_notifier/_new_bkg_section.text.erb +++ /dev/null @@ -1 +0,0 @@ -* New background section for testing diff --git a/test/dummy/app/views/exception_notifier/_new_section.html.erb b/test/dummy/app/views/exception_notifier/_new_section.html.erb deleted file mode 100644 index d5953bee..00000000 --- a/test/dummy/app/views/exception_notifier/_new_section.html.erb +++ /dev/null @@ -1 +0,0 @@ -* New html section for testing diff --git a/test/dummy/app/views/exception_notifier/_new_section.text.erb b/test/dummy/app/views/exception_notifier/_new_section.text.erb deleted file mode 100644 index a2f31e22..00000000 --- a/test/dummy/app/views/exception_notifier/_new_section.text.erb +++ /dev/null @@ -1 +0,0 @@ -* New text section for testing diff --git a/test/dummy/config.ru b/test/dummy/config.ru deleted file mode 100644 index e69de29b..00000000 diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb deleted file mode 100644 index c935c2d0..00000000 --- a/test/dummy/config/application.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'rails' -# Pick the frameworks you want: -# require 'active_model/railtie' -# require 'active_job/railtie' -# require 'active_record/railtie' -require 'action_controller/railtie' -require 'action_mailer/railtie' -require 'action_view/railtie' -# require 'action_cable/engine' -# require 'sprockets/railtie' -require 'rails/test_unit/railtie' - -module Dummy - class Application < Rails::Application - config.eager_load = false - config.action_mailer.delivery_method = :test - - config.middleware.use ExceptionNotification::Rack, - email: { - email_prefix: '[Dummy ERROR] ', - sender_address: %("Dummy Notifier" ), - exception_recipients: %w[dummyexceptions@example.com], - email_headers: { 'X-Custom-Header' => 'foobar' }, - sections: %w[new_section request session environment backtrace], - background_sections: %w[new_bkg_section backtrace data], - pre_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:pre_callback_called] = 1 }, - post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } - } - end -end - -Dummy::Application.initialize! From 9ba2defbbd4e49f6799de0b012cb4ec7e3bca026 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 12:50:29 -0300 Subject: [PATCH 122/156] Use default Ruby JSON library --- lib/exception_notifier/teams_notifier.rb | 2 +- test/exception_notifier/google_chat_notifier_test.rb | 2 +- test/exception_notifier/mattermost_notifier_test.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/exception_notifier/teams_notifier.rb b/lib/exception_notifier/teams_notifier.rb index 26992e45..9c97b3f0 100644 --- a/lib/exception_notifier/teams_notifier.rb +++ b/lib/exception_notifier/teams_notifier.rb @@ -1,6 +1,6 @@ require 'action_dispatch' require 'active_support/core_ext/time' -require 'active_support/core_ext/object/json' +require 'json' module ExceptionNotifier class TeamsNotifier < BaseNotifier diff --git a/test/exception_notifier/google_chat_notifier_test.rb b/test/exception_notifier/google_chat_notifier_test.rb index 80e47fad..dd02d782 100644 --- a/test/exception_notifier/google_chat_notifier_test.rb +++ b/test/exception_notifier/google_chat_notifier_test.rb @@ -2,7 +2,7 @@ require 'rack' require 'httparty' require 'timecop' -require 'active_support/core_ext/object/json' +require 'json' class GoogleChatNotifierTest < ActiveSupport::TestCase URL = 'http://localhost:8000'.freeze diff --git a/test/exception_notifier/mattermost_notifier_test.rb b/test/exception_notifier/mattermost_notifier_test.rb index e4a233aa..6ff8d784 100644 --- a/test/exception_notifier/mattermost_notifier_test.rb +++ b/test/exception_notifier/mattermost_notifier_test.rb @@ -1,7 +1,7 @@ require 'test_helper' require 'httparty' require 'timecop' -require 'active_support/core_ext/object/json' +require 'json' class MattermostNotifierTest < ActiveSupport::TestCase URL = 'http://localhost:8000'.freeze From d88958681b1a3ea3e00b06807d5cfd8c2b6c0037 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 13:26:34 -0300 Subject: [PATCH 123/156] Avoid using strip_heredoc --- test/exception_notifier/email_notifier_test.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index cb9528d4..416e6d31 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -47,7 +47,7 @@ class EmailNotifierTest < ActiveSupport::TestCase assert_equal 'Dummy user_name', @mail.delivery_method.settings[:user_name] assert_equal 'Dummy password', @mail.delivery_method.settings[:password] - body = <<-BODY.strip_heredoc + body = <<-BODY.gsub(/^ /, '') A ZeroDivisionError occurred in background at Sat, 20 Apr 2013 20:58:55 UTC +00:00 : divided by 0 @@ -274,7 +274,7 @@ def index; end assert_equal 'text/plain; charset=UTF-8', @mail.content_type assert_equal [], @mail.attachments - body = <<-BODY.strip_heredoc + body = <<-BODY.gsub(/^ /, '') A ZeroDivisionError occurred in home#index: divided by 0 @@ -301,7 +301,7 @@ def index; end body << " * Rails root : #{Rails.root}\n" if defined?(Rails) && Rails.respond_to?(:root) - body << <<-BODY.strip_heredoc + body << <<-BODY.gsub(/^ /, '') * Process: #{Process.pid} ------------------------------- From 4de18bc87aeb78e63786c91ab943c814160fa246 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Fri, 19 Apr 2019 15:31:18 -0300 Subject: [PATCH 124/156] Use recommended way to load Coveralls --- test/test_helper.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index 9d113e9f..1869697c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,9 +1,5 @@ -begin - require 'coveralls' - Coveralls.wear! -rescue LoadError - warn 'warning: coveralls gem not found; skipping Coveralls' -end +require 'coveralls' +Coveralls.wear! $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'exception_notification' From 7bc0031ea79152cb93893de37f5c9df704c92702 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 20 Apr 2019 12:45:10 -0300 Subject: [PATCH 125/156] Add Probot no-response configuration This is a bot to automatically close github issues that have not received a response to a maintainer's request for more information https://github.com/probot/no-response --- .github/no-response.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/no-response.yml diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 00000000..7193eaa3 --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,13 @@ +# Configuration for probot-no-response - https://github.com/probot/no-response + +# Number of days of inactivity before an Issue is closed for lack of response +daysUntilClose: 14 +# Label requiring a response +responseRequiredLabel: more-information-needed +# Comment to post when closing an Issue for lack of response. Set to `false` to disable +closeComment: > + This issue has been automatically closed because there has been no response + to our request for more information from the original author. With only the + information that is currently in the issue, we don't have enough information + to take action. Please reach out if you have or find the answers we need so + that we can investigate further. From 54cbad0a4a50759ab3801ef1fb9ce7cc8b053fd5 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 20 Apr 2019 12:39:31 -0300 Subject: [PATCH 126/156] Allow to set ignore_crawlers using Rails initializer --- lib/exception_notification/rack.rb | 16 +--------------- lib/exception_notifier.rb | 13 +++++++++++++ .../templates/exception_notification.rb.erb | 3 +++ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/exception_notification/rack.rb b/lib/exception_notification/rack.rb index 34c838c2..628a62f1 100644 --- a/lib/exception_notification/rack.rb +++ b/lib/exception_notification/rack.rb @@ -23,12 +23,7 @@ def initialize(app, options = {}) end end - if options.key?(:ignore_crawlers) - ignore_crawlers = options.delete(:ignore_crawlers) - ExceptionNotifier.ignore_if do |_exception, opts| - opts.key?(:env) && from_crawler(opts[:env], ignore_crawlers) - end - end + ExceptionNotifier.ignore_crawlers(options.delete(:ignore_crawlers)) if options.key?(:ignore_crawlers) @ignore_cascade_pass = options.delete(:ignore_cascade_pass) { true } @@ -56,14 +51,5 @@ def call(env) response end - - private - - def from_crawler(env, ignored_crawlers) - agent = env['HTTP_USER_AGENT'] - Array(ignored_crawlers).any? do |crawler| - agent =~ Regexp.new(crawler) - end - end end end diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index 58ea04c3..5c398fc1 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -95,6 +95,12 @@ def ignore_if(&block) @@ignores << block end + def ignore_crawlers(crawlers) + ignore_if do |_exception, opts| + opts.key?(:env) && from_crawler(opts[:env], crawlers) + end + end + def clear_ignore_conditions! @@ignores.clear end @@ -134,5 +140,12 @@ def create_and_register_notifier(name, options) rescue NameError => e raise UndefinedNotifierError, "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}" end + + def from_crawler(env, ignored_crawlers) + agent = env['HTTP_USER_AGENT'] + Array(ignored_crawlers).any? do |crawler| + agent =~ Regexp.new(crawler) + end + end end end diff --git a/lib/generators/exception_notification/templates/exception_notification.rb.erb b/lib/generators/exception_notification/templates/exception_notification.rb.erb index df0ba160..964c8e5e 100644 --- a/lib/generators/exception_notification/templates/exception_notification.rb.erb +++ b/lib/generators/exception_notification/templates/exception_notification.rb.erb @@ -22,6 +22,9 @@ ExceptionNotification.configure do |config| # not Rails.env.production? # end + # Ignore exceptions generated by crawlers + # config.ignore_crawlers %w{Googlebot bingbot} + # Notifiers ================================================================= # Email notifier sends notifications by email. From f9c351e7d15665195de308dae8ad15e2fc38e99a Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Wed, 24 Apr 2019 22:32:50 -0300 Subject: [PATCH 127/156] Relax Rubocop --- .rubocop_todo.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d447edba..29ebfc99 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -57,3 +57,7 @@ Style/MethodMissing: Exclude: - 'lib/exception_notifier/email_notifier.rb' - 'lib/exception_notifier/teams_notifier.rb' + +Metrics/ModuleLength: + Exclude: + - 'lib/exception_notifier.rb' From 0703b44928fe9cc3de1a8e9b201c9f58d98eb721 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Wed, 24 Apr 2019 22:48:53 -0300 Subject: [PATCH 128/156] Refactor sample app --- .gitignore | 1 - CONTRIBUTING.md | 2 +- examples/sample_app.rb | 20 ++++++++------------ 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 611f0a97..136bc30a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ *.gemfile.lock /Gemfile.lock /.idea/ -sample_app.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c70e03d5..12899257 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ config.middleware.use ExceptionNotification::Rack, ``` 2) Run `ruby examples/sample_app.rb` -If exception notification is working OK, you'll see the message "'Raising exception!" and then "Working OK!" and should receive the notification as configured above. If it's not, you can copy the information printed on the terminal related to exception notification and report an issue with more info! +If exception notification is working OK, the test should pass and trigger a notification as configured above. If it's not, you can copy the information printed on the terminal related to exception notification and report an issue with more info! ## Pull Requests diff --git a/examples/sample_app.rb b/examples/sample_app.rb index f0a53a88..8ff45866 100644 --- a/examples/sample_app.rb +++ b/examples/sample_app.rb @@ -15,27 +15,22 @@ class SampleApp < Rails::Application config.middleware.use ExceptionNotification::Rack, webhook: { - url: 'http://domain.com:5555/hubot/path' + url: 'http://example.com' } config.secret_key_base = 'my secret key base' - file = File.open('sample_app.log', 'w') - logger = Logger.new(file) - Rails.logger = logger + + Rails.logger = Logger.new($stdout) routes.draw do - get 'raise_sample_exception', to: 'exceptions#raise_sample_exception' + get '/', to: 'exceptions#index' end end require 'action_controller/railtie' -require 'active_support' class ExceptionsController < ActionController::Base - include Rails.application.routes.url_helpers - - def raise_sample_exception - puts 'Raising exception!' + def index raise 'Sample exception raised, you should receive a notification!' end end @@ -46,8 +41,9 @@ class Test < Minitest::Test include Rack::Test::Methods def test_raise_exception - get '/raise_sample_exception' - puts 'Working OK!' + get '/' + + assert last_response.server_error? end private From 68ef2260bd47ddcca8f7444fa7cd4f59bd868184 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 5 May 2019 14:44:34 -0300 Subject: [PATCH 129/156] Use constants for attribute list and default options --- lib/exception_notifier/email_notifier.rb | 65 +++++++++++------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index 8a07d9a2..250f1842 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -6,11 +6,32 @@ module ExceptionNotifier class EmailNotifier < BaseNotifier - attr_accessor(:sender_address, :exception_recipients, - :pre_callback, :post_callback, - :email_prefix, :email_format, :sections, :background_sections, - :verbose_subject, :normalize_subject, :include_controller_and_action_names_in_subject, - :delivery_method, :mailer_settings, :email_headers, :mailer_parent, :template_path, :deliver_with) + ATTRIBUTES = [ + :sender_address, :exception_recipients, :pre_callback, :post_callback, + :email_prefix, :email_format, :sections, :background_sections, + :verbose_subject, :normalize_subject, :include_controller_and_action_names_in_subject, + :delivery_method, :mailer_settings, :email_headers, :mailer_parent, :template_path, :deliver_with + ].freeze + + DEFAULT_OPTIONS = { + sender_address: %("Exception Notifier" ), + exception_recipients: [], + email_prefix: '[ERROR] ', + email_format: :text, + sections: %w[request session environment backtrace], + background_sections: %w[backtrace data], + verbose_subject: true, + normalize_subject: false, + include_controller_and_action_names_in_subject: true, + delivery_method: nil, + mailer_settings: nil, + email_headers: {}, + mailer_parent: 'ActionMailer::Base', + template_path: 'exception_notifier', + deliver_with: :default + }.freeze + + attr_accessor *ATTRIBUTES module Mailer class MissingController @@ -135,22 +156,14 @@ def maybe_call(maybe_proc) def initialize(options) super + delivery_method = (options[:delivery_method] || :smtp) mailer_settings_key = "#{delivery_method}_settings".to_sym options[:mailer_settings] = options.delete(mailer_settings_key) - merged_opts = options.reverse_merge(EmailNotifier.default_options) - filtered_opts = merged_opts.select do |k, _v| - %i[ - sender_address exception_recipients pre_callback - post_callback email_prefix email_format - sections background_sections verbose_subject normalize_subject - include_controller_and_action_names_in_subject delivery_method mailer_settings - email_headers mailer_parent template_path deliver_with - ].include?(k) - end + merged_opts = options.reverse_merge(DEFAULT_OPTIONS) - filtered_opts.each { |k, v| send("#{k}=", v) } + merged_opts.each { |k, v| send("#{k}=", v) if ATTRIBUTES.include?(k) } end def options @@ -195,26 +208,6 @@ def create_email(exception, options = {}) end end - def self.default_options - { - sender_address: %("Exception Notifier" ), - exception_recipients: [], - email_prefix: '[ERROR] ', - email_format: :text, - sections: %w[request session environment backtrace], - background_sections: %w[backtrace data], - verbose_subject: true, - normalize_subject: false, - include_controller_and_action_names_in_subject: true, - delivery_method: nil, - mailer_settings: nil, - email_headers: {}, - mailer_parent: 'ActionMailer::Base', - template_path: 'exception_notifier', - deliver_with: :default - } - end - def self.normalize_digits(string) string.gsub(/[0-9]+/, 'N') end From 2b46bfcaf82972fab95cfb1566b033da2b76bcea Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 5 May 2019 17:49:51 -0300 Subject: [PATCH 130/156] Use merge instead of ActiveSupport's reverse_merge --- lib/exception_notifier/email_notifier.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index 250f1842..d616dae6 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/time' require 'action_mailer' require 'action_dispatch' @@ -50,7 +49,10 @@ def exception_notification(env, exception, options = {}, default_options = {}) @env = env @exception = exception - @options = options.reverse_merge(env['exception_notifier.options'] || {}).reverse_merge(default_options) + + env_options = env['exception_notifier.options'] || {} + @options = default_options.merge(env_options).merge(options) + @kontroller = env['action_controller.instance'] || MissingController.new @request = ActionDispatch::Request.new(env) @backtrace = exception.backtrace ? clean_backtrace(exception) : [] @@ -66,7 +68,7 @@ def background_exception_notification(exception, options = {}, default_options = load_custom_views @exception = exception - @options = options.reverse_merge(default_options).symbolize_keys + @options = default_options.merge(options).symbolize_keys @backtrace = exception.backtrace || [] @timestamp = Time.current @sections = @options[:background_sections] @@ -161,7 +163,7 @@ def initialize(options) mailer_settings_key = "#{delivery_method}_settings".to_sym options[:mailer_settings] = options.delete(mailer_settings_key) - merged_opts = options.reverse_merge(DEFAULT_OPTIONS) + merged_opts = DEFAULT_OPTIONS.merge(options) merged_opts.each { |k, v| send("#{k}=", v) if ATTRIBUTES.include?(k) } end From 2e067a4486dc093eeaf5c400915c462711af636f Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 5 May 2019 18:03:31 -0300 Subject: [PATCH 131/156] Refactor method --- lib/exception_notifier/email_notifier.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index d616dae6..37ed67e0 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -199,12 +199,11 @@ def call(exception, options = {}) def create_email(exception, options = {}) env = options[:env] default_options = self.options - if env.nil? - send_notice(exception, options, nil, default_options) do |_, default_opts| + + send_notice(exception, options, nil, default_options) do |_, default_opts| + if env.nil? mailer.background_exception_notification(exception, options, default_opts) - end - else - send_notice(exception, options, nil, default_options) do |_, default_opts| + else mailer.exception_notification(env, exception, options, default_opts) end end From ab314fdaf9edb987e359473c132146887341d57b Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 5 May 2019 19:25:15 -0300 Subject: [PATCH 132/156] Make mailer method private --- lib/exception_notifier/email_notifier.rb | 16 +++++++++------- test/exception_notifier/email_notifier_test.rb | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index 37ed67e0..7f4a6c44 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -174,13 +174,6 @@ def options end end - def mailer - @mailer ||= Class.new(mailer_parent.constantize).tap do |mailer| - mailer.extend(EmailNotifier::Mailer) - mailer.mailer_name = template_path - end - end - def call(exception, options = {}) message = create_email(exception, options) @@ -212,5 +205,14 @@ def create_email(exception, options = {}) def self.normalize_digits(string) string.gsub(/[0-9]+/, 'N') end + + private + + def mailer + @mailer ||= Class.new(mailer_parent.constantize).tap do |mailer| + mailer.extend(EmailNotifier::Mailer) + mailer.mailer_name = template_path + end + end end end diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index 416e6d31..88c78990 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -24,7 +24,7 @@ class EmailNotifierTest < ActiveSupport::TestCase } ) - @email_notifier.mailer.append_view_path "#{File.dirname(__FILE__)}/../support/views" + ActionMailer::Base.append_view_path "#{File.dirname(__FILE__)}/../support/views" @mail = @email_notifier.call( @exception, @@ -247,7 +247,7 @@ def index; end post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } ) - @email_notifier.mailer.append_view_path "#{File.dirname(__FILE__)}/../support/views" + ActionMailer::Base.append_view_path "#{File.dirname(__FILE__)}/../support/views" @controller = HomeController.new @controller.process(:index) From aadc8324b00601fed18962c0cade4aab875d5b6e Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 5 May 2019 20:37:59 -0300 Subject: [PATCH 133/156] Remove redundant tests Sections are tested in the content of the email. Email wouldn't be correctly generated if mailer_parent and template_path were wrong --- .../exception_notifier/email_notifier_test.rb | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index 88c78990..e27d7421 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -77,26 +77,6 @@ class EmailNotifierTest < ActiveSupport::TestCase assert_equal body, @mail.decode_body end - test 'should have default sections overridden' do - %w[new_section request session environment backtrace].each do |section| - assert_includes @email_notifier.sections, section - end - end - - test 'should have default background sections' do - %w[new_bkg_section backtrace data].each do |section| - assert_includes @email_notifier.background_sections, section - end - end - - test 'should have mailer_parent by default' do - assert_equal @email_notifier.mailer_parent, 'ActionMailer::Base' - end - - test 'should have template_path by default' do - assert_equal @email_notifier.template_path, 'exception_notifier' - end - test 'should normalize multiple digits into one N' do assert_equal 'N foo N bar N baz N', ExceptionNotifier::EmailNotifier.normalize_digits('1 foo 12 bar 123 baz 1234') From 7ad02c254f865506d0a72a4f89457cfcf7b6c7d7 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 5 May 2019 20:44:20 -0300 Subject: [PATCH 134/156] Refactor how callbacks are tested --- test/exception_notifier/email_notifier_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index e27d7421..1681dfd5 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -16,8 +16,8 @@ class EmailNotifierTest < ActiveSupport::TestCase email_headers: { 'X-Custom-Header' => 'foobar' }, sections: %w[new_section request session environment backtrace], background_sections: %w[new_bkg_section backtrace data], - pre_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:pre_callback_called] = 1 }, - post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 }, + pre_callback: proc { |_opts, _notifier, _backtrace, _message, _message_opts| @pre_callback_called = true }, + post_callback: proc { |_opts, _notifier, _backtrace, _message, _message_opts| @post_callback_called = true }, smtp_settings: { user_name: 'Dummy user_name', password: 'Dummy password' @@ -33,8 +33,8 @@ class EmailNotifierTest < ActiveSupport::TestCase end test 'should call pre/post_callback if specified' do - assert_equal @email_notifier.options[:pre_callback_called], 1 - assert_equal @email_notifier.options[:post_callback_called], 1 + assert @pre_callback_called + assert @post_callback_called end test 'sends mail with correct content' do From 3838f7ec9ca324a8b552420528f7594a38156ef2 Mon Sep 17 00:00:00 2001 From: Shane Cavanaugh Date: Sat, 19 Jan 2019 14:30:48 -0500 Subject: [PATCH 135/156] Support Rails 6 --- .travis.yml | 9 +++++++++ exception_notification.gemspec | 6 +++--- gemfiles/rails6_0.gemfile | 7 +++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 gemfiles/rails6_0.gemfile diff --git a/.travis.yml b/.travis.yml index 0c7a406f..c5c89ee8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,7 @@ gemfile: - gemfiles/rails5_0.gemfile - gemfiles/rails5_1.gemfile - gemfiles/rails5_2.gemfile + - gemfiles/rails6_0.gemfile matrix: exclude: - rvm: 2.1.10 @@ -34,11 +35,19 @@ matrix: gemfile: gemfiles/rails5_1.gemfile - rvm: 2.1.10 gemfile: gemfiles/rails5_2.gemfile + - rvm: 2.1.10 + gemfile: gemfiles/rails6_0.gemfile + - rvm: 2.2.9 + gemfile: gemfiles/rails6_0.gemfile + - rvm: 2.3.6 + gemfile: gemfiles/rails6_0.gemfile # rails <=4.1 segfaults with ruby 2.4+ - rvm: 2.4.3 gemfile: gemfiles/rails4_0.gemfile - rvm: 2.4.3 gemfile: gemfiles/rails4_1.gemfile + - rvm: 2.4.3 + gemfile: gemfiles/rails6_0.gemfile - rvm: 2.5.0 gemfile: gemfiles/rails4_0.gemfile - rvm: 2.5.0 diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 3857641e..f0f4edba 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -18,8 +18,8 @@ Gem::Specification.new do |s| s.test_files = `git ls-files -- test`.split("\n") s.require_path = 'lib' - s.add_dependency('actionmailer', '>= 4.0', '< 6') - s.add_dependency('activesupport', '>= 4.0', '< 6') + s.add_dependency("actionmailer", ">= 4.0", "< 7") + s.add_dependency("activesupport", ">= 4.0", "< 7") s.add_development_dependency 'appraisal', '~> 2.2.0' s.add_development_dependency 'aws-sdk-sns', '~> 1' @@ -30,7 +30,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'httparty', '~> 0.10.2' s.add_development_dependency 'mock_redis', '~> 0.18.0' s.add_development_dependency 'mocha', '>= 0.13.0' - s.add_development_dependency 'rails', '>= 4.0', '< 6' + s.add_development_dependency "rails", ">= 4.0", "< 7" s.add_development_dependency 'resque', '~> 1.8.0' s.add_development_dependency 'rubocop', '0.50.0' # Sidekiq 3.2.2 does not support Ruby 1.9. diff --git a/gemfiles/rails6_0.gemfile b/gemfiles/rails6_0.gemfile new file mode 100644 index 00000000..15b9b275 --- /dev/null +++ b/gemfiles/rails6_0.gemfile @@ -0,0 +1,7 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 6.0.0" + +gemspec path: "../" From 68c0ff68323a56f872fa9f5bc9210a713a36a7ef Mon Sep 17 00:00:00 2001 From: Shane Cavanaugh Date: Sat, 19 Jan 2019 16:37:08 -0500 Subject: [PATCH 136/156] Rename parent_name to module_parent_name for Rails versions < 6 --- docs/notifiers/google_chat.md | 3 ++- docs/notifiers/mattermost.md | 3 ++- docs/notifiers/teams.md | 3 ++- lib/exception_notifier/teams_notifier.rb | 8 +++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/notifiers/google_chat.md b/docs/notifiers/google_chat.md index 0131b660..2fdec153 100644 --- a/docs/notifiers/google_chat.md +++ b/docs/notifiers/google_chat.md @@ -27,4 +27,5 @@ The Incoming WebHook URL on Google Chats. *String, optional* -Your application name, shown in the notification. Defaults to `Rails.application.class.parent_name.underscore`. +Your application name, shown in the notification. Defaults to `Rails.application.class.module_parent_name.underscore` for Rails versions >= 6; +`Rails.application.class.parent_name.underscore` otherwise. diff --git a/docs/notifiers/mattermost.md b/docs/notifiers/mattermost.md index d60183e5..3575aef5 100644 --- a/docs/notifiers/mattermost.md +++ b/docs/notifiers/mattermost.md @@ -111,4 +111,5 @@ Url of your gitlab or github with your organisation name for issue creation link *String, optional* -Your application name used for issue creation link. Defaults to ```Rails.application.class.parent_name.underscore```. +Your application name used for issue creation link. Defaults to `Rails.application.class.module_parent_name.underscore` for Rails versions >= 6; +`Rails.application.class.parent_name.underscore` otherwise. diff --git a/docs/notifiers/teams.md b/docs/notifiers/teams.md index 53e0ea93..df3b4377 100644 --- a/docs/notifiers/teams.md +++ b/docs/notifiers/teams.md @@ -50,4 +50,5 @@ Url of your Jira instance, adds button for Create Issue screen. Defaults to nil *String, optional* -Your application name used for git issue creation link. Defaults to `Rails.application.class.parent_name.underscore`. +Your application name used for git issue creation link. Defaults to `Rails.application.class.module_parent_name.underscore` for Rails versions >= 6; +`Rails.application.class.parent_name.underscore` otherwise. diff --git a/lib/exception_notifier/teams_notifier.rb b/lib/exception_notifier/teams_notifier.rb index 9c97b3f0..722d9dc2 100644 --- a/lib/exception_notifier/teams_notifier.rb +++ b/lib/exception_notifier/teams_notifier.rb @@ -177,7 +177,13 @@ def hash_presentation(hash) end def rails_app_name - Rails.application.class.parent_name.underscore if defined?(Rails) && Rails.respond_to?(:application) + if defined?(Rails) && Rails.respond_to?(:application) + if ::Gem::Version.new(Rails.version) >= ::Gem::Version.new('6.0') + Rails.application.class.module_parent_name.underscore + else + Rails.application.class.parent_name.underscore + end + end end def env_name From 582962b844a55587cfc27c198c39b47448fd309f Mon Sep 17 00:00:00 2001 From: Shane Cavanaugh Date: Sat, 19 Jan 2019 16:42:34 -0500 Subject: [PATCH 137/156] Use rails 6.0.0.beta1 for now --- gemfiles/rails6_0.gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gemfiles/rails6_0.gemfile b/gemfiles/rails6_0.gemfile index 15b9b275..1005501f 100644 --- a/gemfiles/rails6_0.gemfile +++ b/gemfiles/rails6_0.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 6.0.0" +gem "rails", "~> 6.0.0.beta1" gemspec path: "../" From 0ca952eea5bf3beac97ec4986ae62261f8d0851e Mon Sep 17 00:00:00 2001 From: Shane Cavanaugh Date: Fri, 10 May 2019 15:35:18 -0400 Subject: [PATCH 138/156] Upgrade to rails 6.0.0.rc1 --- gemfiles/rails6_0.gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gemfiles/rails6_0.gemfile b/gemfiles/rails6_0.gemfile index 1005501f..b8bc5786 100644 --- a/gemfiles/rails6_0.gemfile +++ b/gemfiles/rails6_0.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 6.0.0.beta1" +gem "rails", "~> 6.0.0.rc1" gemspec path: "../" From 4f01bb7e7829c028b1f745b1947912bb36543a60 Mon Sep 17 00:00:00 2001 From: Shane Cavanaugh Date: Fri, 10 May 2019 15:49:10 -0400 Subject: [PATCH 139/156] Fix rubocop offenses --- exception_notification.gemspec | 6 +++--- lib/exception_notifier/teams_notifier.rb | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/exception_notification.gemspec b/exception_notification.gemspec index f0f4edba..97228765 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -18,8 +18,8 @@ Gem::Specification.new do |s| s.test_files = `git ls-files -- test`.split("\n") s.require_path = 'lib' - s.add_dependency("actionmailer", ">= 4.0", "< 7") - s.add_dependency("activesupport", ">= 4.0", "< 7") + s.add_dependency('actionmailer', '>= 4.0', '< 7') + s.add_dependency('activesupport', '>= 4.0', '< 7') s.add_development_dependency 'appraisal', '~> 2.2.0' s.add_development_dependency 'aws-sdk-sns', '~> 1' @@ -30,7 +30,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'httparty', '~> 0.10.2' s.add_development_dependency 'mock_redis', '~> 0.18.0' s.add_development_dependency 'mocha', '>= 0.13.0' - s.add_development_dependency "rails", ">= 4.0", "< 7" + s.add_development_dependency 'rails', '>= 4.0', '< 7' s.add_development_dependency 'resque', '~> 1.8.0' s.add_development_dependency 'rubocop', '0.50.0' # Sidekiq 3.2.2 does not support Ruby 1.9. diff --git a/lib/exception_notifier/teams_notifier.rb b/lib/exception_notifier/teams_notifier.rb index 722d9dc2..e32cdd42 100644 --- a/lib/exception_notifier/teams_notifier.rb +++ b/lib/exception_notifier/teams_notifier.rb @@ -177,12 +177,12 @@ def hash_presentation(hash) end def rails_app_name - if defined?(Rails) && Rails.respond_to?(:application) - if ::Gem::Version.new(Rails.version) >= ::Gem::Version.new('6.0') - Rails.application.class.module_parent_name.underscore - else - Rails.application.class.parent_name.underscore - end + return unless defined?(Rails) && Rails.respond_to?(:application) + + if ::Gem::Version.new(Rails.version) >= ::Gem::Version.new('6.0') + Rails.application.class.module_parent_name.underscore + else + Rails.application.class.parent_name.underscore end end From 8189fe7f067de3ef1fca085e128fb1d864bcc14b Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Mon, 6 May 2019 01:35:18 -0300 Subject: [PATCH 140/156] Refactor deliver_with option --- lib/exception_notifier/email_notifier.rb | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index 7f4a6c44..aa6dde23 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -27,7 +27,7 @@ class EmailNotifier < BaseNotifier email_headers: {}, mailer_parent: 'ActionMailer::Base', template_path: 'exception_notifier', - deliver_with: :default + deliver_with: nil }.freeze attr_accessor *ATTRIBUTES @@ -177,16 +177,7 @@ def options def call(exception, options = {}) message = create_email(exception, options) - # FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')` - if deliver_with == :default - if message.respond_to?(:deliver_now) - message.deliver_now - else - message.deliver - end - else - message.send(deliver_with) - end + message.send(deliver_with || default_deliver_with(message)) end def create_email(exception, options = {}) @@ -214,5 +205,10 @@ def mailer mailer.mailer_name = template_path end end + + def default_deliver_with(message) + # FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')` + message.respond_to?(:deliver_now) ? :deliver_now : :deliver + end end end From 74af3ddf01bcff1f48a07ecee63f0b0c730a9599 Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 11 May 2019 13:33:02 -0300 Subject: [PATCH 141/156] Use call method instead of create_email for tests create_email is only used from deprecated methods, so we want to remove it in the near future --- test/exception_notifier/email_notifier_test.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index 1681dfd5..0f817471 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -87,7 +87,7 @@ class EmailNotifierTest < ActiveSupport::TestCase raise ArgumentError rescue StandardError => e @vowel_exception = e - @vowel_mail = @email_notifier.create_email(@vowel_exception) + @vowel_mail = @email_notifier.call(@vowel_exception) end assert_includes @vowel_mail.encoded, "An ArgumentError occurred in background at #{Time.current}" @@ -99,7 +99,7 @@ class EmailNotifierTest < ActiveSupport::TestCase rescue StandardError => e @ignored_exception = e unless ExceptionNotifier.ignored_exceptions.include?(@ignored_exception.class.name) - ignored_mail = @email_notifier.create_email(@ignored_exception) + ignored_mail = @email_notifier.call(@ignored_exception) end end @@ -110,11 +110,10 @@ class EmailNotifierTest < ActiveSupport::TestCase test 'should encode environment strings' do email_notifier = ExceptionNotifier::EmailNotifier.new( sender_address: '', - exception_recipients: %w[dummyexceptions@example.com], - deliver_with: :deliver_now + exception_recipients: %w[dummyexceptions@example.com] ) - mail = email_notifier.create_email( + mail = email_notifier.call( @exception, env: { 'REQUEST_METHOD' => 'GET', From 75e6de54077fdfbe64763ccc5a747553462d263e Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 11 May 2019 19:48:12 -0300 Subject: [PATCH 142/156] Use base_options hash for options instead of instance attributes --- lib/exception_notifier/email_notifier.rb | 28 +++++------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/lib/exception_notifier/email_notifier.rb b/lib/exception_notifier/email_notifier.rb index aa6dde23..394172ec 100644 --- a/lib/exception_notifier/email_notifier.rb +++ b/lib/exception_notifier/email_notifier.rb @@ -5,13 +5,6 @@ module ExceptionNotifier class EmailNotifier < BaseNotifier - ATTRIBUTES = [ - :sender_address, :exception_recipients, :pre_callback, :post_callback, - :email_prefix, :email_format, :sections, :background_sections, - :verbose_subject, :normalize_subject, :include_controller_and_action_names_in_subject, - :delivery_method, :mailer_settings, :email_headers, :mailer_parent, :template_path, :deliver_with - ].freeze - DEFAULT_OPTIONS = { sender_address: %("Exception Notifier" ), exception_recipients: [], @@ -30,8 +23,6 @@ class EmailNotifier < BaseNotifier deliver_with: nil }.freeze - attr_accessor *ATTRIBUTES - module Mailer class MissingController def method_missing(*args, &block); end @@ -163,28 +154,19 @@ def initialize(options) mailer_settings_key = "#{delivery_method}_settings".to_sym options[:mailer_settings] = options.delete(mailer_settings_key) - merged_opts = DEFAULT_OPTIONS.merge(options) - - merged_opts.each { |k, v| send("#{k}=", v) if ATTRIBUTES.include?(k) } - end - - def options - @options ||= {}.tap do |opts| - instance_variables.each { |var| opts[var[1..-1].to_sym] = instance_variable_get(var) } - end + @base_options = DEFAULT_OPTIONS.merge(options) end def call(exception, options = {}) message = create_email(exception, options) - message.send(deliver_with || default_deliver_with(message)) + message.send(base_options[:deliver_with] || default_deliver_with(message)) end def create_email(exception, options = {}) env = options[:env] - default_options = self.options - send_notice(exception, options, nil, default_options) do |_, default_opts| + send_notice(exception, options, nil, base_options) do |_, default_opts| if env.nil? mailer.background_exception_notification(exception, options, default_opts) else @@ -200,9 +182,9 @@ def self.normalize_digits(string) private def mailer - @mailer ||= Class.new(mailer_parent.constantize).tap do |mailer| + @mailer ||= Class.new(base_options[:mailer_parent].constantize).tap do |mailer| mailer.extend(EmailNotifier::Mailer) - mailer.mailer_name = template_path + mailer.mailer_name = base_options[:template_path] end end From f985154814ff01e81dc2aace4bce7452f55bae9b Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sun, 12 May 2019 12:42:48 -0300 Subject: [PATCH 143/156] Set ActionMailer view path in test_helper --- test/exception_notifier/email_notifier_test.rb | 4 ---- test/test_helper.rb | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index 0f817471..2d55d956 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -24,8 +24,6 @@ class EmailNotifierTest < ActiveSupport::TestCase } ) - ActionMailer::Base.append_view_path "#{File.dirname(__FILE__)}/../support/views" - @mail = @email_notifier.call( @exception, data: { job: 'DivideWorkerJob', payload: '1/0', message: 'My Custom Message' } @@ -226,8 +224,6 @@ def index; end post_callback: proc { |_opts, _notifier, _backtrace, _message, message_opts| message_opts[:post_callback_called] = 1 } ) - ActionMailer::Base.append_view_path "#{File.dirname(__FILE__)}/../support/views" - @controller = HomeController.new @controller.process(:index) diff --git a/test/test_helper.rb b/test/test_helper.rb index 1869697c..dc590da3 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,3 +12,4 @@ ExceptionNotifier.testing_mode! Time.zone = 'UTC' ActionMailer::Base.delivery_method = :test +ActionMailer::Base.append_view_path "#{File.dirname(__FILE__)}/support/views" From 4d7356e650e8b32ce900314c185add87ca8c8377 Mon Sep 17 00:00:00 2001 From: Mladen Ilic Date: Wed, 14 Aug 2019 15:41:00 +0200 Subject: [PATCH 144/156] Rails 6.0.0.rc2 is out --- gemfiles/rails6_0.gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gemfiles/rails6_0.gemfile b/gemfiles/rails6_0.gemfile index b8bc5786..575379b2 100644 --- a/gemfiles/rails6_0.gemfile +++ b/gemfiles/rails6_0.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 6.0.0.rc1" +gem "rails", "~> 6.0.0.rc2" gemspec path: "../" From 4887b632f200abf469a54acb96c50d3c097a2b32 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Fri, 16 Aug 2019 17:04:29 -0300 Subject: [PATCH 145/156] bump version to 4.4.0 --- CHANGELOG.rdoc | 14 ++++++++++++++ exception_notification.gemspec | 2 +- gemfiles/rails6_0.gemfile | 2 +- lib/exception_notification/version.rb | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 491ef292..f82ae46d 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,3 +1,17 @@ +== 4.4.0 + +* enhancements + * Rails 6 compatibility (by @shanecav) + * Add Datadog notifier (by @ajain0184) + * Use backtrace cleaner for Slack notifications (by @pomier) + * Add slack channel name override option (by @chaadow) + * Addition of sample application for testing purposes (by @ampeigonet) + +* bug fixes + * Fix error in Resque failure backend (by @EmilioCristalli) + * Remove sqlite dependency (by @EmilioCristalli) + * Configure ignore_crawlers from Rails initializer (by @EmilioCristalli) + == 4.3.0 * enhancements diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 97228765..9070b05d 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -4,7 +4,7 @@ Gem::Specification.new do |s| s.name = 'exception_notification' s.version = ExceptionNotification::VERSION s.authors = ['Jamis Buck', 'Josh Peek'] - s.date = '2018-11-22' + s.date = '2019-08-16' s.summary = 'Exception notification for Rails apps' s.homepage = 'https://smartinez87.github.io/exception_notification/' s.email = 'smartinez87@gmail.com' diff --git a/gemfiles/rails6_0.gemfile b/gemfiles/rails6_0.gemfile index 575379b2..15b9b275 100644 --- a/gemfiles/rails6_0.gemfile +++ b/gemfiles/rails6_0.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 6.0.0.rc2" +gem "rails", "~> 6.0.0" gemspec path: "../" diff --git a/lib/exception_notification/version.rb b/lib/exception_notification/version.rb index f43a8fdf..e0e70d5b 100644 --- a/lib/exception_notification/version.rb +++ b/lib/exception_notification/version.rb @@ -1,3 +1,3 @@ module ExceptionNotification - VERSION = '4.3.0'.freeze + VERSION = '4.4.0'.freeze end From dc9259c07e91b6a9aba6c8621be85b481a220757 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Fri, 16 Aug 2019 17:08:06 -0300 Subject: [PATCH 146/156] Remove outdated `versions` section from readme --- README.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/README.md b/README.md index 74861932..495de077 100644 --- a/README.md +++ b/README.md @@ -270,29 +270,6 @@ or As above, make sure the gem is not listed solely under the `production` group, since this initializer will be loaded regardless of environment. -## Versions - -For v4.2.1, see this tag: - -http://github.com/smartinez87/exception_notification/tree/v4.2.1 - -For v4.2.0, see this tag: - -http://github.com/smartinez87/exception_notification/tree/v4.2.0 - -For previous releases, visit: - -https://github.com/smartinez87/exception_notification/tags - -If you are running Rails 2.3 then see the branch for that: - -http://github.com/smartinez87/exception_notification/tree/2-3-stable - -If you are running pre-rack Rails then see this tag: - -http://github.com/smartinez87/exception_notification/tree/pre-2-3 - - ## Support and tickets Here's the list of [issues](https://github.com/smartinez87/exception_notification/issues) we're currently working on. From 177b10cd5a0a6c46e16435a50a1d2753c95bd8fc Mon Sep 17 00:00:00 2001 From: Anton Rieder Date: Fri, 6 Sep 2019 13:47:58 +0200 Subject: [PATCH 147/156] Add Rail 6.0 to Appraisals file --- Appraisals | 2 +- gemfiles/rails4_0.gemfile | 6 +++--- gemfiles/rails4_1.gemfile | 6 +++--- gemfiles/rails4_2.gemfile | 6 +++--- gemfiles/rails5_0.gemfile | 6 +++--- gemfiles/rails5_1.gemfile | 6 +++--- gemfiles/rails5_2.gemfile | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Appraisals b/Appraisals index 24a06a5b..0684677d 100644 --- a/Appraisals +++ b/Appraisals @@ -1,4 +1,4 @@ -rails_versions = ['~> 4.0.5', '~> 4.1.1', '~> 4.2.0', '~> 5.0.0', '~> 5.1.0', '~> 5.2.0'] +rails_versions = ['~> 4.0.5', '~> 4.1.1', '~> 4.2.0', '~> 5.0.0', '~> 5.1.0', '~> 5.2.0', '~> 6.0.0'] rails_versions.each do |rails_version| appraise "rails#{rails_version.slice(/\d+\.\d+/).tr('.', '_')}" do diff --git a/gemfiles/rails4_0.gemfile b/gemfiles/rails4_0.gemfile index 7117a086..4f47847a 100644 --- a/gemfiles/rails4_0.gemfile +++ b/gemfiles/rails4_0.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'rails', '~> 4.0.5' +gem "rails", "~> 4.0.5" -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/rails4_1.gemfile b/gemfiles/rails4_1.gemfile index 213be9c6..7e4bd92e 100644 --- a/gemfiles/rails4_1.gemfile +++ b/gemfiles/rails4_1.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'rails', '~> 4.1.1' +gem "rails", "~> 4.1.1" -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/rails4_2.gemfile b/gemfiles/rails4_2.gemfile index dbed7dd7..6977eb02 100644 --- a/gemfiles/rails4_2.gemfile +++ b/gemfiles/rails4_2.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'rails', '~> 4.2.0' +gem "rails", "~> 4.2.0" -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/rails5_0.gemfile b/gemfiles/rails5_0.gemfile index 49df9648..10f52e7a 100644 --- a/gemfiles/rails5_0.gemfile +++ b/gemfiles/rails5_0.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'rails', '~> 5.0.0' +gem "rails", "~> 5.0.0" -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/rails5_1.gemfile b/gemfiles/rails5_1.gemfile index 953d45c7..6100e830 100644 --- a/gemfiles/rails5_1.gemfile +++ b/gemfiles/rails5_1.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'rails', '~> 5.1.0' +gem "rails", "~> 5.1.0" -gemspec path: '../' +gemspec path: "../" diff --git a/gemfiles/rails5_2.gemfile b/gemfiles/rails5_2.gemfile index 0b87fd88..5a706dcb 100644 --- a/gemfiles/rails5_2.gemfile +++ b/gemfiles/rails5_2.gemfile @@ -1,7 +1,7 @@ # This file was generated by Appraisal -source 'https://rubygems.org' +source "https://rubygems.org" -gem 'rails', '~> 5.2.0' +gem "rails", "~> 5.2.0" -gemspec path: '../' +gemspec path: "../" From f47cb2f2cb7a786dba99c8c127d87ea0171451b8 Mon Sep 17 00:00:00 2001 From: Anton Rieder Date: Fri, 6 Sep 2019 13:58:00 +0200 Subject: [PATCH 148/156] Replace badges with SVG versions --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 495de077..f67d0344 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Exception Notification -[![Gem Version](https://fury-badge.herokuapp.com/rb/exception_notification.png)](http://badge.fury.io/rb/exception_notification) -[![Travis](https://api.travis-ci.org/smartinez87/exception_notification.png)](http://travis-ci.org/smartinez87/exception_notification) -[![Coverage Status](https://coveralls.io/repos/smartinez87/exception_notification/badge.png?branch=master)](https://coveralls.io/r/smartinez87/exception_notification) -[![Code Climate](https://codeclimate.com/github/smartinez87/exception_notification.png)](https://codeclimate.com/github/smartinez87/exception_notification) +[![Gem Version](https://badge.fury.io/rb/exception_notification.svg)](https://badge.fury.io/rb/exception_notification) +[![Build Status](https://travis-ci.org/smartinez87/exception_notification.svg?branch=master)](https://travis-ci.org/smartinez87/exception_notification) +[![Coverage Status](https://coveralls.io/repos/github/smartinez87/exception_notification/badge.svg?branch=master)](https://coveralls.io/github/smartinez87/exception_notification?branch=master) +[![Maintainability](https://api.codeclimate.com/v1/badges/78a9a12be00a6d305136/maintainability)](https://codeclimate.com/github/smartinez87/exception_notification/maintainability) **THIS README IS FOR THE MASTER BRANCH AND REFLECTS THE WORK CURRENTLY EXISTING ON THE MASTER BRANCH. IF YOU ARE WISHING TO USE A NON-MASTER BRANCH OF EXCEPTION NOTIFICATION, PLEASE CONSULT THAT BRANCH'S README AND NOT THIS ONE.** From efcfb93446f4f47f7184cc93b56ab879a36eaa97 Mon Sep 17 00:00:00 2001 From: Anton Rieder Date: Fri, 6 Sep 2019 14:05:22 +0200 Subject: [PATCH 149/156] Update Ruby versions on CI, add Ruby 2.6 --- .travis.yml | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index c5c89ee8..d1dd97a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,11 @@ language: ruby cache: bundler rvm: - 2.1.10 - - 2.2.9 - - 2.3.6 - - 2.4.3 - - 2.5.0 + - 2.2.10 + - 2.3.8 + - 2.4.7 + - 2.5.6 + - 2.6.4 env: - COVERALLS_SILENT=true before_install: @@ -29,26 +30,32 @@ gemfile: - gemfiles/rails6_0.gemfile matrix: exclude: + # Rails 6 supports Ruby 2.2.2 and up - rvm: 2.1.10 gemfile: gemfiles/rails5_0.gemfile - rvm: 2.1.10 gemfile: gemfiles/rails5_1.gemfile - rvm: 2.1.10 gemfile: gemfiles/rails5_2.gemfile + # Rails 6 supports Ruby 2.5 and up - rvm: 2.1.10 gemfile: gemfiles/rails6_0.gemfile - - rvm: 2.2.9 + - rvm: 2.2.10 gemfile: gemfiles/rails6_0.gemfile - - rvm: 2.3.6 + - rvm: 2.3.8 + gemfile: gemfiles/rails6_0.gemfile + - rvm: 2.4.7 gemfile: gemfiles/rails6_0.gemfile # rails <=4.1 segfaults with ruby 2.4+ - - rvm: 2.4.3 + - rvm: 2.4.7 gemfile: gemfiles/rails4_0.gemfile - - rvm: 2.4.3 + - rvm: 2.4.7 gemfile: gemfiles/rails4_1.gemfile - - rvm: 2.4.3 - gemfile: gemfiles/rails6_0.gemfile - - rvm: 2.5.0 + - rvm: 2.5.6 + gemfile: gemfiles/rails4_0.gemfile + - rvm: 2.5.6 + gemfile: gemfiles/rails4_1.gemfile + - rvm: 2.6.4 gemfile: gemfiles/rails4_0.gemfile - - rvm: 2.5.0 + - rvm: 2.6.4 gemfile: gemfiles/rails4_1.gemfile From 840956ab55edfe3c2e0d7505ded300877c392a0e Mon Sep 17 00:00:00 2001 From: Anton Rieder Date: Fri, 6 Sep 2019 14:50:48 +0200 Subject: [PATCH 150/156] Run RuboCop only once per CI run Right now, RuboCop is called for every job in the CI matrix, however there should never be a different result, depending on Ruby or Rails version. So we only run RuboCop once, in a Lint stage. The `script` part was removed as the default is `bundle exec rake` which is the same as `bundle exec rake test` in the case of this gem. --- .travis.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c5c89ee8..68d91ddf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,9 +16,7 @@ install: - "bundle install --jobs=3 --retry=3 --path=vendor/bundle" - "mkdir -p test/dummy/tmp/cache" - "mkdir -p test/dummy/tmp/non_default_location" -script: - - bundle exec rake test - - bundle exec rubocop + gemfile: - gemfiles/rails4_0.gemfile - gemfiles/rails4_1.gemfile @@ -27,6 +25,7 @@ gemfile: - gemfiles/rails5_1.gemfile - gemfiles/rails5_2.gemfile - gemfiles/rails6_0.gemfile + matrix: exclude: - rvm: 2.1.10 @@ -52,3 +51,9 @@ matrix: gemfile: gemfiles/rails4_0.gemfile - rvm: 2.5.0 gemfile: gemfiles/rails4_1.gemfile + +jobs: + include: + - stage: Lint + rvm: 2.6.4 + script: bundle exec rubocop From 348800eabb401105464bf09cbd3f846bd105391d Mon Sep 17 00:00:00 2001 From: Emilio Cristalli Date: Sat, 7 Sep 2019 12:47:26 -0300 Subject: [PATCH 151/156] Typo in Travis config comment --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d1dd97a2..092e0416 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ gemfile: - gemfiles/rails6_0.gemfile matrix: exclude: - # Rails 6 supports Ruby 2.2.2 and up + # Rails 5 supports Ruby 2.2.2 and up - rvm: 2.1.10 gemfile: gemfiles/rails5_0.gemfile - rvm: 2.1.10 From 909a310043ac3d7a34b167b0b97787c1de9636c7 Mon Sep 17 00:00:00 2001 From: Santanu Karmakar Date: Fri, 8 Dec 2017 15:52:05 +0530 Subject: [PATCH 152/156] Add basic MS Team Webhook support --- lib/exception_notifier.rb | 1 + .../team_webhook_notifier.rb | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 lib/exception_notifier/team_webhook_notifier.rb diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index 5c398fc1..4ae01c13 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -22,6 +22,7 @@ module ExceptionNotifier autoload :SnsNotifier, 'exception_notifier/sns_notifier' autoload :GoogleChatNotifier, 'exception_notifier/google_chat_notifier' autoload :DatadogNotifier, 'exception_notifier/datadog_notifier' + autoload :TeamWebhookNotifier, 'exception_notifier/team_webhook_notifier' class UndefinedNotifierError < StandardError; end diff --git a/lib/exception_notifier/team_webhook_notifier.rb b/lib/exception_notifier/team_webhook_notifier.rb new file mode 100644 index 00000000..8305582a --- /dev/null +++ b/lib/exception_notifier/team_webhook_notifier.rb @@ -0,0 +1,29 @@ +module ExceptionNotifier + class TeamWebhookNotifier < BaseNotifier + + def initialize(options) + super + @default_options = options + end + + # very basic team webhook notifier + def call(exception, options={}) + env = options[:env] + + options = options.reverse_merge(@default_options) + url = options.delete(:url) + http_method = :post + + uri = URI.parse(url) + req = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json') + message = "[#{env} Error]: " + (exception.is_a?(String) ? exception : exception.backtrace.join("\n")) + req.body = { text: message }.to_json + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http.request(req) + end + end + +end + From 04756745c315f47de8bbd8186019b6294e91367f Mon Sep 17 00:00:00 2001 From: Santanu Karmakar Date: Wed, 17 Jan 2018 12:18:21 +0530 Subject: [PATCH 153/156] Simplified backtrace --- lib/exception_notifier/team_webhook_notifier.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/exception_notifier/team_webhook_notifier.rb b/lib/exception_notifier/team_webhook_notifier.rb index 8305582a..3f150a62 100644 --- a/lib/exception_notifier/team_webhook_notifier.rb +++ b/lib/exception_notifier/team_webhook_notifier.rb @@ -16,7 +16,13 @@ def call(exception, options={}) uri = URI.parse(url) req = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json') - message = "[#{env} Error]: " + (exception.is_a?(String) ? exception : exception.backtrace.join("\n")) + + message = if options[:simplified] + "[#{env} Error]: " + (exception.is_a?(String) ? exception : exception.backtrace.reject{|l| l =~ %r|\A[^:]*/gems/|}.join("\n")) + else + "[#{env} Error]: " + (exception.is_a?(String) ? exception : exception.backtrace.join("\n")) + end + req.body = { text: message }.to_json http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true From e2edffa59a0240e961226a2bc2727d89ffe77aca Mon Sep 17 00:00:00 2001 From: Santanu Karmakar Date: Thu, 18 Jan 2018 13:51:48 +0530 Subject: [PATCH 154/156] Fix missing env in the message --- lib/exception_notifier/team_webhook_notifier.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/exception_notifier/team_webhook_notifier.rb b/lib/exception_notifier/team_webhook_notifier.rb index 3f150a62..c16a5c9d 100644 --- a/lib/exception_notifier/team_webhook_notifier.rb +++ b/lib/exception_notifier/team_webhook_notifier.rb @@ -8,22 +8,24 @@ def initialize(options) # very basic team webhook notifier def call(exception, options={}) - env = options[:env] - options = options.reverse_merge(@default_options) + error_precedence = "[#{options[:env]} Error]: " + url = options.delete(:url) http_method = :post uri = URI.parse(url) req = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json') - message = if options[:simplified] - "[#{env} Error]: " + (exception.is_a?(String) ? exception : exception.backtrace.reject{|l| l =~ %r|\A[^:]*/gems/|}.join("\n")) + message = if exception.is_a?(String) + exception + elsif options[:simplified] + exception.backtrace.reject{|l| l =~ %r|\A[^:]*/gems/|}.join("\n") else - "[#{env} Error]: " + (exception.is_a?(String) ? exception : exception.backtrace.join("\n")) + exception.backtrace.join("\n") end - req.body = { text: message }.to_json + req.body = { text: error_precedence + message }.to_json http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE From e5cddb6cbffb07071978c3cc04bcf2e8bc6b07a7 Mon Sep 17 00:00:00 2001 From: Santanu Karmakar Date: Tue, 6 Feb 2018 12:27:03 +0530 Subject: [PATCH 155/156] Format error message --- lib/exception_notifier/team_webhook_notifier.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/exception_notifier/team_webhook_notifier.rb b/lib/exception_notifier/team_webhook_notifier.rb index c16a5c9d..244b8fac 100644 --- a/lib/exception_notifier/team_webhook_notifier.rb +++ b/lib/exception_notifier/team_webhook_notifier.rb @@ -25,7 +25,7 @@ def call(exception, options={}) exception.backtrace.join("\n") end - req.body = { text: error_precedence + message }.to_json + req.body = { text: "```#{error_precedence + message}```" }.to_json http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE From 68011daea946bbf74ec85cf4aa0172e8a162c193 Mon Sep 17 00:00:00 2001 From: Vivek Date: Fri, 14 Jun 2019 11:34:45 +0530 Subject: [PATCH 156/156] added message formatting for teams webhook --- .../team_webhook_notifier.rb | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/exception_notifier/team_webhook_notifier.rb b/lib/exception_notifier/team_webhook_notifier.rb index 244b8fac..1c9d49eb 100644 --- a/lib/exception_notifier/team_webhook_notifier.rb +++ b/lib/exception_notifier/team_webhook_notifier.rb @@ -8,6 +8,7 @@ def initialize(options) # very basic team webhook notifier def call(exception, options={}) + env = options[:env] options = options.reverse_merge(@default_options) error_precedence = "[#{options[:env]} Error]: " @@ -24,13 +25,54 @@ def call(exception, options={}) else exception.backtrace.join("\n") end + unless env.nil? + request = ActionDispatch::Request.new(env) + formatted_message = generate_exception_card(request, exception) + end + + req.body = formatted_message ? + formatted_message.to_json : + { text: "```#{error_precedence + message}```" }.to_json - req.body = { text: "```#{error_precedence + message}```" }.to_json http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE http.request(req) end + + private + + def generate_exception_card(request, exception) + request_items = { + url: request.original_url, + http_method: request.method, + ip_address: request.remote_ip, + parameters: request.filtered_parameters.to_s, + timestamp: Time.current.to_s + } + + return { + "@type": "MessageCard", + "@context": "https://schema.org/extensions", + "summary": "Exception", + "themeColor": "0075FF", + "sections": [ + { + "startGroup": true, + "title": "**#{@default_options[:env]} : #{exception.class.to_s}**", + "activityTitle": "Exception message : #{exception.message}", + "facts": request_items.map do |key, value| + {name: key, value: value} + end + }, + { + "startGroup": true, + "title": "**Backtrace**", + "activitySubtitle": "#{exception.backtrace.first(10).join("\n\n")}" + } + ] + } + end end end