better_errors 0.0.1 → 0.0.4

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
 
@@ -18,6 +25,12 @@ Or install it yourself as:
18
25
 
19
26
  $ gem install better_errors
20
27
 
28
+ If you would like to use Better Errors' **advanced features**, you need to install the `binding_of_caller` gem:
29
+
30
+ $ gem install binding_of_caller
31
+
32
+ This is an optional dependency however, and Better Errors will work without it.
33
+
21
34
  ## Usage
22
35
 
23
36
  If you're using Rails, there's nothing else you need to do.
@@ -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
@@ -14,7 +14,7 @@
14
14
  }
15
15
  header {
16
16
  padding:32px 32px;
17
- border-bottom:1px solid #ea6756;
17
+ border-bottom:1px solid #e64c38;
18
18
  background-color: #ffe5e7;
19
19
  background-image: -webkit-gradient(linear, left top, left bottom, from(#ffe5e7), to(#ffb2b8)); /* Safari 4+, Chrome */
20
20
  background-image: -webkit-linear-gradient(top, #ffe5e7, #ffb2b8); /* Chrome 10+, Safari 5.1+, iOS 5+ */
@@ -41,6 +41,40 @@
41
41
  header table th, header table td {
42
42
  padding:4px 6px;
43
43
  }
44
+ nav {
45
+ display:block;
46
+ padding:12px 12px;
47
+ border-bottom:1px solid #000000;
48
+ background-color: #444444;
49
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222)); /* Safari 4+, Chrome */
50
+ background-image: -webkit-linear-gradient(top, #444444, #222222); /* Chrome 10+, Safari 5.1+, iOS 5+ */
51
+ background-image: -moz-linear-gradient(top, #444444, #222222); /* Firefox 3.6-15 */
52
+ background-image: -o-linear-gradient(top, #444444, #222222); /* Opera 11.10-12.00 */
53
+ background-image: linear-gradient(to bottom, #444444, #222222); /* Firefox 16+, IE10, Opera 12.50+ */
54
+ }
55
+ nav a {
56
+ color:#ffffff;
57
+ text-decoration:none;
58
+ padding:4px 16px;
59
+ border-radius:16px;
60
+ }
61
+ nav a:hover {
62
+ background-color: #666666;
63
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#666666), to(#444444)); /* Safari 4+, Chrome */
64
+ background-image: -webkit-linear-gradient(top, #666666, #444444); /* Chrome 10+, Safari 5.1+, iOS 5+ */
65
+ background-image: -moz-linear-gradient(top, #666666, #444444); /* Firefox 3.6-15 */
66
+ background-image: -o-linear-gradient(top, #666666, #444444); /* Opera 11.10-12.00 */
67
+ background-image: linear-gradient(to bottom, #666666, #444444); /* Firefox 16+, IE10, Opera 12.50+ */
68
+ }
69
+ nav a.selected {
70
+ color:#000000;
71
+ background-color: #efefef;
72
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#efefef), to(#cfcfcf)); /* Safari 4+, Chrome */
73
+ background-image: -webkit-linear-gradient(top, #efefef, #cfcfcf); /* Chrome 10+, Safari 5.1+, iOS 5+ */
74
+ background-image: -moz-linear-gradient(top, #efefef, #cfcfcf); /* Firefox 3.6-15 */
75
+ background-image: -o-linear-gradient(top, #efefef, #cfcfcf); /* Opera 11.10-12.00 */
76
+ background-image: linear-gradient(to bottom, #efefef, #cfcfcf); /* Firefox 16+, IE10, Opera 12.50+ */
77
+ }
44
78
  .frames {
45
79
  width:50%;
46
80
  float:left;
@@ -120,6 +154,7 @@
120
154
  font-weight:normal;
121
155
  border-top:1px solid #cccccc;
122
156
  padding-top:16px;
157
+ margin-bottom:16px;
123
158
  }
124
159
  .var_table {
125
160
  border-collapse:collapse;
@@ -133,6 +168,33 @@
133
168
  .var_table tr {
134
169
  border-bottom:1px solid #cccccc;
135
170
  }
171
+ .repl .console {
172
+ background-color:#ffffff;
173
+ padding:2px 4px;
174
+ border:1px solid #d0d0d0;
175
+ margin-bottom:8px;
176
+ font-family:monospace;
177
+ }
178
+ .repl pre {
179
+ font-size:12px;
180
+ white-space:pre-wrap;
181
+ max-height:400px;
182
+ overflow:auto;
183
+ }
184
+ .repl .prompt {
185
+ font-size:12px;
186
+ position:relative;
187
+ }
188
+ .repl input {
189
+ font-size:12px;
190
+ border:none;
191
+ font-family:monospace;
192
+ outline:none;
193
+ padding:0px;
194
+ position:absolute;
195
+ left:20px;
196
+ right:0px;
197
+ }
136
198
  </style>
137
199
  </head>
138
200
  <body>
@@ -155,6 +217,10 @@
155
217
  <% end %>
156
218
  </table>
157
219
  </header>
220
+ <nav>
221
+ <a href="#" id="application_frames">Application Frames</a>
222
+ <a href="#" id="all_frames">All Frames</a>
223
+ </nav>
158
224
  <section class="backtrace">
159
225
  <ul class="frames">
160
226
  <% backtrace_frames.each_with_index do |frame, index| %>
@@ -177,19 +243,15 @@
177
243
  <div class="location"><span class="filename"><%= frame.pretty_path %></span>, line <span class="line"><%= frame.line %></span></div>
178
244
  <%== highlighted_code_block frame %>
179
245
 
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>
246
+ <div class="repl">
247
+ <h3>REPL</h3>
248
+ <div class="console">
249
+ <pre></pre>
250
+ <div class="prompt">&gt;&gt; <input/></div>
251
+ </div>
252
+ </div>
186
253
 
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>
254
+ <div class="variable_info"></div>
193
255
  </div>
194
256
  <% end %>
195
257
  <div style="clear:both"></div>
@@ -197,34 +259,58 @@
197
259
  </body>
198
260
  <script>
199
261
  (function() {
262
+ var oid = <%== object_id.to_s.inspect %>;
263
+
200
264
  var previous = null;
201
265
  var previousFrameInfo = null;
202
266
  var frames = document.querySelectorAll("ul.frames li");
267
+ var frameInfos = document.querySelectorAll(".frame_info");
203
268
 
204
269
  var header = document.querySelector("header");
205
270
  var headerHeight = header.offsetHeight;
206
271
 
272
+ apiCall = function(method, opts, cb) {
273
+ var req = new XMLHttpRequest();
274
+ req.open("POST", "/__better_errors/" + oid + "/" + method, true);
275
+ req.setRequestHeader("Content-Type", "application/json");
276
+ req.send(JSON.stringify(opts));
277
+ req.onreadystatechange = function() {
278
+ if(req.readyState == 4) {
279
+ var res = JSON.parse(req.responseText);
280
+ cb(res);
281
+ }
282
+ };
283
+ }
284
+
285
+ function escapeHTML(html) {
286
+ return html.replace(/&/, "&amp;").replace(/</g, "&lt;");
287
+ }
288
+
207
289
  function selectFrameInfo(index) {
208
290
  var el = document.getElementById("frame_info_" + index);
209
291
 
292
+ var varInfo = el.querySelector(".variable_info");
293
+ if(varInfo.innerHTML == "") {
294
+ apiCall("variables", { "index": index }, function(response) {
295
+ if(response.error) {
296
+ varInfo.innerHTML = "<span class='error'>" + escapeHTML(response.error) + "</span>";
297
+ } else {
298
+ varInfo.innerHTML = response.html;
299
+ }
300
+ });
301
+ }
302
+
210
303
  if(previousFrameInfo) {
211
304
  previousFrameInfo.style.display = "none";
212
305
  }
213
306
  previousFrameInfo = el;
214
307
  previousFrameInfo.style.display = "block";
308
+
309
+ el.querySelector(".repl input").focus();
215
310
  }
216
311
 
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
312
  for(var i = 0; i < frames.length; i++) {
227
- (function(el) {
313
+ (function(index, el, frameInfo) {
228
314
  el.onclick = function() {
229
315
  if(previous) {
230
316
  previous.className = "";
@@ -233,12 +319,56 @@
233
319
  previous = el;
234
320
 
235
321
  selectFrameInfo(el.attributes["data-index"].value);
236
- updateMarginTop();
237
322
  };
238
- })(frames[i]);
323
+ var replPre = frameInfo.querySelector(".repl pre");
324
+ var replInput = frameInfo.querySelector(".repl input");
325
+ replInput.onkeydown = function(ev) {
326
+ if(ev.keyCode == 13) {
327
+ var text = replInput.value;
328
+ replInput.value = "";
329
+ apiCall("eval", { "index": index, source: text }, function(response) {
330
+ replPre.innerHTML += ">> " + response.highlighted_input + "\n";
331
+ if(response.error) {
332
+ replPre.innerHTML += "!! " + escapeHTML(response.error) + "\n";
333
+ } else {
334
+ replPre.innerHTML += "=> " + escapeHTML(response.result) + "\n";
335
+ }
336
+ replPre.scrollTop = replPre.offsetHeight;
337
+ });
338
+ }
339
+ };
340
+ })(i, frames[i], frameInfos[i]);
239
341
  }
240
342
 
241
343
  document.querySelector(".frames li:first-child").click();
344
+
345
+ var applicationFrames = document.getElementById("application_frames");
346
+ var allFrames = document.getElementById("all_frames");
347
+
348
+ applicationFrames.onclick = function() {
349
+ allFrames.className = "";
350
+ applicationFrames.className = "selected";
351
+ for(var i = 0; i < frames.length; i++) {
352
+ if(frames[i].attributes["data-context"].value == "application") {
353
+ frames[i].style.display = "block";
354
+ } else {
355
+ frames[i].style.display = "none";
356
+ }
357
+ }
358
+ return false;
359
+ };
360
+
361
+ allFrames.onclick = function() {
362
+ applicationFrames.className = "";
363
+ allFrames.className = "selected";
364
+ for(var i = 0; i < frames.length; i++) {
365
+ frames[i].style.display = "block";
366
+ }
367
+ return false;
368
+ };
369
+
370
+ applicationFrames.click();
242
371
  })();
243
372
  </script>
244
373
  </html>
374
+ <!-- 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.4"
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.4
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