lib/toadhopper.rb in toadhopper-0.8 vs lib/toadhopper.rb in toadhopper-0.9
- old
+ new
@@ -1,153 +1,130 @@
-root = File.expand_path(File.dirname(__FILE__))
require 'net/http'
require 'haml'
require 'haml/engine'
require 'nokogiri'
-require File.join(root, 'backtrace')
-module ToadHopper
+# Posts errors to the Hoptoad API
+class ToadHopper
+ VERSION = "0.9"
+
# Hoptoad API response
class Response < Struct.new(:status, :body, :errors); end
- # Posts errors to the Hoptoad API
- class Dispatcher
- attr_reader :api_key
+ attr_reader :api_key
- def initialize(api_key)
- @api_key = api_key
- end
-
- # Sets patterns to [FILTER] out sensitive data such as passwords, emails and credit card numbers.
- #
- # Toadhopper::Dispatcher.new('apikey').filters = /password/, /email/, /credit_card_number/
- def filters=(*filters)
- @filters = filters.flatten
- end
+ def initialize(api_key)
+ @api_key = api_key
+ end
+
+ # Sets patterns to +[FILTER]+ out sensitive data such as +/password/+, +/email/+ and +/credit_card_number/+
+ def filters=(*filters)
+ @filters = filters.flatten
+ end
- # Filters for the Dispatcher
- #
- # @return [Regexp]
- def filters
- [@filters].flatten.compact
- end
+ # @private
+ def filters
+ [@filters].flatten.compact
+ end
- # Posts an exception to hoptoad.
- # Toadhopper::Dispatcher.new('apikey').post!(exception, {:action => 'show', :controller => 'Users'})
- # The Following Keys are available as parameters to the document_options
- # error The actual exception to be reported
- # api_key The api key for your project
- # url The url for the request, required to post but not useful in a console environment
- # component Normally this is your Controller name in an MVC framework
- # action Normally the action for your request in an MVC framework
- # request An object that response to #params and returns a hash
- # notifier_name Say you're a different notifier than ToadHopper
- # notifier_version Specify the version of your custom notifier
- # session A hash of the user session in a web request
- # framework_env The framework environment your app is running under
- # backtrace Normally not needed, parsed automatically from the provided exception parameter
- # environment You MUST scrub your environment if you plan to use this, please do not use it though. :)
- #
- # @return Toadhopper::Response
- def post!(error, document_options={}, header_options={})
- post_document(document_for(error, document_options), header_options)
- end
+ # Posts an exception to hoptoad.
+ # Toadhopper.new('apikey').post!(error, {:action => 'show', :controller => 'Users'})
+ # The Following Keys are available as parameters to the document_options
+ # url The url for the request, required to post but not useful in a console environment
+ # component Normally this is your Controller name in an MVC framework
+ # action Normally the action for your request in an MVC framework
+ # request An object that response to #params and returns a hash
+ # notifier_name Say you're a different notifier than ToadHopper
+ # notifier_version Specify the version of your custom notifier
+ # notifier_url Specify the project URL of your custom notifier
+ # session A hash of the user session in a web request
+ # framework_env The framework environment your app is running under
+ # backtrace Normally not needed, parsed automatically from the provided exception parameter
+ # environment You MUST scrub your environment if you plan to use this, please do not use it though. :)
+ # project_root The root directory of your app
+ #
+ # @return Toadhopper::Response
+ def post!(error, document_options={}, header_options={})
+ post_document(document_for(error, document_options), header_options)
+ end
- # Posts a v2 document error to Hoptoad
- # header_options can be passed in to indicate you're posting from a separate client
- # Toadhopper::Dispatcher.new('API KEY').post_document(doc, 'X-Hoptoad-Client-Name' => 'MyCustomDispatcher')
- #
- # @private
- def post_document(document, header_options={})
- uri = URI.parse("http://hoptoadapp.com:80/notifier_api/v2/notices")
+ # Posts a v2 document error to Hoptoad
+ # header_options can be passed in to indicate you're posting from a separate client
+ # Toadhopper.new('API KEY').post_document(doc, 'X-Hoptoad-Client-Name' => 'MyCustomLib')
+ #
+ # @private
+ def post_document(document, header_options={})
+ uri = URI.parse("http://hoptoadapp.com:80/notifier_api/v2/notices")
- Net::HTTP.start(uri.host, uri.port) do |http|
- headers = {
- 'Content-type' => 'text/xml',
- 'Accept' => 'text/xml, application/xml',
- 'X-Hoptoad-Client-Name' => 'Toadhopper',
- }.merge(header_options)
- http.read_timeout = 5 # seconds
- http.open_timeout = 2 # seconds
- begin
- response = http.post uri.path, document, headers
- response_for(response)
- rescue TimeoutError => e
- Response.new(500, '', ['Timeout error'])
- end
+ Net::HTTP.start(uri.host, uri.port) do |http|
+ headers = {
+ 'Content-type' => 'text/xml',
+ 'Accept' => 'text/xml, application/xml',
+ 'X-Hoptoad-Client-Name' => 'Toadhopper',
+ }.merge(header_options)
+ http.read_timeout = 5 # seconds
+ http.open_timeout = 2 # seconds
+ begin
+ response = http.post(uri.path, document, headers)
+ Response.new response.code.to_i,
+ response.body,
+ Nokogiri::XML.parse(response.body).xpath('//errors/error').map {|e| e.content}
+ rescue TimeoutError => e
+ Response.new(500, '', ['Timeout error'])
end
end
+ end
- # @private
- def document_for(exception, options={})
- locals = {
- :error => exception,
- :api_key => api_key,
- :environment => scrub_environment(ENV.to_hash),
- :backtrace => Backtrace.from_exception(exception),
- :url => 'http://localhost/',
- :component => 'http://localhost/',
- :action => nil,
- :request => nil,
- :notifier_name => 'ToadHopper',
- :notifier_version => '0.8',
- :session => { },
- :framework_env => ENV['RACK_ENV'] || 'development' }.merge(options)
+ # @private
+ def document_for(exception, options={})
+ locals = {
+ :error => exception,
+ :api_key => api_key,
+ :environment => clean(ENV.to_hash),
+ :backtrace => exception.backtrace.map {|l| backtrace_line(l)},
+ :url => 'http://localhost/',
+ :component => 'http://localhost/',
+ :action => nil,
+ :request => nil,
+ :notifier_name => 'ToadHopper',
+ :notifier_version => VERSION,
+ :notifier_url => 'http://github.com/toolmantim/toadhopper',
+ :session => {},
+ :framework_env => ENV['RACK_ENV'] || 'development',
+ :project_root => Dir.pwd
+ }.merge(options)
- Haml::Engine.new(notice_template).render(Object.new, locals)
- end
+ Haml::Engine.new(notice_template).render(Object.new, locals)
+ end
+
+ # @private
+ def backtrace_line(line)
+ Struct.new(:file, :number, :method).new(*line.match(%r{^([^:]+):(\d+)(?::in `([^']+)')?$}).captures)
+ end
+
+ # @private
+ def notice_template
+ File.read(::File.join(::File.dirname(__FILE__), 'notice.haml'))
+ end
- # @private
- def response_for(http_response)
- status = Integer(http_response.code)
- case status
- when 422
- errors = Nokogiri::XML.parse(http_response.body).xpath('//errors/error')
- Response.new(status, http_response.body, errors.map { |error| error.content })
- else
- Response.new(status, http_response.body, [])
- end
- end
-
- # @private
- def filter(hash)
- hash.inject({}) do |acc, (key, val)|
- acc[key] = filter?(key) ? "[FILTERED]" : val
+ # @private
+ def clean(hash)
+ hash.inject({}) do |acc, (k, v)|
+ acc[k] = (v.is_a?(Hash) ? clean(v) : filtered_value(k,v)) if serializable?(v)
acc
- end
end
+ end
- # @private
- def filter?(key)
- filters.any? do |filter|
- key.to_s =~ Regexp.new(filter)
- end
+ # @private
+ def filtered_value(key, value)
+ if filters.any? {|f| key.to_s =~ Regexp.new(f)}
+ "[FILTERED]"
+ else
+ value
end
+ end
- # @private
- def scrub_environment(hash)
- filter(clean_non_serializable_data(hash))
- end
-
- # @private
- def clean_non_serializable_data(data)
- data.select{|k,v| serializable?(v) }.inject({}) do |h, pair|
- h[pair.first] = pair.last.is_a?(Hash) ? clean_non_serializable_data(pair.last) : pair.last
- h
- end
- end
-
- # @private
- def serializable?(value)
- value.is_a?(Fixnum) ||
- value.is_a?(Array) ||
- value.is_a?(String) ||
- value.is_a?(Hash) ||
- value.is_a?(Bignum)
- end
-
- # @private
- def notice_template
- File.read(::File.join(::File.dirname(__FILE__), 'notice.haml'))
- end
+ # @private
+ def serializable?(value)
+ [Fixnum, Array, String, Hash, Bignum].any? {|c| value.is_a?(c)}
end
end