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
+ "
+ Tip: add gem "binding_of_caller" to your Gemfile to enable the REPL and local/instance variable inspection.
+
+<% end %>
+
+
+
Request info
+
+
+ <% if rails_params %>
+
Request parameters
<%== inspect_value rails_params %>
+ <% end %>
+ <% if rack_session %>
+
Rack session
<%== inspect_value rack_session %>
+ <% end %>
+
+
+
+
+
+
Local Variables
+
+
+ <% @frame.local_variables.each do |name, value| %>
+
<%= name %>
<%== inspect_value value %>
+ <% end %>
+
+
+
+
+
+
Instance Variables
+
+
+ <% @frame.instance_variables.each do |name, value| %>
+
<%= name %>
<%== inspect_value value %>
+ <% end %>
+
+
+
+
+
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("",o,">");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+'