better_errors 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -2,11 +2,18 @@
2
2
 
3
3
  Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails.
4
4
 
5
- ![image](http://i.imgur.com/1GepF.png)
5
+ ![image](http://i.imgur.com/quHUZ.png)
6
+
7
+ ## Features
8
+
9
+ * Full stack trace
10
+ * Source code inspection for all stack frames (with highlighting)
11
+ * Local and instance variable inspection
12
+ * Ruby console on every stack frame
6
13
 
7
14
  ## Installation
8
15
 
9
- Add this line to your application's Gemfile:
16
+ Add this line to your application's Gemfile (under the **development** group):
10
17
 
11
18
  gem 'better_errors'
12
19
 
@@ -7,10 +7,7 @@ class Exception
7
7
  unless Thread.current[:__better_errors_exception_lock]
8
8
  Thread.current[:__better_errors_exception_lock] = true
9
9
  begin
10
- @__better_errors_bindings_stack = []
11
- 2.upto(caller.size) do |index|
12
- @__better_errors_bindings_stack << binding.of_caller(index) rescue break
13
- end
10
+ @__better_errors_bindings_stack = binding.callers.drop(1)
14
11
  ensure
15
12
  Thread.current[:__better_errors_exception_lock] = false
16
13
  end
@@ -3,17 +3,22 @@ module BetterErrors
3
3
  def self.from_exception(exception)
4
4
  exception.backtrace.each_with_index.map { |frame, idx|
5
5
  next unless frame =~ /\A(.*):(\d*):in `(.*)'\z/
6
- ErrorFrame.new($1, $2.to_i, $3, exception.__better_errors_bindings_stack[idx])
6
+ if BetterErrors.binding_of_caller_available?
7
+ b = exception.__better_errors_bindings_stack[idx]
8
+ end
9
+ ErrorFrame.new($1, $2.to_i, $3, b)
7
10
  }.compact
8
11
  end
9
12
 
10
13
  attr_reader :filename, :line, :name, :frame_binding
11
14
 
12
- def initialize(filename, line, name, frame_binding)
15
+ def initialize(filename, line, name, frame_binding = nil)
13
16
  @filename = filename
14
17
  @line = line
15
18
  @name = name
16
19
  @frame_binding = frame_binding
20
+
21
+ set_pretty_method_name if frame_binding
17
22
  end
18
23
 
19
24
  def application?
@@ -56,7 +61,14 @@ module BetterErrors
56
61
 
57
62
  def local_variables
58
63
  return {} unless frame_binding
59
- Hash[frame_binding.eval("local_variables").map { |x| [x, frame_binding.eval(x.to_s)] }]
64
+ frame_binding.eval("local_variables").each_with_object({}) do |name, hash|
65
+ begin
66
+ hash[name] = frame_binding.eval(name.to_s)
67
+ rescue NameError => e
68
+ # local_variables sometimes returns broken variables.
69
+ # https://bugs.ruby-lang.org/issues/7536
70
+ end
71
+ end
60
72
  end
61
73
 
62
74
  def instance_variables
@@ -65,6 +77,17 @@ module BetterErrors
65
77
  end
66
78
 
67
79
  private
80
+ def set_pretty_method_name
81
+ name =~ /\A(block (\([^)]+\) )?in )?/
82
+ recv = frame_binding.eval("self")
83
+ method = frame_binding.eval("__method__")
84
+ @name = if recv.is_a? Module
85
+ "#{$1}#{recv}.#{method}"
86
+ else
87
+ "#{$1}#{recv.class}##{method}"
88
+ end
89
+ end
90
+
68
91
  def starts_with?(haystack, needle)
69
92
  haystack[0, needle.length] == needle
70
93
  end
@@ -2,12 +2,12 @@ require "json"
2
2
 
3
3
  module BetterErrors
4
4
  class ErrorPage
5
- def self.template_path
6
- __FILE__.gsub(/\.rb$/, ".erb")
5
+ def self.template_path(template_name)
6
+ File.expand_path("../templates/#{template_name}.erb", __FILE__)
7
7
  end
8
8
 
9
- def self.template
10
- Erubis::EscapedEruby.new(File.read(template_path))
9
+ def self.template(template_name)
10
+ Erubis::EscapedEruby.new(File.read(template_path(template_name)))
11
11
  end
12
12
 
13
13
  attr_reader :exception, :env
@@ -15,20 +15,36 @@ module BetterErrors
15
15
  def initialize(exception, env)
16
16
  @exception = real_exception(exception)
17
17
  @env = env
18
+ @start_time = Time.now.to_f
18
19
  end
19
20
 
20
- def render
21
- self.class.template.result binding
21
+ def render(template_name = "main")
22
+ self.class.template(template_name).result binding
23
+ end
24
+
25
+ def do_variables(opts)
26
+ index = opts["index"].to_i
27
+ @frame = backtrace_frames[index]
28
+ { html: render("variable_info") }
29
+ end
30
+
31
+ def do_eval(opts)
32
+ index = opts["index"].to_i
33
+ response = begin
34
+ result = backtrace_frames[index].frame_binding.eval(opts["source"])
35
+ { result: result.inspect }
36
+ rescue Exception => e
37
+ { error: (e.inspect rescue e.class.name rescue "Exception") }
38
+ end
39
+ response.merge(highlighted_input: CodeRay.scan(opts["source"], :ruby).div(wrap: nil))
22
40
  end
23
41
 
24
42
  private
25
43
  def real_exception(exception)
26
- loop do
27
- case exception
28
- when ActionView::Template::Error; exception = exception.original_exception
29
- else
30
- return exception
31
- end
44
+ if exception.respond_to? :original_exception
45
+ exception.original_exception
46
+ else
47
+ exception
32
48
  end
33
49
  end
34
50
 
@@ -1,3 +1,5 @@
1
+ require "json"
2
+
1
3
  module BetterErrors
2
4
  class Middleware
3
5
  def initialize(app, handler = ErrorPage)
@@ -6,10 +8,28 @@ module BetterErrors
6
8
  end
7
9
 
8
10
  def call(env)
11
+ if env["REQUEST_PATH"] =~ %r{\A/__better_errors/(?<oid>\d+)/(?<method>\w+)\z}
12
+ internal_call env, $~
13
+ else
14
+ app_call env
15
+ end
16
+ end
17
+
18
+ private
19
+ def app_call(env)
9
20
  @app.call env
10
21
  rescue Exception => ex
11
- error_page = @handler.new ex, env
12
- [500, { "Content-Type" => "text/html; charset=utf-8" }, [error_page.render]]
22
+ @error_page = @handler.new ex, env
23
+ [500, { "Content-Type" => "text/html; charset=utf-8" }, [@error_page.render]]
24
+ end
25
+
26
+ def internal_call(env, opts)
27
+ if opts[:oid].to_i != @error_page.object_id
28
+ return [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(error: "Session expired")]]
29
+ end
30
+
31
+ response = @error_page.send("do_#{opts[:method]}", JSON.parse(env["rack.input"].read))
32
+ [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]]
13
33
  end
14
34
  end
15
35
  end
@@ -120,6 +120,7 @@
120
120
  font-weight:normal;
121
121
  border-top:1px solid #cccccc;
122
122
  padding-top:16px;
123
+ margin-bottom:16px;
123
124
  }
124
125
  .var_table {
125
126
  border-collapse:collapse;
@@ -133,6 +134,33 @@
133
134
  .var_table tr {
134
135
  border-bottom:1px solid #cccccc;
135
136
  }
137
+ .repl .console {
138
+ background-color:#ffffff;
139
+ padding:2px 4px;
140
+ border:1px solid #d0d0d0;
141
+ margin-bottom:8px;
142
+ font-family:monospace;
143
+ }
144
+ .repl pre {
145
+ font-size:12px;
146
+ white-space:pre-wrap;
147
+ max-height:400px;
148
+ overflow:auto;
149
+ }
150
+ .repl .prompt {
151
+ font-size:12px;
152
+ position:relative;
153
+ }
154
+ .repl input {
155
+ font-size:12px;
156
+ border:none;
157
+ font-family:monospace;
158
+ outline:none;
159
+ padding:0px;
160
+ position:absolute;
161
+ left:20px;
162
+ right:0px;
163
+ }
136
164
  </style>
137
165
  </head>
138
166
  <body>
@@ -177,19 +205,15 @@
177
205
  <div class="location"><span class="filename"><%= frame.pretty_path %></span>, line <span class="line"><%= frame.line %></span></div>
178
206
  <%== highlighted_code_block frame %>
179
207
 
180
- <h3>Local Variables</h3>
181
- <table class="var_table">
182
- <% frame.local_variables.each do |name, value| %>
183
- <tr><td class="name"><%= name %></td><td><pre><%= value.inspect %></pre></td></tr>
184
- <% end %>
185
- </table>
208
+ <div class="repl">
209
+ <h3>REPL</h3>
210
+ <div class="console">
211
+ <pre></pre>
212
+ <div class="prompt">&gt;&gt; <input/></div>
213
+ </div>
214
+ </div>
186
215
 
187
- <h3>Instance Variables</h3>
188
- <table class="var_table">
189
- <% frame.instance_variables.each do |name, value| %>
190
- <tr><td class="name"><%= name %></td><td><pre><%= value.inspect %></pre></td></tr>
191
- <% end %>
192
- </table>
216
+ <div class="variable_info"></div>
193
217
  </div>
194
218
  <% end %>
195
219
  <div style="clear:both"></div>
@@ -197,34 +221,58 @@
197
221
  </body>
198
222
  <script>
199
223
  (function() {
224
+ var oid = <%== object_id.to_s.inspect %>;
225
+
200
226
  var previous = null;
201
227
  var previousFrameInfo = null;
202
228
  var frames = document.querySelectorAll("ul.frames li");
229
+ var frameInfos = document.querySelectorAll(".frame_info");
203
230
 
204
231
  var header = document.querySelector("header");
205
232
  var headerHeight = header.offsetHeight;
206
233
 
234
+ apiCall = function(method, opts, cb) {
235
+ var req = new XMLHttpRequest();
236
+ req.open("POST", "/__better_errors/" + oid + "/" + method, true);
237
+ req.setRequestHeader("Content-Type", "application/json");
238
+ req.send(JSON.stringify(opts));
239
+ req.onreadystatechange = function() {
240
+ if(req.readyState == 4) {
241
+ var res = JSON.parse(req.responseText);
242
+ cb(res);
243
+ }
244
+ };
245
+ }
246
+
247
+ function escapeHTML(html) {
248
+ return html.replace(/&/, "&amp;").replace(/</g, "&lt;");
249
+ }
250
+
207
251
  function selectFrameInfo(index) {
208
252
  var el = document.getElementById("frame_info_" + index);
209
253
 
254
+ var varInfo = el.querySelector(".variable_info");
255
+ if(varInfo.innerHTML == "") {
256
+ apiCall("variables", { "index": index }, function(response) {
257
+ if(response.error) {
258
+ varInfo.innerHTML = "<span class='error'>" + escapeHTML(response.error) + "</span>";
259
+ } else {
260
+ varInfo.innerHTML = response.html;
261
+ }
262
+ });
263
+ }
264
+
210
265
  if(previousFrameInfo) {
211
266
  previousFrameInfo.style.display = "none";
212
267
  }
213
268
  previousFrameInfo = el;
214
269
  previousFrameInfo.style.display = "block";
270
+
271
+ el.querySelector(".repl input").focus();
215
272
  }
216
273
 
217
- function updateMarginTop() {
218
- return;
219
- if(previousFrameInfo) {
220
- previousFrameInfo.style.marginTop = Math.max(window.scrollY - headerHeight, 0) + "px";
221
- }
222
- }
223
-
224
- window.onscroll = updateMarginTop;
225
-
226
274
  for(var i = 0; i < frames.length; i++) {
227
- (function(el) {
275
+ (function(index, el, frameInfo) {
228
276
  el.onclick = function() {
229
277
  if(previous) {
230
278
  previous.className = "";
@@ -233,12 +281,29 @@
233
281
  previous = el;
234
282
 
235
283
  selectFrameInfo(el.attributes["data-index"].value);
236
- updateMarginTop();
237
284
  };
238
- })(frames[i]);
285
+ var replPre = frameInfo.querySelector(".repl pre");
286
+ var replInput = frameInfo.querySelector(".repl input");
287
+ replInput.onkeydown = function(ev) {
288
+ if(ev.keyCode == 13) {
289
+ var text = replInput.value;
290
+ replInput.value = "";
291
+ apiCall("eval", { "index": index, source: text }, function(response) {
292
+ replPre.innerHTML += ">> " + response.highlighted_input + "\n";
293
+ if(response.error) {
294
+ replPre.innerHTML += "!! " + escapeHTML(response.error) + "\n";
295
+ } else {
296
+ replPre.innerHTML += "=> " + escapeHTML(response.result) + "\n";
297
+ }
298
+ replPre.scrollTop = replPre.offsetHeight;
299
+ });
300
+ }
301
+ };
302
+ })(i, frames[i], frameInfos[i]);
239
303
  }
240
304
 
241
305
  document.querySelector(".frames li:first-child").click();
242
306
  })();
243
307
  </script>
244
308
  </html>
309
+ <!-- generated by Better Errors in <%= Time.now.to_f - @start_time %> seconds -->
@@ -0,0 +1,13 @@
1
+ <h3>Local Variables</h3>
2
+ <table class="var_table">
3
+ <% @frame.local_variables.each do |name, value| %>
4
+ <tr><td class="name"><%= name %></td><td><pre><%= value.inspect %></pre></td></tr>
5
+ <% end %>
6
+ </table>
7
+
8
+ <h3>Instance Variables</h3>
9
+ <table class="var_table">
10
+ <% @frame.instance_variables.each do |name, value| %>
11
+ <tr><td class="name"><%= name %></td><td><pre><%= value.inspect %></pre></td></tr>
12
+ <% end %>
13
+ </table>
@@ -1,3 +1,3 @@
1
1
  module BetterErrors
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_errors
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-08 00:00:00.000000000 Z
12
+ date: 2012-12-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -75,10 +75,11 @@ files:
75
75
  - lib/better_errors.rb
76
76
  - lib/better_errors/core_ext/exception.rb
77
77
  - lib/better_errors/error_frame.rb
78
- - lib/better_errors/error_page.erb
79
78
  - lib/better_errors/error_page.rb
80
79
  - lib/better_errors/middleware.rb
81
80
  - lib/better_errors/rails.rb
81
+ - lib/better_errors/templates/main.erb
82
+ - lib/better_errors/templates/variable_info.erb
82
83
  - lib/better_errors/version.rb
83
84
  - spec/better_errors/error_frame_spec.rb
84
85
  - spec/better_errors/error_page_spec.rb