toadhopper 0.8 → 0.9
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 +6 -7
- data/README.md +6 -7
- data/Rakefile +3 -1
- data/lib/notice.haml +2 -2
- data/lib/toadhopper.rb +107 -130
- data/test/test_filters.rb +30 -0
- data/test/test_posting.rb +5 -5
- metadata +3 -6
- data/lib/backtrace.rb +0 -31
- data/test/test_filter.rb +0 -30
- data/test/test_setters.rb +0 -18
data/.gitignore
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
/
|
2
|
-
|
3
|
-
/
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
toadhopper.gemspec
|
1
|
+
/pkg
|
2
|
+
/.yardoc
|
3
|
+
/vendor
|
4
|
+
/bin
|
5
|
+
/doc
|
6
|
+
/toadhopper.gemspec
|
data/README.md
CHANGED
@@ -2,13 +2,12 @@ A base library for [Hoptoad](http://www.hoptoadapp.com/) error reporting.
|
|
2
2
|
|
3
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
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
puts dispatcher.post!(error)
|
5
|
+
begin
|
6
|
+
raise "Kaboom!"
|
7
|
+
rescue => e
|
8
|
+
require 'toadhopper'
|
9
|
+
ToadHopper.new("YOURAPIKEY").post!(e)
|
10
|
+
end
|
12
11
|
|
13
12
|
You can install it via rubygems:
|
14
13
|
|
data/Rakefile
CHANGED
@@ -14,9 +14,11 @@ Jeweler::Tasks.new do |s|
|
|
14
14
|
s.email = "t.lucas@toolmantim.com"
|
15
15
|
s.homepage = "http://github.com/toolmantim/toadhopper"
|
16
16
|
s.authors = ["Tim Lucas", "Samuel Tesla", "Corey Donohoe"]
|
17
|
-
s.version = "0.8"
|
18
17
|
s.extra_rdoc_files = ["README.md", "LICENSE"]
|
19
18
|
s.executables = nil # stops jeweler automatically adding bin/*
|
19
|
+
|
20
|
+
require File.join(File.dirname(__FILE__), 'lib', 'toadhopper')
|
21
|
+
s.version = ToadHopper::VERSION
|
20
22
|
|
21
23
|
require 'bundler'
|
22
24
|
bundler_env = Bundler::Environment.load(File.dirname(__FILE__) + '/Gemfile')
|
data/lib/notice.haml
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
%notifier
|
5
5
|
%name= notifier_name
|
6
6
|
%version= notifier_version
|
7
|
-
%url
|
7
|
+
%url= notifier_url
|
8
8
|
%error
|
9
9
|
%class= error.class.name
|
10
10
|
%message= "#{error.class.name}: #{error.message}"
|
@@ -30,5 +30,5 @@
|
|
30
30
|
%var{:key => key}= value
|
31
31
|
|
32
32
|
%server-environment
|
33
|
-
%project-root=
|
33
|
+
%project-root= project_root
|
34
34
|
%environment-name= framework_env
|
data/lib/toadhopper.rb
CHANGED
@@ -1,153 +1,130 @@
|
|
1
|
-
root = File.expand_path(File.dirname(__FILE__))
|
2
1
|
require 'net/http'
|
3
2
|
require 'haml'
|
4
3
|
require 'haml/engine'
|
5
4
|
require 'nokogiri'
|
6
|
-
require File.join(root, 'backtrace')
|
7
5
|
|
8
|
-
|
6
|
+
# Posts errors to the Hoptoad API
|
7
|
+
class ToadHopper
|
8
|
+
VERSION = "0.9"
|
9
|
+
|
9
10
|
# Hoptoad API response
|
10
11
|
class Response < Struct.new(:status, :body, :errors); end
|
11
12
|
|
12
|
-
|
13
|
-
class Dispatcher
|
14
|
-
attr_reader :api_key
|
13
|
+
attr_reader :api_key
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
15
|
+
def initialize(api_key)
|
16
|
+
@api_key = api_key
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sets patterns to +[FILTER]+ out sensitive data such as +/password/+, +/email/+ and +/credit_card_number/+
|
20
|
+
def filters=(*filters)
|
21
|
+
@filters = filters.flatten
|
22
|
+
end
|
33
23
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
24
|
+
# @private
|
25
|
+
def filters
|
26
|
+
[@filters].flatten.compact
|
27
|
+
end
|
54
28
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
29
|
+
# Posts an exception to hoptoad.
|
30
|
+
# Toadhopper.new('apikey').post!(error, {:action => 'show', :controller => 'Users'})
|
31
|
+
# The Following Keys are available as parameters to the document_options
|
32
|
+
# url The url for the request, required to post but not useful in a console environment
|
33
|
+
# component Normally this is your Controller name in an MVC framework
|
34
|
+
# action Normally the action for your request in an MVC framework
|
35
|
+
# request An object that response to #params and returns a hash
|
36
|
+
# notifier_name Say you're a different notifier than ToadHopper
|
37
|
+
# notifier_version Specify the version of your custom notifier
|
38
|
+
# notifier_url Specify the project URL of your custom notifier
|
39
|
+
# session A hash of the user session in a web request
|
40
|
+
# framework_env The framework environment your app is running under
|
41
|
+
# backtrace Normally not needed, parsed automatically from the provided exception parameter
|
42
|
+
# environment You MUST scrub your environment if you plan to use this, please do not use it though. :)
|
43
|
+
# project_root The root directory of your app
|
44
|
+
#
|
45
|
+
# @return Toadhopper::Response
|
46
|
+
def post!(error, document_options={}, header_options={})
|
47
|
+
post_document(document_for(error, document_options), header_options)
|
48
|
+
end
|
62
49
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
50
|
+
# Posts a v2 document error to Hoptoad
|
51
|
+
# header_options can be passed in to indicate you're posting from a separate client
|
52
|
+
# Toadhopper.new('API KEY').post_document(doc, 'X-Hoptoad-Client-Name' => 'MyCustomLib')
|
53
|
+
#
|
54
|
+
# @private
|
55
|
+
def post_document(document, header_options={})
|
56
|
+
uri = URI.parse("http://hoptoadapp.com:80/notifier_api/v2/notices")
|
57
|
+
|
58
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
59
|
+
headers = {
|
60
|
+
'Content-type' => 'text/xml',
|
61
|
+
'Accept' => 'text/xml, application/xml',
|
62
|
+
'X-Hoptoad-Client-Name' => 'Toadhopper',
|
63
|
+
}.merge(header_options)
|
64
|
+
http.read_timeout = 5 # seconds
|
65
|
+
http.open_timeout = 2 # seconds
|
66
|
+
begin
|
67
|
+
response = http.post(uri.path, document, headers)
|
68
|
+
Response.new response.code.to_i,
|
69
|
+
response.body,
|
70
|
+
Nokogiri::XML.parse(response.body).xpath('//errors/error').map {|e| e.content}
|
71
|
+
rescue TimeoutError => e
|
72
|
+
Response.new(500, '', ['Timeout error'])
|
77
73
|
end
|
78
74
|
end
|
75
|
+
end
|
79
76
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
77
|
+
# @private
|
78
|
+
def document_for(exception, options={})
|
79
|
+
locals = {
|
80
|
+
:error => exception,
|
81
|
+
:api_key => api_key,
|
82
|
+
:environment => clean(ENV.to_hash),
|
83
|
+
:backtrace => exception.backtrace.map {|l| backtrace_line(l)},
|
84
|
+
:url => 'http://localhost/',
|
85
|
+
:component => 'http://localhost/',
|
86
|
+
:action => nil,
|
87
|
+
:request => nil,
|
88
|
+
:notifier_name => 'ToadHopper',
|
89
|
+
:notifier_version => VERSION,
|
90
|
+
:notifier_url => 'http://github.com/toolmantim/toadhopper',
|
91
|
+
:session => {},
|
92
|
+
:framework_env => ENV['RACK_ENV'] || 'development',
|
93
|
+
:project_root => Dir.pwd
|
94
|
+
}.merge(options)
|
95
|
+
|
96
|
+
Haml::Engine.new(notice_template).render(Object.new, locals)
|
97
|
+
end
|
98
|
+
|
99
|
+
# @private
|
100
|
+
def backtrace_line(line)
|
101
|
+
Struct.new(:file, :number, :method).new(*line.match(%r{^([^:]+):(\d+)(?::in `([^']+)')?$}).captures)
|
102
|
+
end
|
103
|
+
|
104
|
+
# @private
|
105
|
+
def notice_template
|
106
|
+
File.read(::File.join(::File.dirname(__FILE__), 'notice.haml'))
|
107
|
+
end
|
110
108
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
109
|
+
# @private
|
110
|
+
def clean(hash)
|
111
|
+
hash.inject({}) do |acc, (k, v)|
|
112
|
+
acc[k] = (v.is_a?(Hash) ? clean(v) : filtered_value(k,v)) if serializable?(v)
|
115
113
|
acc
|
116
|
-
end
|
117
|
-
end
|
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))
|
129
|
-
end
|
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
|
135
|
-
h
|
136
|
-
end
|
137
114
|
end
|
115
|
+
end
|
138
116
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
value
|
145
|
-
value.is_a?(Bignum)
|
117
|
+
# @private
|
118
|
+
def filtered_value(key, value)
|
119
|
+
if filters.any? {|f| key.to_s =~ Regexp.new(f)}
|
120
|
+
"[FILTERED]"
|
121
|
+
else
|
122
|
+
value
|
146
123
|
end
|
124
|
+
end
|
147
125
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
end
|
126
|
+
# @private
|
127
|
+
def serializable?(value)
|
128
|
+
[Fixnum, Array, String, Hash, Bignum].any? {|c| value.is_a?(c)}
|
152
129
|
end
|
153
130
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
+
|
3
|
+
class ToadHopper::TestFilters < Test::Unit::TestCase
|
4
|
+
def toadhopper
|
5
|
+
@toadhopper ||= ToadHopper.new("test api key")
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_no_filters
|
9
|
+
assert_equal( {:id => "myid", :password => "mypassword"},
|
10
|
+
toadhopper.clean(:id => "myid", :password => "mypassword"))
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_string_filter
|
14
|
+
toadhopper.filters = "pass"
|
15
|
+
assert_equal( {:id => "myid", :password => "[FILTERED]"},
|
16
|
+
toadhopper.clean(:id => "myid", :password => "mypassword"))
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_regex_filter
|
20
|
+
toadhopper.filters = /pas{2}/
|
21
|
+
assert_equal( {:id => "myid", :password => "[FILTERED]"},
|
22
|
+
toadhopper.clean(:id => "myid", :password => "mypassword"))
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_multiple_filters
|
26
|
+
toadhopper.filters = "email", /pas{2}/
|
27
|
+
assert_equal( {:id => "myid", :email => "[FILTERED]", :password => "[FILTERED]"},
|
28
|
+
toadhopper.clean(:id => "myid", :email => "myemail", :password => "mypassword"))
|
29
|
+
end
|
30
|
+
end
|
data/test/test_posting.rb
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
2
|
|
3
|
-
class ToadHopper::
|
3
|
+
class ToadHopper::TestPosting < Test::Unit::TestCase
|
4
4
|
def test_posting
|
5
|
-
|
5
|
+
toadhopper = ToadHopper.new("abc123")
|
6
6
|
error = begin; raise "Kaboom!"; rescue => e; e end
|
7
7
|
|
8
|
-
response =
|
8
|
+
response = toadhopper.post!(error)
|
9
9
|
assert_equal 422, response.status
|
10
10
|
assert_equal ['No project exists with the given API key.'], response.errors
|
11
11
|
end
|
12
12
|
|
13
13
|
if ENV['HOPTOAD_API_KEY']
|
14
14
|
def test_posting_integration
|
15
|
-
|
15
|
+
toadhopper = ToadHopper.new(ENV['HOPTOAD_API_KEY'])
|
16
16
|
error = begin; raise "Kaboom!"; rescue => e; e end
|
17
17
|
|
18
|
-
response =
|
18
|
+
response = toadhopper.post!(error)
|
19
19
|
assert_equal 200, response.status
|
20
20
|
assert_equal [], response.errors
|
21
21
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: toadhopper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: "0.
|
4
|
+
version: "0.9"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Lucas
|
@@ -40,13 +40,11 @@ files:
|
|
40
40
|
- LICENSE
|
41
41
|
- README.md
|
42
42
|
- Rakefile
|
43
|
-
- lib/backtrace.rb
|
44
43
|
- lib/notice.haml
|
45
44
|
- lib/toadhopper.rb
|
46
45
|
- test/helper.rb
|
47
|
-
- test/
|
46
|
+
- test/test_filters.rb
|
48
47
|
- test/test_posting.rb
|
49
|
-
- test/test_setters.rb
|
50
48
|
has_rdoc: true
|
51
49
|
homepage: http://github.com/toolmantim/toadhopper
|
52
50
|
licenses: []
|
@@ -77,6 +75,5 @@ specification_version: 3
|
|
77
75
|
summary: Post error notifications to Hoptoad
|
78
76
|
test_files:
|
79
77
|
- test/helper.rb
|
80
|
-
- test/
|
78
|
+
- test/test_filters.rb
|
81
79
|
- test/test_posting.rb
|
82
|
-
- test/test_setters.rb
|
data/lib/backtrace.rb
DELETED
@@ -1,31 +0,0 @@
|
|
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/test/test_filter.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
-
|
3
|
-
class ToadHopper::Dispatcher::TestFilter < Test::Unit::TestCase
|
4
|
-
def dispatcher
|
5
|
-
@dispatcher ||= ToadHopper::Dispatcher.new("test api key")
|
6
|
-
end
|
7
|
-
|
8
|
-
def test_no_filters
|
9
|
-
assert_equal( {:id => "myid", :password => "mypassword"},
|
10
|
-
dispatcher.filter(:id => "myid", :password => "mypassword"))
|
11
|
-
end
|
12
|
-
|
13
|
-
def test_string_filter
|
14
|
-
dispatcher.filters = "pass"
|
15
|
-
assert_equal( {:id => "myid", :password => "[FILTERED]"},
|
16
|
-
dispatcher.filter(:id => "myid", :password => "mypassword"))
|
17
|
-
end
|
18
|
-
|
19
|
-
def test_regex_filter
|
20
|
-
dispatcher.filters = /pas{2}/
|
21
|
-
assert_equal( {:id => "myid", :password => "[FILTERED]"},
|
22
|
-
dispatcher.filter(:id => "myid", :password => "mypassword"))
|
23
|
-
end
|
24
|
-
|
25
|
-
def test_multiple_filters
|
26
|
-
dispatcher.filters = "email", /pas{2}/
|
27
|
-
assert_equal( {:id => "myid", :email => "[FILTERED]", :password => "[FILTERED]"},
|
28
|
-
dispatcher.filter(:id => "myid", :email => "myemail", :password => "mypassword"))
|
29
|
-
end
|
30
|
-
end
|
data/test/test_setters.rb
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
-
|
3
|
-
class ToadHopper::Dispatcher::TestSetters < Test::Unit::TestCase
|
4
|
-
def test_setting_api_key
|
5
|
-
dispatcher = ToadHopper::Dispatcher.new('abc123')
|
6
|
-
assert_equal "abc123", dispatcher.api_key
|
7
|
-
end
|
8
|
-
def test_setting_single_filter
|
9
|
-
dispatcher = ToadHopper::Dispatcher.new('')
|
10
|
-
dispatcher.filters = /password/
|
11
|
-
assert_equal [/password/], dispatcher.filters
|
12
|
-
end
|
13
|
-
def test_setting_multple_filters
|
14
|
-
dispatcher = ToadHopper::Dispatcher.new('')
|
15
|
-
dispatcher.filters = /password/, /email/
|
16
|
-
assert_equal [/password/, /email/], dispatcher.filters
|
17
|
-
end
|
18
|
-
end
|