bug_hunter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +64 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/bin/bug_hunter +33 -0
- data/bug_hunter.gemspec +104 -0
- data/config.ru +5 -0
- data/lib/bug_hunter.rb +50 -0
- data/lib/bug_hunter/app.rb +108 -0
- data/lib/bug_hunter/config.rb +17 -0
- data/lib/bug_hunter/middleware.rb +28 -0
- data/lib/bug_hunter/models.rb +167 -0
- data/lib/bug_hunter/routes_helper.rb +8 -0
- data/lib/bug_hunter/ui_helper.rb +44 -0
- data/lib/bug_hunter/views/errors/_error_info.haml +89 -0
- data/lib/bug_hunter/views/errors/assign.haml +24 -0
- data/lib/bug_hunter/views/errors/show.haml +76 -0
- data/lib/bug_hunter/views/index.haml +23 -0
- data/lib/bug_hunter/views/layout.haml +27 -0
- data/public/javascripts/bug_hunter.js +4 -0
- data/public/javascripts/jquery.mobile-1.0b1pre.min.js +140 -0
- data/public/stylesheets/highlight.css +57 -0
- data/public/stylesheets/images/ajax-loader.png +0 -0
- data/public/stylesheets/images/icon-search-black.png +0 -0
- data/public/stylesheets/images/icons-18-black.png +0 -0
- data/public/stylesheets/images/icons-18-white.png +0 -0
- data/public/stylesheets/images/icons-36-black.png +0 -0
- data/public/stylesheets/images/icons-36-white.png +0 -0
- data/public/stylesheets/jquery.mobile-1.0b1pre.min.css +9 -0
- data/spec/bug_hunter_spec.rb +7 -0
- data/spec/spec_helper.rb +12 -0
- metadata +254 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module BugHunter
|
2
|
+
def self.config_path
|
3
|
+
Dir.home+"/.bughunterrc"
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.config
|
7
|
+
@config ||= YAML.load_file(self.config_path)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
if !File.exist?(BugHunter.config_path)
|
12
|
+
File.open(BugHunter.config_path, "w") do |f|
|
13
|
+
f.write YAML.dump("username" => "admin", "password" => "admin", "enable_auth" => true)
|
14
|
+
end
|
15
|
+
|
16
|
+
$stdout.puts "Created #{BugHunter.config_path} with username=admin password=admin"
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module BugHunter
|
2
|
+
class Middleware
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
response = nil
|
9
|
+
begin
|
10
|
+
response = @app.call(env)
|
11
|
+
rescue StandardError, LoadError, SyntaxError => e
|
12
|
+
error = BugHunter::Error.build_from(env, e)
|
13
|
+
|
14
|
+
if !error.valid? && !error.errors[:uniqueness].empty?
|
15
|
+
BugHunter::Error.collection.update(error.unique_error_selector,
|
16
|
+
{:$inc => {:times => 1}, :$set => {:updated_at => Time.now.utc}},
|
17
|
+
{:multi => true})
|
18
|
+
else
|
19
|
+
error.save
|
20
|
+
end
|
21
|
+
|
22
|
+
raise e
|
23
|
+
end
|
24
|
+
|
25
|
+
response
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module BugHunter
|
2
|
+
class Error
|
3
|
+
include Mongoid::Document
|
4
|
+
include Mongoid::Timestamps
|
5
|
+
|
6
|
+
field :is_rails, :type => Boolean, :default => false
|
7
|
+
field :message, :type => String, :required => true
|
8
|
+
field :backtrace, :type => Array, :required => true
|
9
|
+
field :url, :type => String, :required => true
|
10
|
+
field :params, :type => Hash, :required => true
|
11
|
+
|
12
|
+
field :file, :type => String, :required => true
|
13
|
+
field :line, :type => Integer, :required => true
|
14
|
+
field :method, :type => String
|
15
|
+
field :line_content, :type => String
|
16
|
+
|
17
|
+
field :request_env, :type => Hash, :required => true
|
18
|
+
|
19
|
+
field :times, :type => Integer, :default => 1
|
20
|
+
|
21
|
+
field :action, :type => String
|
22
|
+
field :controller, :type => String
|
23
|
+
field :assignee, :type => String
|
24
|
+
|
25
|
+
field :resolved, :type => Boolean, :default => false
|
26
|
+
|
27
|
+
field :comments, :type => Array, :default => []
|
28
|
+
field :comments_count
|
29
|
+
|
30
|
+
index :message
|
31
|
+
index [
|
32
|
+
[:message, Mongo::ASCENDING],
|
33
|
+
[:file, Mongo::ASCENDING],
|
34
|
+
[:line, Mongo::ASCENDING],
|
35
|
+
[:method, Mongo::ASCENDING]
|
36
|
+
]
|
37
|
+
|
38
|
+
after_create :update_project
|
39
|
+
|
40
|
+
validate :message do
|
41
|
+
if BugHunter::Error.where(unique_error_selector).only(:_id).first
|
42
|
+
errors.add(:uniqueness, "This error is not unique")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.minimal
|
47
|
+
without(:comments, :request_env, :backtrace)
|
48
|
+
end
|
49
|
+
|
50
|
+
def similar_errors
|
51
|
+
self.class.where(:message => unique_error_selector[:message], :_id.ne => self.id)
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_comment(from, message, ip)
|
55
|
+
comment = {:from => from,
|
56
|
+
:message => message,
|
57
|
+
:created_at => Time.now.utc,
|
58
|
+
:ip => ip}
|
59
|
+
|
60
|
+
self.collection.update({:_id => self.id},
|
61
|
+
{:$push => {:comments => comment},
|
62
|
+
:$inc => {:comments_count => 1}},
|
63
|
+
{:multi => true})
|
64
|
+
end
|
65
|
+
|
66
|
+
def resolve!
|
67
|
+
self.collection.update({:_id => self.id},
|
68
|
+
{:$set => {:resolved => true, :updated_at => Time.now.utc}},
|
69
|
+
{:multi => true})
|
70
|
+
BugHunter::Project.collection.update({:_id => BugHunter::Project.instance.id},
|
71
|
+
{:$inc => {:errors_resolved_count => 1}})
|
72
|
+
end
|
73
|
+
|
74
|
+
def unique_error_selector
|
75
|
+
msg = self[:message]
|
76
|
+
if msg.match(/#<.+>/)
|
77
|
+
msg = /^#{Regexp.escape($`)}/
|
78
|
+
end
|
79
|
+
|
80
|
+
{
|
81
|
+
:message => msg,
|
82
|
+
:file => self.file,
|
83
|
+
:line => self.line,
|
84
|
+
:method => self.method
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def self.build_from(env, exception)
|
90
|
+
doc = self.new
|
91
|
+
doc[:message] = exception.message
|
92
|
+
doc[:backtrace] = exception.backtrace
|
93
|
+
|
94
|
+
env = env.dup.delete_if {|k,v| k.include?(".") }
|
95
|
+
doc[:request_env] = env
|
96
|
+
|
97
|
+
scheme = if env['HTTP_VERSION'] =~ /^HTTPS/i
|
98
|
+
"https://"
|
99
|
+
else
|
100
|
+
"http://"
|
101
|
+
end
|
102
|
+
|
103
|
+
url = "#{scheme}#{env["HTTP_HOST"]}#{env["REQUEST_PATH"]}"
|
104
|
+
params = {}
|
105
|
+
if env["QUERY_STRING"] && !env["QUERY_STRING"].empty?
|
106
|
+
url << "?#{env["QUERY_STRING"]}"
|
107
|
+
|
108
|
+
env["QUERY_STRING"].split("&").each do |e|
|
109
|
+
k,v = e.split("=")
|
110
|
+
params[k] = v
|
111
|
+
end
|
112
|
+
end
|
113
|
+
doc[:url] = url
|
114
|
+
if defined?(Rails)
|
115
|
+
doc[:is_rails] = true
|
116
|
+
doc[:action] = params[:action]
|
117
|
+
doc[:controller] = params[:controller]
|
118
|
+
end
|
119
|
+
|
120
|
+
doc[:params] = params
|
121
|
+
|
122
|
+
exception.backtrace.each do |line|
|
123
|
+
if line !~ /\/usr/ && line =~ /^(.+):(\d+):in `(.+)'/ # I need better way to detect this
|
124
|
+
doc[:file] = $1
|
125
|
+
doc[:line] = $2.to_i
|
126
|
+
doc[:method] = $3
|
127
|
+
|
128
|
+
doc[:line_content] = File.open(doc[:file]).readlines[doc[:line]-1]
|
129
|
+
break
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
doc
|
135
|
+
end
|
136
|
+
|
137
|
+
def update_project
|
138
|
+
BugHunter::Project.collection.update({:_id => BugHunter::Project.instance.id},
|
139
|
+
{:$inc => {:errors_count => 1}})
|
140
|
+
end
|
141
|
+
end # Error
|
142
|
+
|
143
|
+
|
144
|
+
class Project
|
145
|
+
include Mongoid::Document
|
146
|
+
include Mongoid::Timestamps
|
147
|
+
|
148
|
+
field :name, :type => String
|
149
|
+
field :errors_count, :type => Integer, :default => 0
|
150
|
+
field :errors_resolved_count, :type => Integer, :default => 0
|
151
|
+
field :members, :type => Array, :default => []
|
152
|
+
|
153
|
+
validate :on => :create do
|
154
|
+
errors.add(:singleton, "You can't create for than one instance of this model") if BugHunter::Project.first.present?
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.instance
|
158
|
+
if project = BugHunter::Project.first
|
159
|
+
project
|
160
|
+
else
|
161
|
+
BugHunter::Project.create
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
protected
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module BugHunter
|
2
|
+
module UiHelper
|
3
|
+
def title(v)
|
4
|
+
@title = v
|
5
|
+
end
|
6
|
+
|
7
|
+
def content_for(key, &block)
|
8
|
+
sections[key] = capture_haml(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def content(key)
|
12
|
+
if section = sections[key]
|
13
|
+
section.respond_to?(:join) ? section.join : section
|
14
|
+
else
|
15
|
+
""
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def content_tag(name, options={}, &block)
|
20
|
+
"<#{name} #{options.map{|k,v| "#{k}=#{v}" }.join(" ")}>#{block.call}</#{name}>"
|
21
|
+
end
|
22
|
+
|
23
|
+
# list_view(my_collection) {|e| [e.url, e.name, "some extra content"]}
|
24
|
+
def list_view(list = [], options = {}, &_block)
|
25
|
+
content_tag(:ul, :"data-role"=>"listview", :"data-filter"=>options[:filter]||false) do
|
26
|
+
list.map do |e|
|
27
|
+
content_tag :li do
|
28
|
+
url, content, extra = _block.call(e)
|
29
|
+
|
30
|
+
content_tag(:a, :href => url) do
|
31
|
+
content
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end.join
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def sections
|
40
|
+
@sections ||= Hash.new {|k,v| k[v] = [] }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
%table
|
2
|
+
%tr
|
3
|
+
%td
|
4
|
+
%b
|
5
|
+
Asignee:
|
6
|
+
%td
|
7
|
+
&=@error.assignee
|
8
|
+
%a(href="#{error_path(@error)}/assign" data-icon="gear" data-rel="dialog")
|
9
|
+
change...
|
10
|
+
%tr
|
11
|
+
%td
|
12
|
+
%b
|
13
|
+
Message:
|
14
|
+
%td
|
15
|
+
&=@error.message
|
16
|
+
%tr
|
17
|
+
%td
|
18
|
+
%b
|
19
|
+
Times:
|
20
|
+
%td
|
21
|
+
&=@error.times
|
22
|
+
%tr
|
23
|
+
%td
|
24
|
+
%b
|
25
|
+
URL:
|
26
|
+
%td
|
27
|
+
%a(href="#{@error.url}")
|
28
|
+
&=@error.url
|
29
|
+
%tr
|
30
|
+
%td
|
31
|
+
%b
|
32
|
+
Params:
|
33
|
+
%td
|
34
|
+
&=@error.params.inspect
|
35
|
+
|
36
|
+
-if @error.action
|
37
|
+
%tr
|
38
|
+
%td
|
39
|
+
%b
|
40
|
+
Action:
|
41
|
+
%td
|
42
|
+
&=@error.action
|
43
|
+
|
44
|
+
-if @error.controller
|
45
|
+
%tr
|
46
|
+
%td
|
47
|
+
%b
|
48
|
+
Controller:
|
49
|
+
%td
|
50
|
+
&=@error.controller
|
51
|
+
|
52
|
+
%tr
|
53
|
+
%td
|
54
|
+
%b
|
55
|
+
File:
|
56
|
+
%td
|
57
|
+
&=@error.file
|
58
|
+
%tr
|
59
|
+
%td
|
60
|
+
%b
|
61
|
+
Line:
|
62
|
+
%td
|
63
|
+
&=@error.line
|
64
|
+
|
65
|
+
%tr
|
66
|
+
%td
|
67
|
+
%b
|
68
|
+
Method:
|
69
|
+
%td
|
70
|
+
&=@error.method
|
71
|
+
|
72
|
+
%tr
|
73
|
+
%td
|
74
|
+
%b
|
75
|
+
Content:
|
76
|
+
%td
|
77
|
+
&=@error.line_content
|
78
|
+
%tr
|
79
|
+
%td
|
80
|
+
%b
|
81
|
+
Created At:
|
82
|
+
%td
|
83
|
+
&=@error.created_at
|
84
|
+
%tr
|
85
|
+
%td
|
86
|
+
%b
|
87
|
+
Updated At:
|
88
|
+
%td
|
89
|
+
&=@error.updated_at
|
@@ -0,0 +1,24 @@
|
|
1
|
+
-title "Select the member to assign..."
|
2
|
+
|
3
|
+
-content_for :body do
|
4
|
+
%h3
|
5
|
+
Add a new member
|
6
|
+
|
7
|
+
%form(action="#{ENV["BUGHUNTER_PATH"]}/add_member" method="post")
|
8
|
+
%input(type="hidden" name="assign_to" value="#{@error.id}")
|
9
|
+
%div(data-role="fieldcontain")
|
10
|
+
%label(for="name")
|
11
|
+
Name or Email:
|
12
|
+
%input(type="text" name="name" id="name")
|
13
|
+
|
14
|
+
%input(type="submit" value="Add")
|
15
|
+
|
16
|
+
|
17
|
+
-if BugHunter::Project.instance.members.count > 0
|
18
|
+
%h3
|
19
|
+
Or select the member:
|
20
|
+
-(BugHunter::Project.instance.members||[]).each do |member|
|
21
|
+
%a(href="#{error_path(@error)}/assign_to?member=#{member}" data-role="button" data-theme="c")
|
22
|
+
&=member
|
23
|
+
|
24
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
-title "Error: #{@error.message}"
|
2
|
+
|
3
|
+
-content_for :body do
|
4
|
+
=haml :"errors/_error_info"
|
5
|
+
|
6
|
+
%div(data-role="collapsible" data-collapsed=true)
|
7
|
+
%h3
|
8
|
+
Backtrace
|
9
|
+
-@error.backtrace.each do |line|
|
10
|
+
-if line !~ /\/usr/
|
11
|
+
%span.fk_key
|
12
|
+
&=line
|
13
|
+
-else
|
14
|
+
&=line
|
15
|
+
%br
|
16
|
+
%div(data-role="collapsible" data-collapsed=true)
|
17
|
+
%h3
|
18
|
+
Environment
|
19
|
+
%table
|
20
|
+
-@error.request_env.each do |k,v|
|
21
|
+
%tr
|
22
|
+
-if k =~ /^HTTP_/
|
23
|
+
%td.name_key
|
24
|
+
&="#{k}"
|
25
|
+
%td
|
26
|
+
&=v
|
27
|
+
-elsif k =~ /^SERVER_/
|
28
|
+
%td.pk_key
|
29
|
+
&="#{k}"
|
30
|
+
%td
|
31
|
+
&=v
|
32
|
+
-elsif k =~ /^REQUEST_/
|
33
|
+
%td.type_key
|
34
|
+
&="#{k}"
|
35
|
+
%td
|
36
|
+
&=v
|
37
|
+
-else
|
38
|
+
%td
|
39
|
+
&="#{k}"
|
40
|
+
%td
|
41
|
+
&="#{v}"
|
42
|
+
-if @error.similar_errors.count > 0
|
43
|
+
%div(data-role="collapsible" data-collapsed=true)
|
44
|
+
%h3
|
45
|
+
Similar Errors
|
46
|
+
%br
|
47
|
+
=list_view(@error.similar_errors.all) { |error| [error_path(error), h(error.message)] }
|
48
|
+
%br
|
49
|
+
|
50
|
+
%div(data-role="collapsible" data-collapsed=true)
|
51
|
+
%h3
|
52
|
+
Comments
|
53
|
+
-(@error.comments||[]).each do |comment|
|
54
|
+
%div(data-role="collapsible" data-collapsed=false data-theme="b")
|
55
|
+
%h3
|
56
|
+
&= "#{comment["from"]}[#{comment["ip"]}] said on #{comment["created_at"]}"
|
57
|
+
&= comment["message"]
|
58
|
+
|
59
|
+
%hr
|
60
|
+
%h3
|
61
|
+
Add Comment
|
62
|
+
%form(action="#{error_path(@error)}/comment" method="post")
|
63
|
+
%div(data-role="fieldcontain")
|
64
|
+
%label(for="from")
|
65
|
+
From:
|
66
|
+
%input(type="text" name="from" id="from")
|
67
|
+
%div(data-role="fieldcontain")
|
68
|
+
%label(for="message")
|
69
|
+
Message:
|
70
|
+
%textarea(cols="40" rows="8" id="message" name="message")
|
71
|
+
|
72
|
+
%input(type="submit" value="Submit")
|
73
|
+
|
74
|
+
-content_for :footer do
|
75
|
+
%a(href="#{error_path(@error)}/resolve" data-icon="check")
|
76
|
+
Resolve this error
|