better_errors 0.0.2 → 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,13 +2,14 @@
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/O9anD.png)
5
+ ![image](http://i.imgur.com/quHUZ.png)
6
6
 
7
7
  ## Features
8
8
 
9
9
  * Full stack trace
10
10
  * Source code inspection for all stack frames (with highlighting)
11
11
  * Local and instance variable inspection
12
+ * Ruby console on every stack frame
12
13
 
13
14
  ## Installation
14
15
 
@@ -24,6 +25,12 @@ Or install it yourself as:
24
25
 
25
26
  $ gem install better_errors
26
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
+
27
34
  ## Usage
28
35
 
29
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,7 +3,9 @@ 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
- b = exception.__better_errors_bindings_stack[idx]
6
+ if BetterErrors.binding_of_caller_available?
7
+ b = exception.__better_errors_bindings_stack[idx]
8
+ end
7
9
  ErrorFrame.new($1, $2.to_i, $3, b)
8
10
  }.compact
9
11
  end
@@ -59,7 +61,14 @@ module BetterErrors
59
61
 
60
62
  def local_variables
61
63
  return {} unless frame_binding
62
- 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
63
72
  end
64
73
 
65
74
  def instance_variables
@@ -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,10 +15,28 @@ 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
@@ -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,25 +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
312
  for(var i = 0; i < frames.length; i++) {
218
- (function(el) {
313
+ (function(index, el, frameInfo) {
219
314
  el.onclick = function() {
220
315
  if(previous) {
221
316
  previous.className = "";
@@ -225,10 +320,55 @@
225
320
 
226
321
  selectFrameInfo(el.attributes["data-index"].value);
227
322
  };
228
- })(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]);
229
341
  }
230
342
 
231
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();
232
371
  })();
233
372
  </script>
234
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.2"
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.2
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