toadhopper 0.7 → 0.8

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