better_errors 0.0.2 → 0.3.0

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/.gitignore CHANGED
@@ -1,3 +1,6 @@
1
1
  *.gem
2
2
  tmp
3
3
  Gemfile.lock
4
+ coverage
5
+ .yardoc
6
+ doc
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown --no-private
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,9 @@
1
+ # Contributing to Better Errors
2
+
3
+ Please make sure to follow these guidelines when contributing code to Better Errors. They will improve the chances of your pull request being merged and they will make my life easier.
4
+
5
+ * If you are contributing a large-ish change, please split your changes up into small, logical commits. This is so I have the option of merging in some but not all of your commits. If you just give me one huge commit and there are things that I don't want to merge in, I won't.
6
+
7
+ * Whitespace at the end of lines of code is not OK, but empty lines with indentation is fine. Please do not remove these when sending in changes.
8
+
9
+ * Don't change things that are unrelated to your main changes unnecessarily. Send another pull request for these changes - don't try to sneak them in.
data/README.md CHANGED
@@ -1,28 +1,35 @@
1
1
  # Better Errors
2
2
 
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.
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 in any Rack app as Rack middleware.
4
4
 
5
- ![image](http://i.imgur.com/O9anD.png)
5
+ ![image](http://i.imgur.com/zYOXF.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
+ * Live REPL on every stack frame
12
13
 
13
14
  ## Installation
14
15
 
15
- Add this line to your application's Gemfile (under the **development** group):
16
+ Add this to your Gemfile:
16
17
 
17
- gem 'better_errors'
18
+ ```ruby
19
+ group :development do
20
+ gem "better_errors"
21
+ end
22
+ ```
18
23
 
19
- And then execute:
24
+ **NOTE:** It is *critical* you put better\_errors in the **development** section. **Do NOT run better_errors in production, or on Internet facing hosts.**
20
25
 
21
- $ bundle
26
+ If you would like to use Better Errors' **advanced features** (REPL, local/instance variable inspection, pretty stack frame names), you need to add the [`binding_of_caller`](https://github.com/banister/binding_of_caller) gem by [@banisterfiend](http://twitter.com/banisterfiend) to your Gemfile:
22
27
 
23
- Or install it yourself as:
28
+ ```ruby
29
+ gem "binding_of_caller"
30
+ ```
24
31
 
25
- $ gem install better_errors
32
+ This is an optional dependency however, and Better Errors will work without it.
26
33
 
27
34
  ## Usage
28
35
 
@@ -44,6 +51,23 @@ get "/" do
44
51
  end
45
52
  ```
46
53
 
54
+ ## Compatibility
55
+
56
+ * **Supported**
57
+ * MRI 1.9.2, 1.9.3
58
+ * JRuby (1.9 mode) - *advanced features unsupported*
59
+ * Rubinius (1.9 mode) - *advanced features unsupported*
60
+ * **Coming soon**
61
+ * MRI 2.0.0 - the official API for grabbing caller bindings is slated for MRI 2.0.0, but it has not been implemented yet
62
+
63
+ ## Known issues
64
+
65
+ * Calling `yield` from the REPL segfaults MRI 1.9.x.
66
+
67
+ ## Get in touch!
68
+
69
+ If you're using better_errors, I'd love to hear from you. Drop me a line and tell me what you think!
70
+
47
71
  ## Contributing
48
72
 
49
73
  1. Fork it
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.version = BetterErrors::VERSION
9
9
  s.authors = ["Charlie Somerville"]
10
10
  s.email = ["charlie@charliesomerville.com"]
11
- s.description = %q{Better Errors gives Rails a better error page.}
12
- s.summary = %q{Better Errors gives Rails a better error page}
11
+ s.description = %q{Provides a better error page for Rails and other Rack apps. Includes source code inspection, a live REPL and local/instance variable inspection for all stack frames.}
12
+ s.summary = %q{Better error page for Rails and other Rack apps}
13
13
  s.homepage = "https://github.com/charliesome/better_errors"
14
14
  s.license = "MIT"
15
15
 
@@ -18,9 +18,15 @@ Gem::Specification.new do |s|
18
18
  s.require_paths = ["lib"]
19
19
 
20
20
  s.add_development_dependency "rake"
21
+ s.add_development_dependency "rspec", "~> 2.12.0"
22
+ s.add_development_dependency "binding_of_caller"
23
+ s.add_development_dependency "simplecov"
24
+ s.add_development_dependency "yard"
21
25
 
22
- s.add_dependency "erubis"
23
- s.add_dependency "coderay"
24
- # optional dependency:
26
+ s.add_dependency "erubis", ">= 2.7.0"
27
+ s.add_dependency "coderay", ">= 1.0.0"
28
+
29
+ # optional dependencies:
25
30
  # s.add_dependency "binding_of_caller"
31
+ # s.add_dependency "pry"
26
32
  end
@@ -0,0 +1,61 @@
1
+ module BetterErrors
2
+ # @private
3
+ class CodeFormatter
4
+ FILE_TYPES = {
5
+ ".rb" => :ruby,
6
+ "" => :ruby,
7
+ ".html" => :html,
8
+ ".erb" => :erb,
9
+ ".haml" => :haml
10
+ }
11
+
12
+ attr_reader :filename, :line, :context
13
+
14
+ def initialize(filename, line, context = 5)
15
+ @filename = filename
16
+ @line = line
17
+ @context = context
18
+ end
19
+
20
+ def html
21
+ %{<div class="code">#{formatted_lines.join}</div>}
22
+ rescue Errno::ENOENT, Errno::EINVAL
23
+ source_unavailable
24
+ end
25
+
26
+ def source_unavailable
27
+ "<p class='unavailable'>Source unavailable</p>"
28
+ end
29
+
30
+ def coderay_scanner
31
+ ext = File.extname(filename)
32
+ FILE_TYPES[ext] || :text
33
+ end
34
+
35
+ def formatted_lines
36
+ line_range.zip(highlighted_lines).map do |current_line, str|
37
+ class_name = current_line == line ? "highlight" : ""
38
+ sprintf '<pre class="%s">%5d %s</pre>', class_name, current_line, str
39
+ end
40
+ end
41
+
42
+ def highlighted_lines
43
+ CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines
44
+ end
45
+
46
+ def context_lines
47
+ range = line_range
48
+ source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
49
+ end
50
+
51
+ def source_lines
52
+ @source_lines ||= File.readlines(filename)
53
+ end
54
+
55
+ def line_range
56
+ min = [line - context, 1].max
57
+ max = [line + context, source_lines.count].min
58
+ min..max
59
+ end
60
+ end
61
+ end
@@ -1,20 +1,21 @@
1
1
  class Exception
2
- attr_reader :__better_errors_bindings_stack
3
-
4
2
  original_initialize = instance_method(:initialize)
5
3
 
6
- define_method :initialize do |*args|
7
- unless Thread.current[:__better_errors_exception_lock]
8
- Thread.current[:__better_errors_exception_lock] = true
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
4
+ if BetterErrors.binding_of_caller_available?
5
+ define_method :initialize do |*args|
6
+ unless Thread.current[:__better_errors_exception_lock]
7
+ Thread.current[:__better_errors_exception_lock] = true
8
+ begin
9
+ @__better_errors_bindings_stack = binding.callers.drop(1)
10
+ ensure
11
+ Thread.current[:__better_errors_exception_lock] = false
13
12
  end
14
- ensure
15
- Thread.current[:__better_errors_exception_lock] = false
16
13
  end
14
+ original_initialize.bind(self).call(*args)
17
15
  end
18
- original_initialize.bind(self).call(*args)
19
16
  end
20
- end
17
+
18
+ def __better_errors_bindings_stack
19
+ @__better_errors_bindings_stack || []
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ module BetterErrors
2
+ # @private
3
+ class DisableLoggingMiddleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ original_level = BetterErrors.logger.level
10
+ BetterErrors.logger.level = Logger::ERROR if env['PATH_INFO'].index("/__better_errors") == 0
11
+ @app.call(env)
12
+ ensure
13
+ BetterErrors.logger.level = original_level
14
+ end
15
+ end
16
+ end
@@ -1,27 +1,82 @@
1
+ require "cgi"
1
2
  require "json"
2
3
 
3
4
  module BetterErrors
5
+ # @private
4
6
  class ErrorPage
5
- def self.template_path
6
- __FILE__.gsub(/\.rb$/, ".erb")
7
+ def self.template_path(template_name)
8
+ File.expand_path("../templates/#{template_name}.erb", __FILE__)
7
9
  end
8
10
 
9
- def self.template
10
- Erubis::EscapedEruby.new(File.read(template_path))
11
+ def self.template(template_name)
12
+ Erubis::EscapedEruby.new(File.read(template_path(template_name)))
11
13
  end
12
14
 
13
- attr_reader :exception, :env
15
+ attr_reader :exception, :env, :repls
14
16
 
15
17
  def initialize(exception, env)
16
18
  @exception = real_exception(exception)
17
19
  @env = env
20
+ @start_time = Time.now.to_f
21
+ @repls = []
18
22
  end
19
23
 
20
- def render
21
- self.class.template.result binding
24
+ def render(template_name = "main")
25
+ self.class.template(template_name).result binding
26
+ end
27
+
28
+ def do_variables(opts)
29
+ index = opts["index"].to_i
30
+ @frame = backtrace_frames[index]
31
+ @var_start_time = Time.now.to_f
32
+ { html: render("variable_info") }
33
+ end
34
+
35
+ def do_eval(opts)
36
+ index = opts["index"].to_i
37
+ code = opts["source"]
38
+
39
+ unless binding = backtrace_frames[index].frame_binding
40
+ return { error: "REPL unavailable in this stack frame" }
41
+ end
42
+
43
+ result, prompt =
44
+ (@repls[index] ||= REPL.provider.new(binding)).send_input(code)
45
+
46
+ { result: result,
47
+ prompt: prompt,
48
+ highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) }
49
+ end
50
+
51
+ def backtrace_frames
52
+ @backtrace_frames ||= StackFrame.from_exception(exception)
22
53
  end
23
54
 
24
55
  private
56
+ def editor_url(frame)
57
+ BetterErrors.editor[frame.filename, frame.line]
58
+ end
59
+
60
+ def rack_session
61
+ env['rack.session']
62
+ end
63
+
64
+ def rails_params
65
+ env['action_dispatch.request.parameters']
66
+ end
67
+
68
+ def uri_prefix
69
+ env["SCRIPT_NAME"] || ""
70
+ end
71
+
72
+ def exception_message
73
+ if exception.is_a?(SyntaxError) && exception.message =~ /\A.*:\d*: (.*)$/
74
+ $1
75
+ else
76
+ exception.message
77
+ end
78
+ end
79
+
25
80
  def real_exception(exception)
26
81
  if exception.respond_to? :original_exception
27
82
  exception.original_exception
@@ -31,50 +86,19 @@ module BetterErrors
31
86
  end
32
87
 
33
88
  def request_path
34
- env["REQUEST_PATH"]
35
- end
36
-
37
- def backtrace_frames
38
- @backtrace_frames ||= ErrorFrame.from_exception(exception)
39
- end
40
-
41
- def coderay_scanner_for_ext(ext)
42
- case ext
43
- when "rb"; :ruby
44
- when "html"; :html
45
- when "erb"; :erb
46
- when "haml"; :haml
47
- end
48
- end
49
-
50
- def file_extension(filename)
51
- filename.split(".").last
52
- end
53
-
54
- def code_extract(frame, lines_of_context = 5)
55
- lines = File.readlines(frame.filename)
56
- min_line = [1, frame.line - lines_of_context].max - 1
57
- max_line = [frame.line + lines_of_context, lines.count + 1].min - 1
58
- [min_line, max_line, lines[min_line..max_line].join]
89
+ env["PATH_INFO"]
59
90
  end
60
91
 
61
92
  def highlighted_code_block(frame)
62
- ext = file_extension(frame.filename)
63
- scanner = coderay_scanner_for_ext(ext)
64
- min_line, max_line, code = code_extract(frame)
65
- highlighted_code = CodeRay.scan(code, scanner).div wrap: nil
66
- "".tap do |html|
67
- html << "<div class='code'>"
68
- highlighted_code.each_line.each_with_index do |str, index|
69
- if min_line + index + 1 == frame.line
70
- html << "<pre class='highlight'>"
71
- else
72
- html << "<pre>"
73
- end
74
- html << sprintf("%5d", min_line + index + 1) << " " << str << "</pre>"
75
- end
76
- html << "</div>"
77
- end
93
+ CodeFormatter.new(frame.filename, frame.line).html
94
+ end
95
+
96
+ def inspect_value(obj)
97
+ CGI.escapeHTML(obj.inspect)
98
+ rescue NoMethodError
99
+ "<span class='unsupported'>(object doesn't support inspect)</span>"
100
+ rescue Exception => e
101
+ "<span class='unsupported'>(exception was raised in inspect)</span>"
78
102
  end
79
103
  end
80
104
  end
@@ -1,15 +1,90 @@
1
+ require "json"
2
+
1
3
  module BetterErrors
4
+ # Better Errors' error handling middleware. Including this in your middleware
5
+ # stack will show a Better Errors error page for exceptions raised below this
6
+ # middleware.
7
+ #
8
+ # If you are using Ruby on Rails, you do not need to manually insert this
9
+ # middleware into your middleware stack.
10
+ #
11
+ # @example Sinatra
12
+ # require "better_errors"
13
+ #
14
+ # if development?
15
+ # use BetterErrors::Middleware
16
+ # end
17
+ #
18
+ # @example Rack
19
+ # require "better_errors"
20
+ # if ENV["RACK_ENV"] == "development"
21
+ # use BetterErrors::Middleware
22
+ # end
23
+ #
2
24
  class Middleware
25
+ # A new instance of BetterErrors::Middleware
26
+ #
27
+ # @param app The Rack app/middleware to wrap with Better Errors
28
+ # @param handler The error handler to use.
3
29
  def initialize(app, handler = ErrorPage)
4
30
  @app = app
5
31
  @handler = handler
6
32
  end
7
33
 
34
+ # Calls the Better Errors middleware
35
+ #
36
+ # @param [Hash] env
37
+ # @return [Array]
8
38
  def call(env)
39
+ case env["PATH_INFO"]
40
+ when %r{\A/__better_errors/(?<oid>-?\d+)/(?<method>\w+)\z}
41
+ internal_call env, $~
42
+ when %r{\A/__better_errors/?\z}
43
+ show_error_page env
44
+ else
45
+ app_call env
46
+ end
47
+ end
48
+
49
+ private
50
+ def app_call(env)
9
51
  @app.call env
10
52
  rescue Exception => ex
11
- error_page = @handler.new ex, env
12
- [500, { "Content-Type" => "text/html; charset=utf-8" }, [error_page.render]]
53
+ @error_page = @handler.new ex, env
54
+ log_exception
55
+ show_error_page(env)
56
+ end
57
+
58
+ def show_error_page(env)
59
+ content = if @error_page
60
+ @error_page.render
61
+ else
62
+ "<h1>No errors</h1><p>No errors have been recorded yet.</p><hr>" +
63
+ "<code>Better Errors v#{BetterErrors::VERSION}</code>"
64
+ end
65
+
66
+ [500, { "Content-Type" => "text/html; charset=utf-8" }, [content]]
67
+ end
68
+
69
+
70
+ def log_exception
71
+ return unless BetterErrors.logger
72
+
73
+ message = "\n#{@error_page.exception.class} - #{@error_page.exception.message}:\n"
74
+ @error_page.backtrace_frames.each do |frame|
75
+ message << " #{frame}\n"
76
+ end
77
+
78
+ BetterErrors.logger.fatal message
79
+ end
80
+
81
+ def internal_call(env, opts)
82
+ if opts[:oid].to_i != @error_page.object_id
83
+ return [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(error: "Session expired")]]
84
+ end
85
+
86
+ response = @error_page.send("do_#{opts[:method]}", JSON.parse(env["rack.input"].read))
87
+ [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]]
13
88
  end
14
89
  end
15
90
  end
@@ -1,10 +1,13 @@
1
1
  module BetterErrors
2
+ # @private
2
3
  class Railtie < Rails::Railtie
3
4
  initializer "better_errors.configure_rails_initialization" do
4
- middleware = Rails.application.middleware
5
- middleware.use BetterErrors::Middleware
6
-
7
- BetterErrors.application_root = Rails.root.to_s
5
+ unless Rails.env.production?
6
+ Rails.application.middleware.use BetterErrors::Middleware
7
+ Rails.application.middleware.insert_before Rails::Rack::Logger, BetterErrors::DisableLoggingMiddleware
8
+ BetterErrors.logger = Rails.logger
9
+ BetterErrors.application_root = Rails.root.to_s
10
+ end
8
11
  end
9
12
  end
10
13
  end
@@ -0,0 +1,20 @@
1
+ module BetterErrors
2
+ module REPL
3
+ class Basic
4
+ def initialize(binding)
5
+ @binding = binding
6
+ end
7
+
8
+ def send_input(str)
9
+ [execute(str), ">>"]
10
+ end
11
+
12
+ private
13
+ def execute(str)
14
+ "=> #{@binding.eval(str).inspect}\n"
15
+ rescue Exception => e
16
+ "!! #{e.inspect rescue e.class.to_s rescue "Exception"}\n"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,77 @@
1
+ require "fiber"
2
+ require "pry"
3
+
4
+ module BetterErrors
5
+ module REPL
6
+ class Pry
7
+ class Input
8
+ def readline
9
+ Fiber.yield
10
+ end
11
+ end
12
+
13
+ class Output
14
+ def initialize
15
+ @buffer = ""
16
+ end
17
+
18
+ def puts(*args)
19
+ args.each do |arg|
20
+ @buffer << "#{arg.chomp}\n"
21
+ end
22
+ end
23
+
24
+ def tty?
25
+ false
26
+ end
27
+
28
+ def read_buffer
29
+ @buffer
30
+ ensure
31
+ @buffer = ""
32
+ end
33
+ end
34
+
35
+ def initialize(binding)
36
+ @fiber = Fiber.new do
37
+ @pry.repl binding
38
+ end
39
+ @input = Input.new
40
+ @output = Output.new
41
+ @pry = ::Pry.new input: @input, output: @output
42
+ @pry.hooks.clear_all
43
+ @continued_expression = false
44
+ @pry.hooks.add_hook :after_read, "better_errors hacky hook" do
45
+ @continued_expression = false
46
+ end
47
+ @fiber.resume
48
+ end
49
+
50
+ def pry_indent
51
+ @pry.instance_variable_get(:@indent)
52
+ end
53
+
54
+ def send_input(str)
55
+ local ::Pry.config, color: false, pager: false do
56
+ @continued_expression = true
57
+ @fiber.resume "#{str}\n"
58
+ [@output.read_buffer, @continued_expression ? ".." : ">>"]
59
+ end
60
+ end
61
+
62
+ private
63
+ def local(obj, attrs)
64
+ old_attrs = {}
65
+ attrs.each do |k, v|
66
+ old_attrs[k] = obj.send k
67
+ obj.send "#{k}=", v
68
+ end
69
+ yield
70
+ ensure
71
+ old_attrs.each do |k, v|
72
+ obj.send "#{k}=", v
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,30 @@
1
+ module BetterErrors
2
+ # @private
3
+ module REPL
4
+ PROVIDERS = [
5
+ { impl: "better_errors/repl/basic",
6
+ const: :Basic },
7
+ ]
8
+
9
+ def self.provider
10
+ @provider ||= const_get detect[:const]
11
+ end
12
+
13
+ def self.provider=(prov)
14
+ @provider = prov
15
+ end
16
+
17
+ def self.detect
18
+ PROVIDERS.find do |prov|
19
+ test_provider prov
20
+ end
21
+ end
22
+
23
+ def self.test_provider(provider)
24
+ require provider[:impl]
25
+ true
26
+ rescue LoadError
27
+ false
28
+ end
29
+ end
30
+ end