hoptoad_notifier 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ module HoptoadNotifier
2
+ # Sends out the notice to Hoptoad
3
+ class Sender
4
+
5
+ NOTICES_URI = '/notifier_api/v2/notices/'.freeze
6
+
7
+ def initialize(options = {})
8
+ [:proxy_host, :proxy_port, :proxy_user, :proxy_pass, :protocol,
9
+ :host, :port, :secure, :http_open_timeout, :http_read_timeout].each do |option|
10
+ instance_variable_set("@#{option}", options[option])
11
+ end
12
+ end
13
+
14
+ # Sends the notice data off to Hoptoad for processing.
15
+ #
16
+ # @param [String] data The XML notice to be sent off
17
+ def send_to_hoptoad(data)
18
+ logger.debug { "Sending request to #{url.to_s}:\n#{data}" } if logger
19
+
20
+ http =
21
+ Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_pass).
22
+ new(url.host, url.port)
23
+
24
+ http.read_timeout = http_read_timeout
25
+ http.open_timeout = http_open_timeout
26
+ http.use_ssl = secure
27
+
28
+ response = begin
29
+ http.post(url.path, data, HEADERS)
30
+ rescue TimeoutError => e
31
+ log :error, "Timeout while contacting the Hoptoad server."
32
+ nil
33
+ end
34
+
35
+ case response
36
+ when Net::HTTPSuccess then
37
+ log :info, "Success: #{response.class}", response
38
+ else
39
+ log :error, "Failure: #{response.class}", response
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :proxy_host, :proxy_port, :proxy_user, :proxy_pass, :protocol,
46
+ :host, :port, :secure, :http_open_timeout, :http_read_timeout
47
+
48
+ def url
49
+ URI.parse("#{protocol}://#{host}:#{port}").merge(NOTICES_URI)
50
+ end
51
+
52
+ def log(level, message, response = nil)
53
+ logger.send level, LOG_PREFIX + message if logger
54
+ HoptoadNotifier.report_environment_info
55
+ HoptoadNotifier.report_response_body(response.body) if response && response.respond_to?(:body)
56
+ end
57
+
58
+ def logger
59
+ HoptoadNotifier.logger
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,91 @@
1
+ require 'hoptoad_notifier'
2
+
3
+ namespace :hoptoad do
4
+ desc "Notify Hoptoad of a new deploy."
5
+ task :deploy => :environment do
6
+ require 'hoptoad_tasks'
7
+ HoptoadTasks.deploy(:rails_env => ENV['TO'],
8
+ :scm_revision => ENV['REVISION'],
9
+ :scm_repository => ENV['REPO'],
10
+ :local_username => ENV['USER'],
11
+ :api_key => ENV['API_KEY'])
12
+ end
13
+
14
+ task :log_stdout do
15
+ require 'logger'
16
+ RAILS_DEFAULT_LOGGER = Logger.new(STDOUT)
17
+ end
18
+
19
+ desc "Verify your gem installation by sending a test exception to the hoptoad service"
20
+ task :test => ['hoptoad:log_stdout', :environment] do
21
+ RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
22
+
23
+ require 'action_controller/test_process'
24
+ require 'app/controllers/application' if File.exists?('app/controllers/application.rb')
25
+
26
+ request = ActionController::TestRequest.new
27
+ response = ActionController::TestResponse.new
28
+
29
+ class HoptoadTestingException < RuntimeError; end
30
+
31
+ unless HoptoadNotifier.configuration.api_key
32
+ puts "Hoptoad needs an API key configured! Check the README to see how to add it."
33
+ exit
34
+ end
35
+
36
+ HoptoadNotifier.configuration.development_environments = []
37
+
38
+ in_controller = ApplicationController.included_modules.include? HoptoadNotifier::Catcher
39
+ in_base = ActionController::Base.included_modules.include? HoptoadNotifier::Catcher
40
+ if !in_controller || !in_base
41
+ puts "HoptoadNotifier::Catcher must be included inside your ApplicationController class."
42
+ exit
43
+ end
44
+
45
+ puts "Configuration:"
46
+ HoptoadNotifier.configuration.to_hash.each do |key, value|
47
+ puts sprintf("%25s: %s", key.to_s, value.inspect.slice(0, 55))
48
+ end
49
+
50
+ puts 'Setting up the Controller.'
51
+ class ApplicationController
52
+ # This is to bypass any filters that may prevent access to the action.
53
+ prepend_before_filter :test_hoptoad
54
+ def test_hoptoad
55
+ puts "Raising '#{exception_class.name}' to simulate application failure."
56
+ raise exception_class.new, 'Testing hoptoad via "rake hoptoad:test". If you can see this, it works.'
57
+ end
58
+
59
+ def rescue_action exception
60
+ rescue_action_in_public exception
61
+ end
62
+
63
+ # Ensure we actually have an action to go to.
64
+ def verify; end
65
+
66
+ def consider_all_requests_local
67
+ false
68
+ end
69
+
70
+ def local_request?
71
+ false
72
+ end
73
+
74
+ def exception_class
75
+ exception_name = ENV['EXCEPTION'] || "HoptoadTestingException"
76
+ Object.const_get(exception_name)
77
+ rescue
78
+ Object.const_set(exception_name, Class.new(Exception))
79
+ end
80
+
81
+ def logger
82
+ nil
83
+ end
84
+ end
85
+
86
+ puts 'Processing request.'
87
+ class HoptoadVerificationController < ApplicationController; end
88
+ HoptoadVerificationController.new.process(request, response)
89
+ end
90
+ end
91
+
@@ -0,0 +1,3 @@
1
+ module HoptoadNotifier
2
+ VERSION = "2.1.0".freeze
3
+ end
@@ -0,0 +1,146 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'rubygems'
4
+ require 'active_support'
5
+ require 'hoptoad_notifier/version'
6
+ require 'hoptoad_notifier/configuration'
7
+ require 'hoptoad_notifier/notice'
8
+ require 'hoptoad_notifier/sender'
9
+ require 'hoptoad_notifier/catcher'
10
+ require 'hoptoad_notifier/backtrace'
11
+
12
+ # Gem for applications to automatically post errors to the Hoptoad of their choice.
13
+ module HoptoadNotifier
14
+
15
+ API_VERSION = "2.0"
16
+ LOG_PREFIX = "** [Hoptoad] "
17
+
18
+ HEADERS = {
19
+ 'Content-type' => 'text/xml',
20
+ 'Accept' => 'text/xml, application/xml'
21
+ }
22
+
23
+ class << self
24
+ # The sender object is responsible for delivering formatted data to the Hoptoad server.
25
+ # Must respond to #send_to_hoptoad. See HoptoadNotifier::Sender.
26
+ attr_accessor :sender
27
+
28
+ # A Hoptoad configuration object. Must act like a hash and return sensible
29
+ # values for all Hoptoad configuration options. See HoptoadNotifier::Configuration.
30
+ attr_accessor :configuration
31
+
32
+ # Tell the log that the Notifier is good to go
33
+ def report_ready
34
+ write_verbose_log("Notifier #{VERSION} ready to catch errors")
35
+ end
36
+
37
+ # Prints out the environment info to the log for debugging help
38
+ def report_environment_info
39
+ write_verbose_log("Environment Info: #{environment_info}")
40
+ end
41
+
42
+ # Prints out the response body from Hoptoad for debugging help
43
+ def report_response_body(response)
44
+ write_verbose_log("Response from Hoptoad: \n#{response}")
45
+ end
46
+
47
+ # Returns the Ruby version, Rails version, and current Rails environment
48
+ def environment_info
49
+ info = "[Ruby: #{RUBY_VERSION}]"
50
+ info << " [Rails: #{::Rails::VERSION::STRING}]" if defined?(Rails)
51
+ info << " [Env: #{configuration.environment_name}]"
52
+ end
53
+
54
+ # Writes out the given message to the #logger
55
+ def write_verbose_log(message)
56
+ logger.info LOG_PREFIX + message if logger
57
+ end
58
+
59
+ # Look for the Rails logger currently defined
60
+ def logger
61
+ self.configuration.logger
62
+ end
63
+
64
+ # Call this method to modify defaults in your initializers.
65
+ #
66
+ # @example
67
+ # HoptoadNotifier.configure do |config|
68
+ # config.api_key = '1234567890abcdef'
69
+ # config.secure = false
70
+ # end
71
+ def configure(silent = false)
72
+ self.configuration ||= Configuration.new
73
+ yield(configuration)
74
+ self.sender = Sender.new(configuration)
75
+ report_ready unless silent
76
+ end
77
+
78
+ # Sends an exception manually using this method, even when you are not in a controller.
79
+ #
80
+ # @param [Exception] exception The exception you want to notify Hoptoad about.
81
+ # @param [Hash] opts Data that will be sent to Hoptoad.
82
+ #
83
+ # @option opts [String] :api_key The API key for this project. The API key is a unique identifier that Hoptoad uses for identification.
84
+ # @option opts [String] :error_message The error returned by the exception (or the message you want to log).
85
+ # @option opts [String] :backtrace A backtrace, usually obtained with +caller+.
86
+ # @option opts [String] :request The controller's request object.
87
+ # @option opts [String] :session The contents of the user's session.
88
+ # @option opts [String] :environment ENV merged with the contents of the request's environment.
89
+ def notify(exception, opts = {})
90
+ send_notice(build_notice_for(exception, opts))
91
+ end
92
+
93
+ # Sends the notice unless it is one of the default ignored exceptions
94
+ # @see HoptoadNotifier.notify
95
+ def notify_or_ignore(exception, opts = {})
96
+ notice = build_notice_for(exception, opts)
97
+ send_notice(notice) unless notice.ignore?
98
+ end
99
+
100
+ def build_lookup_hash_for(exception, options = {})
101
+ notice = build_notice_for(exception, options)
102
+
103
+ result = {}
104
+ result[:action] = notice.action rescue nil
105
+ result[:component] = notice.component rescue nil
106
+ result[:error_class] = notice.error_class if notice.error_class
107
+ result[:environment_name] = 'production'
108
+
109
+ unless notice.backtrace.lines.empty?
110
+ result[:file] = notice.backtrace.lines.first.file
111
+ result[:line_number] = notice.backtrace.lines.first.number
112
+ end
113
+
114
+ result
115
+ end
116
+
117
+ private
118
+
119
+ def send_notice(notice)
120
+ if configuration.public?
121
+ sender.send_to_hoptoad(notice.to_xml)
122
+ end
123
+ end
124
+
125
+ def build_notice_for(exception, opts = {})
126
+ exception = unwrap_exception(exception)
127
+ if exception.respond_to?(:to_hash)
128
+ opts = opts.merge(exception)
129
+ else
130
+ opts = opts.merge(:exception => exception)
131
+ end
132
+ Notice.new(configuration.merge(opts))
133
+ end
134
+
135
+ def unwrap_exception(exception)
136
+ if exception.respond_to?(:original_exception)
137
+ exception.original_exception
138
+ elsif exception.respond_to?(:continued_exception)
139
+ exception.continued_exception
140
+ else
141
+ exception
142
+ end
143
+ end
144
+ end
145
+ end
146
+
@@ -0,0 +1,37 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'active_support'
4
+
5
+ # Capistrano tasks for notifying Hoptoad of deploys
6
+ module HoptoadTasks
7
+
8
+ # Alerts Hoptoad of a deploy.
9
+ #
10
+ # @param [Hash] opts Data about the deploy that is set to Hoptoad
11
+ #
12
+ # @option opts [String] :rails_env Environment of the deploy (production, staging)
13
+ # @option opts [String] :scm_revision The given revision/sha that is being deployed
14
+ # @option opts [String] :scm_repository Address of your repository to help with code lookups
15
+ # @option opts [String] :local_username Who is deploying
16
+ def self.deploy(opts = {})
17
+ if HoptoadNotifier.configuration.api_key.blank?
18
+ puts "I don't seem to be configured with an API key. Please check your configuration."
19
+ return false
20
+ end
21
+
22
+ if opts[:rails_env].blank?
23
+ puts "I don't know to which Rails environment you are deploying (use the TO=production option)."
24
+ return false
25
+ end
26
+
27
+ params = {'api_key' => opts.delete(:api_key) ||
28
+ HoptoadNotifier.configuration.api_key}
29
+ opts.each {|k,v| params["deploy[#{k}]"] = v }
30
+
31
+ url = URI.parse("http://#{HoptoadNotifier.configuration.host || 'hoptoadapp.com'}/deploys.txt")
32
+ response = Net::HTTP.post_form(url, params)
33
+ puts response.body
34
+ return Net::HTTPSuccess === response
35
+ end
36
+ end
37
+
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'hoptoad_notifier/rails'
@@ -0,0 +1,89 @@
1
+ namespace :hoptoad do
2
+ desc "Notify Hoptoad of a new deploy."
3
+ task :deploy => :environment do
4
+ require 'hoptoad_tasks'
5
+ HoptoadTasks.deploy(:rails_env => ENV['TO'],
6
+ :scm_revision => ENV['REVISION'],
7
+ :scm_repository => ENV['REPO'],
8
+ :local_username => ENV['USER'],
9
+ :api_key => ENV['API_KEY'])
10
+ end
11
+
12
+ task :log_stdout do
13
+ require 'logger'
14
+ RAILS_DEFAULT_LOGGER = Logger.new(STDOUT)
15
+ end
16
+
17
+ desc "Verify your gem installation by sending a test exception to the hoptoad service"
18
+ task :test => ['hoptoad:log_stdout', :environment] do
19
+ RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
20
+
21
+ require 'action_controller/test_process'
22
+ require 'app/controllers/application' if File.exists?('app/controllers/application.rb')
23
+
24
+ request = ActionController::TestRequest.new
25
+ response = ActionController::TestResponse.new
26
+
27
+ class HoptoadTestingException < RuntimeError; end
28
+
29
+ unless HoptoadNotifier.configuration.api_key
30
+ puts "Hoptoad needs an API key configured! Check the README to see how to add it."
31
+ exit
32
+ end
33
+
34
+ HoptoadNotifier.configuration.development_environments = []
35
+
36
+ in_controller = ApplicationController.included_modules.include? HoptoadNotifier::Catcher
37
+ in_base = ActionController::Base.included_modules.include? HoptoadNotifier::Catcher
38
+ if !in_controller || !in_base
39
+ puts "HoptoadNotifier::Catcher must be included inside your ApplicationController class."
40
+ exit
41
+ end
42
+
43
+ puts "Configuration:"
44
+ HoptoadNotifier.configuration.to_hash.each do |key, value|
45
+ puts sprintf("%25s: %s", key.to_s, value.inspect.slice(0, 55))
46
+ end
47
+
48
+ puts 'Setting up the Controller.'
49
+ class ApplicationController
50
+ # This is to bypass any filters that may prevent access to the action.
51
+ prepend_before_filter :test_hoptoad
52
+ def test_hoptoad
53
+ puts "Raising '#{exception_class.name}' to simulate application failure."
54
+ raise exception_class.new, 'Testing hoptoad via "rake hoptoad:test". If you can see this, it works.'
55
+ end
56
+
57
+ def rescue_action exception
58
+ rescue_action_in_public exception
59
+ end
60
+
61
+ # Ensure we actually have an action to go to.
62
+ def verify; end
63
+
64
+ def consider_all_requests_local
65
+ false
66
+ end
67
+
68
+ def local_request?
69
+ false
70
+ end
71
+
72
+ def exception_class
73
+ exception_name = ENV['EXCEPTION'] || "HoptoadTestingException"
74
+ Object.const_get(exception_name)
75
+ rescue
76
+ Object.const_set(exception_name, Class.new(Exception))
77
+ end
78
+
79
+ def logger
80
+ nil
81
+ end
82
+ end
83
+
84
+ puts 'Processing request.'
85
+ class HoptoadVerificationController < ApplicationController; end
86
+ HoptoadVerificationController.new.process(request, response)
87
+ end
88
+ end
89
+
@@ -0,0 +1,118 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class BacktraceTest < Test::Unit::TestCase
4
+
5
+ should "parse a backtrace into lines" do
6
+ array = [
7
+ "app/models/user.rb:13:in `magic'",
8
+ "app/controllers/users_controller.rb:8:in `index'"
9
+ ]
10
+
11
+ backtrace = HoptoadNotifier::Backtrace.parse(array)
12
+
13
+ line = backtrace.lines.first
14
+ assert_equal '13', line.number
15
+ assert_equal 'app/models/user.rb', line.file
16
+ assert_equal 'magic', line.method
17
+
18
+ line = backtrace.lines.last
19
+ assert_equal '8', line.number
20
+ assert_equal 'app/controllers/users_controller.rb', line.file
21
+ assert_equal 'index', line.method
22
+ end
23
+
24
+ should "be equal with equal lines" do
25
+ one = build_backtrace_array
26
+ two = one.dup
27
+ assert_equal one, two
28
+
29
+ assert_equal HoptoadNotifier::Backtrace.parse(one), HoptoadNotifier::Backtrace.parse(two)
30
+ end
31
+
32
+ should "parse massive one-line exceptions into multiple lines" do
33
+ original_backtrace = HoptoadNotifier::Backtrace.
34
+ parse(["one:1:in `one'\n two:2:in `two'\n three:3:in `three`"])
35
+ expected_backtrace = HoptoadNotifier::Backtrace.
36
+ parse(["one:1:in `one'", "two:2:in `two'", "three:3:in `three`"])
37
+
38
+ assert_equal expected_backtrace, original_backtrace
39
+ end
40
+
41
+ context "with a project root" do
42
+ setup do
43
+ @project_root = '/some/path'
44
+ HoptoadNotifier.configure {|config| config.project_root = @project_root }
45
+ end
46
+
47
+ teardown do
48
+ reset_config
49
+ end
50
+
51
+ should "filter out the project root" do
52
+ backtrace_with_root = HoptoadNotifier::Backtrace.parse(
53
+ ["#{@project_root}/app/models/user.rb:7:in `latest'",
54
+ "#{@project_root}/app/controllers/users_controller.rb:13:in `index'",
55
+ "/lib/something.rb:41:in `open'"],
56
+ :filters => default_filters)
57
+ backtrace_without_root = HoptoadNotifier::Backtrace.parse(
58
+ ["[PROJECT_ROOT]/app/models/user.rb:7:in `latest'",
59
+ "[PROJECT_ROOT]/app/controllers/users_controller.rb:13:in `index'",
60
+ "/lib/something.rb:41:in `open'"])
61
+
62
+ assert_equal backtrace_without_root, backtrace_with_root
63
+ end
64
+ end
65
+
66
+ context "with a blank project root" do
67
+ setup do
68
+ HoptoadNotifier.configure {|config| config.project_root = '' }
69
+ end
70
+
71
+ teardown do
72
+ reset_config
73
+ end
74
+
75
+ should "not filter line numbers with respect to any project root" do
76
+ backtrace = ["/app/models/user.rb:7:in `latest'",
77
+ "/app/controllers/users_controller.rb:13:in `index'",
78
+ "/lib/something.rb:41:in `open'"]
79
+
80
+ backtrace_with_root =
81
+ HoptoadNotifier::Backtrace.parse(backtrace, :filters => default_filters)
82
+
83
+ backtrace_without_root =
84
+ HoptoadNotifier::Backtrace.parse(backtrace)
85
+
86
+ assert_equal backtrace_without_root, backtrace_with_root
87
+ end
88
+ end
89
+
90
+ should "remove notifier trace" do
91
+ inside_notifier = ['lib/hoptoad_notifier.rb:13:in `voodoo`']
92
+ outside_notifier = ['users_controller:8:in `index`']
93
+
94
+ without_inside = HoptoadNotifier::Backtrace.parse(outside_notifier)
95
+ with_inside = HoptoadNotifier::Backtrace.parse(inside_notifier + outside_notifier,
96
+ :filters => default_filters)
97
+
98
+ assert_equal without_inside, with_inside
99
+ end
100
+
101
+ should "run filters on the backtrace" do
102
+ filters = [lambda { |line| line.sub('foo', 'bar') }]
103
+ input = HoptoadNotifier::Backtrace.parse(["foo:13:in `one'", "baz:14:in `two'"],
104
+ :filters => filters)
105
+ expected = HoptoadNotifier::Backtrace.parse(["bar:13:in `one'", "baz:14:in `two'"])
106
+ assert_equal expected, input
107
+ end
108
+
109
+ def build_backtrace_array
110
+ ["app/models/user.rb:13:in `magic'",
111
+ "app/controllers/users_controller.rb:8:in `index'"]
112
+ end
113
+
114
+ def default_filters
115
+ HoptoadNotifier::Configuration::DEFAULT_BACKTRACE_FILTERS
116
+ end
117
+
118
+ end