bug_hunter 0.0.1

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.
@@ -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