toadhopper 0.7 → 0.8

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.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ /dist/
2
+ .yardoc
3
+ /doc/bin
4
+ vendor
5
+ bin
6
+ doc
7
+ toadhopper.gemspec
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --no-private
2
+ -r
3
+ README.md
4
+ -
5
+ LICENSE
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ only :release do
2
+ gem 'haml', '~> 2.0'
3
+ end
4
+
5
+ only :development do
6
+ gem 'mg'
7
+ gem 'yard'
8
+ gem 'jeweler'
9
+ end
10
+
11
+ only :test do
12
+ gem 'rake'
13
+ gem 'nokogiri'
14
+ gem 'test-unit', :require_as => 'test/unit'
15
+ end
16
+
data/README.md CHANGED
@@ -4,12 +4,35 @@ Toadhopper can be used to report plain old Ruby exceptions, or to build a framew
4
4
 
5
5
  require 'toadhopper'
6
6
 
7
- Toadhopper.api_key = "YOURAPIKEY"
7
+ dispatcher = Toadhopper::Dispatcher.new("YOURAPIKEY")
8
8
 
9
9
  error = begin; raise "Kaboom!"; rescue => e; e; end
10
10
 
11
- puts Toadhopper.post!(error)
11
+ puts dispatcher.post!(error)
12
12
 
13
13
  You can install it via rubygems:
14
14
 
15
15
  gem install toadhopper
16
+
17
+ ## Development
18
+
19
+ Firstly, `gem install bundler`, then:
20
+
21
+ % git clone git://github.com/toolmantim/toadhopper.git
22
+ % cd toadhopper
23
+ % gem bundle
24
+ % bin/rake test
25
+
26
+ If you set a `HOPTOAD_API_KEY` environment variable it'll test actually posting to the Hoptoad API. For example:
27
+
28
+ % bin/rake test HOPTOAD_API_KEY=abc123
29
+
30
+ To generate the docs:
31
+
32
+ % bin/yardoc
33
+
34
+ ## Contributors
35
+
36
+ * [Tim Lucas](http://github.com/toolmantim)
37
+ * [Samuel Tesla](http://github.com/stesla)
38
+ * [atmos](http://github.com/atmos)
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ Bundler.require_env(:development)
2
+
1
3
  require 'rake/testtask'
2
4
 
3
5
  Rake::TestTask.new do |t|
@@ -6,18 +8,19 @@ Rake::TestTask.new do |t|
6
8
  t.verbose = true
7
9
  end
8
10
 
9
- begin
10
- gem "sr-mg", "<= 0.0.5"
11
- require "mg"
12
- MG.new("toadhopper.gemspec")
13
- rescue Gem::LoadError
14
- end
11
+ Jeweler::Tasks.new do |s|
12
+ s.name = "toadhopper"
13
+ s.summary = "Post error notifications to Hoptoad"
14
+ s.email = "t.lucas@toolmantim.com"
15
+ s.homepage = "http://github.com/toolmantim/toadhopper"
16
+ s.authors = ["Tim Lucas", "Samuel Tesla", "Corey Donohoe"]
17
+ s.version = "0.8"
18
+ s.extra_rdoc_files = ["README.md", "LICENSE"]
19
+ s.executables = nil # stops jeweler automatically adding bin/*
15
20
 
16
- begin
17
- gem "yard"
18
- require 'yard'
19
- YARD::Rake::YardocTask.new do |t|
20
- t.options = ['-r', 'Readme.md', '--files', 'LICENSE'] # optional
21
+ require 'bundler'
22
+ bundler_env = Bundler::Environment.load(File.dirname(__FILE__) + '/Gemfile')
23
+ bundler_env.dependencies.each do |d|
24
+ s.add_dependency(d.name, d.version) if d.in?(:release)
21
25
  end
22
- rescue Gem::LoadError
23
26
  end
data/lib/backtrace.rb ADDED
@@ -0,0 +1,31 @@
1
+ module ToadHopper
2
+ # A line in a ruby Backtrace
3
+ #
4
+ # @private
5
+ class BacktraceLine < Struct.new(:file, :number, :method); end
6
+
7
+ # A collection of BacktraceLines representing an entire ruby backtrace
8
+ #
9
+ # @private
10
+ class Backtrace
11
+ INPUT_FORMAT = %r{^([^:]+):(\d+)(?::in `([^']+)')?$}.freeze
12
+
13
+ # the collection of lines in the backtrace
14
+ attr_reader :lines
15
+
16
+ # create a collection of BacktraceLines from an exception
17
+ def self.from_exception(exception)
18
+ @lines = exception.backtrace.map do |line|
19
+ _, file, number, method = line.match(INPUT_FORMAT).to_a
20
+ BacktraceLine.new(file, number, method)
21
+ end
22
+ end
23
+
24
+ # iterate over the lines in a Backtrace
25
+ def each_line(&block)
26
+ lines.each do |line|
27
+ yield line
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/notice.haml ADDED
@@ -0,0 +1,34 @@
1
+ !!!XML
2
+ %notice{:version => '2.0.0'}
3
+ %api-key= api_key
4
+ %notifier
5
+ %name= notifier_name
6
+ %version= notifier_version
7
+ %url http://github.com/toolmantim/toadhopper
8
+ %error
9
+ %class= error.class.name
10
+ %message= "#{error.class.name}: #{error.message}"
11
+ %backtrace
12
+ - backtrace.each do |line|
13
+ %line{:method => line.method, :file => line.file, :number => line.number}
14
+ %request
15
+ %url= url
16
+ %component= component
17
+ - if action && !action.blank?
18
+ %action= action
19
+ - if request && request.params.any?
20
+ %params
21
+ - request.params.each do |key,value|
22
+ %var{:key => key}= value
23
+ - if session && session.any?
24
+ %session
25
+ - session.each do |key,value|
26
+ %var{:key => key}= value
27
+ - if environment && environment.any?
28
+ %cgi-data
29
+ - environment.each do |key,value|
30
+ %var{:key => key}= value
31
+
32
+ %server-environment
33
+ %project-root= Dir.pwd
34
+ %environment-name= framework_env
data/lib/toadhopper.rb CHANGED
@@ -1,80 +1,153 @@
1
+ root = File.expand_path(File.dirname(__FILE__))
1
2
  require 'net/http'
2
- require 'yaml'
3
+ require 'haml'
4
+ require 'haml/engine'
5
+ require 'nokogiri'
6
+ require File.join(root, 'backtrace')
3
7
 
4
- module Toadhopper
5
- class << self
6
- # Set the API key
7
- def api_key=(key)
8
- @api_key = key
9
- end
10
- # Returns the API key
11
- def api_key
12
- @api_key
13
- end
14
- # Sets patterns to [FILTER] out sensitive data such as passwords, emails and credit card numbers.
15
- #
16
- # Toadhopper.filters = /password/, /email/, /credit_card_number/
17
- def filters=(*filters)
18
- @filters = filters.flatten
19
- end
20
- # Returns the filters
21
- def filters
22
- [@filters].flatten.compact
23
- end
24
- # Posts an error to Hoptoad
25
- def post!(error, options={}, header_options={})
26
- uri = URI.parse("http://hoptoadapp.com/notices/")
27
- Net::HTTP.start(uri.host, uri.port) do |http|
28
- headers = {
29
- 'Content-type' => 'application/x-yaml',
30
- 'Accept' => 'text/xml, application/xml',
31
- 'X-Hoptoad-Client-Name' => 'Toadhopper',
32
- }.merge(header_options)
33
- http.read_timeout = 5 # seconds
34
- http.open_timeout = 2 # seconds
35
- begin
36
- http.post uri.path, {"notice" => notice_params(error, options)}.to_yaml, headers
37
- rescue TimeoutError => e
38
- nil
8
+ module ToadHopper
9
+ # Hoptoad API response
10
+ class Response < Struct.new(:status, :body, :errors); end
11
+
12
+ # Posts errors to the Hoptoad API
13
+ class Dispatcher
14
+ attr_reader :api_key
15
+
16
+ def initialize(api_key)
17
+ @api_key = api_key
18
+ end
19
+
20
+ # Sets patterns to [FILTER] out sensitive data such as passwords, emails and credit card numbers.
21
+ #
22
+ # Toadhopper::Dispatcher.new('apikey').filters = /password/, /email/, /credit_card_number/
23
+ def filters=(*filters)
24
+ @filters = filters.flatten
25
+ end
26
+
27
+ # Filters for the Dispatcher
28
+ #
29
+ # @return [Regexp]
30
+ def filters
31
+ [@filters].flatten.compact
32
+ end
33
+
34
+ # Posts an exception to hoptoad.
35
+ # Toadhopper::Dispatcher.new('apikey').post!(exception, {:action => 'show', :controller => 'Users'})
36
+ # The Following Keys are available as parameters to the document_options
37
+ # error The actual exception to be reported
38
+ # api_key The api key for your project
39
+ # url The url for the request, required to post but not useful in a console environment
40
+ # component Normally this is your Controller name in an MVC framework
41
+ # action Normally the action for your request in an MVC framework
42
+ # request An object that response to #params and returns a hash
43
+ # notifier_name Say you're a different notifier than ToadHopper
44
+ # notifier_version Specify the version of your custom notifier
45
+ # session A hash of the user session in a web request
46
+ # framework_env The framework environment your app is running under
47
+ # backtrace Normally not needed, parsed automatically from the provided exception parameter
48
+ # environment You MUST scrub your environment if you plan to use this, please do not use it though. :)
49
+ #
50
+ # @return Toadhopper::Response
51
+ def post!(error, document_options={}, header_options={})
52
+ post_document(document_for(error, document_options), header_options)
53
+ end
54
+
55
+ # Posts a v2 document error to Hoptoad
56
+ # header_options can be passed in to indicate you're posting from a separate client
57
+ # Toadhopper::Dispatcher.new('API KEY').post_document(doc, 'X-Hoptoad-Client-Name' => 'MyCustomDispatcher')
58
+ #
59
+ # @private
60
+ def post_document(document, header_options={})
61
+ uri = URI.parse("http://hoptoadapp.com:80/notifier_api/v2/notices")
62
+
63
+ Net::HTTP.start(uri.host, uri.port) do |http|
64
+ headers = {
65
+ 'Content-type' => 'text/xml',
66
+ 'Accept' => 'text/xml, application/xml',
67
+ 'X-Hoptoad-Client-Name' => 'Toadhopper',
68
+ }.merge(header_options)
69
+ http.read_timeout = 5 # seconds
70
+ http.open_timeout = 2 # seconds
71
+ begin
72
+ response = http.post uri.path, document, headers
73
+ response_for(response)
74
+ rescue TimeoutError => e
75
+ Response.new(500, '', ['Timeout error'])
76
+ end
39
77
  end
40
- end
41
- end
42
- private
43
- # Replaces the values of the keys matching Toadhopper.filters with [FILTERED]. Typically used on the params and environment hashes.
78
+ end
79
+
80
+ # @private
81
+ def document_for(exception, options={})
82
+ locals = {
83
+ :error => exception,
84
+ :api_key => api_key,
85
+ :environment => scrub_environment(ENV.to_hash),
86
+ :backtrace => Backtrace.from_exception(exception),
87
+ :url => 'http://localhost/',
88
+ :component => 'http://localhost/',
89
+ :action => nil,
90
+ :request => nil,
91
+ :notifier_name => 'ToadHopper',
92
+ :notifier_version => '0.8',
93
+ :session => { },
94
+ :framework_env => ENV['RACK_ENV'] || 'development' }.merge(options)
95
+
96
+ Haml::Engine.new(notice_template).render(Object.new, locals)
97
+ end
98
+
99
+ # @private
100
+ def response_for(http_response)
101
+ status = Integer(http_response.code)
102
+ case status
103
+ when 422
104
+ errors = Nokogiri::XML.parse(http_response.body).xpath('//errors/error')
105
+ Response.new(status, http_response.body, errors.map { |error| error.content })
106
+ else
107
+ Response.new(status, http_response.body, [])
108
+ end
109
+ end
110
+
111
+ # @private
44
112
  def filter(hash)
45
113
  hash.inject({}) do |acc, (key, val)|
46
- acc[key] = filters.any? {|f| key.to_s =~ Regexp.new(f)} ? "[FILTERED]" : val
47
- acc
114
+ acc[key] = filter?(key) ? "[FILTERED]" : val
115
+ acc
48
116
  end
49
117
  end
50
- def notice_params(error, options={}) # :nodoc:
51
- clean_non_serializable_data(stringify_keys(
52
- {
53
- :api_key => api_key,
54
- :error_class => error.class.name,
55
- :error_message => error.message,
56
- :backtrace => error.backtrace,
57
- }.merge(options)
58
- ))
118
+
119
+ # @private
120
+ def filter?(key)
121
+ filters.any? do |filter|
122
+ key.to_s =~ Regexp.new(filter)
123
+ end
124
+ end
125
+
126
+ # @private
127
+ def scrub_environment(hash)
128
+ filter(clean_non_serializable_data(hash))
59
129
  end
60
- def stringify_keys(hash) #:nodoc:
61
- hash.inject({}) do |h, pair|
62
- h[pair.first.to_s] = pair.last.is_a?(Hash) ? stringify_keys(pair.last) : pair.last
130
+
131
+ # @private
132
+ def clean_non_serializable_data(data)
133
+ data.select{|k,v| serializable?(v) }.inject({}) do |h, pair|
134
+ h[pair.first] = pair.last.is_a?(Hash) ? clean_non_serializable_data(pair.last) : pair.last
63
135
  h
64
136
  end
65
137
  end
66
- def serializable?(value) #:nodoc:
138
+
139
+ # @private
140
+ def serializable?(value)
67
141
  value.is_a?(Fixnum) ||
68
142
  value.is_a?(Array) ||
69
143
  value.is_a?(String) ||
70
144
  value.is_a?(Hash) ||
71
145
  value.is_a?(Bignum)
72
146
  end
73
- def clean_non_serializable_data(data) #:nodoc:
74
- data.select{|k,v| serializable?(v) }.inject({}) do |h, pair|
75
- h[pair.first] = pair.last.is_a?(Hash) ? clean_non_serializable_data(pair.last) : pair.last
76
- h
77
- end
147
+
148
+ # @private
149
+ def notice_template
150
+ File.read(::File.join(::File.dirname(__FILE__), 'notice.haml'))
78
151
  end
152
+ end
79
153
  end
80
- end
data/test/helper.rb ADDED
@@ -0,0 +1,3 @@
1
+ Bundler.require_env(:test)
2
+
3
+ require File.join(File.dirname(__FILE__), "..", "lib", "toadhopper")
data/test/test_filter.rb CHANGED
@@ -1,28 +1,30 @@
1
- require File.dirname(__FILE__) + "/../lib/toadhopper"
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
2
 
3
- require 'test/unit'
4
-
5
- class Toadhopper::TestFilter < Test::Unit::TestCase
6
- def setup
7
- Toadhopper.filters = nil
3
+ class ToadHopper::Dispatcher::TestFilter < Test::Unit::TestCase
4
+ def dispatcher
5
+ @dispatcher ||= ToadHopper::Dispatcher.new("test api key")
8
6
  end
7
+
9
8
  def test_no_filters
10
9
  assert_equal( {:id => "myid", :password => "mypassword"},
11
- Toadhopper.filter(:id => "myid", :password => "mypassword"))
10
+ dispatcher.filter(:id => "myid", :password => "mypassword"))
12
11
  end
12
+
13
13
  def test_string_filter
14
- Toadhopper.filters = "pass"
14
+ dispatcher.filters = "pass"
15
15
  assert_equal( {:id => "myid", :password => "[FILTERED]"},
16
- Toadhopper.filter(:id => "myid", :password => "mypassword"))
16
+ dispatcher.filter(:id => "myid", :password => "mypassword"))
17
17
  end
18
+
18
19
  def test_regex_filter
19
- Toadhopper.filters = /pas{2}/
20
+ dispatcher.filters = /pas{2}/
20
21
  assert_equal( {:id => "myid", :password => "[FILTERED]"},
21
- Toadhopper.filter(:id => "myid", :password => "mypassword"))
22
+ dispatcher.filter(:id => "myid", :password => "mypassword"))
22
23
  end
24
+
23
25
  def test_multiple_filters
24
- Toadhopper.filters = "email", /pas{2}/
26
+ dispatcher.filters = "email", /pas{2}/
25
27
  assert_equal( {:id => "myid", :email => "[FILTERED]", :password => "[FILTERED]"},
26
- Toadhopper.filter(:id => "myid", :email => "myemail", :password => "mypassword"))
28
+ dispatcher.filter(:id => "myid", :email => "myemail", :password => "mypassword"))
27
29
  end
28
30
  end
@@ -0,0 +1,23 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ class ToadHopper::Dispatcher::TestPosting < Test::Unit::TestCase
4
+ def test_posting
5
+ dispatcher = ToadHopper::Dispatcher.new("abc123")
6
+ error = begin; raise "Kaboom!"; rescue => e; e end
7
+
8
+ response = dispatcher.post!(error)
9
+ assert_equal 422, response.status
10
+ assert_equal ['No project exists with the given API key.'], response.errors
11
+ end
12
+
13
+ if ENV['HOPTOAD_API_KEY']
14
+ def test_posting_integration
15
+ dispatcher = ToadHopper::Dispatcher.new(ENV['HOPTOAD_API_KEY'])
16
+ error = begin; raise "Kaboom!"; rescue => e; e end
17
+
18
+ response = dispatcher.post!(error)
19
+ assert_equal 200, response.status
20
+ assert_equal [], response.errors
21
+ end
22
+ end
23
+ end
data/test/test_setters.rb CHANGED
@@ -1,22 +1,18 @@
1
- require File.dirname(__FILE__) + "/../lib/toadhopper"
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
2
 
3
- require 'test/unit'
4
-
5
- class Toadhopper::TestSetters < Test::Unit::TestCase
6
- def setup
7
- Toadhopper.api_key = nil
8
- Toadhopper.filters = nil
9
- end
3
+ class ToadHopper::Dispatcher::TestSetters < Test::Unit::TestCase
10
4
  def test_setting_api_key
11
- Toadhopper.api_key = "abc123"
12
- assert_equal "abc123", Toadhopper.api_key
5
+ dispatcher = ToadHopper::Dispatcher.new('abc123')
6
+ assert_equal "abc123", dispatcher.api_key
13
7
  end
14
8
  def test_setting_single_filter
15
- Toadhopper.filters = /password/
16
- assert_equal [/password/], Toadhopper.filters
9
+ dispatcher = ToadHopper::Dispatcher.new('')
10
+ dispatcher.filters = /password/
11
+ assert_equal [/password/], dispatcher.filters
17
12
  end
18
13
  def test_setting_multple_filters
19
- Toadhopper.filters = /password/, /email/
20
- assert_equal [/password/, /email/], Toadhopper.filters
14
+ dispatcher = ToadHopper::Dispatcher.new('')
15
+ dispatcher.filters = /password/, /email/
16
+ assert_equal [/password/, /email/], dispatcher.filters
21
17
  end
22
18
  end
metadata CHANGED
@@ -1,40 +1,59 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toadhopper
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.7"
4
+ version: "0.8"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Lucas
8
+ - Samuel Tesla
9
+ - Corey Donohoe
8
10
  autorequire:
9
11
  bindir: bin
10
12
  cert_chain: []
11
13
 
12
- date: 2009-09-17 00:00:00 +10:00
14
+ date: 2009-12-15 00:00:00 +11:00
13
15
  default_executable:
14
- dependencies: []
15
-
16
- description: Post error notifications to Hoptoad
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: haml
19
+ type: :runtime
20
+ version_requirement:
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ~>
24
+ - !ruby/object:Gem::Version
25
+ version: "2.0"
26
+ version:
27
+ description:
17
28
  email: t.lucas@toolmantim.com
18
29
  executables: []
19
30
 
20
31
  extensions: []
21
32
 
22
33
  extra_rdoc_files:
23
- - Readme.md
34
+ - LICENSE
35
+ - README.md
24
36
  files:
37
+ - .gitignore
38
+ - .yardopts
39
+ - Gemfile
40
+ - LICENSE
25
41
  - README.md
26
42
  - Rakefile
27
- - LICENSE
43
+ - lib/backtrace.rb
44
+ - lib/notice.haml
28
45
  - lib/toadhopper.rb
46
+ - test/helper.rb
29
47
  - test/test_filter.rb
30
- - test/test_notice_params.rb
48
+ - test/test_posting.rb
31
49
  - test/test_setters.rb
32
- - Readme.md
33
50
  has_rdoc: true
34
51
  homepage: http://github.com/toolmantim/toadhopper
35
- post_install_message:
36
- rdoc_options: []
52
+ licenses: []
37
53
 
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --charset=UTF-8
38
57
  require_paths:
39
58
  - lib
40
59
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -51,10 +70,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
70
  version:
52
71
  requirements: []
53
72
 
54
- rubyforge_project: toadhopper
55
- rubygems_version: 1.3.1
73
+ rubyforge_project:
74
+ rubygems_version: 1.3.5
56
75
  signing_key:
57
- specification_version: 2
76
+ specification_version: 3
58
77
  summary: Post error notifications to Hoptoad
59
- test_files: []
60
-
78
+ test_files:
79
+ - test/helper.rb
80
+ - test/test_filter.rb
81
+ - test/test_posting.rb
82
+ - test/test_setters.rb
data/Readme.md DELETED
@@ -1,15 +0,0 @@
1
- A base library for [Hoptoad](http://www.hoptoadapp.com/) error reporting.
2
-
3
- Toadhopper can be used to report plain old Ruby exceptions, or to build a framework-specific gem such as [toadhopper-sinatra](http://github.com/toolmantim/toadhopper-sinatra).
4
-
5
- require 'toadhopper'
6
-
7
- Toadhopper.api_key = "YOURAPIKEY"
8
-
9
- error = begin; raise "Kaboom!"; rescue => e; e; end
10
-
11
- puts Toadhopper.post!(error)
12
-
13
- You can install it via rubygems:
14
-
15
- gem install toadhopper
@@ -1,29 +0,0 @@
1
- require File.dirname(__FILE__) + "/../lib/toadhopper"
2
-
3
- require 'test/unit'
4
-
5
- class Toadhopper::TestNoticeParams < Test::Unit::TestCase
6
- def test_notice_params
7
- Toadhopper.api_key = "abc123"
8
- error = begin; raise "Kaboom!"; rescue => e; e end
9
- def error.backtrace; ["backtrace line 1", "backtrace line 2"] end
10
- assert_equal({
11
- "api_key" => "abc123",
12
- "error_class" => "RuntimeError",
13
- "error_message" => "Kaboom!",
14
- "backtrace" => ["backtrace line 1", "backtrace line 2"],
15
- "request" => {"request_var" => "request_val"},
16
- "environment" => {"env_var" => "env_val"},
17
- "session" => {"session_var" => "session_val"},
18
- },
19
- Toadhopper.notice_params(
20
- error,
21
- {
22
- "request" => {"request_var" => "request_val"},
23
- "environment" => {"env_var" => "env_val"},
24
- "session" => {"session_var" => "session_val"}
25
- }
26
- )
27
- )
28
- end
29
- end