bug_hunter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,8 @@
1
+ module BugHunter
2
+ module RoutesHelper
3
+ def error_path(error)
4
+ "#{ENV["BUGHUNTER_PATH"]}/errors/#{error.id}"
5
+ end
6
+ end
7
+ end
8
+
@@ -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