From 130bda08047b83d6291166514fb457ff0e97c26c Mon Sep 17 00:00:00 2001 From: sauy7 Date: Wed, 18 May 2016 00:46:14 +0200 Subject: [PATCH 01/10] removes deprecation warning --- test/dummy/config/boot.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb index 4489e586..f2830ae3 100644 --- a/test/dummy/config/boot.rb +++ b/test/dummy/config/boot.rb @@ -3,4 +3,4 @@ # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) From c3a59ee0f211ee2e344d7b89586d237a39d8b828 Mon Sep 17 00:00:00 2001 From: sauy7 Date: Wed, 18 May 2016 00:47:13 +0200 Subject: [PATCH 02/10] fixed typo --- test/exception_notifier/email_notifier_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/exception_notifier/email_notifier_test.rb b/test/exception_notifier/email_notifier_test.rb index 53688d89..20af2b67 100644 --- a/test/exception_notifier/email_notifier_test.rb +++ b/test/exception_notifier/email_notifier_test.rb @@ -80,7 +80,7 @@ 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 + test "mail should be plain text and UTF-8 encoded by default" do assert_equal @mail.content_type, "text/plain; charset=UTF-8" end From 4cd4ec1dba923def27b56fcc249a60a6ad4859a3 Mon Sep 17 00:00:00 2001 From: sauy7 Date: Wed, 18 May 2016 00:48:00 +0200 Subject: [PATCH 03/10] updated schema --- test/dummy/db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index d4de93a0..504313e7 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -13,7 +13,7 @@ ActiveRecord::Schema.define(version: 20110729022608) do - create_table "posts", force: true do |t| + create_table "posts", force: :cascade do |t| t.string "title" t.text "body" t.string "secret" From c60a2343e2449e06c1b12817eb6a5afe8537c022 Mon Sep 17 00:00:00 2001 From: sauy7 Date: Wed, 18 May 2016 00:49:34 +0200 Subject: [PATCH 04/10] Adds a GitHubNotifier with basic testing that does not yet cover enough edge cases in exception, environment and other data. --- CHANGELOG.rdoc | 3 +- README.md | 63 ++++++++ exception_notification.gemspec | 1 + lib/exception_notifier.rb | 1 + lib/exception_notifier/github_notifier.rb | 145 ++++++++++++++++++ .../github_notifier_test.rb | 73 +++++++++ 6 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 lib/exception_notifier/github_notifier.rb create mode 100644 test/exception_notifier/github_notifier_test.rb diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index db88dc0c..6aa0b892 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -4,6 +4,7 @@ * update URL in gemspec (by @ktdreyer) * Add `hostname` to Slack notifier (by @juanazam) * Allow `exception_recipients` to be a proc (by @kellyjosephprice) + * Add `GithubNotifier` (by @sauy7) == 4.1.4 @@ -108,7 +109,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/README.md b/README.md index 132722aa..35046475 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ ExceptionNotification relies on notifiers to deliver notifications when errors o * [IRC notifier](#irc-notifier) * [Slack notifier](#slack-notifier) * [WebHook notifier](#webhook-notifier) +* [GitHub notifier](#github-notifier) But, you also can easily implement your own [custom notifier](#custom-notifier). @@ -650,6 +651,68 @@ Rails.application.config.middleware.use ExceptionNotification::Rack, For more HTTParty options, check out the [documentation](https://github.com/jnunemaker/httparty). +### GitHub notifier + +This notifier sends notifications, creating issues on GitHub. + +#### Usage + +Just add the [octokit](https://github.com/github/octokit) gem to your `Gemfile`: + +```ruby +gem 'octokit' +``` + +To configure it, you need to set the `repo`, `login` and `password` options, like this: + +```ruby +Rails.application.config.middleware.use ExceptionNotification::Rack, + :email => { + :email_prefix => "[PREFIX] ", + :sender_address => %{"notifier" }, + :exception_recipients => %w{exceptions@example.com} + }, + :github => { + :prefix => "[PREFIX] ", + :repo => 'owner/repo', + :login => ENV['GITHUB_LOGIN'], + :password => ENV['GITHUB_PASSWORD'] + } +``` + +#### Options + +##### repo + +*String, required* + +The repo owner and repo name, separated by a forward slash. + +##### login + +*String, required* + +A GitHub username with access rights to the repo + +##### password + +*String, required* + +The username's password. + +##### prefix + +*String, optional* + +A prefix prepended to the issue title. + +##### other options + +Authentication using OAuth tokens are not (yet) supported. + +Assignee, milestone and labels are not (yet) supported. + + ### 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: diff --git a/exception_notification.gemspec b/exception_notification.gemspec index 1b2bb77a..78975ac8 100644 --- a/exception_notification.gemspec +++ b/exception_notification.gemspec @@ -31,4 +31,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 "octokit", ">= 4.3.0" end diff --git a/lib/exception_notifier.rb b/lib/exception_notifier.rb index f958ac57..96208153 100644 --- a/lib/exception_notifier.rb +++ b/lib/exception_notifier.rb @@ -14,6 +14,7 @@ module ExceptionNotifier autoload :WebhookNotifier, 'exception_notifier/webhook_notifier' autoload :IrcNotifier, 'exception_notifier/irc_notifier' autoload :SlackNotifier, 'exception_notifier/slack_notifier' + autoload :GithubNotifier, 'exception_notifier/github_notifier' class UndefinedNotifierError < StandardError; end diff --git a/lib/exception_notifier/github_notifier.rb b/lib/exception_notifier/github_notifier.rb new file mode 100644 index 00000000..77a5a1bb --- /dev/null +++ b/lib/exception_notifier/github_notifier.rb @@ -0,0 +1,145 @@ +require 'action_dispatch' +require 'pp' + +module ExceptionNotifier + class GithubNotifier < BaseNotifier + attr_accessor :body, :client, :title, :repo + + def initialize(options) + super + begin + @client = Octokit::Client.new(login: options.delete(:login), + password: options.delete(:password)) + @repo = options.delete(:repo) + @prefix = options.delete(:prefix) || '[Error] ' + end + end + + def call(exception, options = {}) + @exception = exception + @env = options[:env] + @kontroller = @env['action_controller.instance'] + @data = (@env && @env['exception_notifier.exception_data'] || {}).merge(options[:data] || {}) + unless @env.nil? + @request = ActionDispatch::Request.new(@env) + @request_hash = hash_from_request + @session = @request.session + @environment = @request.filtered_env + end + @title = compose_title + @body = compose_body + issue_options = { title: @title, body: @body } + send_notice(@exception, options, nil, issue_options) do |_msg, opts| + @client.create_issue(@repo, opts[:title], opts[:body]) if @client.basic_authenticated? + end + end + + private + + def compose_backtrace_section + return '' if @exception.backtrace.empty? + out = sub_title('Backtrace') + out << "
#{@exception.backtrace.join("\n")}
\n" + end + + def compose_body + body = compose_header + if @env.nil? + body << compose_backtrace_section + else + body << compose_request_section + body << compose_session_section + body << compose_environment_section + body << compose_backtrace_section + end + body << compose_data_section + end + + def compose_data_section + return '' if @data.empty? + out = sub_title('Data') + out << "`#{PP.pp(@data, '')}`\n\n" + end + + def compose_environment_section + out = sub_title('Environment') + max = @environment.keys.map(&:to_s).max { |a, b| a.length <=> b.length } + @environment.keys.map(&:to_s).sort.each do |key| + out << "* `#{"%-*s: %s" % [max.length, key, inspect_object(@environment[key])]}`\n" + end + out << "\n" + end + + def compose_header + header = @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' + header << format(" %s occurred", @exception.class.to_s) + if @kontroller + header << format("in %s#%s", + @kontroller.controller_name, + @kontroller.action_name) + end + header << format(":\n\n") + header << "`#{@exception.message}\n" + header << "#{@exception.backtrace.first}`\n\n" + end + + def compose_request_section + return '' if @request_hash.empty? + out = sub_title('Request') + out << "* URL : `#{@request_hash[:url]}`\n" + out << "* HTTP Method: `#{@request_hash[:http_method]}`\n" + out << "* IP address : `#{@request_hash[:ip_address]}`\n" + out << "* Parameters : `#{@request_hash[:parameters].inspect}`\n" + out << "* Timestamp : `#{@request_hash[:timestamp]}`\n" + out << "* Server : `#{Socket.gethostname}`\n" + if defined?(Rails) && Rails.respond_to?(:root) + out << "* Rails root : `#{Rails.root}`\n" + end + out << "* Process : `#{$$}`\n\n" + end + + def compose_session_section + out = sub_title('Session') + id = if @request.ssl? + out << "[FILTERED]" + else + rack_session_id = (@request.env["rack.session.options"] and @request.env["rack.session.options"][:id]) + (@request.session['session_id'] || rack_session_id).inspect + end + out << format("* session id: `%s`\n", id) + out << "* data: `#{PP.pp(@request.session.to_hash, '')}`\n\n" + end + + def compose_title + subject = "#{@prefix}" + if @kontroller + subject << "#{@kontroller.controller_name}##{@kontroller.action_name}" + end + subject << " (#{@exception.class.to_s})" + subject.length > 120 ? subject[0...120] + "..." : subject + end + + def hash_from_request + { + http_method: @request.method, + ip_address: @request.remote_ip, + parameters: @request.filtered_parameters, + timestamp: Time.current, + url: @request.original_url + } + end + + def inspect_object(object) + case object + when Hash, Array + object.inspect + else + object.to_s + end + end + + def sub_title(text) + "## #{text}:\n\n" + end + end +end diff --git a/test/exception_notifier/github_notifier_test.rb b/test/exception_notifier/github_notifier_test.rb new file mode 100644 index 00000000..01ca55bf --- /dev/null +++ b/test/exception_notifier/github_notifier_test.rb @@ -0,0 +1,73 @@ +require 'test_helper' +require 'octokit' + +class GithubNotifierTest < ActiveSupport::TestCase + + test "should create github issue if properly configured" do + Octokit::Client.any_instance.expects(:create_issue) + + options = { + :prefix => '[Prefix] ', + :repo => 'some/repo', + :login => 'login', + :password => 'password' + } + + github = ExceptionNotifier::GithubNotifier.new(options) + github.call(fake_exception, + :env => { "REQUEST_METHOD" => "GET", "rack.input" => "" }, + :data => {}) + end + + test "does not create an authenticated github client if badly configured" do + incomplete_options = { + :prefix => '[Prefix] ', + :repo => 'some/repo', + :login => nil, + :password => 'password' + } + + github = ExceptionNotifier::GithubNotifier.new(incomplete_options) + github.call(fake_exception, + :env => { "REQUEST_METHOD" => "GET", "rack.input" => "" }, + :data => {}) + + refute github.client.basic_authenticated? + end + + test "github issue is formed with data" do + Octokit::Client.any_instance.expects(:create_issue) + + options = { + :prefix => '[Prefix] ', + :repo => 'some/repo', + :login => 'login', + :password => 'password' + } + + github = ExceptionNotifier::GithubNotifier.new(options) + github.call(fake_exception, + :env => { "REQUEST_METHOD" => "GET", "rack.input" => "" }, + :data => {}) + + assert_includes github.title, '[Prefix] (ZeroDivisionError)' + assert_includes github.body, 'A ZeroDivisionError occurred:' + assert_includes github.body, 'divided by 0' + assert_includes github.body, '## Request:' + assert_includes github.body, "* HTTP Method: `GET`" + assert_includes github.body, "## Session:" + assert_includes github.body, "* session id: `nil`" + assert_includes github.body, "## Environment:" + assert_includes github.body, "* `REQUEST_METHOD : GET`" + assert_includes github.body, "## Backtrace:" + assert_includes github.body, "`fake_exception'" + end + + private + + def fake_exception + 5/0 + rescue Exception => e + e + end +end From b91bb614c0311c4673fed739c14552d2f42fd681 Mon Sep 17 00:00:00 2001 From: sauy7 Date: Mon, 6 Jun 2016 09:56:38 +0200 Subject: [PATCH 05/10] tweaks to outpur formatting, prefer pre tags to back ticks --- lib/exception_notifier/github_notifier.rb | 31 ++++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/exception_notifier/github_notifier.rb b/lib/exception_notifier/github_notifier.rb index 77a5a1bb..567d0ef8 100644 --- a/lib/exception_notifier/github_notifier.rb +++ b/lib/exception_notifier/github_notifier.rb @@ -64,38 +64,39 @@ def compose_data_section def compose_environment_section out = sub_title('Environment') max = @environment.keys.map(&:to_s).max { |a, b| a.length <=> b.length } + out << "
"
       @environment.keys.map(&:to_s).sort.each do |key|
-        out << "* `#{"%-*s: %s" % [max.length, key, inspect_object(@environment[key])]}`\n"
+        out << "* #{"%-*s: %s" % [max.length, key, inspect_object(@environment[key])]}\n"
       end
-      out << "\n"
+      out << "
" end def compose_header header = @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A' header << format(" %s occurred", @exception.class.to_s) if @kontroller - header << format("in %s#%s", + header << format(" in %s#%s", @kontroller.controller_name, @kontroller.action_name) end header << format(":\n\n") - header << "`#{@exception.message}\n" - header << "#{@exception.backtrace.first}`\n\n" + header << "
#{@exception.message}\n"
+      header << "#{@exception.backtrace.first}
" end def compose_request_section return '' if @request_hash.empty? out = sub_title('Request') - out << "* URL : `#{@request_hash[:url]}`\n" - out << "* HTTP Method: `#{@request_hash[:http_method]}`\n" - out << "* IP address : `#{@request_hash[:ip_address]}`\n" - out << "* Parameters : `#{@request_hash[:parameters].inspect}`\n" - out << "* Timestamp : `#{@request_hash[:timestamp]}`\n" - out << "* Server : `#{Socket.gethostname}`\n" + out << "
* URL        : #{@request_hash[:url]}\n"
+      out << "* HTTP Method: #{@request_hash[:http_method]}\n"
+      out << "* IP address : #{@request_hash[:ip_address]}\n"
+      out << "* Parameters : #{@request_hash[:parameters].inspect}\n"
+      out << "* Timestamp : #{@request_hash[:timestamp]}\n"
+      out << "* Server : #{Socket.gethostname}\n"
       if defined?(Rails) && Rails.respond_to?(:root)
-        out << "* Rails root : `#{Rails.root}`\n"
+        out << "* Rails root : #{Rails.root}\n"
       end
-      out << "* Process : `#{$$}`\n\n"
+      out << "* Process : #{$$}
" end def compose_session_section @@ -106,8 +107,8 @@ def compose_session_section rack_session_id = (@request.env["rack.session.options"] and @request.env["rack.session.options"][:id]) (@request.session['session_id'] || rack_session_id).inspect end - out << format("* session id: `%s`\n", id) - out << "* data: `#{PP.pp(@request.session.to_hash, '')}`\n\n" + out << format("
* session id: %s\n", id)
+      out << "* data: #{PP.pp(@request.session.to_hash, '')}
" end def compose_title From 95c07f6df6e42670ea879ec916f75df8b4c6ba77 Mon Sep 17 00:00:00 2001 From: Tim Heighes Date: Fri, 1 Jul 2016 21:44:28 +0100 Subject: [PATCH 06/10] Updated output for sessions under SSL --- lib/exception_notifier/github_notifier.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/exception_notifier/github_notifier.rb b/lib/exception_notifier/github_notifier.rb index 567d0ef8..82a9b9f6 100644 --- a/lib/exception_notifier/github_notifier.rb +++ b/lib/exception_notifier/github_notifier.rb @@ -102,7 +102,7 @@ def compose_request_section def compose_session_section out = sub_title('Session') id = if @request.ssl? - out << "[FILTERED]" + '[FILTERED]' else rack_session_id = (@request.env["rack.session.options"] and @request.env["rack.session.options"][:id]) (@request.session['session_id'] || rack_session_id).inspect From eb18a1d25f1f12562f9452e01f8ceeace0997155 Mon Sep 17 00:00:00 2001 From: Tim Heighes Date: Mon, 11 Jul 2016 22:08:30 +0200 Subject: [PATCH 07/10] Updated output formatting --- lib/exception_notifier/github_notifier.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/exception_notifier/github_notifier.rb b/lib/exception_notifier/github_notifier.rb index 82a9b9f6..6fac909b 100644 --- a/lib/exception_notifier/github_notifier.rb +++ b/lib/exception_notifier/github_notifier.rb @@ -58,7 +58,7 @@ def compose_body def compose_data_section return '' if @data.empty? out = sub_title('Data') - out << "`#{PP.pp(@data, '')}`\n\n" + out << "
#{PP.pp(@data, '')}
" end def compose_environment_section @@ -91,12 +91,12 @@ def compose_request_section out << "* HTTP Method: #{@request_hash[:http_method]}\n" out << "* IP address : #{@request_hash[:ip_address]}\n" out << "* Parameters : #{@request_hash[:parameters].inspect}\n" - out << "* Timestamp : #{@request_hash[:timestamp]}\n" - out << "* Server : #{Socket.gethostname}\n" + out << "* Timestamp : #{@request_hash[:timestamp]}\n" + out << "* Server : #{Socket.gethostname}\n" if defined?(Rails) && Rails.respond_to?(:root) out << "* Rails root : #{Rails.root}\n" end - out << "* Process : #{$$}" + out << "* Process : #{$$}" end def compose_session_section @@ -108,7 +108,7 @@ def compose_session_section (@request.session['session_id'] || rack_session_id).inspect end out << format("
* session id: %s\n", id)
-      out << "* data: #{PP.pp(@request.session.to_hash, '')}
" + out << "* data : #{PP.pp(@request.session.to_hash, '')}" end def compose_title @@ -140,7 +140,7 @@ def inspect_object(object) end def sub_title(text) - "## #{text}:\n\n" + "\n\n-------------------- #{text} --------------------\n\n" end end end From d849c33abcb5bf6d11605772499cc71948997de3 Mon Sep 17 00:00:00 2001 From: Tim Heighes Date: Sat, 26 Nov 2016 21:17:02 +0100 Subject: [PATCH 08/10] Updated test to match revised output --- test/exception_notifier/github_notifier_test.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/exception_notifier/github_notifier_test.rb b/test/exception_notifier/github_notifier_test.rb index 01ca55bf..39be6876 100644 --- a/test/exception_notifier/github_notifier_test.rb +++ b/test/exception_notifier/github_notifier_test.rb @@ -53,13 +53,13 @@ class GithubNotifierTest < ActiveSupport::TestCase assert_includes github.title, '[Prefix] (ZeroDivisionError)' assert_includes github.body, 'A ZeroDivisionError occurred:' assert_includes github.body, 'divided by 0' - assert_includes github.body, '## Request:' - assert_includes github.body, "* HTTP Method: `GET`" - assert_includes github.body, "## Session:" - assert_includes github.body, "* session id: `nil`" - assert_includes github.body, "## Environment:" - assert_includes github.body, "* `REQUEST_METHOD : GET`" - assert_includes github.body, "## Backtrace:" + assert_includes github.body, '-------------------- Request --------------------' + assert_includes github.body, "* HTTP Method: GET" + assert_includes github.body, "-------------------- Session --------------------" + assert_includes github.body, "* session id: nil" + assert_includes github.body, "-------------------- Environment --------------------" + assert_includes github.body, "* REQUEST_METHOD : GET" + assert_includes github.body, "-------------------- Backtrace --------------------" assert_includes github.body, "`fake_exception'" end From 1585933c7af7566c4190ddb35d8d78dfc7d382ea Mon Sep 17 00:00:00 2001 From: sauy7 Date: Wed, 22 Mar 2017 09:49:11 +0100 Subject: [PATCH 09/10] Fixing case when @env is nil --- lib/exception_notifier/github_notifier.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/exception_notifier/github_notifier.rb b/lib/exception_notifier/github_notifier.rb index 567d0ef8..560c5e1f 100644 --- a/lib/exception_notifier/github_notifier.rb +++ b/lib/exception_notifier/github_notifier.rb @@ -18,9 +18,9 @@ def initialize(options) def call(exception, options = {}) @exception = exception @env = options[:env] - @kontroller = @env['action_controller.instance'] @data = (@env && @env['exception_notifier.exception_data'] || {}).merge(options[:data] || {}) unless @env.nil? + @kontroller = @env['action_controller.instance'] @request = ActionDispatch::Request.new(@env) @request_hash = hash_from_request @session = @request.session From b86f325694ae3264ced736c2e5eb89565ac1889b Mon Sep 17 00:00:00 2001 From: Tim Heighes Date: Sat, 29 Jul 2017 21:02:23 +0200 Subject: [PATCH 10/10] Added request referer --- lib/exception_notifier/github_notifier.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/exception_notifier/github_notifier.rb b/lib/exception_notifier/github_notifier.rb index 6fac909b..51c247c0 100644 --- a/lib/exception_notifier/github_notifier.rb +++ b/lib/exception_notifier/github_notifier.rb @@ -88,6 +88,7 @@ def compose_request_section return '' if @request_hash.empty? out = sub_title('Request') out << "
* URL        : #{@request_hash[:url]}\n"
+      out << "* Referer    : #{@request_hash[:referer]}\n"
       out << "* HTTP Method: #{@request_hash[:http_method]}\n"
       out << "* IP address : #{@request_hash[:ip_address]}\n"
       out << "* Parameters : #{@request_hash[:parameters].inspect}\n"
@@ -122,6 +123,7 @@ def compose_title
 
     def hash_from_request
       {
+        referer: @request.referer,
         http_method: @request.method,
         ip_address: @request.remote_ip,
         parameters: @request.filtered_parameters,