hoptoad_notifier 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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