diff --git a/Gemfile b/Gemfile index bfc0b773..a04059bf 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ unless RUBY_PLATFORM =~ /w32/ gem 'rubyzip' gem 'zip-zip' end + gem 'seems_rateable', path: 'lib/seems_rateable' gem "rails", "3.2.13" gem "jquery-rails", "~> 2.0.2" @@ -15,6 +16,11 @@ gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] gem "builder", "3.0.0" gem 'acts-as-taggable-on' +group :development do + gem 'better_errors', path: 'lib/better_errors' + gem 'rack-mini-profiler', path: 'lib/rack-mini-profiler' +end + # Optional gem for LDAP authentication group :ldap do gem "net-ldap", "~> 0.3.1" @@ -70,16 +76,6 @@ else warn("Please configure your config/database.yml first") end -group :development do - gem "rdoc", ">= 2.4.2" - if nil - gem 'thin' - gem 'rack-mini-profiler' - end -end - - - local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local") if File.exists?(local_gemfile) puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v` diff --git a/Gemfile.lock b/Gemfile.lock index 54d89f38..25cc9f48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,16 @@ +PATH + remote: lib/better_errors + specs: + better_errors (1.1.0) + coderay (>= 1.0.0) + erubis (>= 2.6.6) + +PATH + remote: lib/rack-mini-profiler + specs: + rack-mini-profiler (0.9.1) + rack (>= 1.1.3) + PATH remote: lib/seems_rateable specs: @@ -105,6 +118,7 @@ DEPENDENCIES activerecord-jdbc-adapter (= 1.2.5) activerecord-jdbcmysql-adapter acts-as-taggable-on + better_errors! builder (= 3.0.0) coderay (~> 1.0.6) fastercsv (~> 1.5.0) @@ -112,8 +126,8 @@ DEPENDENCIES jquery-rails (~> 2.0.2) mysql2 (~> 0.3.11) net-ldap (~> 0.3.1) + rack-mini-profiler! rack-openid rails (= 3.2.13) - rdoc (>= 2.4.2) ruby-openid (~> 2.1.4) seems_rateable! diff --git a/lib/better_errors/.travis.yml b/lib/better_errors/.travis.yml new file mode 100644 index 00000000..ce51187c --- /dev/null +++ b/lib/better_errors/.travis.yml @@ -0,0 +1,4 @@ +language: ruby +rvm: + - 2.1.0 + - 2.0.0 diff --git a/lib/better_errors/.yardopts b/lib/better_errors/.yardopts new file mode 100644 index 00000000..73034ccf --- /dev/null +++ b/lib/better_errors/.yardopts @@ -0,0 +1 @@ +--markup markdown --no-private diff --git a/lib/better_errors/CHANGELOG.md b/lib/better_errors/CHANGELOG.md new file mode 100644 index 00000000..00fc2466 --- /dev/null +++ b/lib/better_errors/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +See https://github.com/charliesome/better_errors/releases diff --git a/lib/better_errors/Gemfile b/lib/better_errors/Gemfile new file mode 100644 index 00000000..287f7749 --- /dev/null +++ b/lib/better_errors/Gemfile @@ -0,0 +1,10 @@ +source 'https://rubygems.org' + +gemspec + +gem "rake" +gem "rspec", "2.14.1" +gem "binding_of_caller", platforms: :ruby +gem "pry", "0.9.12" +gem "yard" +gem "kramdown" diff --git a/lib/better_errors/LICENSE.txt b/lib/better_errors/LICENSE.txt new file mode 100644 index 00000000..755ce77a --- /dev/null +++ b/lib/better_errors/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2014 Charlie Somerville + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/better_errors/README.md b/lib/better_errors/README.md new file mode 100644 index 00000000..91488695 --- /dev/null +++ b/lib/better_errors/README.md @@ -0,0 +1,103 @@ +# Better Errors [![Gem Version](http://img.shields.io/gem/v/better_errors.svg)](https://rubygems.org/gems/better_errors) [![Build Status](https://travis-ci.org/charliesome/better_errors.svg)](https://travis-ci.org/charliesome/better_errors) [![Code Climate](http://img.shields.io/codeclimate/github/charliesome/better_errors.svg)](https://codeclimate.com/github/charliesome/better_errors) + +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. + +![image](http://i.imgur.com/6zBGAAb.png) + +## Features + +* Full stack trace +* Source code inspection for all stack frames (with highlighting) +* Local and instance variable inspection +* Live REPL on every stack frame + +## Installation + +Add this to your Gemfile: + +```ruby +group :development do + gem "better_errors" +end +``` + +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: + +```ruby +gem "binding_of_caller" +``` + +This is an optional dependency however, and Better Errors will work without it. + +_Note: If you discover that Better Errors isn't working - particularly after upgrading from version 0.5.0 or less - be sure to set `config.consider_all_requests_local = true` in `config/environments/development.rb`._ + +## Security + +**NOTE:** It is *critical* you put better\_errors in the **development** section. **Do NOT run better_errors in production, or on Internet facing hosts.** + +You will notice that the only machine that gets the Better Errors page is localhost, which means you get the default error page if you are developing on a remote host (or a virtually remote host, such as a Vagrant box). Obviously, the REPL is not something you want to expose to the public, but there may also be other pieces of sensitive information available in the backtrace. + +To poke selective holes in this security mechanism, you can add a line like this to your startup (for example, on Rails it would be `config/environments/development.rb`) + +```ruby +BetterErrors::Middleware.allow_ip! ENV['TRUSTED_IP'] if ENV['TRUSTED_IP'] +``` + +Then run Rails like this: + +```shell +TRUSTED_IP=66.68.96.220 rails s +``` + +Note that the `allow_ip!` is actually backed by a `Set`, so you can add more than one IP address or subnet. + +**Tip:** You can find your apparent IP by hitting the old error page's "Show env dump" and looking at "REMOTE_ADDR". + +**VirtualBox:** If you are using VirtualBox and are accessing the guest from your host's browser, you will need to use `allow_ip!` to see the error page. + +## Usage + +If you're using Rails, there's nothing else you need to do. + +If you're not using Rails, you need to insert `BetterErrors::Middleware` into your middleware stack, and optionally set `BetterErrors.application_root` if you'd like Better Errors to abbreviate filenames within your application. + +Here's an example using Sinatra: + +```ruby +require "sinatra" +require "better_errors" + +configure :development do + use BetterErrors::Middleware + BetterErrors.application_root = __dir__ +end + +get "/" do + raise "oops" +end +``` + +### Unicorn, Puma, and other multi-worker servers + +Better Errors works by leaving a lot of context in server process memory. If +you're using a web server that runs multiple "workers" it's likely that a second +request (as happens when you click on a stack frame) will hit a different +worker. That worker won't have the necessary context in memory, and you'll see +a `Session Expired` message. + +If this is the case for you, consider turning the number of workers to one (1) +in `development`. Another option would be to use Webrick, Mongrel, Thin, +or another single-process server as your `rails server`, when you are trying +to troubleshoot an issue in development. + +## Get in touch! + +If you're using better_errors, I'd love to hear from you. Drop me a line and tell me what you think! + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/lib/better_errors/Rakefile b/lib/better_errors/Rakefile new file mode 100644 index 00000000..b6329726 --- /dev/null +++ b/lib/better_errors/Rakefile @@ -0,0 +1,13 @@ +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +namespace :test do + RSpec::Core::RakeTask.new(:with_binding_of_caller) + + without_task = RSpec::Core::RakeTask.new(:without_binding_of_caller) + without_task.ruby_opts = "-I spec -r without_binding_of_caller" + + task :all => [:with_binding_of_caller, :without_binding_of_caller] +end + +task :default => "test:all" diff --git a/lib/better_errors/better_errors.gemspec b/lib/better_errors/better_errors.gemspec new file mode 100644 index 00000000..315c110c --- /dev/null +++ b/lib/better_errors/better_errors.gemspec @@ -0,0 +1,27 @@ +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'better_errors/version' + +Gem::Specification.new do |s| + s.name = "better_errors" + s.version = BetterErrors::VERSION + s.authors = ["Charlie Somerville"] + s.email = ["charlie@charliesomerville.com"] + 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.} + s.summary = %q{Better error page for Rails and other Rack apps} + s.homepage = "https://github.com/charliesome/better_errors" + s.license = "MIT" + + s.files = `git ls-files`.split($/) + s.test_files = s.files.grep(%r{^(test|spec|features)/}) + s.require_paths = ["lib"] + + s.required_ruby_version = ">= 2.0.0" + + s.add_dependency "erubis", ">= 2.6.6" + s.add_dependency "coderay", ">= 1.0.0" + + # optional dependencies: + # s.add_dependency "binding_of_caller" + # s.add_dependency "pry" +end diff --git a/lib/better_errors/lib/better_errors.rb b/lib/better_errors/lib/better_errors.rb new file mode 100644 index 00000000..394060ec --- /dev/null +++ b/lib/better_errors/lib/better_errors.rb @@ -0,0 +1,146 @@ +require "pp" +require "erubis" +require "coderay" +require "uri" + +require "better_errors/code_formatter" +require "better_errors/error_page" +require "better_errors/middleware" +require "better_errors/raised_exception" +require "better_errors/repl" +require "better_errors/stack_frame" +require "better_errors/version" + +module BetterErrors + POSSIBLE_EDITOR_PRESETS = [ + { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" }, + { symbols: [:macvim, :mvim], sniff: /vim/i, url: proc { |file, line| "mvim://open?url=file://#{file}&line=#{line}" } }, + { symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" }, + { symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" }, + ] + + class << self + # The path to the root of the application. Better Errors uses this property + # to determine if a file in a backtrace should be considered an application + # frame. If you are using Better Errors with Rails, you do not need to set + # this attribute manually. + # + # @return [String] + attr_accessor :application_root + + # The logger to use when logging exception details and backtraces. If you + # are using Better Errors with Rails, you do not need to set this attribute + # manually. If this attribute is `nil`, nothing will be logged. + # + # @return [Logger, nil] + attr_accessor :logger + + # @private + attr_accessor :binding_of_caller_available + + # @private + alias_method :binding_of_caller_available?, :binding_of_caller_available + + # The ignored instance variables. + # @return [Array] + attr_accessor :ignored_instance_variables + end + @ignored_instance_variables = [] + + # Returns a proc, which when called with a filename and line number argument, + # returns a URL to open the filename and line in the selected editor. + # + # Generates TextMate URLs by default. + # + # BetterErrors.editor["/some/file", 123] + # # => txmt://open?url=file:///some/file&line=123 + # + # @return [Proc] + def self.editor + @editor + end + + # Configures how Better Errors generates open-in-editor URLs. + # + # @overload BetterErrors.editor=(sym) + # Uses one of the preset editor configurations. Valid symbols are: + # + # * `:textmate`, `:txmt`, `:tm` + # * `:sublime`, `:subl`, `:st` + # * `:macvim` + # + # @param [Symbol] sym + # + # @overload BetterErrors.editor=(str) + # Uses `str` as the format string for generating open-in-editor URLs. + # + # Use `%{file}` and `%{line}` as placeholders for the actual values. + # + # @example + # BetterErrors.editor = "my-editor://open?url=%{file}&line=%{line}" + # + # @param [String] str + # + # @overload BetterErrors.editor=(proc) + # Uses `proc` to generate open-in-editor URLs. The proc will be called + # with `file` and `line` parameters when a URL needs to be generated. + # + # Your proc should take care to escape `file` appropriately with + # `URI.encode_www_form_component` (please note that `URI.escape` is **not** + # a suitable substitute.) + # + # @example + # BetterErrors.editor = proc { |file, line| + # "my-editor://open?url=#{URI.encode_www_form_component file}&line=#{line}" + # } + # + # @param [Proc] proc + # + def self.editor=(editor) + POSSIBLE_EDITOR_PRESETS.each do |config| + if config[:symbols].include?(editor) + return self.editor = config[:url] + end + end + + if editor.is_a? String + self.editor = proc { |file, line| editor % { file: URI.encode_www_form_component(file), line: line } } + else + if editor.respond_to? :call + @editor = editor + else + raise TypeError, "Expected editor to be a valid editor key, a format string or a callable." + end + end + end + + # Enables experimental Pry support in the inline REPL + # + # If you encounter problems while using Pry, *please* file a bug report at + # https://github.com/charliesome/better_errors/issues + def self.use_pry! + REPL::PROVIDERS.unshift const: :Pry, impl: "better_errors/repl/pry" + end + + # Automatically sniffs a default editor preset based on the EDITOR + # environment variable. + # + # @return [Symbol] + def self.default_editor + POSSIBLE_EDITOR_PRESETS.detect(-> { {} }) { |config| + ENV["EDITOR"] =~ config[:sniff] + }[:url] || :textmate + end + + BetterErrors.editor = default_editor +end + +begin + require "binding_of_caller" + require "better_errors/exception_extension" + BetterErrors.binding_of_caller_available = true +rescue LoadError => e + BetterErrors.binding_of_caller_available = false +end + +require "better_errors/rails" if defined? Rails::Railtie diff --git a/lib/better_errors/lib/better_errors/code_formatter.rb b/lib/better_errors/lib/better_errors/code_formatter.rb new file mode 100644 index 00000000..77827241 --- /dev/null +++ b/lib/better_errors/lib/better_errors/code_formatter.rb @@ -0,0 +1,63 @@ +module BetterErrors + # @private + class CodeFormatter + require "better_errors/code_formatter/html" + require "better_errors/code_formatter/text" + + FILE_TYPES = { + ".rb" => :ruby, + "" => :ruby, + ".html" => :html, + ".erb" => :erb, + ".haml" => :haml + } + + attr_reader :filename, :line, :context + + def initialize(filename, line, context = 5) + @filename = filename + @line = line + @context = context + end + + def output + formatted_code + rescue Errno::ENOENT, Errno::EINVAL + source_unavailable + end + + def formatted_code + formatted_lines.join + end + + def coderay_scanner + ext = File.extname(filename) + FILE_TYPES[ext] || :text + end + + def each_line_of(lines, &blk) + line_range.zip(lines).map { |current_line, str| + yield (current_line == line), current_line, str + } + end + + def highlighted_lines + CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines + end + + def context_lines + range = line_range + source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL + end + + def source_lines + @source_lines ||= File.readlines(filename) + end + + def line_range + min = [line - context, 1].max + max = [line + context, source_lines.count].min + min..max + end + end +end diff --git a/lib/better_errors/lib/better_errors/code_formatter/html.rb b/lib/better_errors/lib/better_errors/code_formatter/html.rb new file mode 100644 index 00000000..b3ab0543 --- /dev/null +++ b/lib/better_errors/lib/better_errors/code_formatter/html.rb @@ -0,0 +1,26 @@ +module BetterErrors + # @private + class CodeFormatter::HTML < CodeFormatter + def source_unavailable + "

Source is not available

" + end + + def formatted_lines + each_line_of(highlighted_lines) { |highlight, current_line, str| + class_name = highlight ? "highlight" : "" + sprintf '
%s
', class_name, str + } + end + + def formatted_nums + each_line_of(highlighted_lines) { |highlight, current_line, str| + class_name = highlight ? "highlight" : "" + sprintf '%5d', class_name, current_line + } + end + + def formatted_code + %{
#{formatted_nums.join}
#{super}
} + end + end +end diff --git a/lib/better_errors/lib/better_errors/code_formatter/text.rb b/lib/better_errors/lib/better_errors/code_formatter/text.rb new file mode 100644 index 00000000..cd63806d --- /dev/null +++ b/lib/better_errors/lib/better_errors/code_formatter/text.rb @@ -0,0 +1,14 @@ +module BetterErrors + # @private + class CodeFormatter::Text < CodeFormatter + def source_unavailable + "# Source is not available" + end + + def formatted_lines + each_line_of(context_lines) { |highlight, current_line, str| + sprintf '%s %3d %s', (highlight ? '>' : ' '), current_line, str + } + end + end +end diff --git a/lib/better_errors/lib/better_errors/error_page.rb b/lib/better_errors/lib/better_errors/error_page.rb new file mode 100644 index 00000000..849020f1 --- /dev/null +++ b/lib/better_errors/lib/better_errors/error_page.rb @@ -0,0 +1,110 @@ +require "cgi" +require "json" +require "securerandom" + +module BetterErrors + # @private + class ErrorPage + def self.template_path(template_name) + File.expand_path("../templates/#{template_name}.erb", __FILE__) + end + + def self.template(template_name) + Erubis::EscapedEruby.new(File.read(template_path(template_name))) + end + + attr_reader :exception, :env, :repls + + def initialize(exception, env) + @exception = RaisedException.new(exception) + @env = env + @start_time = Time.now.to_f + @repls = [] + end + + def id + @id ||= SecureRandom.hex(8) + end + + def render(template_name = "main") + self.class.template(template_name).result binding + end + + def do_variables(opts) + index = opts["index"].to_i + @frame = backtrace_frames[index] + @var_start_time = Time.now.to_f + { html: render("variable_info") } + end + + def do_eval(opts) + index = opts["index"].to_i + code = opts["source"] + + unless binding = backtrace_frames[index].frame_binding + return { error: "REPL unavailable in this stack frame" } + end + + result, prompt, prefilled_input = + (@repls[index] ||= REPL.provider.new(binding)).send_input(code) + + { result: result, + prompt: prompt, + prefilled_input: prefilled_input, + highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) } + end + + def backtrace_frames + exception.backtrace + end + + def application_frames + backtrace_frames.select(&:application?) + end + + def first_frame + application_frames.first || backtrace_frames.first + end + + private + def editor_url(frame) + BetterErrors.editor[frame.filename, frame.line] + end + + def rack_session + env['rack.session'] + end + + def rails_params + env['action_dispatch.request.parameters'] + end + + def uri_prefix + env["SCRIPT_NAME"] || "" + end + + def request_path + env["PATH_INFO"] + end + + def html_formatted_code_block(frame) + CodeFormatter::HTML.new(frame.filename, frame.line).output + end + + def text_formatted_code_block(frame) + CodeFormatter::Text.new(frame.filename, frame.line).output + end + + def text_heading(char, str) + str + "\n" + char*str.size + end + + def inspect_value(obj) + CGI.escapeHTML(obj.inspect) + rescue NoMethodError + "(object doesn't support inspect)" + rescue Exception => e + "(exception was raised in inspect)" + end + end +end diff --git a/lib/better_errors/lib/better_errors/exception_extension.rb b/lib/better_errors/lib/better_errors/exception_extension.rb new file mode 100644 index 00000000..e97b052f --- /dev/null +++ b/lib/better_errors/lib/better_errors/exception_extension.rb @@ -0,0 +1,17 @@ +module BetterErrors + module ExceptionExtension + prepend_features Exception + + def set_backtrace(*) + if caller_locations.none? { |loc| loc.path == __FILE__ } + @__better_errors_bindings_stack = binding.callers.drop(1) + end + + super + end + + def __better_errors_bindings_stack + @__better_errors_bindings_stack || [] + end + end +end diff --git a/lib/better_errors/lib/better_errors/middleware.rb b/lib/better_errors/lib/better_errors/middleware.rb new file mode 100644 index 00000000..ce9aaad4 --- /dev/null +++ b/lib/better_errors/lib/better_errors/middleware.rb @@ -0,0 +1,141 @@ +require "json" +require "ipaddr" +require "set" + +module BetterErrors + # Better Errors' error handling middleware. Including this in your middleware + # stack will show a Better Errors error page for exceptions raised below this + # middleware. + # + # If you are using Ruby on Rails, you do not need to manually insert this + # middleware into your middleware stack. + # + # @example Sinatra + # require "better_errors" + # + # if development? + # use BetterErrors::Middleware + # end + # + # @example Rack + # require "better_errors" + # if ENV["RACK_ENV"] == "development" + # use BetterErrors::Middleware + # end + # + class Middleware + # The set of IP addresses that are allowed to access Better Errors. + # + # Set to `{ "127.0.0.1/8", "::1/128" }` by default. + ALLOWED_IPS = Set.new + + # Adds an address to the set of IP addresses allowed to access Better + # Errors. + def self.allow_ip!(addr) + ALLOWED_IPS << IPAddr.new(addr) + end + + allow_ip! "127.0.0.0/8" + allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support + + # A new instance of BetterErrors::Middleware + # + # @param app The Rack app/middleware to wrap with Better Errors + # @param handler The error handler to use. + def initialize(app, handler = ErrorPage) + @app = app + @handler = handler + end + + # Calls the Better Errors middleware + # + # @param [Hash] env + # @return [Array] + def call(env) + if allow_ip? env + better_errors_call env + else + @app.call env + end + end + + private + + def allow_ip?(env) + # REMOTE_ADDR is not in the rack spec, so some application servers do + # not provide it. + return true unless env["REMOTE_ADDR"] and !env["REMOTE_ADDR"].strip.empty? + ip = IPAddr.new env["REMOTE_ADDR"].split("%").first + ALLOWED_IPS.any? { |subnet| subnet.include? ip } + end + + def better_errors_call(env) + case env["PATH_INFO"] + when %r{/__better_errors/(?.+?)/(?\w+)\z} + internal_call env, $~ + when %r{/__better_errors/?\z} + show_error_page env + else + protected_app_call env + end + end + + def protected_app_call(env) + @app.call env + rescue Exception => ex + @error_page = @handler.new ex, env + log_exception + show_error_page(env, ex) + end + + def show_error_page(env, exception=nil) + type, content = if @error_page + if text?(env) + [ 'plain', @error_page.render('text') ] + else + [ 'html', @error_page.render ] + end + else + [ 'html', no_errors_page ] + end + + status_code = 500 + if defined? ActionDispatch::ExceptionWrapper + status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code + end + + [status_code, { "Content-Type" => "text/#{type}; charset=utf-8" }, [content]] + end + + def text?(env) + env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" || + !env["HTTP_ACCEPT"].to_s.include?('html') + end + + def log_exception + return unless BetterErrors.logger + + message = "\n#{@error_page.exception.type} - #{@error_page.exception.message}:\n" + @error_page.backtrace_frames.each do |frame| + message << " #{frame}\n" + end + + BetterErrors.logger.fatal message + end + + def internal_call(env, opts) + if opts[:id] != @error_page.id + return [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(error: "Session expired")]] + end + + env["rack.input"].rewind + response = @error_page.send("do_#{opts[:method]}", JSON.parse(env["rack.input"].read)) + [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]] + end + + def no_errors_page + "

No errors

No errors have been recorded yet.


" + + "Better Errors v#{BetterErrors::VERSION}" + end + end +end diff --git a/lib/better_errors/lib/better_errors/rails.rb b/lib/better_errors/lib/better_errors/rails.rb new file mode 100644 index 00000000..36e386a5 --- /dev/null +++ b/lib/better_errors/lib/better_errors/rails.rb @@ -0,0 +1,28 @@ +module BetterErrors + # @private + class Railtie < Rails::Railtie + initializer "better_errors.configure_rails_initialization" do + if use_better_errors? + insert_middleware + BetterErrors.logger = Rails.logger + BetterErrors.application_root = Rails.root.to_s + end + end + + def insert_middleware + if defined? ActionDispatch::DebugExceptions + app.middleware.insert_after ActionDispatch::DebugExceptions, BetterErrors::Middleware + else + app.middleware.use BetterErrors::Middleware + end + end + + def use_better_errors? + !Rails.env.production? and app.config.consider_all_requests_local + end + + def app + Rails.application + end + end +end diff --git a/lib/better_errors/lib/better_errors/raised_exception.rb b/lib/better_errors/lib/better_errors/raised_exception.rb new file mode 100644 index 00000000..07d0d7b2 --- /dev/null +++ b/lib/better_errors/lib/better_errors/raised_exception.rb @@ -0,0 +1,66 @@ +# @private +module BetterErrors + class RaisedException + attr_reader :exception, :message, :backtrace + + def initialize(exception) + if exception.respond_to?(:original_exception) && exception.original_exception + exception = exception.original_exception + end + + @exception = exception + @message = exception.message + + setup_backtrace + massage_syntax_error + end + + def type + exception.class + end + + private + def has_bindings? + exception.respond_to?(:__better_errors_bindings_stack) && exception.__better_errors_bindings_stack.any? + end + + def setup_backtrace + if has_bindings? + setup_backtrace_from_bindings + else + setup_backtrace_from_backtrace + end + end + + def setup_backtrace_from_bindings + @backtrace = exception.__better_errors_bindings_stack.map { |binding| + file = binding.eval "__FILE__" + line = binding.eval "__LINE__" + name = binding.frame_description + StackFrame.new(file, line, name, binding) + } + end + + def setup_backtrace_from_backtrace + @backtrace = (exception.backtrace || []).map { |frame| + if /\A(?.*?):(?\d+)(:in `(?.*)')?/ =~ frame + StackFrame.new(file, line.to_i, name) + end + }.compact + end + + def massage_syntax_error + case exception.class.to_s + when "Haml::SyntaxError" + if /\A(.+?):(\d+)/ =~ exception.backtrace.first + backtrace.unshift(StackFrame.new($1, $2.to_i, "")) + end + when "SyntaxError" + if /\A(.+?):(\d+): (.*)/m =~ exception.message + backtrace.unshift(StackFrame.new($1, $2.to_i, "")) + @message = $3 + end + end + end + end +end diff --git a/lib/better_errors/lib/better_errors/repl.rb b/lib/better_errors/lib/better_errors/repl.rb new file mode 100644 index 00000000..c0f6463e --- /dev/null +++ b/lib/better_errors/lib/better_errors/repl.rb @@ -0,0 +1,30 @@ +module BetterErrors + # @private + module REPL + PROVIDERS = [ + { impl: "better_errors/repl/basic", + const: :Basic }, + ] + + def self.provider + @provider ||= const_get detect[:const] + end + + def self.provider=(prov) + @provider = prov + end + + def self.detect + PROVIDERS.find { |prov| + test_provider prov + } + end + + def self.test_provider(provider) + require provider[:impl] + true + rescue LoadError + false + end + end +end diff --git a/lib/better_errors/lib/better_errors/repl/basic.rb b/lib/better_errors/lib/better_errors/repl/basic.rb new file mode 100644 index 00000000..7011e448 --- /dev/null +++ b/lib/better_errors/lib/better_errors/repl/basic.rb @@ -0,0 +1,20 @@ +module BetterErrors + module REPL + class Basic + def initialize(binding) + @binding = binding + end + + def send_input(str) + [execute(str), ">>", ""] + end + + private + def execute(str) + "=> #{@binding.eval(str).inspect}\n" + rescue Exception => e + "!! #{e.inspect rescue e.class.to_s rescue "Exception"}\n" + end + end + end +end diff --git a/lib/better_errors/lib/better_errors/repl/pry.rb b/lib/better_errors/lib/better_errors/repl/pry.rb new file mode 100644 index 00000000..412be013 --- /dev/null +++ b/lib/better_errors/lib/better_errors/repl/pry.rb @@ -0,0 +1,78 @@ +require "fiber" +require "pry" + +module BetterErrors + module REPL + class Pry + class Input + def readline + Fiber.yield + end + end + + class Output + def initialize + @buffer = "" + end + + def puts(*args) + args.each do |arg| + @buffer << "#{arg.chomp}\n" + end + end + + def tty? + false + end + + def read_buffer + @buffer + ensure + @buffer = "" + end + end + + def initialize(binding) + @fiber = Fiber.new do + @pry.repl binding + end + @input = Input.new + @output = Output.new + @pry = ::Pry.new input: @input, output: @output + @pry.hooks.clear_all if defined?(@pry.hooks.clear_all) + @fiber.resume + end + + def send_input(str) + local ::Pry.config, color: false, pager: false do + @fiber.resume "#{str}\n" + [@output.read_buffer, *prompt] + end + end + + def prompt + if indent = @pry.instance_variable_get(:@indent) and !indent.indent_level.empty? + ["..", indent.indent_level] + else + [">>", ""] + end + rescue + [">>", ""] + end + + private + def local(obj, attrs) + old_attrs = {} + attrs.each do |k, v| + old_attrs[k] = obj.send k + obj.send "#{k}=", v + end + yield + ensure + old_attrs.each do |k, v| + obj.send "#{k}=", v + end + end + end + end +end diff --git a/lib/better_errors/lib/better_errors/stack_frame.rb b/lib/better_errors/lib/better_errors/stack_frame.rb new file mode 100644 index 00000000..4130425f --- /dev/null +++ b/lib/better_errors/lib/better_errors/stack_frame.rb @@ -0,0 +1,111 @@ +require "set" + +module BetterErrors + # @private + class StackFrame + def self.from_exception(exception) + RaisedException.new(exception).backtrace + end + + attr_reader :filename, :line, :name, :frame_binding + + def initialize(filename, line, name, frame_binding = nil) + @filename = filename + @line = line + @name = name + @frame_binding = frame_binding + + set_pretty_method_name if frame_binding + end + + def application? + if root = BetterErrors.application_root + filename.index(root) == 0 && filename.index("#{root}/vendor") != 0 + end + end + + def application_path + filename[(BetterErrors.application_root.length+1)..-1] + end + + def gem? + Gem.path.any? { |path| filename.index(path) == 0 } + end + + def gem_path + if path = Gem.path.detect { |path| filename.index(path) == 0 } + gem_name_and_version, path = filename.sub("#{path}/gems/", "").split("/", 2) + /(?.+)-(?[\w.]+)/ =~ gem_name_and_version + "#{gem_name} (#{gem_version}) #{path}" + end + end + + def class_name + @class_name + end + + def method_name + @method_name || @name + end + + def context + if gem? + :gem + elsif application? + :application + else + :dunno + end + end + + def pretty_path + case context + when :application; application_path + when :gem; gem_path + else filename + end + end + + def local_variables + return {} unless frame_binding + frame_binding.eval("local_variables").each_with_object({}) do |name, hash| + if defined?(frame_binding.local_variable_get) + hash[name] = frame_binding.local_variable_get(name) + else + hash[name] = frame_binding.eval(name.to_s) + end + end + end + + def instance_variables + return {} unless frame_binding + Hash[visible_instance_variables.map { |x| + [x, frame_binding.eval(x.to_s)] + }] + end + + def visible_instance_variables + frame_binding.eval("instance_variables") - BetterErrors.ignored_instance_variables + end + + def to_s + "#{pretty_path}:#{line}:in `#{name}'" + end + + private + def set_pretty_method_name + name =~ /\A(block (\([^)]+\) )?in )?/ + recv = frame_binding.eval("self") + + return unless method_name = frame_binding.eval("::Kernel.__method__") + + if Module === recv + @class_name = "#{$1}#{recv}" + @method_name = ".#{method_name}" + else + @class_name = "#{$1}#{Kernel.instance_method(:class).bind(recv).call}" + @method_name = "##{method_name}" + end + end + end +end diff --git a/lib/better_errors/lib/better_errors/templates/main.erb b/lib/better_errors/lib/better_errors/templates/main.erb new file mode 100644 index 00000000..7c3ff9fe --- /dev/null +++ b/lib/better_errors/lib/better_errors/templates/main.erb @@ -0,0 +1,1031 @@ + + + + <%= exception.type %> at <%= request_path %> + + + <%# Stylesheets are placed in the for Turbolinks compatibility. %> + + + <%# IE8 compatibility crap %> + + + <%# + If Rails's Turbolinks is used, the Better Errors page is probably + rendered in the host app's layout. Let's empty out the styles of the + host app. + %> + + +
+
+

<%= exception.type %> at <%= request_path %>

+

<%= exception.message %>

+
+
+ +
+ + + <% backtrace_frames.each_with_index do |frame, index| %> + + <% end %> +
+ + + + + diff --git a/lib/better_errors/lib/better_errors/templates/text.erb b/lib/better_errors/lib/better_errors/templates/text.erb new file mode 100644 index 00000000..fe9068bb --- /dev/null +++ b/lib/better_errors/lib/better_errors/templates/text.erb @@ -0,0 +1,21 @@ +<%== text_heading("=", "%s at %s" % [exception.type, request_path]) %> + +> <%== exception.message %> +<% if backtrace_frames.any? %> + +<%== text_heading("-", "%s, line %i" % [first_frame.pretty_path, first_frame.line]) %> + +``` ruby +<%== text_formatted_code_block(first_frame) %>``` + +App backtrace +------------- + +<%== application_frames.map { |s| " - #{s}" }.join("\n") %> + +Full backtrace +-------------- + +<%== backtrace_frames.map { |s| " - #{s}" }.join("\n") %> + +<% end %> diff --git a/lib/better_errors/lib/better_errors/templates/variable_info.erb b/lib/better_errors/lib/better_errors/templates/variable_info.erb new file mode 100644 index 00000000..bde5daab --- /dev/null +++ b/lib/better_errors/lib/better_errors/templates/variable_info.erb @@ -0,0 +1,70 @@ +
+
+

<%= @frame.name %>

+ +
+
+ <%== html_formatted_code_block @frame %> +
+ + <% if BetterErrors.binding_of_caller_available? && @frame.frame_binding %> +
+
+

+                
>>
+
+
+ <% end %> +
+ +<% if BetterErrors.binding_of_caller_available? && @frame.frame_binding %> +
+ This is a live shell. Type in here. +
+ +
+<% end %> + +<% unless BetterErrors.binding_of_caller_available? %> +
+ Tip: add gem "binding_of_caller" to your Gemfile to enable the REPL and local/instance variable inspection. +
+<% end %> + +
+

Request info

+
+ + <% if rails_params %> + + <% end %> + <% if rack_session %> + + <% end %> +
Request parameters
<%== inspect_value rails_params %>
Rack session
<%== inspect_value rack_session %>
+
+
+ +
+

Local Variables

+
+ + <% @frame.local_variables.each do |name, value| %> + + <% end %> +
<%= name %>
<%== inspect_value value %>
+
+
+ +
+

Instance Variables

+
+ + <% @frame.instance_variables.each do |name, value| %> + + <% end %> +
<%= name %>
<%== inspect_value value %>
+
+
+ + diff --git a/lib/better_errors/lib/better_errors/version.rb b/lib/better_errors/lib/better_errors/version.rb new file mode 100644 index 00000000..81769ce8 --- /dev/null +++ b/lib/better_errors/lib/better_errors/version.rb @@ -0,0 +1,3 @@ +module BetterErrors + VERSION = "1.1.0" +end diff --git a/lib/better_errors/spec/better_errors/code_formatter_spec.rb b/lib/better_errors/spec/better_errors/code_formatter_spec.rb new file mode 100644 index 00000000..c084e2f9 --- /dev/null +++ b/lib/better_errors/spec/better_errors/code_formatter_spec.rb @@ -0,0 +1,92 @@ +require "spec_helper" + +module BetterErrors + describe CodeFormatter do + let(:filename) { File.expand_path("../support/my_source.rb", __FILE__) } + + let(:formatter) { CodeFormatter.new(filename, 8) } + + it "picks an appropriate scanner" do + formatter.coderay_scanner.should == :ruby + end + + it "shows 5 lines of context" do + formatter.line_range.should == (3..13) + + formatter.context_lines.should == [ + "three\n", + "four\n", + "five\n", + "six\n", + "seven\n", + "eight\n", + "nine\n", + "ten\n", + "eleven\n", + "twelve\n", + "thirteen\n" + ] + end + + it "works when the line is right on the edge" do + formatter = CodeFormatter.new(filename, 20) + formatter.line_range.should == (15..20) + end + + describe CodeFormatter::HTML do + it "highlights the erroring line" do + formatter = CodeFormatter::HTML.new(filename, 8) + formatter.output.should =~ /highlight.*eight/ + end + + it "works when the line is right on the edge" do + formatter = CodeFormatter::HTML.new(filename, 20) + formatter.output.should_not == formatter.source_unavailable + end + + it "doesn't barf when the lines don't make any sense" do + formatter = CodeFormatter::HTML.new(filename, 999) + formatter.output.should == formatter.source_unavailable + end + + it "doesn't barf when the file doesn't exist" do + formatter = CodeFormatter::HTML.new("fkdguhskd7e l", 1) + formatter.output.should == formatter.source_unavailable + end + end + + describe CodeFormatter::Text do + it "highlights the erroring line" do + formatter = CodeFormatter::Text.new(filename, 8) + formatter.output.should == <<-TEXT.gsub(/^ /, "") + 3 three + 4 four + 5 five + 6 six + 7 seven + > 8 eight + 9 nine + 10 ten + 11 eleven + 12 twelve + 13 thirteen + TEXT + end + + it "works when the line is right on the edge" do + formatter = CodeFormatter::Text.new(filename, 20) + formatter.output.should_not == formatter.source_unavailable + end + + it "doesn't barf when the lines don't make any sense" do + formatter = CodeFormatter::Text.new(filename, 999) + formatter.output.should == formatter.source_unavailable + end + + it "doesn't barf when the file doesn't exist" do + formatter = CodeFormatter::Text.new("fkdguhskd7e l", 1) + formatter.output.should == formatter.source_unavailable + end + end + end +end diff --git a/lib/better_errors/spec/better_errors/error_page_spec.rb b/lib/better_errors/spec/better_errors/error_page_spec.rb new file mode 100644 index 00000000..daea57be --- /dev/null +++ b/lib/better_errors/spec/better_errors/error_page_spec.rb @@ -0,0 +1,76 @@ +require "spec_helper" + +module BetterErrors + describe ErrorPage do + let!(:exception) { raise ZeroDivisionError, "you divided by zero you silly goose!" rescue $! } + + let(:error_page) { ErrorPage.new exception, { "PATH_INFO" => "/some/path" } } + + let(:response) { error_page.render } + + let(:empty_binding) { + local_a = :value_for_local_a + local_b = :value_for_local_b + + @inst_c = :value_for_inst_c + @inst_d = :value_for_inst_d + + binding + } + + it "includes the error message" do + response.should include("you divided by zero you silly goose!") + end + + it "includes the request path" do + response.should include("/some/path") + end + + it "includes the exception class" do + response.should include("ZeroDivisionError") + end + + context "variable inspection" do + let(:exception) { empty_binding.eval("raise") rescue $! } + + if BetterErrors.binding_of_caller_available? + it "shows local variables" do + html = error_page.do_variables("index" => 0)[:html] + html.should include("local_a") + html.should include(":value_for_local_a") + html.should include("local_b") + html.should include(":value_for_local_b") + end + else + it "tells the user to add binding_of_caller to their gemfile to get fancy features" do + html = error_page.do_variables("index" => 0)[:html] + html.should include(%{gem "binding_of_caller"}) + end + end + + it "shows instance variables" do + html = error_page.do_variables("index" => 0)[:html] + html.should include("inst_c") + html.should include(":value_for_inst_c") + html.should include("inst_d") + html.should include(":value_for_inst_d") + end + + it "shows filter instance variables" do + BetterErrors.stub(:ignored_instance_variables).and_return([ :@inst_d ]) + html = error_page.do_variables("index" => 0)[:html] + html.should include("inst_c") + html.should include(":value_for_inst_c") + html.should_not include('@inst_d') + html.should_not include("
:value_for_inst_d
") + end + end + + it "doesn't die if the source file is not a real filename" do + exception.stub(:backtrace).and_return([ + ":10:in `spawn_rack_application'" + ]) + response.should include("Source unavailable") + end + end +end diff --git a/lib/better_errors/spec/better_errors/middleware_spec.rb b/lib/better_errors/spec/better_errors/middleware_spec.rb new file mode 100644 index 00000000..2c638bfa --- /dev/null +++ b/lib/better_errors/spec/better_errors/middleware_spec.rb @@ -0,0 +1,146 @@ +require "spec_helper" + +module BetterErrors + describe Middleware do + let(:app) { Middleware.new(->env { ":)" }) } + let(:exception) { RuntimeError.new("oh no :(") } + + it "passes non-error responses through" do + app.call({}).should == ":)" + end + + it "calls the internal methods" do + app.should_receive :internal_call + app.call("PATH_INFO" => "/__better_errors/1/preform_awesomness") + end + + it "calls the internal methods on any subfolder path" do + app.should_receive :internal_call + app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/1/preform_awesomness") + end + + it "shows the error page" do + app.should_receive :show_error_page + app.call("PATH_INFO" => "/__better_errors/") + end + + it "shows the error page on any subfolder path" do + app.should_receive :show_error_page + app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/") + end + + it "doesn't show the error page to a non-local address" do + app.should_not_receive :better_errors_call + app.call("REMOTE_ADDR" => "1.2.3.4") + end + + it "shows to a whitelisted IP" do + BetterErrors::Middleware.allow_ip! '77.55.33.11' + app.should_receive :better_errors_call + app.call("REMOTE_ADDR" => "77.55.33.11") + end + + it "doesn't blow up when given a blank REMOTE_ADDR" do + expect { app.call("REMOTE_ADDR" => " ") }.to_not raise_error + end + + it "doesn't blow up when given an IP address with a zone index" do + expect { app.call("REMOTE_ADDR" => "0:0:0:0:0:0:0:1%0" ) }.to_not raise_error + end + + context "when requesting the /__better_errors manually" do + let(:app) { Middleware.new(->env { ":)" }) } + + it "shows that no errors have been recorded" do + status, headers, body = app.call("PATH_INFO" => "/__better_errors") + body.join.should match /No errors have been recorded yet./ + end + + it "shows that no errors have been recorded on any subfolder path" do + status, headers, body = app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors") + body.join.should match /No errors have been recorded yet./ + end + end + + context "when handling an error" do + let(:app) { Middleware.new(->env { raise exception }) } + + it "returns status 500" do + status, headers, body = app.call({}) + + status.should == 500 + end + + context "original_exception" do + class OriginalExceptionException < Exception + attr_reader :original_exception + + def initialize(message, original_exception = nil) + super(message) + @original_exception = original_exception + end + end + + it "shows Original Exception if it responds_to and has an original_exception" do + app = Middleware.new(->env { + raise OriginalExceptionException.new("Other Exception", Exception.new("Original Exception")) + }) + + status, _, body = app.call({}) + + status.should == 500 + body.join.should_not match(/Other Exception/) + body.join.should match(/Original Exception/) + end + + it "won't crash if the exception responds_to but doesn't have an original_exception" do + app = Middleware.new(->env { + raise OriginalExceptionException.new("Other Exception") + }) + + status, _, body = app.call({}) + + status.should == 500 + body.join.should match(/Other Exception/) + end + end + + it "returns ExceptionWrapper's status_code" do + ad_ew = double("ActionDispatch::ExceptionWrapper") + ad_ew.stub('new').with({}, exception ){ double("ExceptionWrapper", status_code: 404) } + stub_const('ActionDispatch::ExceptionWrapper', ad_ew) + + status, headers, body = app.call({}) + + status.should == 404 + end + + it "returns UTF-8 error pages" do + status, headers, body = app.call({}) + + headers["Content-Type"].should match /charset=utf-8/ + end + + it "returns text pages by default" do + status, headers, body = app.call({}) + + headers["Content-Type"].should match /text\/plain/ + end + + it "returns HTML pages by default" do + # Chrome's 'Accept' header looks similar this. + status, headers, body = app.call("HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*") + + headers["Content-Type"].should match /text\/html/ + end + + it "logs the exception" do + logger = Object.new + logger.should_receive :fatal + BetterErrors.stub(:logger).and_return(logger) + + app.call({}) + end + end + end +end diff --git a/lib/better_errors/spec/better_errors/raised_exception_spec.rb b/lib/better_errors/spec/better_errors/raised_exception_spec.rb new file mode 100644 index 00000000..605ab409 --- /dev/null +++ b/lib/better_errors/spec/better_errors/raised_exception_spec.rb @@ -0,0 +1,52 @@ +require "spec_helper" + +module BetterErrors + describe RaisedException do + let(:exception) { RuntimeError.new("whoops") } + subject { RaisedException.new(exception) } + + its(:exception) { should == exception } + its(:message) { should == "whoops" } + its(:type) { should == RuntimeError } + + context "when the exception wraps another exception" do + let(:original_exception) { RuntimeError.new("something went wrong!") } + let(:exception) { double(:original_exception => original_exception) } + + its(:exception) { should == original_exception } + its(:message) { should == "something went wrong!" } + end + + context "when the exception is a syntax error" do + let(:exception) { SyntaxError.new("foo.rb:123: you made a typo!") } + + its(:message) { should == "you made a typo!" } + its(:type) { should == SyntaxError } + + it "has the right filename and line number in the backtrace" do + subject.backtrace.first.filename.should == "foo.rb" + subject.backtrace.first.line.should == 123 + end + end + + context "when the exception is a HAML syntax error" do + before do + stub_const("Haml::SyntaxError", Class.new(SyntaxError)) + end + + let(:exception) { + Haml::SyntaxError.new("you made a typo!").tap do |ex| + ex.set_backtrace(["foo.rb:123", "haml/internals/blah.rb:123456"]) + end + } + + its(:message) { should == "you made a typo!" } + its(:type) { should == Haml::SyntaxError } + + it "has the right filename and line number in the backtrace" do + subject.backtrace.first.filename.should == "foo.rb" + subject.backtrace.first.line.should == 123 + end + end + end +end diff --git a/lib/better_errors/spec/better_errors/repl/basic_spec.rb b/lib/better_errors/spec/better_errors/repl/basic_spec.rb new file mode 100644 index 00000000..c533f632 --- /dev/null +++ b/lib/better_errors/spec/better_errors/repl/basic_spec.rb @@ -0,0 +1,18 @@ +require "spec_helper" +require "better_errors/repl/basic" +require "better_errors/repl/shared_examples" + +module BetterErrors + module REPL + describe Basic do + let(:fresh_binding) { + local_a = 123 + binding + } + + let(:repl) { Basic.new fresh_binding } + + it_behaves_like "a REPL provider" + end + end +end diff --git a/lib/better_errors/spec/better_errors/repl/pry_spec.rb b/lib/better_errors/spec/better_errors/repl/pry_spec.rb new file mode 100644 index 00000000..1aa502c5 --- /dev/null +++ b/lib/better_errors/spec/better_errors/repl/pry_spec.rb @@ -0,0 +1,40 @@ +require "spec_helper" +require "pry" +require "better_errors/repl/pry" +require "better_errors/repl/shared_examples" + +module BetterErrors + module REPL + describe Pry do + let(:fresh_binding) { + local_a = 123 + binding + } + + let(:repl) { Pry.new fresh_binding } + + it "does line continuation" do + output, prompt, filled = repl.send_input "" + output.should == "=> nil\n" + prompt.should == ">>" + filled.should == "" + + output, prompt, filled = repl.send_input "def f(x)" + output.should == "" + prompt.should == ".." + filled.should == " " + + output, prompt, filled = repl.send_input "end" + if RUBY_VERSION >= "2.1.0" + output.should == "=> :f\n" + else + output.should == "=> nil\n" + end + prompt.should == ">>" + filled.should == "" + end + + it_behaves_like "a REPL provider" + end + end +end diff --git a/lib/better_errors/spec/better_errors/repl/shared_examples.rb b/lib/better_errors/spec/better_errors/repl/shared_examples.rb new file mode 100644 index 00000000..0154851d --- /dev/null +++ b/lib/better_errors/spec/better_errors/repl/shared_examples.rb @@ -0,0 +1,18 @@ +shared_examples_for "a REPL provider" do + it "evaluates ruby code in a given context" do + repl.send_input("local_a = 456") + fresh_binding.eval("local_a").should == 456 + end + + it "returns a tuple of output and the new prompt" do + output, prompt = repl.send_input("1 + 2") + output.should == "=> 3\n" + prompt.should == ">>" + end + + it "doesn't barf if the code throws an exception" do + output, prompt = repl.send_input("raise Exception") + output.should include "Exception: Exception" + prompt.should == ">>" + end +end diff --git a/lib/better_errors/spec/better_errors/stack_frame_spec.rb b/lib/better_errors/spec/better_errors/stack_frame_spec.rb new file mode 100644 index 00000000..420111ee --- /dev/null +++ b/lib/better_errors/spec/better_errors/stack_frame_spec.rb @@ -0,0 +1,157 @@ +require "spec_helper" + +module BetterErrors + describe StackFrame do + context "#application?" do + it "is true for application filenames" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") + + frame.application?.should be_true + end + + it "is false for everything else" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + frame = StackFrame.new("/abc/nope", 123, "foo") + + frame.application?.should be_false + end + + it "doesn't care if no application_root is set" do + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") + + frame.application?.should be_false + end + end + + context "#gem?" do + it "is true for gem filenames" do + Gem.stub(:path).and_return(["/abc/xyz"]) + frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") + + frame.gem?.should be_true + end + + it "is false for everything else" do + Gem.stub(:path).and_return(["/abc/xyz"]) + frame = StackFrame.new("/abc/nope", 123, "foo") + + frame.gem?.should be_false + end + end + + context "#application_path" do + it "chops off the application root" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") + + frame.application_path.should == "app/controllers/crap_controller.rb" + end + end + + context "#gem_path" do + it "chops of the gem path and stick (gem) there" do + Gem.stub(:path).and_return(["/abc/xyz"]) + frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") + + frame.gem_path.should == "whatever (1.2.3) lib/whatever.rb" + end + + it "prioritizes gem path over application path" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + Gem.stub(:path).and_return(["/abc/xyz/vendor"]) + frame = StackFrame.new("/abc/xyz/vendor/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") + + frame.gem_path.should == "whatever (1.2.3) lib/whatever.rb" + end + end + + context "#pretty_path" do + it "returns #application_path for application paths" do + BetterErrors.stub(:application_root).and_return("/abc/xyz") + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") + frame.pretty_path.should == frame.application_path + end + + it "returns #gem_path for gem paths" do + Gem.stub(:path).and_return(["/abc/xyz"]) + frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") + + frame.pretty_path.should == frame.gem_path + end + end + + it "special cases SyntaxErrors" do + begin + eval(%{ raise SyntaxError, "you wrote bad ruby!" }, nil, "my_file.rb", 123) + rescue SyntaxError => syntax_error + end + frames = StackFrame.from_exception(syntax_error) + frames.first.filename.should == "my_file.rb" + frames.first.line.should == 123 + end + + it "doesn't blow up if no method name is given" do + error = StandardError.allocate + + error.stub(:backtrace).and_return(["foo.rb:123"]) + frames = StackFrame.from_exception(error) + frames.first.filename.should == "foo.rb" + frames.first.line.should == 123 + + error.stub(:backtrace).and_return(["foo.rb:123: this is an error message"]) + frames = StackFrame.from_exception(error) + frames.first.filename.should == "foo.rb" + frames.first.line.should == 123 + end + + it "ignores a backtrace line if its format doesn't make any sense at all" do + error = StandardError.allocate + error.stub(:backtrace).and_return(["foo.rb:123:in `foo'", "C:in `find'", "bar.rb:123:in `bar'"]) + frames = StackFrame.from_exception(error) + frames.count.should == 2 + end + + it "doesn't blow up if a filename contains a colon" do + error = StandardError.allocate + error.stub(:backtrace).and_return(["crap:filename.rb:123"]) + frames = StackFrame.from_exception(error) + frames.first.filename.should == "crap:filename.rb" + end + + it "doesn't blow up with a BasicObject as frame binding" do + obj = BasicObject.new + def obj.my_binding + ::Kernel.binding + end + frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index", obj.my_binding) + frame.class_name.should == 'BasicObject' + end + + it "sets method names properly" do + obj = "string" + def obj.my_method + begin + raise "foo" + rescue => err + err + end + end + + frame = StackFrame.from_exception(obj.my_method).first + if BetterErrors.binding_of_caller_available? + frame.method_name.should == "#my_method" + frame.class_name.should == "String" + else + frame.method_name.should == "my_method" + frame.class_name.should == nil + end + end + + if RUBY_ENGINE == "java" + it "doesn't blow up on a native Java exception" do + expect { StackFrame.from_exception(java.lang.Exception.new) }.to_not raise_error + end + end + end +end diff --git a/lib/better_errors/spec/better_errors/support/my_source.rb b/lib/better_errors/spec/better_errors/support/my_source.rb new file mode 100644 index 00000000..6dfea0f6 --- /dev/null +++ b/lib/better_errors/spec/better_errors/support/my_source.rb @@ -0,0 +1,20 @@ +one +two +three +four +five +six +seven +eight +nine +ten +eleven +twelve +thirteen +fourteen +fifteen +sixteen +seventeen +eighteen +nineteen +twenty diff --git a/lib/better_errors/spec/better_errors_spec.rb b/lib/better_errors/spec/better_errors_spec.rb new file mode 100644 index 00000000..e9b26b67 --- /dev/null +++ b/lib/better_errors/spec/better_errors_spec.rb @@ -0,0 +1,73 @@ +require "spec_helper" + +describe BetterErrors do + context ".editor" do + it "defaults to textmate" do + subject.editor["foo.rb", 123].should == "txmt://open?url=file://foo.rb&line=123" + end + + it "url escapes the filename" do + subject.editor["&.rb", 0].should == "txmt://open?url=file://%26.rb&line=0" + end + + [:emacs, :emacsclient].each do |editor| + it "uses emacs:// scheme when set to #{editor.inspect}" do + subject.editor = editor + subject.editor[].should start_with "emacs://" + end + end + + [:macvim, :mvim].each do |editor| + it "uses mvim:// scheme when set to #{editor.inspect}" do + subject.editor = editor + subject.editor[].should start_with "mvim://" + end + end + + [:sublime, :subl, :st].each do |editor| + it "uses subl:// scheme when set to #{editor.inspect}" do + subject.editor = editor + subject.editor[].should start_with "subl://" + end + end + + [:textmate, :txmt, :tm].each do |editor| + it "uses txmt:// scheme when set to #{editor.inspect}" do + subject.editor = editor + subject.editor[].should start_with "txmt://" + end + end + + ["emacsclient", "/usr/local/bin/emacsclient"].each do |editor| + it "uses emacs:// scheme when EDITOR=#{editor}" do + ENV["EDITOR"] = editor + subject.editor = subject.default_editor + subject.editor[].should start_with "emacs://" + end + end + + ["mvim -f", "/usr/local/bin/mvim -f"].each do |editor| + it "uses mvim:// scheme when EDITOR=#{editor}" do + ENV["EDITOR"] = editor + subject.editor = subject.default_editor + subject.editor[].should start_with "mvim://" + end + end + + ["subl -w", "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"].each do |editor| + it "uses mvim:// scheme when EDITOR=#{editor}" do + ENV["EDITOR"] = editor + subject.editor = subject.default_editor + subject.editor[].should start_with "subl://" + end + end + + ["mate -w", "/usr/bin/mate -w"].each do |editor| + it "uses txmt:// scheme when EDITOR=#{editor}" do + ENV["EDITOR"] = editor + subject.editor = subject.default_editor + subject.editor[].should start_with "txmt://" + end + end + end +end diff --git a/lib/better_errors/spec/spec_helper.rb b/lib/better_errors/spec/spec_helper.rb new file mode 100644 index 00000000..40d63261 --- /dev/null +++ b/lib/better_errors/spec/spec_helper.rb @@ -0,0 +1,5 @@ +$: << File.expand_path("../../lib", __FILE__) + +ENV["EDITOR"] = nil + +require "better_errors" diff --git a/lib/better_errors/spec/without_binding_of_caller.rb b/lib/better_errors/spec/without_binding_of_caller.rb new file mode 100644 index 00000000..035e2e90 --- /dev/null +++ b/lib/better_errors/spec/without_binding_of_caller.rb @@ -0,0 +1,9 @@ +module Kernel + alias_method :require_with_binding_of_caller, :require + + def require(feature) + raise LoadError if feature == "binding_of_caller" + + require_with_binding_of_caller(feature) + end +end diff --git a/lib/rack-mini-profiler/.travis.yml b/lib/rack-mini-profiler/.travis.yml new file mode 100644 index 00000000..61ab0d68 --- /dev/null +++ b/lib/rack-mini-profiler/.travis.yml @@ -0,0 +1,9 @@ +language: ruby +rvm: + - 1.9.3 + - 2.0.0 + - 2.1.1 +bundler_args: "" +services: + - redis + - memcached diff --git a/lib/rack-mini-profiler/CHANGELOG b/lib/rack-mini-profiler/CHANGELOG new file mode 100644 index 00000000..7c08a89e --- /dev/null +++ b/lib/rack-mini-profiler/CHANGELOG @@ -0,0 +1,181 @@ +28-June-2012 - Sam + + * Started change log + * Corrected profiler so it properly captures POST requests (was supressing non 200s) + * Amended Rack.MiniProfiler.config[:user_provider] to use ip addres for identity + * Fixed bug where unviewed missing ids never got cleared + * Supress all '/assets/' in the rails tie (makes debugging easier) + * record_sql was mega buggy + * added MemcacheStore + +9-July-2012 - Sam + + * Cleaned up mechanism for profiling in production, all you need to do now + is call Rack::MiniProfiler.authorize_request to get profiling working in + production + * Added option to display full backtraces pp=full-backtrace + * Cleaned up railties, got rid of the post authorize callback + * Version 0.1.3 + +12-July-2012 - Sam + + * Fixed incorrect profiling steps (was not indenting or measuring start time right + * Implemented native PG and MySql2 interceptors, this gives way more accurate times + * Refactored context so its a proper class and not a hash + * Added some more client probing built in to rails + * More tests + +18-July-2012 - Sam + + * Added First Paint time for chrome + * Bug fix to ensure non Rails installs have mini profiler + * Version 0.1.7 + +30-July-2012 - Sam + + * Made compliant with ancient versions of Rack (including Rack used by Rails2) + * Fixed broken share link + * Fixed crashes on startup (in MemoryStore and FileStore) + * Version 0.1.8 + * Unicode fix + * Version 0.1.9 + +7-August-2012 - Sam + + * Added option to disable profiler for the current session (pp=disable / pp=enable) + * yajl compatability contributed by Sven Riedel + +10-August-2012 - Sam + + * Added basic prepared statement profiling for postgres + +20-August-2012 - Sam + + * 1.12.pre + * Cap X-MiniProfiler-Ids at 10, otherwise the header can get killed + +3-September-2012 - Sam + + * 1.13.pre + * pg gem prepared statements were not being logged correctly + * added setting config.backtrace_ignores = [] - an array of regexes that match on caller lines that get ignored + * added setting config.backtrace_includes = [] - an array of regexes that get included in the trace by default + * cleaned up the way client settings are stored + * made pp=full-backtrace "sticky" + * added pp=normal-backtrace to clear the "sticky" state + * change "pp=sample" to work with "caller" no need for stack trace gem + +4-September-2012 - Sam + + * 1.15.pre + * fixed annoying bug where client settings were not sticking + * fixed long standing issue with Rack::ConditionalGet stopping MiniProfiler from working properly + +5-September-2012 - Sam + + * 1.16 + * fixed long standing problem specs (issue with memory store) + * fixed issue where profiler would be dumped when you got a 404 in production (and any time rails is bypassed) + * implemented stacktrace properly + +9-September-2012 - Sam + + * 1.17 + * pp=sample was bust unless stacktrace was installed + +10-September-2012 - Sam + + * 1.19 + * fix compat issue with 1.8.7 + +12-September-2012 - Sam + + * 1.20 + * Added pp=profile-gc , it allows you to profile the GC in Ruby 1.9.3 + +17-September-2012 + * 1.21 + * New MemchacedStore + * Rails 4 support + +17-September-2012 + * Allow rack-mini-profiler to be sourced from github + * Extracted the pp=profile-gc-time out, the object space profiler needs to disable gc + +20-September-2012 + * 1.22 + * Fix permission issue in the gem + +8-April-2013 + * 1.24 + * Flame Graph Support see: http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler + * Fix file retention leak in file_store + * New toggle_shortcut and start_hidden options + * Fix for AngularJS support and MooTools + * More robust gc profiling + * Mongoid support + * Fix for html5 implicit body tags + * script tag initialized via data-attributes + * new - Rack::MiniProfiler.counter counter_name {} + * Allow usage of existing jQuery if its already loaded + * Fix pp=enable + * 1.8.7 support ... grrr + * Net:HTTP profiling + * pre authorize to run in all non development? and production? modes + +8-April-2013 + * 1.25 + * Missed flamegraph.html from build + +11-April-2013 + * 1.26 + * (minor) allow Rack::MiniProfilerRails.initialize!(Rails.application), for post config intialization + +26-June-2013 + * 1.27 + * Disable global ajax handlers on MP requests @JP + * Add Rack::MiniProfiler.config.backtrace_threshold_ms + * jQuery 2.0 support + +18-July-2013 + * 1.28 + * diagnostics in abstract storage was raising not implemented killing + ?pp=env and others + * SOLR xml unescaped by mistake + +20-August-2013 + * 1.29 + * Bugfix: SOLR patching had an incorrect monkey patch + * Implemented exception tracing using TracePoint see pp=trace-exceptions + +30-August-2013 + + * 1.30 + * Feature: Added Rack::MiniProfiler.counter_method(klass,name) for injecting counters + * Bug: Counters were not shifting the table correctly + +3-September-2013 + + * Ripped out flamegraph so it can be isolated into a gem + * Flamegraph now has much increased fidelity + * Ripped out pp=sample it just was never really used + +17-September-2013 - Ross Wilson + * Instead of supressing all "/assets/" requests we now check the configured + config.assets.prefix path since developers can rename the path to serve Asset Pipeline + files from + +12-December-2013 - Sam Saffron + * Version 0.9.0.pre (bumped up to reflect the stability of the project) + * Improved reports for pp=profile-gc + * pp=flamegraph&flamegraph_sample_rate=1 , allow you to specify sampling rates + +13-March-2014 - Sam Saffron + * Version 0.9.1 + * Added back Ruby 1.8 support (thanks Malet) + * Corrected Rails 3.0 support (thanks Zlatko) + * Corrected fix possible XSS (admin only) + * Amend Railstie so MiniProfiler can be launched with action view or action controller (Thanks Akira) + * Corrected Sql patching to avoid setting instance vars on nil which is frozen (thanks Andy, huoxito) + + diff --git a/lib/rack-mini-profiler/Gemfile b/lib/rack-mini-profiler/Gemfile new file mode 100644 index 00000000..d65e2a66 --- /dev/null +++ b/lib/rack-mini-profiler/Gemfile @@ -0,0 +1,3 @@ +source 'http://rubygems.org' + +gemspec diff --git a/lib/rack-mini-profiler/README.md b/lib/rack-mini-profiler/README.md new file mode 100644 index 00000000..550dc1e0 --- /dev/null +++ b/lib/rack-mini-profiler/README.md @@ -0,0 +1,271 @@ +# rack-mini-profiler + +[![Code Climate](https://codeclimate.com/github/MiniProfiler/rack-mini-profiler.png)](https://codeclimate.com/github/MiniProfiler/rack-mini-profiler) [![Build Status](https://travis-ci.org/MiniProfiler/rack-mini-profiler.png)](https://travis-ci.org/MiniProfiler/rack-mini-profiler) + +Middleware that displays speed badge for every html page. Designed to work both in production and in development. + +#### Features + +* database profiling. Currently supports Mysql2, Postgres, and Mongoid3 (with fallback support to ActiveRecord) + +#### Learn more + +* [Visit our community](http://community.miniprofiler.com) +* [Watch the RailsCast](http://railscasts.com/episodes/368-miniprofiler) +* [Read about Flame graphs in rack-mini-profiler](http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler) +* [Read the announcement posts from 2012](http://samsaffron.com/archive/2012/07/12/miniprofiler-ruby-edition) + +## rack-mini-profiler needs your help + +We have decided to restructure our repository so there is a central UI repo and the various language implementation have their own. + +**WE NEED HELP.** + +- Setting up a build that reuses https://github.com/MiniProfiler/ui +- Migrating the internal data structures [per the spec](https://github.com/MiniProfiler/ui) +- Cleaning up the [horrendous class structure that is using strings as keys and crazy non-objects](https://github.com/MiniProfiler/rack-mini-profiler/blob/master/lib/mini_profiler/sql_timer_struct.rb#L36-L44) + +If you feel like taking on any of this start an issue and update us on your progress. + +## Installation + +Install/add to Gemfile + +```ruby +gem 'rack-mini-profiler' +``` + +NOTE: Be sure to require rack_mini_profiler below the `pg` and `mysql` gems in your Gemfile. rack_mini_profiler will identify these gems if they are loaded to insert instrumentation. If included too early no SQL will show up. + +#### Rails + +All you have to do is include the Gem and you're good to go in development. See notes below for use in production. + +#### Rails and manual initialization + +In case you need to make sure rack_mini_profiler initialized after all other gems. +Or you want to execute some code before rack_mini_profiler required. + +```ruby +gem 'rack-mini-profiler', require: false +``` +Note the `require: false` part - if omitted, it will cause the Railtie for the mino-profiler to +be loaded outright, and an attempt to re-initialize it manually will raise an exception. + +Then put initialize code in file like `config/initializers/rack_profiler.rb` + +```ruby +if Rails.env == 'development' + require 'rack-mini-profiler' + + # initialization is skipped so trigger it + Rack::MiniProfilerRails.initialize!(Rails.application) +end +``` + +#### Rack Builder + +```ruby +require 'rack-mini-profiler' +builder = Rack::Builder.new do + use Rack::MiniProfiler + + map('/') { run get } +end +``` + +#### Sinatra + +```ruby +require 'rack-mini-profiler' +class MyApp < Sinatra::Base + use Rack::MiniProfiler +end +``` + +### Flamegraphs + +To generate [flamegraphs](http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler): + +* add the **flamegraph** gem to your Gemfile +* visit a page in your app with `?pp=flamegraph` + +Flamegraph generation is supported in MRI 2.0 and 2.1 only. + + +## Access control in production + +rack-mini-profiler is designed with production profiling in mind. To enable that just run `Rack::MiniProfiler.authorize_request` once you know a request is allowed to profile. + +```ruby +# A hook in your ApplicationController +def authorize + if current_user.is_admin? + Rack::MiniProfiler.authorize_request + end +end +``` + +## Configuration + +Various aspects of rack-mini-profiler's behavior can be configured when your app boots. +For example in a Rails app, this should be done in an initializer: +**config/initializers/mini_profiler.rb** + +### Storage + +rack-mini-profiler stores its results so they can be shared later and aren't lost at the end of the request. + +There are 4 storage options: `MemoryStore`, `RedisStore`, `MemcacheStore`, and `FileStore`. + +`FileStore` is the default in Rails environments and will write files to `tmp/miniprofiler/*`. `MemoryStore` is the default otherwise. + +```ruby +# set MemoryStore +Rack::MiniProfiler.config.storage = Rack::MiniProfiler::MemoryStore + +# set RedisStore +if Rails.env.production? + uri = URI.parse(ENV["REDIS_SERVER_URL"]) + Rack::MiniProfiler.config.storage_options = { :host => uri.host, :port => uri.port, :password => uri.password } + Rack::MiniProfiler.config.storage = Rack::MiniProfiler::RedisStore +end +``` + +MemoryStore stores results in a processes heap - something that does not work well in a multi process environment. +FileStore stores results in the file system - something that may not work well in a multi machine environment. +RedisStore/MemcacheStore work in multi process and multi machine environments (RedisStore only saves results for up to 24 hours so it won't continue to fill up Redis). + +Additionally you may implement an AbstractStore for your own provider. + +### User result segregation + +MiniProfiler will attempt to keep all user results isolated, out-of-the-box the user provider uses the ip address: + +```ruby +Rack::MiniProfiler.config.user_provider = Proc.new{|env| Rack::Request.new(env).ip} +``` + +You can override (something that is very important in a multi-machine production setup): + +```ruby +Rack::MiniProfiler.config.user_provider = Proc.new{ |env| CurrentUser.get(env) } +``` + +The string this function returns should be unique for each user on the system (for anonymous you may need to fall back to ip address) + +### Configuration Options + +You can set configuration options using the configuration accessor on `Rack::MiniProfiler`. +For example: + +```ruby +Rack::MiniProfiler.config.position = 'right' +Rack::MiniProfiler.config.start_hidden = true +``` +The available configuration options are: + +* pre_authorize_cb - A lambda callback you can set to determine whether or not mini_profiler should be visible on a given request. Default in a Rails environment is only on in development mode. If in a Rack app, the default is always on. +* position - Can either be 'right' or 'left'. Default is 'left'. +* skip_schema_queries - Whether or not you want to log the queries about the schema of your tables. Default is 'false', 'true' in rails development. +* auto_inject (default true) - when false the miniprofiler script is not injected in the page +* backtrace_filter - a regex you can use to filter out unwanted lines from the backtraces +* toggle_shortcut (default Alt+P) - a jquery.hotkeys.js-style keyboard shortcut, used to toggle the mini_profiler's visibility. See http://code.google.com/p/js-hotkeys/ for more info. +* start_hidden (default false) - Whether or not you want the mini_profiler to be visible when loading a page +* backtrace_threshold_ms (default zero) - Minimum SQL query elapsed time before a backtrace is recorded. Backtrace recording can take a couple of milliseconds on rubies earlier than 2.0, impacting performance for very small queries. +* flamegraph_sample_rate (default 0.5ms) - How often fast_stack should get stack trace info to generate flamegraphs + +### Custom middleware ordering (required if using `Rack::Deflate` with Rails) + +If you are using `Rack::Deflate` with rails and rack-mini-profiler in its default configuration, +`Rack::MiniProfiler` will be injected (as always) at position 0 in the middleware stack. This +will result in it attempting to inject html into the already-compressed response body. To fix this, +the middleware ordering must be overriden. + +To do this, first add `, require: false` to the gemfile entry for rack-mini-profiler. +This will prevent the railtie from running. Then, customize the initialization +in the initializer like so: + +```ruby +require 'rack-mini-profiler' + +Rack::MiniProfilerRails.initialize!(Rails.application) + +Rails.application.middleware.delete(Rack::MiniProfiler) +Rails.application.middleware.insert_after(Rack::Deflater, Rack::MiniProfiler) +``` + +Deleting the middleware and then reinserting it is a bit inelegant, but +a sufficient and costless solution. It is possible that rack-mini-profiler might +support this scenario more directly if it is found that +there is significant need for this confriguration or that +the above recipe causes problems. + + +## Special query strings + +If you include the query string `pp=help` at the end of your request you will see the various options available. You can use these options to extend or contract the amount of diagnostics rack-mini-profiler gathers. + + +## Rails 2.X support + +To get MiniProfiler working with Rails 2.3.X you need to do the initialization manually as well as monkey patch away an incompatibility between activesupport and json_pure. + +Add the following code to your environment.rb (or just in a specific environment such as development.rb) for initialization and configuration of MiniProfiler. + +```ruby +# configure and initialize MiniProfiler +require 'rack-mini-profiler' +c = ::Rack::MiniProfiler.config +c.pre_authorize_cb = lambda { |env| + Rails.env.development? || Rails.env.production? +} +tmp = Rails.root.to_s + "/tmp/miniprofiler" +FileUtils.mkdir_p(tmp) unless File.exists?(tmp) +c.storage_options = {:path => tmp} +c.storage = ::Rack::MiniProfiler::FileStore +config.middleware.use(::Rack::MiniProfiler) +::Rack::MiniProfiler.profile_method(ActionController::Base, :process) {|action| "Executing action: #{action}"} +::Rack::MiniProfiler.profile_method(ActionView::Template, :render) {|x,y| "Rendering: #{path_without_format_and_extension}"} + +# monkey patch away an activesupport and json_pure incompatability +# http://pivotallabs.com/users/alex/blog/articles/1332-monkey-patch-of-the-day-activesupport-vs-json-pure-vs-ruby-1-8 +if JSON.const_defined?(:Pure) + class JSON::Pure::Generator::State + include ActiveSupport::CoreExtensions::Hash::Except + end +end +``` + +## Running the Specs + +``` +$ rake build +$ rake spec +``` + +Additionally you can also run `autotest` if you like. + +## Licence + +The MIT License (MIT) + +Copyright (c) 2013 Sam Saffron + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/rack-mini-profiler/Rakefile b/lib/rack-mini-profiler/Rakefile new file mode 100644 index 00000000..a908b5b6 --- /dev/null +++ b/lib/rack-mini-profiler/Rakefile @@ -0,0 +1,46 @@ +# Rakefile +require 'rubygems' +require 'bundler' +Bundler.setup(:default, :test) + +task :default => [:spec] + +require 'rspec/core' +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = FileList['spec/**/*_spec.rb'] +end + +desc "builds a gem" +task :build => :update_asset_version do + `gem build rack-mini-profiler.gemspec 1>&2` +end + +desc "compile less" +task :compile_less => :copy_files do + `lessc lib/html/includes.less > lib/html/includes.css` +end + +desc "update asset version file" +task :update_asset_version => :compile_less do + require 'digest/md5' + h = [] + Dir.glob('lib/html/*.{js,html,css,tmpl}').each do |f| + h << Digest::MD5.hexdigest(::File.read(f)) + end + File.open('lib/mini_profiler/version.rb','w') do |f| + f.write \ +"module Rack + class MiniProfiler + VERSION = '#{Digest::MD5.hexdigest(h.sort.join(''))}'.freeze + end +end" + end +end + + +desc "copy files from other parts of the tree" +task :copy_files do + # TODO grab files from MiniProfiler/UI +end + diff --git a/lib/rack-mini-profiler/autotest/discover.rb b/lib/rack-mini-profiler/autotest/discover.rb new file mode 100644 index 00000000..de32a012 --- /dev/null +++ b/lib/rack-mini-profiler/autotest/discover.rb @@ -0,0 +1,2 @@ +Autotest.add_discovery { "rspec2" } + diff --git a/lib/rack-mini-profiler/lib/html/includes.css b/lib/rack-mini-profiler/lib/html/includes.css new file mode 100644 index 00000000..d96528b8 --- /dev/null +++ b/lib/rack-mini-profiler/lib/html/includes.css @@ -0,0 +1,451 @@ +.profiler-result, +.profiler-queries { + color: #555; + line-height: 1; + font-size: 12px; +} +.profiler-result pre, +.profiler-queries pre, +.profiler-result code, +.profiler-queries code, +.profiler-result label, +.profiler-queries label, +.profiler-result table, +.profiler-queries table, +.profiler-result tbody, +.profiler-queries tbody, +.profiler-result thead, +.profiler-queries thead, +.profiler-result tfoot, +.profiler-queries tfoot, +.profiler-result tr, +.profiler-queries tr, +.profiler-result th, +.profiler-queries th, +.profiler-result td, +.profiler-queries td { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + background-color: transparent; + overflow: visible; + max-height: none; +} +.profiler-result table, +.profiler-queries table { + border-collapse: collapse; + border-spacing: 0; +} +.profiler-result a, +.profiler-queries a, +.profiler-result a:hover, +.profiler-queries a:hover { + cursor: pointer; + color: #0077cc; +} +.profiler-result a, +.profiler-queries a { + text-decoration: none; +} +.profiler-result a:hover, +.profiler-queries a:hover { + text-decoration: underline; +} +.profiler-result { + font-family: Helvetica, Arial, sans-serif; +} +.profiler-result .profiler-toggle-duration-with-children { + float: right; +} +.profiler-result table.profiler-client-timings { + margin-top: 10px; +} +.profiler-result .profiler-label { + color: #555555; + overflow: hidden; + text-overflow: ellipsis; +} +.profiler-result .profiler-unit { + color: #aaaaaa; +} +.profiler-result .profiler-trivial { + display: none; +} +.profiler-result .profiler-trivial td, +.profiler-result .profiler-trivial td * { + color: #aaaaaa !important; +} +.profiler-result pre, +.profiler-result code, +.profiler-result .profiler-number, +.profiler-result .profiler-unit { + font-family: Consolas, monospace, serif; +} +.profiler-result .profiler-number { + color: #111111; +} +.profiler-result .profiler-info { + text-align: right; +} +.profiler-result .profiler-info .profiler-name { + float: left; +} +.profiler-result .profiler-info .profiler-server-time { + white-space: nowrap; +} +.profiler-result .profiler-timings th { + background-color: #fff; + color: #aaaaaa; + text-align: right; +} +.profiler-result .profiler-timings th, +.profiler-result .profiler-timings td { + white-space: nowrap; +} +.profiler-result .profiler-timings .profiler-duration-with-children { + display: none; +} +.profiler-result .profiler-timings .profiler-duration { + font-family: Consolas, monospace, serif; + color: #111111; + text-align: right; +} +.profiler-result .profiler-timings .profiler-indent { + letter-spacing: 4px; +} +.profiler-result .profiler-timings .profiler-queries-show .profiler-number, +.profiler-result .profiler-timings .profiler-queries-show .profiler-unit { + color: #0077cc; +} +.profiler-result .profiler-timings .profiler-queries-duration { + padding-left: 6px; +} +.profiler-result .profiler-timings .profiler-percent-in-sql { + white-space: nowrap; + text-align: right; +} +.profiler-result .profiler-timings tfoot td { + padding-top: 10px; + text-align: right; +} +.profiler-result .profiler-timings tfoot td a { + font-size: 95%; + display: inline-block; + margin-left: 12px; +} +.profiler-result .profiler-timings tfoot td a:first-child { + float: left; + margin-left: 0px; +} +.profiler-result .profiler-timings tfoot td a.profiler-custom-link { + float: left; +} +.profiler-result .profiler-queries { + font-family: Helvetica, Arial, sans-serif; +} +.profiler-result .profiler-queries .profiler-stack-trace { + margin-bottom: 15px; +} +.profiler-result .profiler-queries pre { + font-family: Consolas, monospace, serif; + white-space: pre-wrap; +} +.profiler-result .profiler-queries th { + background-color: #fff; + border-bottom: 1px solid #555; + font-weight: bold; + padding: 15px; + white-space: nowrap; +} +.profiler-result .profiler-queries td { + padding: 15px; + text-align: left; + background-color: #fff; +} +.profiler-result .profiler-queries td:last-child { + padding-right: 25px; +} +.profiler-result .profiler-queries .profiler-odd td { + background-color: #e5e5e5; +} +.profiler-result .profiler-queries .profiler-since-start, +.profiler-result .profiler-queries .profiler-duration { + text-align: right; +} +.profiler-result .profiler-queries .profiler-info div { + text-align: right; + margin-bottom: 5px; +} +.profiler-result .profiler-queries .profiler-gap-info, +.profiler-result .profiler-queries .profiler-gap-info td { + background-color: #ccc; +} +.profiler-result .profiler-queries .profiler-gap-info .profiler-unit { + color: #777; +} +.profiler-result .profiler-queries .profiler-gap-info .profiler-info { + text-align: right; +} +.profiler-result .profiler-queries .profiler-gap-info.profiler-trivial-gaps { + display: none; +} +.profiler-result .profiler-queries .profiler-trivial-gap-container { + text-align: center; +} +.profiler-result .profiler-queries .str { + color: #800000; +} +.profiler-result .profiler-queries .kwd { + color: #00008b; +} +.profiler-result .profiler-queries .com { + color: #808080; +} +.profiler-result .profiler-queries .typ { + color: #2b91af; +} +.profiler-result .profiler-queries .lit { + color: #800000; +} +.profiler-result .profiler-queries .pun { + color: #000000; +} +.profiler-result .profiler-queries .pln { + color: #000000; +} +.profiler-result .profiler-queries .tag { + color: #800000; +} +.profiler-result .profiler-queries .atn { + color: #ff0000; +} +.profiler-result .profiler-queries .atv { + color: #0000ff; +} +.profiler-result .profiler-queries .dec { + color: #800080; +} +.profiler-result .profiler-warning, +.profiler-result .profiler-warning *, +.profiler-result .profiler-warning .profiler-queries-show, +.profiler-result .profiler-warning .profiler-queries-show .profiler-unit { + color: #f00; +} +.profiler-result .profiler-warning:hover, +.profiler-result .profiler-warning *:hover, +.profiler-result .profiler-warning .profiler-queries-show:hover, +.profiler-result .profiler-warning .profiler-queries-show .profiler-unit:hover { + color: #f00; +} +.profiler-result .profiler-nuclear { + color: #f00; + font-weight: bold; + padding-right: 2px; +} +.profiler-result .profiler-nuclear:hover { + color: #f00; +} +.profiler-results { + z-index: 2147483643; + position: fixed; + top: 0px; +} +.profiler-results.profiler-left { + left: 0px; +} +.profiler-results.profiler-left.profiler-no-controls .profiler-result:last-child .profiler-button, +.profiler-results.profiler-left .profiler-controls { + -webkit-border-bottom-right-radius: 10px; + -moz-border-radius-bottomright: 10px; + border-bottom-right-radius: 10px; +} +.profiler-results.profiler-left .profiler-button, +.profiler-results.profiler-left .profiler-controls { + border-right: 1px solid #888888; +} +.profiler-results.profiler-right { + right: 0px; +} +.profiler-results.profiler-right.profiler-no-controls .profiler-result:last-child .profiler-button, +.profiler-results.profiler-right .profiler-controls { + -webkit-border-bottom-left-radius: 10px; + -moz-border-radius-bottomleft: 10px; + border-bottom-left-radius: 10px; +} +.profiler-results.profiler-right .profiler-button, +.profiler-results.profiler-right .profiler-controls { + border-left: 1px solid #888888; +} +.profiler-results .profiler-button, +.profiler-results .profiler-controls { + display: none; + z-index: 2147483640; + border-bottom: 1px solid #888888; + background-color: #fff; + padding: 4px 7px; + text-align: right; + cursor: pointer; +} +.profiler-results .profiler-button.profiler-button-active, +.profiler-results .profiler-controls.profiler-button-active { + background-color: maroon; +} +.profiler-results .profiler-button.profiler-button-active .profiler-number, +.profiler-results .profiler-controls.profiler-button-active .profiler-number, +.profiler-results .profiler-button.profiler-button-active .profiler-nuclear, +.profiler-results .profiler-controls.profiler-button-active .profiler-nuclear { + color: #fff; + font-weight: bold; +} +.profiler-results .profiler-button.profiler-button-active .profiler-unit, +.profiler-results .profiler-controls.profiler-button-active .profiler-unit { + color: #fff; + font-weight: normal; +} +.profiler-results .profiler-controls { + display: block; + font-size: 12px; + font-family: Consolas, monospace, serif; + cursor: default; + text-align: center; +} +.profiler-results .profiler-controls span { + border-right: 1px solid #aaaaaa; + padding-right: 5px; + margin-right: 5px; + cursor: pointer; +} +.profiler-results .profiler-controls span:last-child { + border-right: none; +} +.profiler-results .profiler-popup { + display: none; + z-index: 2147483641; + position: absolute; + background-color: #fff; + border: 1px solid #aaa; + padding: 5px 10px; + text-align: left; + line-height: 18px; + overflow: auto; + -moz-box-shadow: 0px 1px 15px #555555; + -webkit-box-shadow: 0px 1px 15px #555555; + box-shadow: 0px 1px 15px #555555; +} +.profiler-results .profiler-popup .profiler-info { + margin-bottom: 3px; + padding-bottom: 2px; + border-bottom: 1px solid #ddd; +} +.profiler-results .profiler-popup .profiler-info .profiler-name { + font-size: 110%; + font-weight: bold; +} +.profiler-results .profiler-popup .profiler-info .profiler-name .profiler-overall-duration { + display: none; +} +.profiler-results .profiler-popup .profiler-info .profiler-server-time { + font-size: 95%; +} +.profiler-results .profiler-popup .profiler-timings th, +.profiler-results .profiler-popup .profiler-timings td { + padding-left: 6px; + padding-right: 6px; +} +.profiler-results .profiler-popup .profiler-timings th { + font-size: 95%; + padding-bottom: 3px; +} +.profiler-results .profiler-popup .profiler-timings .profiler-label { + max-width: 275px; +} +.profiler-results .profiler-queries { + display: none; + z-index: 2147483643; + position: absolute; + overflow-y: auto; + overflow-x: auto; + background-color: #fff; +} +.profiler-results .profiler-queries th { + font-size: 17px; +} +.profiler-results.profiler-min .profiler-result { + display: none; +} +.profiler-results.profiler-min .profiler-controls span { + display: none; +} +.profiler-results.profiler-min .profiler-controls .profiler-min-max { + border-right: none; + padding: 0px; + margin: 0px; +} +.profiler-queries-bg { + z-index: 2147483642; + display: none; + background: #000; + opacity: 0.7; + position: absolute; + top: 0px; + left: 0px; + min-width: 100%; +} +.profiler-result-full .profiler-result { + width: 950px; + margin: 30px auto; +} +.profiler-result-full .profiler-result .profiler-button { + display: none; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-info { + font-size: 25px; + border-bottom: 1px solid #aaaaaa; + padding-bottom: 3px; + margin-bottom: 25px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-info .profiler-overall-duration { + padding-right: 20px; + font-size: 80%; + color: #888; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings td, +.profiler-result-full .profiler-result .profiler-popup .profiler-timings th { + padding-left: 8px; + padding-right: 8px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings th { + padding-bottom: 7px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings td { + font-size: 14px; + padding-bottom: 4px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings td:first-child { + padding-left: 10px; +} +.profiler-result-full .profiler-result .profiler-popup .profiler-timings .profiler-label { + max-width: 550px; +} +.profiler-result-full .profiler-result .profiler-queries { + margin: 25px 0; +} +.profiler-result-full .profiler-result .profiler-queries table { + width: 100%; +} +.profiler-result-full .profiler-result .profiler-queries th { + font-size: 16px; + color: #555; + line-height: 20px; +} +.profiler-result-full .profiler-result .profiler-queries td { + padding: 15px 10px; + text-align: left; +} +.profiler-result-full .profiler-result .profiler-queries .profiler-info div { + text-align: right; + margin-bottom: 5px; +} diff --git a/lib/rack-mini-profiler/lib/html/includes.js b/lib/rack-mini-profiler/lib/html/includes.js new file mode 100644 index 00000000..d8b4de08 --- /dev/null +++ b/lib/rack-mini-profiler/lib/html/includes.js @@ -0,0 +1,960 @@ +"use strict"; +var MiniProfiler = (function () { + var $; + + var options, + container, + controls, + fetchedIds = [], + fetchingIds = [], // so we never pull down a profiler twice + ajaxStartTime + ; + + var hasLocalStorage = function () { + try { + return 'localStorage' in window && window['localStorage'] !== null; + } catch (e) { + return false; + } + }; + + var getVersionedKey = function (keyPrefix) { + return keyPrefix + '-' + options.version; + }; + + var save = function (keyPrefix, value) { + if (!hasLocalStorage()) { return; } + + // clear old keys with this prefix, if any + for (var i = 0; i < localStorage.length; i++) { + if ((localStorage.key(i) || '').indexOf(keyPrefix) > -1) { + localStorage.removeItem(localStorage.key(i)); + } + } + + // save under this version + localStorage[getVersionedKey(keyPrefix)] = value; + }; + + var load = function (keyPrefix) { + if (!hasLocalStorage()) { return null; } + + return localStorage[getVersionedKey(keyPrefix)]; + }; + + var fetchTemplates = function (success) { + var key = 'templates', + cached = load(key); + + if (cached) { + $('body').append(cached); + success(); + } + else { + $.get(options.path + 'includes.tmpl?v=' + options.version, function (data) { + if (data) { + save(key, data); + $('body').append(data); + success(); + } + }); + } + }; + + var getClientPerformance = function() { + return window.performance == null ? null : window.performance; + }; + + var fetchResults = function (ids) { + var clientPerformance, clientProbes, i, j, p, id, idx; + + for (i = 0; i < ids.length; i++) { + id = ids[i]; + + clientPerformance = null; + clientProbes = null; + + if (window.mPt) { + clientProbes = mPt.results(); + for (j = 0; j < clientProbes.length; j++) { + clientProbes[j].d = clientProbes[j].d.getTime(); + } + mPt.flush(); + } + + if (id == options.currentId) { + + clientPerformance = getClientPerformance(); + + if (clientPerformance != null) { + // ie is buggy strip out functions + var copy = { navigation: {}, timing: {} }; + + var timing = $.extend({}, clientPerformance.timing); + + for (p in timing) { + if (timing.hasOwnProperty(p) && !$.isFunction(timing[p])) { + copy.timing[p] = timing[p]; + } + } + if (clientPerformance.navigation) { + copy.navigation.redirectCount = clientPerformance.navigation.redirectCount; + } + clientPerformance = copy; + + // hack to add chrome timings + if (window.chrome && window.chrome.loadTimes) { + var chromeTimes = window.chrome.loadTimes(); + if (chromeTimes.firstPaintTime) { + clientPerformance.timing["First Paint Time"] = Math.round(chromeTimes.firstPaintTime * 1000); + } + if (chromeTimes.firstPaintTime) { + clientPerformance.timing["First Paint After Load Time"] = Math.round(chromeTimes.firstPaintAfterLoadTime * 1000); + } + + } + } + } else if (ajaxStartTime != null && clientProbes && clientProbes.length > 0) { + clientPerformance = { timing: { navigationStart: ajaxStartTime.getTime() } }; + ajaxStartTime = null; + } + + if ($.inArray(id, fetchedIds) < 0 && $.inArray(id, fetchingIds) < 0) { + idx = fetchingIds.push(id) - 1; + + $.ajax({ + url: options.path + 'results', + data: { id: id, clientPerformance: clientPerformance, clientProbes: clientProbes, popup: 1 }, + dataType: 'json', + global: false, + type: 'POST', + success: function (json) { + fetchedIds.push(id); + if (json != "hidden") { + buttonShow(json); + } + }, + complete: function () { + fetchingIds.splice(idx, 1); + } + }); + } + } + }; + + var renderTemplate = function (json) { + return $('#profilerTemplate').tmpl(json); + }; + + var buttonShow = function (json) { + var result = renderTemplate(json); + + if (controls) + result.insertBefore(controls); + else + result.appendTo(container); + + var button = result.find('.profiler-button'), + popup = result.find('.profiler-popup'); + + // button will appear in corner with the total profiling duration - click to show details + button.click(function () { buttonClick(button, popup); }); + + // small duration steps and the column with aggregate durations are hidden by default; allow toggling + toggleHidden(popup); + + // lightbox in the queries + popup.find('.profiler-queries-show').click(function () { queriesShow($(this), result); }); + + // limit count + if (container.find('.profiler-result').length > options.maxTracesToShow) + container.find('.profiler-result').first().remove(); + button.show(); + }; + + var toggleHidden = function (popup) { + var trivial = popup.find('.profiler-toggle-trivial'); + var childrenTime = popup.find('.profiler-toggle-duration-with-children'); + var trivialGaps = popup.parent().find('.profiler-toggle-trivial-gaps'); + + var toggleIt = function (node) { + var link = $(node), + klass = "profiler-" + link.attr('class').substr('profiler-toggle-'.length), + isHidden = link.text().indexOf('show') > -1; + + popup.parent().find('.' + klass).toggle(isHidden); + link.text(link.text().replace(isHidden ? 'show' : 'hide', isHidden ? 'hide' : 'show')); + + popupPreventHorizontalScroll(popup); + }; + + childrenTime.add(trivial).add(trivialGaps).click(function () { + toggleIt(this); + }); + + // if option is set or all our timings are trivial, go ahead and show them + if (options.showTrivial || trivial.data('show-on-load')) { + toggleIt(trivial); + } + // if option is set, go ahead and show time with children + if (options.showChildrenTime) { + toggleIt(childrenTime); + } + }; + + var buttonClick = function (button, popup) { + // we're toggling this button/popup + if (popup.is(':visible')) { + popupHide(button, popup); + } + else { + var visiblePopups = container.find('.profiler-popup:visible'), + theirButtons = visiblePopups.siblings('.profiler-button'); + + // hide any other popups + popupHide(theirButtons, visiblePopups); + + // before showing the one we clicked + popupShow(button, popup); + } + }; + + var popupShow = function (button, popup) { + button.addClass('profiler-button-active'); + + popupSetDimensions(button, popup); + + popup.show(); + + popupPreventHorizontalScroll(popup); + }; + + var popupSetDimensions = function (button, popup) { + var top = button.position().top - 1, // position next to the button we clicked + windowHeight = $(window).height(), + maxHeight = windowHeight - top - 40; // make sure the popup doesn't extend below the fold + + popup + .css({ 'top': top, 'max-height': maxHeight }) + .css(options.renderPosition, button.outerWidth() - 3); // move left or right, based on config + }; + + var popupPreventHorizontalScroll = function (popup) { + var childrenHeight = 0; + + popup.children().each(function () { childrenHeight += $(this).height(); }); + + popup.css({ 'padding-right': childrenHeight > popup.height() ? 40 : 10 }); + }; + + var popupHide = function (button, popup) { + button.removeClass('profiler-button-active'); + popup.hide(); + }; + + var queriesShow = function (link, result) { + + var px = 30, + win = $(window), + width = win.width() - 2 * px, + height = win.height() - 2 * px, + queries = result.find('.profiler-queries'); + + // opaque background + $('
').appendTo('body').css({ 'height': $(document).height() }).show(); + + // center the queries and ensure long content is scrolled + queries.css({ 'top': px, 'max-height': height, 'width': width }).css(options.renderPosition, px) + .find('table').css({ 'width': width }); + + // have to show everything before we can get a position for the first query + queries.show(); + + queriesScrollIntoView(link, queries, queries); + + // syntax highlighting + prettyPrint(); + }; + + var queriesScrollIntoView = function (link, queries, whatToScroll) { + var id = link.closest('tr').attr('data-timing-id'), + cells = queries.find('tr[data-timing-id="' + id + '"] td'); + + // ensure they're in view + whatToScroll.scrollTop(whatToScroll.scrollTop() + cells.first().position().top - 100); + + // highlight and then fade back to original bg color; do it ourselves to prevent any conflicts w/ jquery.UI or other implementations of Resig's color plugin + cells.each(function () { + var cell = $(this), + highlightHex = '#FFFFBB', + highlightRgb = getRGB(highlightHex), + originalRgb = getRGB(cell.css('background-color')), + getColorDiff = function (fx, i) { + // adapted from John Resig's color plugin: http://plugins.jquery.com/project/color + return Math.max(Math.min(parseInt((fx.pos * (originalRgb[i] - highlightRgb[i])) + highlightRgb[i], 10), 255), 0); + }; + + // we need to animate some other property to piggy-back on the step function, so I choose you, opacity! + cell.css({ 'opacity': 1, 'background-color': highlightHex }) + .animate({ 'opacity': 1 }, { duration: 2000, step: function (now, fx) { + fx.elem.style.backgroundColor = "rgb(" + [getColorDiff(fx, 0), getColorDiff(fx, 1), getColorDiff(fx, 2)].join(",") + ")"; + } + }); + }); + }; + + // Color Conversion functions from highlightFade + // By Blair Mitchelmore + // http://jquery.offput.ca/highlightFade/ + // Parse strings looking for color tuples [255,255,255] + var getRGB = function (color) { + var result; + + // Check if we're already dealing with an array of colors + if (color && color.constructor == Array && color.length == 3) return color; + + // Look for rgb(num,num,num) + if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])]; + + // Look for rgb(num%,num%,num%) + if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) return [parseFloat(result[1]) * 2.55, parseFloat(result[2]) * 2.55, parseFloat(result[3]) * 2.55]; + + // Look for #a0b1c2 + if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]; + + // Look for #fff + if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) return [parseInt(result[1] + result[1], 16), parseInt(result[2] + result[2], 16), parseInt(result[3] + result[3], 16)]; + + // Look for rgba(0, 0, 0, 0) == transparent in Safari 3 + if (result = /rgba\(0, 0, 0, 0\)/.exec(color)) return colors['transparent']; + + return null; + }; + + var bindDocumentEvents = function () { + $(document).bind('click keyup', function (e) { + + // this happens on every keystroke, and :visible is crazy expensive in IE <9 + // and in this case, the display:none check is sufficient. + var popup = $('.profiler-popup').filter(function () { return $(this).css("display") !== "none"; }); + + if (!popup.length) { + return; + } + + var button = popup.siblings('.profiler-button'), + queries = popup.closest('.profiler-result').find('.profiler-queries'), + bg = $('.profiler-queries-bg'), + isEscPress = e.type == 'keyup' && e.which == 27, + hidePopup = false, + hideQueries = false; + + if (bg.is(':visible')) { + hideQueries = isEscPress || (e.type == 'click' && !$.contains(queries[0], e.target) && !$.contains(popup[0], e.target)); + } + else if (popup.is(':visible')) { + hidePopup = isEscPress || (e.type == 'click' && !$.contains(popup[0], e.target) && !$.contains(button[0], e.target) && button[0] != e.target); + } + + if (hideQueries) { + bg.remove(); + queries.hide(); + } + + if (hidePopup) { + popupHide(button, popup); + } + }); + $(document).bind('keydown', options.toggleShortcut, function(e) { + $('.profiler-results').toggle(); + }); + }; + + var initFullView = function () { + + // first, get jquery tmpl, then render and bind handlers + fetchTemplates(function () { + + // profiler will be defined in the full page's head + renderTemplate(profiler).appendTo(container); + + var popup = $('.profiler-popup'); + + toggleHidden(popup); + + prettyPrint(); + + // since queries are already shown, just highlight and scroll when clicking a "1 sql" link + popup.find('.profiler-queries-show').click(function () { + queriesScrollIntoView($(this), $('.profiler-queries'), $(document)); + }); + }); + }; + + var initControls = function (container) { + if (options.showControls) { + controls = $('
mc
').appendTo(container); + + $('.profiler-controls .profiler-min-max').click(function () { + container.toggleClass('profiler-min'); + }); + + container.hover(function () { + if ($(this).hasClass('profiler-min')) { + $(this).find('.profiler-min-max').show(); + } + }, + function () { + if ($(this).hasClass('profiler-min')) { + $(this).find('.profiler-min-max').hide(); + } + }); + + $('.profiler-controls .profiler-clear').click(function () { + container.find('.profiler-result').remove(); + }); + } + else { + container.addClass('profiler-no-controls'); + } + }; + + var initPopupView = function () { + + if (options.authorized) { + // all fetched profilings will go in here + container = $('
').appendTo('body'); + + // MiniProfiler.RenderIncludes() sets which corner to render in - default is upper left + container.addClass("profiler-" + options.renderPosition); + + //initialize the controls + initControls(container); + + // we'll render results json via a jquery.tmpl - after we get the templates, we'll fetch the initial json to populate it + fetchTemplates(function () { + // get master page profiler results + fetchResults(options.ids); + }); + if (options.startHidden) container.hide(); + } + else { + fetchResults(options.ids); + } + + var jQueryAjaxComplete = function (e, xhr, settings) { + if (xhr) { + // should be an array of strings, e.g. ["008c4813-9bd7-443d-9376-9441ec4d6a8c","16ff377b-8b9c-4c20-a7b5-97cd9fa7eea7"] + var stringIds = xhr.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + } + }; + + // fetch profile results for any ajax calls + // note, this does not use $ cause we want to hook into the main jQuery + if (jQuery && jQuery(document) && jQuery(document).ajaxComplete) { + jQuery(document).ajaxComplete(jQueryAjaxComplete); + } + + if (jQuery && jQuery(document).ajaxStart) + jQuery(document).ajaxStart(function () { ajaxStartTime = new Date(); }); + + // fetch results after ASP Ajax calls + if (typeof (Sys) != 'undefined' && typeof (Sys.WebForms) != 'undefined' && typeof (Sys.WebForms.PageRequestManager) != 'undefined') { + // Get the instance of PageRequestManager. + var PageRequestManager = Sys.WebForms.PageRequestManager.getInstance(); + + PageRequestManager.add_endRequest(function (sender, args) { + if (args) { + var response = args.get_response(); + if (response.get_responseAvailable() && response._xmlHttpRequest != null) { + var stringIds = args.get_response().getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + } + } + }); + } + + // more Asp.Net callbacks + if (typeof (WebForm_ExecuteCallback) == "function") { + WebForm_ExecuteCallback = (function (callbackObject) { + // Store original function + var original = WebForm_ExecuteCallback; + + return function (callbackObject) { + original(callbackObject); + + var stringIds = callbackObject.xmlRequest.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + }; + + })(); + } + + // also fetch results after ExtJS requests, in case it is being used + if (typeof (Ext) != 'undefined' && typeof (Ext.Ajax) != 'undefined' && typeof (Ext.Ajax.on) != 'undefined') { + // Ext.Ajax is a singleton, so we just have to attach to its 'requestcomplete' event + Ext.Ajax.on('requestcomplete', function (e, xhr, settings) { + //iframed file uploads don't have headers + if (!xhr || !xhr.getResponseHeader) { + return; + } + + var stringIds = xhr.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + }); + } + + if (typeof (MooTools) != 'undefined' && typeof (Request) != 'undefined') { + Request.prototype.addEvents({ + onComplete: function() { + var stringIds = this.xhr.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + } + }); + } + + // add support for AngularJS, which use the basic XMLHttpRequest object. + if (window.angular && typeof (XMLHttpRequest) != 'undefined') { + var _send = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.send = function sendReplacement(data) { + this._onreadystatechange = this.onreadystatechange; + + this.onreadystatechange = function onReadyStateChangeReplacement() { + if (this.readyState == 4) { + var stringIds = this.getResponseHeader('X-MiniProfiler-Ids'); + if (stringIds) { + var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds); + fetchResults(ids); + } + } + + return this._onreadystatechange.apply(this, arguments); + }; + + return _send.apply(this, arguments); + }; + } + + // some elements want to be hidden on certain doc events + bindDocumentEvents(); + }; + + return { + + init: function () { + var script = document.getElementById('mini-profiler'); + if (!script || !script.getAttribute) return; + + options = (function () { + var version = script.getAttribute('data-version'); + var path = script.getAttribute('data-path'); + + var currentId = script.getAttribute('data-current-id'); + + var ids = script.getAttribute('data-ids'); + if (ids) ids = ids.split(','); + + var position = script.getAttribute('data-position'); + + var toggleShortcut = script.getAttribute('data-toggle-shortcut'); + + if (script.getAttribute('data-max-traces')) { + var maxTraces = parseInt(script.getAttribute('data-max-traces'), 10); + } + + if (script.getAttribute('data-trivial') === 'true') var trivial = true; + if (script.getAttribute('data-children') == 'true') var children = true; + if (script.getAttribute('data-controls') == 'true') var controls = true; + if (script.getAttribute('data-authorized') == 'true') var authorized = true; + if (script.getAttribute('data-start-hidden') == 'true') var startHidden = true; + + return { + ids: ids, + path: path, + version: version, + renderPosition: position, + showTrivial: trivial, + showChildrenTime: children, + maxTracesToShow: maxTraces, + showControls: controls, + currentId: currentId, + authorized: authorized, + toggleShortcut: toggleShortcut, + startHidden: startHidden + }; + })(); + + var doInit = function () { + // when rendering a shared, full page, this div will exist + container = $('.profiler-result-full'); + if (container.length) { + if (window.location.href.indexOf("&trivial=1") > 0) { + options.showTrivial = true; + } + initFullView(); + } + else { + initPopupView(); + } + }; + + // this preserves debugging + var load = function (s, f) { + var sc = document.createElement("script"); + sc.async = "async"; + sc.type = "text/javascript"; + sc.src = s; + var done = false; + sc.onload = sc.onreadystatechange = function (_, abort) { + if (!sc.readyState || /loaded|complete/.test(sc.readyState)) { + if (!abort && !done) { done = true; f(); } + } + }; + document.getElementsByTagName('head')[0].appendChild(sc); + }; + + var wait = 0; + var finish = false; + var deferInit = function() { + if (finish) return; + if (window.performance && window.performance.timing && window.performance.timing.loadEventEnd === 0 && wait < 10000) { + setTimeout(deferInit, 100); + wait += 100; + } else { + finish = true; + init(); + } + }; + + var init = function() { + if (options.authorized) { + var url = options.path + "includes.css?v=" + options.version; + if (document.createStyleSheet) { + document.createStyleSheet(url); + } else { + $('head').append($('')); + } + if (!$.tmpl) { + load(options.path + 'jquery.tmpl.js?v=' + options.version, doInit); + } else { + doInit(); + } + } else { + doInit(); + } + + // jquery.hotkeys.js + // https://github.com/jeresig/jquery.hotkeys/blob/master/jquery.hotkeys.js + + (function(d){function h(g){if("string"===typeof g.data){var h=g.handler,j=g.data.toLowerCase().split(" ");g.handler=function(b){if(!(this!==b.target&&(/textarea|select/i.test(b.target.nodeName)||"text"===b.target.type))){var c="keypress"!==b.type&&d.hotkeys.specialKeys[b.which],e=String.fromCharCode(b.which).toLowerCase(),a="",f={};b.altKey&&"alt"!==c&&(a+="alt+");b.ctrlKey&&"ctrl"!==c&&(a+="ctrl+");b.metaKey&&(!b.ctrlKey&&"meta"!==c)&&(a+="meta+");b.shiftKey&&"shift"!==c&&(a+="shift+");c?f[a+c]= + !0:(f[a+e]=!0,f[a+d.hotkeys.shiftNums[e]]=!0,"shift+"===a&&(f[d.hotkeys.shiftNums[e]]=!0));c=0;for(e=j.length;c","/":"?","\\":"|"}};d.each(["keydown","keyup","keypress"],function(){d.event.special[this]={add:h}})})(MiniProfiler.jQuery); + + }; + + var major, minor; + if (typeof(jQuery) == 'function') { + var jQueryVersion = jQuery.fn.jquery.split('.'); + major = parseInt(jQueryVersion[0], 10); + minor = parseInt(jQueryVersion[1], 10); + } + + + if (major === 2 || (major === 1 && minor >= 7)) { + MiniProfiler.jQuery = $ = jQuery; + $(deferInit); + } else { + load(options.path + "jquery.1.7.1.js?v=" + options.version, function() { + MiniProfiler.jQuery = $ = jQuery.noConflict(true); + $(deferInit); + }); + } + }, + + getClientTimingByName: function (clientTiming, name) { + + for (var i = 0; i < clientTiming.Timings.length; i++) { + if (clientTiming.Timings[i].Name == name) { + return clientTiming.Timings[i]; + } + } + return { Name: name, Duration: "", Start: "" }; + }, + + renderDate: function (jsonDate) { // JavaScriptSerializer sends dates as /Date(1308024322065)/ + if (jsonDate) { + return (typeof jsonDate === 'string') ? new Date(parseInt(jsonDate.replace("/Date(", "").replace(")/", ""), 10)).toUTCString() : jsonDate; + } + }, + + renderIndent: function (depth) { + var result = ''; + for (var i = 0; i < depth; i++) { + result += ' '; + } + return result; + }, + + renderExecuteType: function (typeId) { + // see StackExchange.Profiling.ExecuteType enum + switch (typeId) { + case 0: return 'None'; + case 1: return 'NonQuery'; + case 2: return 'Scalar'; + case 3: return 'Reader'; + } + }, + + shareUrl: function (id) { + return options.path + 'results?id=' + id; + }, + + getClientTimings: function (clientTimings) { + var list = []; + var t; + + if (!clientTimings.Timings) return []; + + for (var i = 0; i < clientTimings.Timings.length; i++) { + t = clientTimings.Timings[i]; + var trivial = t.Name != "Dom Complete" && t.Name != "Response" && t.Name != "First Paint Time"; + trivial = t.Duration < 2 ? trivial : false; + list.push( + { + isTrivial: trivial, + name: t.Name, + duration: t.Duration, + start: t.Start + }); + } + + list.sort(function (a, b) { return a.start - b.start; }); + return list; + }, + + getSqlTimings: function (root) { + var result = [], + addToResults = function (timing) { + if (timing.SqlTimings) { + for (var i = 0, sqlTiming; i < timing.SqlTimings.length; i++) { + sqlTiming = timing.SqlTimings[i]; + + // HACK: add info about the parent Timing to each SqlTiming so UI can render + sqlTiming.ParentTimingName = timing.Name; + result.push(sqlTiming); + } + } + + if (timing.Children) { + for (var i = 0; i < timing.Children.length; i++) { + addToResults(timing.Children[i]); + } + } + }; + + // start adding at the root and recurse down + addToResults(root); + + var removeDuration = function(list, duration) { + + var newList = []; + for (var i = 0; i < list.length; i++) { + + var item = list[i]; + if (duration.start > item.start) { + if (duration.start > item.finish) { + newList.push(item); + continue; + } + newList.push({ start: item.start, finish: duration.start }); + } + + if (duration.finish < item.finish) { + if (duration.finish < item.start) { + newList.push(item); + continue; + } + newList.push({ start: duration.finish, finish: item.finish }); + } + } + + return newList; + }; + + var processTimes = function (elem, parent) { + var duration = { start: elem.StartMilliseconds, finish: (elem.StartMilliseconds + elem.DurationMilliseconds) }; + elem.richTiming = [duration]; + if (parent != null) { + elem.parent = parent; + elem.parent.richTiming = removeDuration(elem.parent.richTiming, duration); + } + + if (elem.Children) { + for (var i = 0; i < elem.Children.length; i++) { + processTimes(elem.Children[i], elem); + } + } + }; + + processTimes(root, null); + + // sort results by time + result.sort(function (a, b) { return a.StartMilliseconds - b.StartMilliseconds; }); + + var determineOverlap = function(gap, node) { + var overlap = 0; + for (var i = 0; i < node.richTiming.length; i++) { + var current = node.richTiming[i]; + if (current.start > gap.finish) { + break; + } + if (current.finish < gap.start) { + continue; + } + + overlap += Math.min(gap.finish, current.finish) - Math.max(gap.start, current.start); + } + return overlap; + }; + + var determineGap = function (gap, node, match) { + var overlap = determineOverlap(gap, node); + if (match == null || overlap > match.duration) { + match = { name: node.Name, duration: overlap }; + } + else if (match.name == node.Name) { + match.duration += overlap; + } + + if (node.Children) { + for (var i = 0; i < node.Children.length; i++) { + match = determineGap(gap, node.Children[i], match); + } + } + return match; + }; + + var time = 0; + var prev = null; + $.each(result, function () { + this.prevGap = { + duration: (this.StartMilliseconds - time).toFixed(2), + start: time, + finish: this.StartMilliseconds + }; + + this.prevGap.topReason = determineGap(this.prevGap, root, null); + + time = this.StartMilliseconds + this.DurationMilliseconds; + prev = this; + }); + + + if (result.length > 0) { + var me = result[result.length - 1]; + me.nextGap = { + duration: (root.DurationMilliseconds - time).toFixed(2), + start: time, + finish: root.DurationMilliseconds + }; + me.nextGap.topReason = determineGap(me.nextGap, root, null); + } + + return result; + }, + + getSqlTimingsCount: function (root) { + var result = 0, + countSql = function (timing) { + if (timing.SqlTimings) { + result += timing.SqlTimings.length; + } + + if (timing.Children) { + for (var i = 0; i < timing.Children.length; i++) { + countSql(timing.Children[i]); + } + } + }; + countSql(root); + return result; + }, + + fetchResultsExposed: function (ids) { + return fetchResults(ids); + }, + + formatDuration: function (duration) { + return (duration || 0).toFixed(1); + } + }; +})(); + +MiniProfiler.init(); + +if (typeof prettyPrint === "undefined") { + + // prettify.js + // http://code.google.com/p/google-code-prettify/ + + window.PR_SHOULD_USE_CONTINUATION=true;window.PR_TAB_WIDTH=8;window.PR_normalizedHtml=window.PR=window.prettyPrintOne=window.prettyPrint=void 0;window._pr_isIE6=function(){var y=navigator&&navigator.userAgent&&navigator.userAgent.match(/\bMSIE ([678])\./);y=y?+y[1]:false;window._pr_isIE6=function(){return y};return y}; + (function(){function y(b){return b.replace(L,"&").replace(M,"<").replace(N,">")}function H(b,f,i){switch(b.nodeType){case 1:var o=b.tagName.toLowerCase();f.push("<",o);var l=b.attributes,n=l.length;if(n){if(i){for(var r=[],j=n;--j>=0;)r[j]=l[j];r.sort(function(q,m){return q.name"); + for(l=b.firstChild;l;l=l.nextSibling)H(l,f,i);if(b.firstChild||!/^(?:br|link|img)$/.test(o))f.push("");break;case 3:case 4:f.push(y(b.nodeValue));break}}function O(b){function f(c){if(c.charAt(0)!=="\\")return c.charCodeAt(0);switch(c.charAt(1)){case "b":return 8;case "t":return 9;case "n":return 10;case "v":return 11;case "f":return 12;case "r":return 13;case "u":case "x":return parseInt(c.substring(2),16)||c.charCodeAt(1);case "0":case "1":case "2":case "3":case "4":case "5":case "6":case "7":return parseInt(c.substring(1), + 8);default:return c.charCodeAt(1)}}function i(c){if(c<32)return(c<16?"\\x0":"\\x")+c.toString(16);c=String.fromCharCode(c);if(c==="\\"||c==="-"||c==="["||c==="]")c="\\"+c;return c}function o(c){var d=c.substring(1,c.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g"));c=[];for(var a=[],k=d[0]==="^",e=k?1:0,h=d.length;e122)){s<65||g>90||a.push([Math.max(65,g)|32,Math.min(s,90)|32]);s<97||g>122||a.push([Math.max(97,g)&-33,Math.min(s,122)&-33])}}a.sort(function(v,w){return v[0]-w[0]||w[1]-v[1]});d=[];g=[NaN,NaN];for(e=0;eh[0]){h[1]+1>h[0]&&a.push("-"); + a.push(i(h[1]))}}a.push("]");return a.join("")}function l(c){for(var d=c.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g")),a=d.length,k=[],e=0,h=0;e=2&&c==="[")d[e]=o(g);else if(c!=="\\")d[e]=g.replace(/[a-zA-Z]/g,function(s){s=s.charCodeAt(0);return"["+String.fromCharCode(s&-33,s|32)+"]"})}return d.join("")}for(var n=0,r=false,j=false,q=0,m=b.length;q=0;l-=16)o.push(" ".substring(0,l));l=n+1;break;case "\n":f=0;break;default:++f}if(!o)return i;o.push(i.substring(l));return o.join("")}}function I(b, + f,i,o){if(f){b={source:f,c:b};i(b);o.push.apply(o,b.d)}}function B(b,f){var i={},o;(function(){for(var r=b.concat(f),j=[],q={},m=0,t=r.length;m=0;)i[c.charAt(d)]=p;p=p[1];c=""+p;if(!q.hasOwnProperty(c)){j.push(p);q[c]=null}}j.push(/[\0-\uffff]/);o=O(j)})();var l=f.length;function n(r){for(var j=r.c,q=[j,z],m=0,t=r.source.match(o)||[],p={},c=0,d=t.length;c=5&&"lang-"===k.substring(0,5))&&!(e&&typeof e[1]==="string")){h=false;k=P}h||(p[a]=k)}g=m;m+=a.length;if(h){h=e[1];var s=a.indexOf(h),v=s+h.length;if(e[2]){v=a.length-e[2].length;s=v-h.length}k=k.substring(5);I(j+g,a.substring(0,s),n,q);I(j+g+s,h,Q(k,h),q);I(j+g+v,a.substring(v),n,q)}else q.push(j+g,k)}r.d=q}return n}function x(b){var f=[],i=[];if(b.tripleQuotedStrings)f.push([A,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/, + null,"'\""]);else b.multiLineStrings?f.push([A,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"]):f.push([A,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"]);b.verbatimStrings&&i.push([A,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null]);if(b.hashComments)if(b.cStyleComments){f.push([C,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"]);i.push([A,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/, + null])}else f.push([C,/^#[^\r\n]*/,null,"#"]);if(b.cStyleComments){i.push([C,/^\/\/[^\r\n]*/,null]);i.push([C,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}b.regexLiterals&&i.push(["lang-regex",RegExp("^"+Z+"(/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/)")]);b=b.keywords.replace(/^\s+|\s+$/g,"");b.length&&i.push([R,RegExp("^(?:"+b.replace(/\s+/g,"|")+")\\b"),null]);f.push([z,/^\s+/,null," \r\n\t\u00a0"]);i.push([J,/^@[a-z_$][a-z_$@0-9]*/i,null],[S,/^@?[A-Z]+[a-z][A-Za-z_$@0-9]*/, + null],[z,/^[a-z_$][a-z_$@0-9]*/i,null],[J,/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],[E,/^.[^\s\w\.$@\'\"\`\/\#]*/,null]);return B(f,i)}function $(b){function f(D){if(D>r){if(j&&j!==q){n.push("");j=null}if(!j&&q){j=q;n.push('')}var T=y(p(i.substring(r,D))).replace(e?d:c,"$1 ");e=k.test(T);n.push(T.replace(a,s));r=D}}var i=b.source,o=b.g,l=b.d,n=[],r=0,j=null,q=null,m=0,t=0,p=Y(window.PR_TAB_WIDTH),c=/([\r\n ]) /g, + d=/(^| ) /gm,a=/\r\n?|\n/g,k=/[ \r\n]$/,e=true,h=window._pr_isIE6();h=h?b.b.tagName==="PRE"?h===6?" \r\n":h===7?" 
\r":" \r":" 
":"
";var g=b.b.className.match(/\blinenums\b(?::(\d+))?/),s;if(g){for(var v=[],w=0;w<10;++w)v[w]=h+'
  • ';var F=g[1]&&g[1].length?g[1]-1:0;n.push('
    1. ");s=function(){var D=v[++F%10];return j?""+D+'':D}}else s=h; + for(;;)if(m");j=null}n.push(o[m+1]);m+=2}else if(t");g&&n.push("
    ");b.a=n.join("")}function u(b,f){for(var i=f.length;--i>=0;){var o=f[i];if(G.hasOwnProperty(o))"console"in window&&console.warn("cannot override language handler %s",o);else G[o]=b}}function Q(b,f){b&&G.hasOwnProperty(b)||(b=/^\s*1&&m.charAt(0)==="<"){if(!ba.test(m))if(ca.test(m)){f.push(m.substring(9,m.length-3));n+=m.length-12}else if(da.test(m)){f.push("\n");++n}else if(m.indexOf(V)>=0&&m.replace(/\s(\w+)\s*=\s*(?:\"([^\"]*)\"|'([^\']*)'|(\S+))/g,' $1="$2$3$4"').match(/[cC][lL][aA][sS][sS]=\"[^\"]*\bnocode\b/)){var t=m.match(W)[2],p=1,c;c=j+1;a:for(;c=0;){var e=p.indexOf(";",k);if(e>=0){var h=p.substring(k+3,e),g=10;if(h&&h.charAt(0)==="x"){h=h.substring(1);g=16}var s=parseInt(h,g);isNaN(s)||(p=p.substring(0,k)+String.fromCharCode(s)+p.substring(e+1))}}a=p.replace(ea,"<").replace(fa,">").replace(ga,"'").replace(ha,'"').replace(ia," ").replace(ja, + "&")}f.push(a);n+=a.length}}o={source:f.join(""),h:r};var v=o.source;b.source=v;b.c=0;b.g=o.h;Q(i,v)(b);$(b)}catch(w){if("console"in window)console.log(w&&w.stack?w.stack:w)}}var A="str",R="kwd",C="com",S="typ",J="lit",E="pun",z="pln",P="src",V="nocode",Z=function(){for(var b=["!","!=","!==","#","%","%=","&","&&","&&=","&=","(","*","*=","+=",",","-=","->","/","/=",":","::",";","<","<<","<<=","<=","=","==","===",">",">=",">>",">>=",">>>",">>>=","?","@","[","^","^=","^^","^^=","{","|","|=","||","||=", + "~","break","case","continue","delete","do","else","finally","instanceof","return","throw","try","typeof"],f="(?:^^|[+-]",i=0;i:&a-z])/g,"\\$1");f+=")\\s*";return f}(),L=/&/g,M=//g,X=/\"/g,ea=/</g,fa=/>/g,ga=/'/g,ha=/"/g,ja=/&/g,ia=/ /g,ka=/[\r\n]/g,K=null,aa=RegExp("[^<]+|