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