├── .babelrc ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── stale.yml └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .prettierrc.json ├── .rspec ├── .rubocop.yml ├── .stylelintrc.json ├── .tool-versions ├── Appraisals ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── app ├── adapters │ └── rails_mini_profiler │ │ └── database_adapter.rb ├── assets │ ├── config │ │ └── rails_mini_profiler_manifest.js │ ├── javascripts │ │ └── rails_mini_profiler.js │ └── stylesheets │ │ └── rails_mini_profiler │ │ └── application.css ├── controllers │ └── rails_mini_profiler │ │ ├── application_controller.rb │ │ ├── flamegraphs_controller.rb │ │ └── profiled_requests_controller.rb ├── helpers │ └── rails_mini_profiler │ │ ├── application_helper.rb │ │ └── profiled_requests_helper.rb ├── javascript │ ├── images │ │ ├── bookmark.svg │ │ ├── chart.svg │ │ ├── check.svg │ │ ├── chevron.svg │ │ ├── copy.svg │ │ ├── delete.svg │ │ ├── filter.svg │ │ ├── graph.svg │ │ ├── logo.svg │ │ ├── logo_variant.svg │ │ ├── search.svg │ │ ├── setting.svg │ │ └── show.svg │ ├── js │ │ ├── checklist_controller.js │ │ ├── clipboard_controller.js │ │ ├── enable_controller.js │ │ ├── filter_controller.js │ │ ├── search_controller.js │ │ └── select_controller.js │ ├── packs │ │ └── rails-mini-profiler.js │ └── stylesheets │ │ ├── components │ │ ├── buttons.scss │ │ ├── dropdown.scss │ │ ├── input.scss │ │ ├── navbar.scss │ │ ├── page_header.scss │ │ ├── pagination.scss │ │ ├── placeholder.scss │ │ ├── profiled_request_table.scss │ │ └── trace.scss │ │ ├── flamegraph.scss │ │ ├── flashes.scss │ │ ├── profiled_requests.scss │ │ ├── rails-mini-profiler.scss │ │ └── traces.scss ├── models │ └── rails_mini_profiler │ │ ├── application_record.rb │ │ ├── flamegraph.rb │ │ ├── profiled_request.rb │ │ └── trace.rb ├── presenters │ └── rails_mini_profiler │ │ ├── base_presenter.rb │ │ ├── controller_trace_presenter.rb │ │ ├── instantiation_trace_presenter.rb │ │ ├── profiled_request_presenter.rb │ │ ├── render_partial_trace_presenter.rb │ │ ├── render_template_trace_presenter.rb │ │ ├── rmp_trace_presenter.rb │ │ ├── sequel_trace_presenter.rb │ │ └── trace_presenter.rb ├── search │ └── rails_mini_profiler │ │ ├── base_search.rb │ │ ├── profiled_request_search.rb │ │ └── trace_search.rb └── views │ ├── layouts │ └── rails_mini_profiler │ │ ├── application.html.erb │ │ └── flamegraph.html.erb │ ├── models │ ├── _flamegraph.json.jb │ ├── _profiled_request.jb │ └── _trace.jb │ └── rails_mini_profiler │ ├── badge.html.erb │ ├── flamegraphs │ ├── show.html.erb │ └── show.json.jb │ ├── profiled_requests │ ├── index.html.erb │ ├── index.json.jb │ ├── shared │ │ ├── header │ │ │ └── _header.erb │ │ └── table │ │ │ ├── _placeholder.erb │ │ │ ├── _table.erb │ │ │ ├── _table_head.erb │ │ │ └── _table_row.erb │ ├── show.html.erb │ ├── show.json.jb │ └── show │ │ ├── _trace.html.erb │ │ ├── _trace_list.erb │ │ ├── _trace_list_header.erb │ │ └── _trace_list_placeholder.erb │ └── shared │ ├── _flashes.html.erb │ ├── _head.erb │ └── _navbar.html.erb ├── bin ├── annotate ├── appraisal ├── rails ├── rake └── rspec ├── commitlint.config.js ├── config └── routes.rb ├── db └── migrate │ └── 20210621185018_create_rmp.rb ├── docker-compose.yml ├── docs └── images │ ├── logo.png │ ├── overview.png │ ├── trace-details.png │ └── trace.png ├── gemfiles ├── rails_6.0.gemfile ├── rails_6.1.gemfile └── rails_7.0.gemfile ├── lib ├── generators │ └── rails_mini_profiler │ │ ├── USAGE │ │ ├── install_generator.rb │ │ └── templates │ │ ├── rails_mini_profiler.js.erb │ │ └── rails_mini_profiler.rb.erb ├── rails_mini_profiler.rb ├── rails_mini_profiler │ ├── badge.rb │ ├── configuration.rb │ ├── configuration │ │ ├── storage.rb │ │ └── user_interface.rb │ ├── engine.rb │ ├── flamegraph_guard.rb │ ├── guard.rb │ ├── logger.rb │ ├── middleware.rb │ ├── models │ │ └── base_model.rb │ ├── redirect.rb │ ├── request_context.rb │ ├── request_wrapper.rb │ ├── response_wrapper.rb │ ├── tracers.rb │ ├── tracers │ │ ├── controller_tracer.rb │ │ ├── instantiation_tracer.rb │ │ ├── null_trace.rb │ │ ├── registry.rb │ │ ├── rmp_tracer.rb │ │ ├── sequel_tracer.rb │ │ ├── sequel_tracker.rb │ │ ├── subscriptions.rb │ │ ├── trace.rb │ │ ├── trace_factory.rb │ │ ├── tracer.rb │ │ └── view_tracer.rb │ ├── user.rb │ └── version.rb └── tasks │ └── rails_mini_profiler_tasks.rake ├── package-lock.json ├── package.json ├── public └── rails_mini_profiler │ └── speedscope │ ├── LICENSE │ ├── demangle-cpp.1768f4cc.js │ ├── demangle-cpp.1768f4cc.js.map │ ├── favicon-16x16.f74b3187.png │ ├── favicon-32x32.bc503437.png │ ├── file-format-schema.json │ ├── import.a03bf119.js │ ├── import.a03bf119.js.map │ ├── index.html │ ├── release.txt │ ├── reset.8c46b7a1.css │ ├── reset.8c46b7a1.css.map │ ├── source-map.438fa06b.js │ ├── source-map.438fa06b.js.map │ ├── speedscope.eee21de6.js │ └── speedscope.eee21de6.js.map ├── rails_mini_profiler.gemspec ├── rakelib ├── speedscope.rake └── util │ └── speedscope.rb ├── rollup.config.js └── spec ├── adapters └── rails_mini_profiler │ └── database_adapter_spec.rb ├── dummy ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ └── stylesheets │ │ │ ├── application.css │ │ │ ├── movies.css │ │ │ └── scaffold.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── movies_controller.rb │ ├── helpers │ │ ├── application_helper.rb │ │ └── movies_helper.rb │ ├── javascript │ │ └── packs │ │ │ └── application.js │ ├── jobs │ │ └── application_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── movie.rb │ └── views │ │ ├── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb │ │ └── movies │ │ ├── _form.html.erb │ │ ├── _movie.json.jb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── index.json.jb │ │ ├── new.html.erb │ │ ├── show.html.erb │ │ └── show.json.jb ├── bin │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── credentials.yml.enc │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── permissions_policy.rb │ │ ├── rails_mini_profiler.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ └── storage.yml ├── db │ ├── migrate │ │ └── 20210702114107_add_movies.rb │ ├── movies.csv │ ├── schema.rb │ └── seeds.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ └── favicon.ico ├── storage │ └── .keep └── tmp │ ├── .keep │ ├── development_secret.txt │ ├── pids │ └── .keep │ └── storage │ └── .keep ├── helpers └── rails_mini_profiler │ └── profiled_request_helper_spec.rb ├── lib └── rails_mini_profiler │ ├── badge_spec.rb │ ├── guard_spec.rb │ ├── redirect_spec.rb │ └── tracers │ ├── controller_tracer_spec.rb │ ├── registry_spec.rb │ ├── sequel_tracer_spec.rb │ ├── sequel_tracker_spec.rb │ ├── trace_factory_spec.rb │ ├── tracer_spec.rb │ └── view_tracer_spec.rb ├── models └── rails_mini_profiler │ └── profiled_request_spec.rb ├── presenters └── rails_mini_profiler │ └── trace_presenter_spec.rb ├── rails_helper.rb ├── rails_mini_profiler_spec.rb ├── requests └── rails_mini_profiler │ ├── application_spec.rb │ ├── flamegraph_request_spec.rb │ └── profiled_requests_spec.rb ├── routing └── rails_mini_profiler │ └── profiled_requests_routing_spec.rb ├── search └── rails_mini_profiler │ ├── profiled_request_search_spec.rb │ └── trace_search_spec.rb ├── spec_helper.rb └── support └── spec_helpers.rb /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": false 5 | }] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "globals": { 8 | "ENV": true 9 | }, 10 | "parser": "@babel/eslint-parser", 11 | "extends": [ 12 | "eslint:recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaVersion": 2020, 17 | "sourceType": "module" 18 | }, 19 | "ignorePatterns": [ 20 | "public", 21 | "node_modules" 22 | ], 23 | "rules": { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug Report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 🐛 6 | assignees: '' 7 | 8 | --- 9 | 10 | Found a bug? Please fill out the sections below! 👍 11 | 12 | ### Issue Summary 13 | 14 | A summary of the issue. 15 | 16 | ### Steps to Reproduce 17 | 18 | 1. (for example) Start a new Rails project with Rails Mini Profiler 19 | 2. Edit something.rb as follows... 20 | 3. ... 21 | 22 | Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead? 23 | 24 | ### Technical details 25 | 26 | * Rails version: Run `rails --version` to find out. 27 | * Ruby version: Run `ruby -v` to find out. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest an idea for improving Rails Mini Profiler 4 | title: '' 5 | labels: feature 🚀 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your proposal related to a problem? 11 | 12 | (Provide a clear and concise description of what the problem is.) 13 | 14 | ### Describe the solution you'd like 15 | 16 | (Provide a clear and concise description of what you want to happen.) 17 | 18 | ### Describe any alternatives you've considered 19 | 20 | (Let us know about other solutions you've tried or researched.) 21 | 22 | ### Additional context 23 | 24 | (Is there anything else you can add about the proposal? You might want to link to related issues here, if you haven't already.) 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thanks for contributing to Rails Mini Profiler! 🎉 2 | 3 | Before submitting, please review the [contributor guidelines](/CONTRIBUTING.md). In particular: 4 | 5 | - [ ] Your tests pass on all supported Rails versions. Run `bin/appraisal rspec` to verify. 6 | - [ ] Your code adheres to the repository code style. Run `bin/rake lint` to verify. 7 | - [ ] For functional updates, has the documentation been updated accordingly? 8 | 9 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 60 2 | daysUntilClose: 14 3 | staleLabel: stale 4 | markComment: > 5 | This issue has been automatically marked as stale because it has not had 6 | recent activity. It will be closed if no further activity occurs. Thank you 7 | for your contributions. 8 | exemptProjects: true 9 | closeComment: false 10 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.6 3 | NewCops: enable 4 | SuggestExtensions: false 5 | Exclude: 6 | - vendor/**/* 7 | - node_modules/**/* 8 | - gemfiles/**/* 9 | - db/migrate/*.rb 10 | - bin/* 11 | - !ruby/regexp /schema\.rb$/ 12 | 13 | Layout/LineLength: 14 | Max: 120 15 | 16 | Layout/MultilineMethodCallIndentation: 17 | EnforcedStyle: indented_relative_to_receiver 18 | 19 | Metrics/AbcSize: 20 | Max: 20 21 | 22 | Metrics/MethodLength: 23 | CountAsOne: ['array', 'heredoc'] 24 | Max: 15 25 | 26 | Metrics/BlockLength: 27 | IgnoredMethods: ['describe', 'context'] 28 | 29 | Style/Documentation: 30 | Enabled: false 31 | 32 | Style/OpenStructUse: 33 | Exclude: ['**/*_spec.rb'] 34 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-idiomatic-order" 5 | ], 6 | "plugins": [ 7 | "stylelint-order", 8 | "stylelint-prettier", 9 | "stylelint-scss" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.3.0 2 | nodejs 16.4.2 3 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'rails-6.0' do 4 | gem 'rails', '~> 6.0.0' 5 | 6 | group :test, :development do 7 | remove_gem 'sprockets-rails' 8 | end 9 | end 10 | 11 | appraise 'rails-6.1' do 12 | gem 'rails', '~> 6.1.0' 13 | group :test, :development do 14 | remove_gem 'sprockets-rails' 15 | end 16 | end 17 | 18 | appraise 'rails-7.0' do 19 | gem 'rails', '~> 7.0.0' 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in rails_mini_profiler.gemspec. 6 | gemspec 7 | 8 | group :development do 9 | gem 'annotate', '~> 3.1', github: 'ctran/annotate_models' 10 | gem 'appraisal', '~> 2.4' 11 | gem 'httparty', '~> 0.20.0', require: false 12 | gem 'puma', '~> 6.0' 13 | gem 'rubyzip', '~> 2.3', require: false 14 | end 15 | 16 | group :test, :development do 17 | gem 'activerecord-import', '~> 1.4' 18 | gem 'jb', '~> 0.8' 19 | gem 'pg', '~> 1.5' 20 | gem 'pry', '~> 0.14' 21 | gem 'rubocop', '~> 1.39' 22 | gem 'sprockets-rails' 23 | gem 'sqlite3', '~> 1.5' 24 | gem 'stackprof', '~> 0.2' 25 | end 26 | 27 | group :test do 28 | gem 'rspec-rails', '~> 6.0' 29 | gem 'simplecov', '~> 0.21' 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 hschne 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | 5 | APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__) 6 | load 'rails/tasks/engine.rake' 7 | load 'rails/tasks/statistics.rake' 8 | 9 | require 'bundler/gem_tasks' 10 | 11 | require 'rspec/core/rake_task' 12 | rspec = RSpec::Core::RakeTask.new(:spec) 13 | rspec.verbose = false 14 | 15 | require 'rubocop/rake_task' 16 | RuboCop::RakeTask.new 17 | 18 | task lint: %i[rubocop lint:js lint:css lint:commit] 19 | namespace :lint do 20 | task :js do 21 | system 'npm run lint' 22 | end 23 | 24 | task :css do 25 | system 'npm run lint:scss' 26 | end 27 | 28 | task :commit do 29 | system 'npm run lint:commit' 30 | end 31 | end 32 | 33 | task default: %i[spec rubocop] 34 | -------------------------------------------------------------------------------- /app/adapters/rails_mini_profiler/database_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class DatabaseAdapter 5 | class << self 6 | def cast_to_text(column) 7 | if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' 8 | # Cast json field to text to have access to the LIKE operator 9 | "#{column}::text" 10 | else 11 | column 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/assets/config/rails_mini_profiler_manifest.js: -------------------------------------------------------------------------------- 1 | //= link_directory ../stylesheets/rails_mini_profiler .css 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/rails_mini_profiler.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require_tree . 15 | //= require rails-mini-profiler 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/rails_mini_profiler/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | *= require rails-mini-profiler 16 | */ 17 | -------------------------------------------------------------------------------- /app/controllers/rails_mini_profiler/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class ApplicationController < RailsMiniProfiler.configuration.ui.base_controller 5 | rescue_from ActiveRecord::RecordNotFound, with: ->(error) { handle(error, 404) } 6 | 7 | before_action :check_rmp_user 8 | 9 | private 10 | 11 | def handle(error, status = 500) 12 | respond_to do |format| 13 | format.html { redirect_back(fallback_location: profiled_requests_path, alert: error.to_s) } 14 | format.json { render status: status, json: { message: error.to_s } } 15 | end 16 | end 17 | 18 | def check_rmp_user 19 | user = User.get(request.env).present? 20 | redirect_back(fallback_location: fallback_location) unless user 21 | end 22 | 23 | def fallback_location 24 | defined?(main_app.root_path) ? main_app.root_path : '/' 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/rails_mini_profiler/flamegraphs_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_dependency 'rails_mini_profiler/application_controller' 4 | 5 | module RailsMiniProfiler 6 | class FlamegraphsController < ApplicationController 7 | layout 'rails_mini_profiler/flamegraph' 8 | 9 | before_action :set_flamegraph, only: %i[show] 10 | 11 | def show; end 12 | 13 | private 14 | 15 | def set_flamegraph 16 | @flamegraph = Flamegraph.find_by!(rmp_profiled_request_id: params[:id]).json_data 17 | end 18 | 19 | def configuration 20 | @configuration ||= RailsMiniProfiler.configuration 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/helpers/rails_mini_profiler/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module ApplicationHelper 5 | include Pagy::Frontend 6 | 7 | def present(model, presenter_class = nil, **kwargs) 8 | klass = presenter_class || "#{model.class}Presenter".constantize 9 | presenter = klass.new(model, self, **kwargs) 10 | yield(presenter) if block_given? 11 | presenter 12 | end 13 | 14 | def inline_svg(path, options = {}) 15 | if defined?(Webpacker::Engine) && RailsMiniProfiler.configuration.ui.webpacker_enabled 16 | path = "media/images/#{path}" 17 | inline_svg_pack_tag(path, options) 18 | else 19 | inline_svg_tag(path, options) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/helpers/rails_mini_profiler/profiled_requests_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module ProfiledRequestsHelper 5 | include ApplicationHelper 6 | 7 | def formatted_duration(duration) 8 | duration = (duration.to_f / 100) 9 | duration < 1 ? duration : duration.round 10 | end 11 | 12 | def formatted_allocations(allocations) 13 | number_to_human(allocations, units: { unit: '', thousand: 'k', million: 'M', billion: 'B', trillion: 'T' }) 14 | end 15 | 16 | def trace_display_name(name) 17 | { 18 | 'sql.active_record': 'ActiveRecord Query', 19 | 'instantiation.active_record': 'ActiveRecord Instantiation', 20 | 'render_template.action_view': 'Render View', 21 | 'render_partial.action_view': 'Render Partial', 22 | 'process_action.action_controller': 'Controller' 23 | }[name.to_sym] 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/javascript/images/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Iconly/Bulk/Bookmark 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/javascript/images/chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Iconly/Bulk/Chart 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/javascript/images/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/javascript/images/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/javascript/images/copy.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /app/javascript/images/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/javascript/images/filter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/images/graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/javascript/images/logo.svg: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /app/javascript/images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/javascript/images/setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Iconly/Bulk/Setting 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/javascript/images/show.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/javascript/js/checklist_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["count"]; 5 | 6 | connect() { 7 | this.setCount(); 8 | } 9 | 10 | checkAll() { 11 | this.setAllCheckboxes(true); 12 | this.setCount(); 13 | } 14 | 15 | checkNone() { 16 | this.setAllCheckboxes(false); 17 | this.setCount(); 18 | } 19 | 20 | onChecked() { 21 | this.setCount(); 22 | } 23 | 24 | setAllCheckboxes(checked) { 25 | this.checkboxes.forEach((el) => { 26 | const checkbox = el; 27 | 28 | if (!checkbox.disabled) { 29 | checkbox.checked = checked; 30 | } 31 | }); 32 | } 33 | 34 | setCount() { 35 | if (this.hasCountTarget) { 36 | const count = this.selectedCheckboxes.length; 37 | this.countTarget.innerHTML = `${count} selected`; 38 | } 39 | } 40 | 41 | get selectedCheckboxes() { 42 | return this.checkboxes.filter((c) => c.checked); 43 | } 44 | 45 | get checkboxes() { 46 | return new Array(...this.element.querySelectorAll("input[type=checkbox]")); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/javascript/js/clipboard_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["button", "source"]; 5 | 6 | connect() { 7 | if (!this.hasButtonTarget) return; 8 | 9 | this.originalText = this.buttonTarget.innerText; 10 | this.successDuration = 2000; 11 | } 12 | 13 | copy(event) { 14 | event.preventDefault(); 15 | 16 | let text = this.sourceTarget.innerText; 17 | const filter = this.data.get("filter"); 18 | if (filter) { 19 | text = new RegExp(filter).exec(text)[0]; 20 | } 21 | const temporaryInput = document.createElement("textarea"); 22 | temporaryInput.value = text; 23 | document.body.appendChild(temporaryInput); 24 | temporaryInput.select(); 25 | document.execCommand("copy"); 26 | document.body.removeChild(temporaryInput); 27 | 28 | this.copied(); 29 | } 30 | 31 | copied() { 32 | if (!this.hasButtonTarget) return; 33 | 34 | if (this.timeout) { 35 | clearTimeout(this.timeout); 36 | } 37 | 38 | const copiedClass = this.data.get("copiedClass"); 39 | if (copiedClass) { 40 | this.buttonTarget.classList.add(copiedClass); 41 | } 42 | const copiedMessage = this.data.get("copiedMessage"); 43 | const content = this.buttonTarget.innerHTML; 44 | if (copiedMessage) { 45 | this.buttonTarget.innerHTML = copiedMessage; 46 | } 47 | 48 | this.timeout = setTimeout(() => { 49 | this.buttonTarget.classList.remove(copiedClass); 50 | this.buttonTarget.innerHTML = content; 51 | }, this.successDuration); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/javascript/js/enable_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["enable"]; 5 | 6 | enable() { 7 | this.enableTarget.disabled = false; 8 | } 9 | 10 | disable() { 11 | this.enableTarget.disabled = true; 12 | } 13 | 14 | change(event) { 15 | if (event.type.match(/rmp:select:.*/)) { 16 | if (event.detail.count > 0) { 17 | this.enable(); 18 | } else { 19 | this.disable(); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/javascript/js/filter_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["filter"]; 5 | 6 | apply() { 7 | location.href = `${window.location.pathname}?${this.params}`; 8 | } 9 | 10 | reset() { 11 | location.href = `${window.location.pathname}`; 12 | } 13 | 14 | post() { 15 | const token = document.head.querySelector( 16 | 'meta[name="csrf-token"]' 17 | ).content; 18 | const path = `${window.location.pathname}/destroy_all?${this.params}`; 19 | fetch(path, { 20 | method: "DELETE", 21 | redirect: "follow", 22 | headers: { 23 | "Content-Type": "application/json", 24 | credentials: "same-origin", 25 | }, 26 | body: JSON.stringify({ authenticity_token: token }), 27 | }).then((response) => { 28 | if (response.redirected) { 29 | window.location.href = response.url; 30 | } 31 | }); 32 | } 33 | 34 | get params() { 35 | return this.activeFilterTargets() 36 | .map((t) => `${t.name}=${t.value}`) 37 | .join("&"); 38 | } 39 | 40 | activeFilterTargets() { 41 | return this.filterTargets.filter(function (target) { 42 | if (target.type === "checkbox" || target.type === "radio") 43 | return target.checked; 44 | 45 | return target.value.length > 0; 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/javascript/js/search_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["field"]; 5 | 6 | clear() { 7 | this.eventTarget.value = null; 8 | window.dispatchEvent(new CustomEvent("search-controller:submit", {})); 9 | } 10 | 11 | submit(event) { 12 | event.preventDefault(); 13 | 14 | if (event.key === "Enter" || event.type === "click") { 15 | window.dispatchEvent(new CustomEvent("search-controller:submit", {})); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/javascript/js/select_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["all", "selectable"]; 5 | 6 | selectAll(event) { 7 | const checked = event.target.checked; 8 | this.allTarget.indeterminate = false; 9 | this._setAllCheckboxes(checked); 10 | this._dispatch("change", { count: this.selectedCount }); 11 | } 12 | 13 | onSelected() { 14 | this.allTarget.indeterminate = !!this._indeterminate; 15 | this._dispatch("change", { count: this.selectedCount }); 16 | } 17 | 18 | get selectedCount() { 19 | return this.selected.length; 20 | } 21 | 22 | get selected() { 23 | return this.selectables.filter((c) => c.checked); 24 | } 25 | 26 | get selectables() { 27 | return new Array(...this.selectableTargets); 28 | } 29 | 30 | _setAllCheckboxes(checked) { 31 | this.selectables.forEach((el) => { 32 | const checkbox = el; 33 | 34 | if (!checkbox.disabled) { 35 | checkbox.checked = checked; 36 | } 37 | }); 38 | } 39 | 40 | get _indeterminate() { 41 | return ( 42 | this.selected.length !== this.selectableTargets.length && 43 | this.selected.length > 0 44 | ); 45 | } 46 | 47 | _dispatch(name, detail) { 48 | window.dispatchEvent( 49 | new CustomEvent(`rmp:select:${name}`, { bubbles: true, detail }) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/components/buttons.scss: -------------------------------------------------------------------------------- 1 | button, 2 | .button { 3 | display: inline-block; 4 | padding: 0.5em 0.5em; 5 | border: none; 6 | border-radius: 5px; 7 | cursor: pointer; 8 | font-size: 1rem; 9 | text-align: center; 10 | text-decoration: none; 11 | 12 | &:disabled { 13 | background: var(--grey-200); 14 | cursor: not-allowed; 15 | } 16 | } 17 | 18 | .btn-red { 19 | background: var(--red-500); 20 | color: white; 21 | font-weight: 600; 22 | outline: none; 23 | 24 | &:hover { 25 | background: var(--red-600); 26 | } 27 | } 28 | 29 | .btn-grey { 30 | background: var(--grey-200); 31 | 32 | &:hover { 33 | background: var(--grey-100); 34 | } 35 | } 36 | 37 | .btn-white { 38 | border: 1px solid var(--grey-200); 39 | background: white; 40 | 41 | &:hover { 42 | background: var(--grey-100); 43 | } 44 | } 45 | 46 | button:hover { 47 | box-shadow: 0 0.25rem 0.25rem 0 var(--grey-50); 48 | } 49 | 50 | button.none { 51 | padding: 0; 52 | border: none; 53 | background: none; 54 | outline: none; 55 | 56 | &:hover { 57 | box-shadow: none; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/components/dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown-body { 2 | padding: 0.33rem 0.5rem; 3 | } 4 | 5 | .dropdown-search-field { 6 | padding: 0.5rem 0.5rem; 7 | border: 1px solid var(--grey-400); 8 | border-radius: 5px; 9 | color: var(--grey-700); 10 | } 11 | 12 | .dropdown-toggle { 13 | display: flex; 14 | align-items: center; 15 | color: inherit; 16 | 17 | &:hover { 18 | color: var(--grey-900); 19 | } 20 | } 21 | 22 | .dropdown-container { 23 | position: absolute; 24 | z-index: 1; 25 | display: flex; 26 | overflow: hidden; 27 | min-width: 240px; 28 | box-sizing: border-box; 29 | flex-direction: column; 30 | border: 1px solid var(--grey-200); 31 | margin-top: 0.3em; 32 | background: white; 33 | border-radius: 5px; 34 | box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); 35 | color: var(--grey-400); 36 | cursor: default; 37 | } 38 | 39 | .dropdown-search { 40 | display: flex; 41 | flex-direction: column; 42 | } 43 | 44 | .dropdown-search button { 45 | display: flex; 46 | flex-direction: column; 47 | } 48 | 49 | .dropdown-entry { 50 | display: flex; 51 | padding: 0.33rem 1rem; 52 | text-decoration: none; 53 | 54 | &:hover { 55 | background: var(--grey-100); 56 | color: var(--grey-900); 57 | } 58 | } 59 | 60 | .dropdown-entry input { 61 | margin-right: 0.5em; 62 | } 63 | 64 | .dropdown-header { 65 | display: flex; 66 | align-items: center; 67 | justify-content: space-between; 68 | padding: 0.5rem 1rem; 69 | border-bottom: 1px solid var(--grey-200); 70 | background: var(--grey-100); 71 | font-size: 0.833rem; 72 | text-align: left; 73 | } 74 | 75 | .dropdown-header button { 76 | padding: 0; 77 | border: none; 78 | background: none; 79 | color: var(--grey-500); 80 | font-size: 0.833rem; 81 | outline: none; 82 | text-decoration: underline; 83 | 84 | &:hover { 85 | box-shadow: none; 86 | color: var(--grey-900); 87 | } 88 | } 89 | 90 | .dropdown-footer { 91 | width: 100%; 92 | padding: 0.5rem; 93 | border: none; 94 | background: var(--red-500); 95 | border-radius: 0; 96 | color: white; 97 | font-weight: 600; 98 | outline: none; 99 | 100 | &:hover { 101 | background: var(--red-600); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/components/input.scss: -------------------------------------------------------------------------------- 1 | input[type=submit] { 2 | display: inline-block; 3 | padding: 0.5em 0.5em; 4 | border: none; 5 | border-radius: 5px; 6 | cursor: pointer; 7 | font-size: 1rem; 8 | text-align: center; 9 | text-decoration: none; 10 | } 11 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/components/navbar.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | justify-content: center; 4 | padding: 1.5rem 0; 5 | margin: 0; 6 | background: var(--primary); 7 | box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2); 8 | } 9 | 10 | .header a { 11 | color: white; 12 | text-decoration: none; 13 | } 14 | 15 | .nav { 16 | display: flex; 17 | width: var(--main-width); 18 | justify-content: space-between; 19 | } 20 | 21 | .home { 22 | display: flex; 23 | align-items: center; 24 | text-decoration: none; 25 | } 26 | 27 | .home-logo { 28 | width: 64px; 29 | height: 64px; 30 | } 31 | 32 | .home-title { 33 | padding: 0 0 0 1rem; 34 | margin: 0; 35 | } 36 | 37 | .header-links { 38 | display: flex; 39 | align-items: center; 40 | padding: 0; 41 | margin: 0; 42 | font-weight: 700; 43 | list-style: none; 44 | } 45 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/components/page_header.scss: -------------------------------------------------------------------------------- 1 | .page-header { 2 | padding: 2.5rem 0 2.5rem; 3 | } 4 | 5 | .page-header h1 { 6 | font-size: 28px; 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/components/pagination.scss: -------------------------------------------------------------------------------- 1 | .pagy-nav { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | padding: 1em 0; 6 | } 7 | 8 | .pagy-nav > .page.disabled, 9 | .pagy-nav > .page.active, 10 | .pagy-nav > .page > a { 11 | padding: 0.5rem 1rem; 12 | border: 1px solid var(--border-color); 13 | border-right: none; 14 | background: white; 15 | color: var(--grey-900); 16 | cursor: pointer; 17 | text-decoration: none; 18 | } 19 | 20 | .pagy-nav > .page.prev a, 21 | .pagy-nav > .page.prev.disabled { 22 | border-radius: 5px 0 0 5px; 23 | } 24 | 25 | .pagy-nav > .page.next a, 26 | .pagy-nav > .page.next.disabled { 27 | border-right: 1px solid var(--border-color); 28 | border-radius: 0 5px 5px 0; 29 | } 30 | 31 | .pagy-nav > .page.active { 32 | border-color: var(--red-500); 33 | background: var(--red-500); 34 | color: white; 35 | cursor: default; 36 | 37 | &:hover { 38 | background: var(--red-500); 39 | } 40 | } 41 | 42 | .pagy-nav > .page.disabled { 43 | color: var(--grey-500); 44 | cursor: default; 45 | 46 | &:hover { 47 | background: white; 48 | } 49 | } 50 | 51 | .pagy-nav > .page.disabled:hover, 52 | .pagy-nav > .page.active:hover, 53 | .pagy-nav > .page > a:hover { 54 | background: var(--grey-100); 55 | } 56 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/components/placeholder.scss: -------------------------------------------------------------------------------- 1 | .placeholder { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | padding-bottom: 2rem; 8 | } 9 | 10 | .placeholder-image { 11 | width: 30%; 12 | height: 30%; 13 | -webkit-filter: grayscale(1) brightness(2.5); 14 | } 15 | 16 | .placeholder-text { 17 | padding: 1rem 0; 18 | color: var(--grey-400); 19 | text-align: center; 20 | } 21 | 22 | .placeholder-text h2 { 23 | padding-bottom: 1rem; 24 | } 25 | 26 | .placeholder-link { 27 | color: var(--grey-400); 28 | 29 | &:visited { 30 | color: var(--grey-400); 31 | } 32 | 33 | &:hover { 34 | color: var(--grey-900); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/components/profiled_request_table.scss: -------------------------------------------------------------------------------- 1 | .table { 2 | width: 100%; 3 | border: hidden 1px var(--border-color); 4 | 5 | /* Hack to get table row borders, see https://stackoverflow.com/a/2586780/2553104 */ 6 | border-collapse: collapse; 7 | border-radius: 5px; 8 | box-shadow: 0 0 0 1px var(--border-color); 9 | table-layout: fixed; 10 | } 11 | 12 | .table th, 13 | .table td { 14 | padding: 0.5rem 1rem; 15 | } 16 | 17 | .table thead tr { 18 | color: var(--grey-500); 19 | } 20 | 21 | .table tr:nth-child(even) { 22 | background: var(--grey-50); 23 | } 24 | 25 | .table thead th { 26 | font-weight: 400; 27 | } 28 | 29 | .table-filter-icon { 30 | width: 14px; 31 | margin-left: 0.5rem; 32 | } 33 | 34 | .table tbody tr:not(.no-row) { 35 | border: solid 1px var(--border-color); 36 | color: var(--grey-700); 37 | cursor: pointer; 38 | } 39 | 40 | .table tbody tr:not(.no-row):hover { 41 | background: var(--grey-100); 42 | } 43 | 44 | .request-checkbox { 45 | z-index: 1; 46 | width: 1rem; 47 | height: 1rem; 48 | } 49 | 50 | .request-path { 51 | overflow: hidden; 52 | max-width: 280px; 53 | text-overflow: ellipsis; 54 | white-space: nowrap; 55 | } 56 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/components/trace.scss: -------------------------------------------------------------------------------- 1 | .trace { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-start; 5 | padding: 0.25em 0; 6 | list-style: none; 7 | } 8 | 9 | .trace:nth-child(odd) { 10 | background: var(--grey-100); 11 | } 12 | 13 | .trace .trace-bar { 14 | position: relative; 15 | height: 16px; 16 | padding: 0; 17 | margin: 0; 18 | background: linear-gradient(to top right, var(--grey-500), var(--grey-400)); 19 | cursor: pointer; 20 | } 21 | 22 | .instantiation-trace .trace-bar { 23 | background: linear-gradient(to top right, var(--green-400), var(--green-300)); 24 | } 25 | 26 | .sequel-trace .trace-bar { 27 | background: linear-gradient(to top right, var(--green-500), var(--green-400)); 28 | } 29 | 30 | .controller-trace .trace-bar { 31 | background: linear-gradient(to top right, var(--yellow-500), var(--yellow-400)); 32 | } 33 | 34 | .render-template-trace .trace-bar, 35 | .render-partial-trace .trace-bar { 36 | background: linear-gradient(to top right, var(--blue-500), var(--blue-400)); 37 | } 38 | 39 | .trace-name { 40 | overflow: hidden; 41 | box-sizing: border-box; 42 | padding: 0 0.5em; 43 | margin: 0; 44 | color: var(--grey-400); 45 | font-size: 14px; 46 | text-align: right; 47 | text-overflow: ellipsis; 48 | } 49 | 50 | .trace-payload { 51 | margin: 0; 52 | } 53 | 54 | .sequel-trace-query { 55 | overflow: auto; 56 | max-height: 100px; 57 | padding: 1em 1em; 58 | background: var(--grey-100); 59 | white-space: pre-wrap; 60 | } 61 | 62 | .sequel-trace-binds { 63 | overflow: auto; 64 | max-height: 100px; 65 | padding: 0.5em 1em; 66 | margin: 0 0 1em 0; 67 | background: var(--grey-50); 68 | font-size: 12px; 69 | white-space: pre-wrap; 70 | } 71 | 72 | .backtrace { 73 | display: flex; 74 | flex-direction: row; 75 | align-items: center; 76 | justify-content: space-between; 77 | padding: 0.5em; 78 | background: var(--grey-100); 79 | } 80 | 81 | .backtrace button { 82 | overflow: auto; 83 | height: 20px; 84 | padding: 0; 85 | margin: 0; 86 | background: none; 87 | color: var(--grey-500); 88 | } 89 | 90 | .backtrace button svg { 91 | width: 20px; 92 | height: 20px; 93 | } 94 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/flamegraph.scss: -------------------------------------------------------------------------------- 1 | #wrapper { 2 | width: 100%; 3 | height: 100vh; 4 | } 5 | 6 | #speedscope-iframe { 7 | width: 100%; 8 | height: 100%; 9 | border: none; 10 | } 11 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/flashes.scss: -------------------------------------------------------------------------------- 1 | .flash { 2 | padding: 1rem 1rem; 3 | margin-top: 1rem; 4 | border-radius: 5px; 5 | } 6 | 7 | .flash-error { 8 | background: var(--red-500); 9 | color: white; 10 | } 11 | 12 | .flash-notice { 13 | background: var(--green-400); 14 | color: white; 15 | } 16 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/profiled_requests.scss: -------------------------------------------------------------------------------- 1 | @import 'components/profiled_request_table'; 2 | 3 | .search-field { 4 | box-sizing: border-box; 5 | padding: 0.5rem; 6 | border: 1px solid var(--grey-400); 7 | border-radius: 5px; 8 | } 9 | 10 | .profiled-requests-header { 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | justify-content: space-between; 15 | } 16 | 17 | .clear-action button { 18 | background: var(--red-500); 19 | color: white; 20 | font-weight: 600; 21 | 22 | &:hover { 23 | background: var(--red-600); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/traces.scss: -------------------------------------------------------------------------------- 1 | .profiled-request-details { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | padding-bottom: 2rem; 6 | } 7 | 8 | .request-details-data { 9 | display: flex; 10 | } 11 | 12 | .data-item { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: flex-start; 16 | padding: 0; 17 | margin-right: 3rem; 18 | list-style: none; 19 | } 20 | 21 | .data-item small { 22 | color: var(--grey-400); 23 | } 24 | 25 | .data-item span { 26 | margin: 0.25rem 0; 27 | } 28 | 29 | [class*='request-status-2'], 30 | [class*='request-method-get'] { 31 | background: var(--green-400) !important; 32 | color: white; 33 | } 34 | 35 | [class*='request-status-4'], 36 | [class*='request-method-put'], 37 | [class*='request-method-patch'] { 38 | background: var(--yellow-400) !important; 39 | color: white; 40 | } 41 | 42 | [class*='request-status-5'], 43 | [class*='request-method-delete'] { 44 | background: var(--red-500) !important; 45 | color: white; 46 | } 47 | 48 | .trace-list { 49 | padding: 1rem; 50 | border: 1px solid var(--grey-200); 51 | border-radius: 5px; 52 | } 53 | 54 | .trace-details-table { 55 | width: 100%; 56 | border: hidden 1px var(--border-color); 57 | margin-top: 1em; 58 | border-collapse: collapse; 59 | } 60 | 61 | .trace-details-table tr th { 62 | font-weight: var(--fw-semibold); 63 | } 64 | 65 | .trace-list-header { 66 | display: flex; 67 | align-items: center; 68 | justify-content: space-between; 69 | padding-bottom: 1rem; 70 | } 71 | 72 | .trace-list-filters { 73 | display: flex; 74 | width: 60%; 75 | align-items: center; 76 | justify-content: space-between; 77 | color: var(--grey-500); 78 | } 79 | -------------------------------------------------------------------------------- /app/models/rails_mini_profiler/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class ApplicationRecord < ActiveRecord::Base 5 | if RailsMiniProfiler.storage_configuration.database.present? 6 | establish_connection(RailsMiniProfiler.storage_configuration.database) 7 | end 8 | 9 | self.abstract_class = true 10 | 11 | def self.record_timestamps 12 | # Some applications may disable timestamp setting, but in the context of the engine we always want to record 13 | # timestamps, as engine functionality relies on it. 14 | true 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/rails_mini_profiler/flamegraph.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: rmp_flamegraphs 6 | # 7 | # id :integer not null, primary key 8 | # rmp_profiled_request_id :integer not null 9 | # data :binary 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | # Indexes 14 | # 15 | # index_rmp_flamegraphs_on_rmp_profiled_request_id (rmp_profiled_request_id) 16 | # 17 | module RailsMiniProfiler 18 | class Flamegraph < RailsMiniProfiler::ApplicationRecord 19 | self.table_name = RailsMiniProfiler.storage_configuration.flamegraphs_table 20 | 21 | belongs_to :profiled_request, 22 | class_name: 'RailsMiniProfiler::ProfiledRequest', 23 | foreign_key: :rmp_profiled_request_id 24 | 25 | before_save :compress 26 | 27 | def json_data 28 | @json_data = ActiveSupport::Gzip.decompress(data) 29 | end 30 | 31 | private 32 | 33 | def compress 34 | self.data = ActiveSupport::Gzip.compress(data) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/rails_mini_profiler/trace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: rmp_traces 6 | # 7 | # id :integer not null, primary key 8 | # rmp_profiled_request_id :integer not null 9 | # name :string 10 | # start :integer 11 | # finish :integer 12 | # duration :integer 13 | # allocations :integer 14 | # payload :json 15 | # backtrace :json 16 | # created_at :datetime not null 17 | # updated_at :datetime not null 18 | # 19 | # Indexes 20 | # 21 | # index_rmp_traces_on_rmp_profiled_request_id (rmp_profiled_request_id) 22 | # 23 | module RailsMiniProfiler 24 | class Trace < RailsMiniProfiler::ApplicationRecord 25 | self.table_name = RailsMiniProfiler.storage_configuration.traces_table 26 | 27 | belongs_to :profiled_request, 28 | class_name: 'RailsMiniProfiler::ProfiledRequest', 29 | foreign_key: :rmp_profiled_request_id 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/presenters/rails_mini_profiler/base_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class BasePresenter < SimpleDelegator 5 | def initialize(model, view, **_kwargs) 6 | @h = view 7 | super(model) 8 | end 9 | 10 | attr_reader :h 11 | 12 | alias model __getobj__ 13 | 14 | # To avoid having to address the view context explicitly we try to delegate to it 15 | def method_missing(method, *args, &block) 16 | h.public_send(method, *args, &block) 17 | rescue NoMethodError 18 | super 19 | end 20 | 21 | def respond_to_missing?(method_name, *args) 22 | h.respond_to?(method_name, *args) || super 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/presenters/rails_mini_profiler/controller_trace_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class ControllerTracePresenter < TracePresenter 5 | def label 6 | 'Action Controller' 7 | end 8 | 9 | def view_runtime 10 | payload['view_runtime'] 11 | end 12 | 13 | def db_runtime 14 | payload['db_runtime'] 15 | end 16 | 17 | def content 18 | content_tag('div') do 19 | content_tag('pre', class: 'trace-payload') do 20 | content_tag(:div, "View Time: #{view_runtime} ms, DB Time: #{db_runtime} ms", 21 | class: 'sequel-trace-query') 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/presenters/rails_mini_profiler/instantiation_trace_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class InstantiationTracePresenter < TracePresenter 5 | def label 6 | "#{class_name} Instantiation" 7 | end 8 | 9 | def class_name 10 | payload['class_name'] 11 | end 12 | 13 | def record_count 14 | payload['record_count'] 15 | end 16 | 17 | def db_runtime 18 | payload['db_runtime'] 19 | end 20 | 21 | def description 22 | record_string = 'Record'.pluralize(record_count) 23 | "Instantiated #{record_count} #{class_name} #{record_string}" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/presenters/rails_mini_profiler/profiled_request_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class ProfiledRequestPresenter < BasePresenter 5 | def request_name 6 | model.request_path 7 | end 8 | 9 | def duration 10 | formatted_duration(model.duration) 11 | end 12 | 13 | def allocations 14 | formatted_allocations(model.allocations) 15 | end 16 | 17 | def created_at 18 | from_time = Time.now 19 | created_at = model.created_at.in_time_zone(Time.zone) 20 | distance = if from_time - created_at < 5.minutes 21 | 'Now' 22 | else 23 | "#{distance_of_time_in_words(from_time, created_at)} ago" 24 | end 25 | time_tag(created_at) { content_tag('span', distance) } 26 | end 27 | 28 | def flamegraph_button 29 | return nil unless RailsMiniProfiler.configuration.flamegraph_enabled 30 | 31 | return nil unless model.flamegraph.present? 32 | 33 | link_to(flamegraph_path(model.id), title: 'Show Flamegraph') do 34 | content_tag('button', 'Flamegraph', class: 'btn-grey') 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/presenters/rails_mini_profiler/render_partial_trace_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class RenderPartialTracePresenter < TracePresenter 5 | def identifier 6 | payload['identifier'] 7 | end 8 | 9 | def label 10 | root = Rails.root.to_s.split('/').to_set 11 | id = identifier.split('/').to_set 12 | (root ^ id).drop(2).join('/').reverse.truncate(30).reverse 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/presenters/rails_mini_profiler/render_template_trace_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class RenderTemplateTracePresenter < TracePresenter 5 | def identifier 6 | payload['identifier'] 7 | end 8 | 9 | def label 10 | root = Rails.root.to_s.split('/').to_set 11 | id = identifier.split('/').to_set 12 | (root ^ id).drop(2).join('/').reverse.truncate(30).reverse 13 | end 14 | 15 | def description 16 | "Render #{label}" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/presenters/rails_mini_profiler/rmp_trace_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class RmpTracePresenter < TracePresenter 5 | def label 6 | 'Total Time' 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/presenters/rails_mini_profiler/sequel_trace_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class SequelTracePresenter < TracePresenter 5 | def label 6 | sql_description 7 | end 8 | 9 | def name 10 | payload['name'] 11 | end 12 | 13 | def sql 14 | payload['sql'] 15 | end 16 | 17 | def binds 18 | payload['binds'] 19 | end 20 | 21 | alias description label 22 | 23 | def content 24 | return nil if transaction? 25 | 26 | content_tag('div') do 27 | content_tag('pre', class: 'trace-payload') do 28 | content_tag(:div, sql, class: 'sequel-trace-query') 29 | end + binding_content 30 | end 31 | end 32 | 33 | private 34 | 35 | def transaction? 36 | model.payload['name'] == 'TRANSACTION' 37 | end 38 | 39 | def schema? 40 | model.payload['name'] == 'SCHEMA' 41 | end 42 | 43 | def sql_description 44 | if transaction? 45 | transaction_description 46 | elsif schema? 47 | 'Load Schema' 48 | elsif model.payload['name'].present? 49 | model.payload['name'] 50 | else 51 | model.payload['sql'].truncate(15) 52 | end 53 | end 54 | 55 | def transaction_description 56 | # The raw SQL is something like 'BEGIN TRANSACTION', and we just turn it into 'Begin Transaction', which is less 57 | # loud and nicer to look at. 58 | model.sql.split.map(&:capitalize).join(' ') 59 | end 60 | 61 | def binding_content 62 | return nil if simple_binds.empty? 63 | 64 | content = simple_binds.collect do |hash| 65 | flat = hash.to_a.flatten 66 | "#{flat.first}=#{flat.second}" 67 | end 68 | content_tag(:pre, content.join(', '), class: 'sequel-trace-binds') 69 | end 70 | 71 | def simple_binds 72 | return [] if binds.nil? || binds.empty? 73 | 74 | binds.each_with_object({}) do |hash, object| 75 | name = hash['name'] 76 | value = hash['value'] 77 | object[name] = value 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /app/presenters/rails_mini_profiler/trace_presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class TracePresenter < BasePresenter 5 | def initialize(trace, view, context: {}) 6 | super(trace, view) 7 | @start = context[:start] 8 | @finish = context[:finish] 9 | @total_duration = context[:total_duration] 10 | @total_allocations = context[:total_allocations] 11 | end 12 | 13 | def label 14 | '' 15 | end 16 | 17 | def description 18 | label 19 | end 20 | 21 | def content 22 | nil 23 | end 24 | 25 | def backtrace 26 | return if model.backtrace.empty? 27 | 28 | model.backtrace.first 29 | end 30 | 31 | def type 32 | # Turn this class name into a dasherized version for use in assigning CSS classes. E.g. 'RmpTracePresenter' 33 | # becomes 'rmp-trace' 34 | self.class.name.demodulize.delete_suffix('Presenter') 35 | .underscore 36 | .dasherize 37 | end 38 | 39 | def duration 40 | formatted_duration(model.duration) 41 | end 42 | 43 | def duration_percent 44 | (model.duration.to_f / @total_duration * 100).round 45 | end 46 | 47 | def allocations 48 | formatted_allocations(model.allocations) 49 | end 50 | 51 | def allocations_percent 52 | (model.allocations.to_f / @total_allocations * 100).round 53 | end 54 | 55 | def from_start 56 | (model.start - @start).to_f / 100 57 | end 58 | 59 | def from_start_percent 60 | ((model.start - @start).to_f / (@finish - @start)).to_f * 100 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /app/search/rails_mini_profiler/base_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class BaseSearch 5 | def initialize(params = nil, **kwargs) 6 | config = self.class.config 7 | options = params&.to_h || {} 8 | options = options.merge(kwargs) 9 | @scope = options.delete(:scope) || (config[:scope] && instance_eval(&config[:scope])) 10 | @options = (options.delete(:options) || config[:options]).stringify_keys 11 | @params = options.stringify_keys.slice(*@options.keys) 12 | end 13 | 14 | def results 15 | @results ||= apply 16 | end 17 | 18 | def results? 19 | results.any? 20 | end 21 | 22 | class << self 23 | def scope(&block) 24 | config[:scope] = block 25 | end 26 | 27 | def option(name, options = {}, &block) 28 | name = name.to_s 29 | handler = options[:with] || block 30 | 31 | config[:options][name] = normalize_search_handler(handler, name) 32 | 33 | define_method(name) { @search.param name } 34 | end 35 | 36 | def results(**kwargs) 37 | new(**kwargs).results 38 | end 39 | 40 | def config 41 | @config ||= { options: {} } 42 | end 43 | 44 | private 45 | 46 | def normalize_search_handler(handler, name) 47 | case handler 48 | when Symbol 49 | ->(scope, value) { method(handler).call scope, value } 50 | when Proc 51 | handler 52 | else 53 | ->(scope, value) { scope.where name => value unless value.blank? } 54 | end 55 | end 56 | end 57 | 58 | private 59 | 60 | def apply 61 | @params.inject(@scope) do |scope, (name, value)| 62 | new_scope = instance_exec scope, value, &@options[name] 63 | new_scope || scope 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/search/rails_mini_profiler/profiled_request_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class ProfiledRequestSearch < BaseSearch 5 | option(:id) do |scope, value| 6 | scope.where(id: value) 7 | end 8 | 9 | option(:path) do |scope, value| 10 | scope.where('request_path LIKE ?', "%#{value}%") 11 | end 12 | 13 | option(:method) do |scope, value| 14 | scope.where(request_method: value) 15 | end 16 | 17 | option(:media_type) do |scope, value| 18 | scope.where(response_media_type: value) 19 | end 20 | 21 | option(:status) do |scope, value| 22 | value = value.map(&:to_i) 23 | min = value.min 24 | max = value.max + 100 25 | scope.where('response_status >= :lower', lower: min) 26 | .where('response_status < :upper', upper: max) 27 | end 28 | 29 | option(:duration) do |scope, value| 30 | scope.where('duration > :duration', duration: value) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/search/rails_mini_profiler/trace_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class TraceSearch < BaseSearch 5 | option(:name) do |scope, value| 6 | scope.where(name: value) 7 | end 8 | 9 | option(:duration) do |scope, value| 10 | scope.where('duration > :duration', duration: value) 11 | end 12 | 13 | option(:allocations) do |scope, value| 14 | scope.where('allocations > :allocations', allocations: value) 15 | end 16 | 17 | option(:payload) do |scope, value| 18 | payload_column = DatabaseAdapter.cast_to_text(:payload) 19 | scope.where("#{payload_column} LIKE ?", "%#{value}%") 20 | end 21 | 22 | option(:backtrace) do |scope, value| 23 | backtrace_column = Adapters::DatabaseAdapter.cast_to_text(:backtrace) 24 | scope.where("#{backtrace_column} LIKE ?", "%#{value}%") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/views/layouts/rails_mini_profiler/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= render 'rails_mini_profiler/shared/head' %> 4 | 5 | 6 | <%= render 'rails_mini_profiler/shared/navbar' %> 7 |
8 |
9 | <%= render 'rails_mini_profiler/shared/flashes' %> 10 | <%= yield %> 11 |
12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/views/layouts/rails_mini_profiler/flamegraph.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= render 'rails_mini_profiler/shared/head' %> 4 | 5 | 6 |
7 | <%= yield %> 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/views/models/_flamegraph.json.jb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | flamegraph 4 | -------------------------------------------------------------------------------- /app/views/models/_profiled_request.jb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | profiled_request.attributes 4 | -------------------------------------------------------------------------------- /app/views/models/_trace.jb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | trace.attributes 4 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/badge.html.erb: -------------------------------------------------------------------------------- 1 | 34 | 35 | <%= inline_svg('logo_variant.svg') %> 36 | <%= (@profiled_request.duration.to_f / 100).round %>ms 37 | 38 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/flamegraphs/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 14 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/flamegraphs/show.json.jb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | render 'models/flamegraph', flamegraph: @flamegraph 4 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
5 | <%= render "rails_mini_profiler/profiled_requests/shared/header/header" %> 6 | <%= render "rails_mini_profiler/profiled_requests/shared/table/table" %> 7 |
8 | <%== pagy_nav(@pagy) if @pagy.pages > 1 %> 9 |
10 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/index.json.jb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | render partial: 'models/profiled_request', collection: @profiled_requests, as: :profiled_request 4 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/shared/header/_header.erb: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/shared/table/_placeholder.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | <%= inline_svg('logo_variant.svg', class: 'placeholder-image') %> 5 |
6 |

No Requests found!

7 |

Send some requests to your app or modify your filters

8 |
9 | <%= link_to('Clear Filters', profiled_requests_path, class: 'placeholder-link') %> 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/shared/table/_table.erb: -------------------------------------------------------------------------------- 1 | 4 | <%= render "rails_mini_profiler/profiled_requests/shared/table/table_head" %> 5 | 6 | <% if @profiled_requests.empty? %> 7 | <%= render "rails_mini_profiler/profiled_requests/shared/table/placeholder" %> 8 | <% else %> 9 | <% @profiled_requests.each do |profiled_request| %> 10 | <%= render "rails_mini_profiler/profiled_requests/shared/table/table_row", profiled_request: profiled_request %> 11 | <% end %> 12 | <% end %> 13 | 14 |
15 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/shared/table/_table_row.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= label_tag nil do %> 4 | <%= check_box_tag 'id[]', profiled_request.id, 5 | params.fetch(:selected, []).include?(profiled_request.id), 6 | class: "request-checkbox", 7 | data: { 8 | 'selectable-target': 'selectable', 9 | 'filters-target': 'filter', 10 | action: 'click->selectable#onSelected' 11 | }, 12 | onclick: "event.stopPropagation();" %> 13 | <% end %> 14 | 15 | <%= profiled_request.request_name %> 16 | <%= profiled_request.request_method %> 17 | <%= profiled_request.response_status %> 18 | <%= profiled_request.response_media_type %> 19 | <%= profiled_request.duration %> ms 20 | <%= profiled_request.created_at %> 21 | 22 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/show.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 24 | <%= @profiled_request.flamegraph_button %> 25 |
26 | 27 | <%= render "rails_mini_profiler/profiled_requests/show/trace_list" %> 28 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/show.json.jb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | request = render 'models/profiled_request', profiled_request: @profiled_request 4 | request[:traces] = render partial: 'models/trace', collection: @traces, as: :trace 5 | request 6 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/show/_trace.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= trace.label %> 3 |
    4 |
    5 |
    6 |

    <%= trace.description %>

    7 | 8 |
    9 |
    10 | <%= trace.content %> 11 | 12 | <% if trace.backtrace %> 13 | 30 | <% end %> 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
    Response TimeAllocations
    Total<%= trace.duration %>ms<%= trace.allocations %>
    Relative<%= trace.duration_percent %>%<%= trace.allocations_percent %>%
    52 |
    53 | 54 | 55 |
    56 |
    57 |
  • 58 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/show/_trace_list.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= render "rails_mini_profiler/profiled_requests/show/trace_list_header" %> 3 | <% if @traces.empty? %> 4 | <%= render "rails_mini_profiler/profiled_requests/show/trace_list_placeholder" %> 5 | <% else %> 6 |
      7 | <% @traces.each do |trace| %> 8 | <%= render "rails_mini_profiler/profiled_requests/show/trace", trace: trace %> 9 | <% end %> 10 |
    11 | <% end %> 12 |
    13 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/profiled_requests/show/_trace_list_placeholder.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | <%= inline_svg('logo_variant.svg', class: 'placeholder-image') %> 5 |
    6 |

    No Traces found!

    7 |

    Modify filters or reload this page

    8 |
    9 | <%= link_to('Clear Filters', profiled_request_url(@profiled_request.id), class: 'placeholder-link') %> 10 |
    11 | 12 | 13 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/shared/_flashes.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |type, message| %> 2 | <% if type == "alert" %> 3 |
    <%= message %>
    4 | <% end %> 5 | <% if type == "notice" %> 6 |
    <%= message %>
    7 | <% end %> 8 | <% end %> 9 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/shared/_head.erb: -------------------------------------------------------------------------------- 1 | 2 | Rails Mini Profiler 3 | <%= csrf_meta_tags %> 4 | <%= csp_meta_tag %> 5 | 6 | <% if defined?(Webpacker::Engine) && RailsMiniProfiler.configuration.ui.webpacker_enabled %> 7 | <%= javascript_pack_tag "rails-mini-profiler" %> 8 | <%= stylesheet_pack_tag 'rails-mini-profiler' %> 9 | <% else %> 10 | <%= javascript_include_tag "rails_mini_profiler" %> 11 | <%= stylesheet_link_tag "rails_mini_profiler/application", media: "all" %> 12 | <%end %> 13 | 14 | -------------------------------------------------------------------------------- /app/views/rails_mini_profiler/shared/_navbar.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | 15 |
    16 | -------------------------------------------------------------------------------- /bin/annotate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'annotate' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("annotate", "annotate") 30 | -------------------------------------------------------------------------------- /bin/appraisal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'appraisal' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("appraisal", "appraisal") 30 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # This command will automatically be run when you run "rails" with Rails gems 5 | # installed from the root of your application. 6 | 7 | ENGINE_ROOT = File.expand_path('..', __dir__) 8 | ENGINE_PATH = File.expand_path('../lib/rails/mini/profiler/engine', __dir__) 9 | APP_PATH = File.expand_path('../test/dummy/config/application', __dir__) 10 | 11 | # Set up gems listed in the Gemfile. 12 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 13 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 14 | 15 | require 'rails/all' 16 | require 'rails/engine/commands' 17 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rake", "rake") 30 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rspec-core", "rspec") 30 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "body-max-line-length": [2, "always", 120], 5 | "footer-max-line-length": [1, "always", 120], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RailsMiniProfiler::Engine.routes.draw do 4 | root 'profiled_requests#index' 5 | resources :profiled_requests, only: %i[index show destroy] do 6 | collection do 7 | delete 'destroy_all' 8 | end 9 | end 10 | resources :flamegraphs, only: %i[show] 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20210621185018_create_rmp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateRmp < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :rmp_profiled_requests, charset: 'utf8' do |t| 6 | t.string :user_id 7 | t.bigint :start 8 | t.bigint :finish 9 | t.integer :duration 10 | t.bigint :allocations 11 | t.string :request_path 12 | t.string :request_query_string 13 | t.string :request_method 14 | t.json :request_headers 15 | t.text :request_body 16 | t.integer :response_status 17 | t.text :response_body 18 | t.json :response_headers 19 | t.string :response_media_type 20 | 21 | t.timestamps 22 | 23 | t.index :created_at 24 | end 25 | 26 | create_table :rmp_traces, charset: 'utf8' do |t| 27 | t.belongs_to :rmp_profiled_request, null: false, foreign_key: true 28 | t.string :name 29 | t.bigint :start 30 | t.bigint :finish 31 | t.integer :duration 32 | t.bigint :allocations 33 | t.json :payload 34 | t.json :backtrace 35 | 36 | t.timestamps 37 | end 38 | 39 | create_table :rmp_flamegraphs, charset: 'utf8' do |t| 40 | t.belongs_to :rmp_profiled_request, null: false, foreign_key: true 41 | t.binary :data 42 | 43 | t.timestamps 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | postgres: 5 | image: postgres:15 6 | environment: 7 | POSTGRES_USER: dummy 8 | POSTGRES_PASSWORD: postgres 9 | volumes: 10 | - postgres-data:/var/lib/postgresql/data 11 | ports: 12 | - 5432:5432 13 | restart: unless-stopped 14 | 15 | volumes: 16 | postgres-data: 17 | 18 | -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/docs/images/overview.png -------------------------------------------------------------------------------- /docs/images/trace-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/docs/images/trace-details.png -------------------------------------------------------------------------------- /docs/images/trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/docs/images/trace.png -------------------------------------------------------------------------------- /gemfiles/rails_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 6.0.0" 6 | 7 | group :development do 8 | gem "annotate", "~> 3.1", github: "ctran/annotate_models" 9 | gem "appraisal", "~> 2.4" 10 | gem "httparty", "~> 0.20.0", require: false 11 | gem "puma", "~> 6.0" 12 | gem "rubyzip", "~> 2.3", require: false 13 | end 14 | 15 | group :test, :development do 16 | gem "activerecord-import", "~> 1.4" 17 | gem "jb", "~> 0.8" 18 | gem "pg", "~> 1.4" 19 | gem "pry", "~> 0.14" 20 | gem "rubocop", "~> 1.36" 21 | gem "sqlite3", "~> 1.5" 22 | gem "stackprof", "~> 0.2" 23 | end 24 | 25 | group :test do 26 | gem "rspec-rails", "~> 5.1" 27 | gem "simplecov", "~> 0.21" 28 | end 29 | 30 | gemspec path: "../" 31 | -------------------------------------------------------------------------------- /gemfiles/rails_6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 6.1.0" 6 | 7 | group :development do 8 | gem "annotate", "~> 3.1", github: "ctran/annotate_models" 9 | gem "appraisal", "~> 2.4" 10 | gem "httparty", "~> 0.20.0", require: false 11 | gem "puma", "~> 6.0" 12 | gem "rubyzip", "~> 2.3", require: false 13 | end 14 | 15 | group :test, :development do 16 | gem "activerecord-import", "~> 1.4" 17 | gem "jb", "~> 0.8" 18 | gem "pg", "~> 1.4" 19 | gem "pry", "~> 0.14" 20 | gem "rubocop", "~> 1.36" 21 | gem "sqlite3", "~> 1.5" 22 | gem "stackprof", "~> 0.2" 23 | end 24 | 25 | group :test do 26 | gem "rspec-rails", "~> 5.1" 27 | gem "simplecov", "~> 0.21" 28 | end 29 | 30 | gemspec path: "../" 31 | -------------------------------------------------------------------------------- /gemfiles/rails_7.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 7.0.0" 6 | 7 | group :development do 8 | gem "annotate", "~> 3.1", github: "ctran/annotate_models" 9 | gem "appraisal", "~> 2.4" 10 | gem "httparty", "~> 0.20.0", require: false 11 | gem "puma", "~> 6.0" 12 | gem "rubyzip", "~> 2.3", require: false 13 | end 14 | 15 | group :test, :development do 16 | gem "activerecord-import", "~> 1.4" 17 | gem "jb", "~> 0.8" 18 | gem "pg", "~> 1.4" 19 | gem "pry", "~> 0.14" 20 | gem "rubocop", "~> 1.36" 21 | gem "sprockets-rails" 22 | gem "sqlite3", "~> 1.5" 23 | gem "stackprof", "~> 0.2" 24 | end 25 | 26 | group :test do 27 | gem "rspec-rails", "~> 5.1" 28 | gem "simplecov", "~> 0.21" 29 | end 30 | 31 | gemspec path: "../" 32 | -------------------------------------------------------------------------------- /lib/generators/rails_mini_profiler/USAGE: -------------------------------------------------------------------------------- 1 | description: 2 | Enable rails-mini-profiler in development for your application. 3 | -------------------------------------------------------------------------------- /lib/generators/rails_mini_profiler/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | # Generators for Rails Mini Profiler 5 | module Generators 6 | # A basic installation generator to help set up users apps 7 | class InstallGenerator < Rails::Generators::Base 8 | source_root File.expand_path('templates', __dir__) 9 | 10 | # Install Rails Mini Profiler to your Rails app 11 | # 12 | # Updates the routes file to mount the engine, adds an initializer and copies a migration. 13 | desc 'Install rails-mini-profiler' 14 | 15 | def install 16 | route("mount RailsMiniProfiler::Engine => '/rails_mini_profiler'") 17 | template 'rails_mini_profiler.rb.erb', 'config/initializers/rails_mini_profiler.rb' 18 | system('rails rails_mini_profiler:install:migrations') 19 | webpacker_install if defined?(Webpacker::Engine) 20 | end 21 | 22 | private 23 | 24 | def webpacker_install 25 | webpacker_config_file = Rails.root.join('config', 'webpacker.yml') 26 | unless File.exist?(webpacker_config_file) 27 | say "Webpacker is not installed. Run 'rails webpacker:install' and rerun installation to complete setup" 28 | return 29 | end 30 | 31 | run 'yarn add @rails-mini-profiler/assets' 32 | webpack_config = YAML.load_file(webpacker_config_file)[Rails.env] 33 | destination = Rails.root.join(webpack_config['source_path'], 34 | webpack_config['source_entry_path'], 35 | 'rails-mini-profiler.js') 36 | template('rails_mini_profiler.js.erb', destination) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/generators/rails_mini_profiler/templates/rails_mini_profiler.js.erb: -------------------------------------------------------------------------------- 1 | import "@rails-mini-profiler/assets" 2 | 3 | // Import styles 4 | import "@rails-mini-profiler/assets/dist/rails-mini-profiler.css"; 5 | 6 | // Import images 7 | // 8 | // You may simplify this when using a glob loader (e.g glob-import-loader) 9 | <%- Dir.chdir(RailsMiniProfiler::Engine.root.join('app/javascript/images')) do -%> 10 | <%- Dir.glob('**/*').each do |file| -%> 11 | import "@rails-mini-profiler/assets/dist/images/<%= file %>"; 12 | <%- end -%> 13 | <%- end -%> 14 | -------------------------------------------------------------------------------- /lib/generators/rails_mini_profiler/templates/rails_mini_profiler.rb.erb: -------------------------------------------------------------------------------- 1 | # Rails Mini Profiler Initializer (<%= RailsMiniProfiler::VERSION %>) 2 | 3 | # Customize to your hearts content. If you remove this file, Rails Mini Profiler will use sensible defaults. 4 | # For more information see https://github.com/hschne/rails-mini-profiler#configuration 5 | RailsMiniProfiler.configure do |config| 6 | # Customize when Rails Mini Profiler should run 7 | config.enabled = proc { |env| Rails.env.development? || env['HTTP_RMP_ENABLED'].present? } 8 | 9 | # Configure Flamegraph generation 10 | config.flamegraph_enabled = true 11 | # config.flamegraph_sample_rate = 0.5 12 | 13 | # Configure endpoints to profile 14 | config.skip_paths = [] 15 | 16 | # Configure how Rails Mini Profiler stores profiling information 17 | # config.storage.database = :rmp_database 18 | # config.storage.profiled_requests_table = :rmp_profiled_requests 19 | # config.storage.traces_table = :rmp_traces 20 | # config.storage.flamegraphs_table = :rmp_flamegraphs 21 | 22 | # Configure the Rails Mini Profiler User Interface 23 | # config.ui.badge_enabled = true 24 | # config.ui.badge_position = 'top-left' 25 | # config.ui.base_controller = ApplicationController 26 | # config.ui.page_size = 25 27 | # config.ui.webpacker_enabled = true 28 | 29 | # Customize how users are detected 30 | config.user_provider = proc { |env| Rack::Request.new(env).ip } 31 | end 32 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | require 'inline_svg' 5 | require 'pagy' 6 | 7 | require 'rails_mini_profiler/version' 8 | require 'rails_mini_profiler/engine' 9 | 10 | require 'rails_mini_profiler/models/base_model' 11 | require 'rails_mini_profiler/tracers' 12 | require 'rails_mini_profiler/configuration' 13 | 14 | require 'rails_mini_profiler/user' 15 | require 'rails_mini_profiler/request_context' 16 | require 'rails_mini_profiler/logger' 17 | require 'rails_mini_profiler/request_wrapper' 18 | require 'rails_mini_profiler/response_wrapper' 19 | require 'rails_mini_profiler/guard' 20 | require 'rails_mini_profiler/flamegraph_guard' 21 | require 'rails_mini_profiler/redirect' 22 | require 'rails_mini_profiler/badge' 23 | require 'rails_mini_profiler/middleware' 24 | 25 | # Main namespace for Rails Mini Profiler 26 | module RailsMiniProfiler 27 | class << self 28 | # Create a new configuration object 29 | # 30 | # @return [Configuration] a new configuration 31 | def configuration 32 | @configuration ||= Configuration.new 33 | end 34 | 35 | # Configure Rails Mini Profiler 36 | # 37 | # You may use this to configure where and how Rails Mini Profiler stores profiling and storage information. 38 | # 39 | # @see https://github.com/hschne/rails-mini-profiler#configuration 40 | # 41 | # @yieldreturn [Configuration] a new configuration 42 | def configure 43 | yield(configuration) 44 | end 45 | 46 | # Access storage configuration. 47 | # 48 | # 49 | # @return [Storage] a new storage configuration 50 | def storage_configuration 51 | configuration.storage 52 | end 53 | 54 | # Access the current logger 55 | # 56 | # @return [Logger] the logger instance 57 | def logger 58 | @logger ||= configuration.logger 59 | end 60 | 61 | # Authorize the current user for this request 62 | # 63 | # @param current_user [Object] the current user 64 | # 65 | # @see User#current_user 66 | def authorize!(current_user) 67 | RailsMiniProfiler::User.current_user = current_user 68 | end 69 | 70 | # Set the current user for this request 71 | # 72 | # @param current_user [Object] the current user 73 | # 74 | # @see User#current_user 75 | def current_user=(current_user) 76 | RailsMiniProfiler::User.current_user = current_user 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_mini_profiler/configuration/storage' 4 | require 'rails_mini_profiler/configuration/user_interface' 5 | 6 | module RailsMiniProfiler 7 | # The main Rails Mini Profiler configuration object 8 | # 9 | # @!attribute [r] logger 10 | # @return [Logger] the current logger 11 | # @!attribute enabled 12 | # @return [Boolean] if the profiler is enabled 13 | # @!attribute flamegraph_enabled 14 | # @return [Boolean] if Flamegraph recording is enabled 15 | # @!attribute flamegraph_sample_rate 16 | # @return [Float] the sample rate in samples per millisecond 17 | # @!attribute skip_paths 18 | # @return [Array] a list of regex patterns for paths to skip 19 | # @!attribute storage 20 | # @return [Storage] the storage configuration 21 | # @!attribute tracers 22 | # @return [Array] the list of enabled tracers 23 | # @!attribute ui 24 | # @return [UserInterface] the ui configuration 25 | # @!attribute user_provider 26 | # @return [Proc] a proc to identify a user based on a rack env 27 | class Configuration 28 | attr_reader :logger 29 | 30 | attr_accessor :enabled, 31 | :flamegraph_enabled, 32 | :flamegraph_sample_rate, 33 | :skip_paths, 34 | :storage, 35 | :tracers, 36 | :ui, 37 | :user_provider 38 | 39 | def initialize(**kwargs) 40 | reset 41 | kwargs.each { |key, value| instance_variable_set("@#{key}", value) } 42 | end 43 | 44 | # Reset the configuration to default values 45 | def reset 46 | @enabled = proc { |_env| Rails.env.development? || Rails.env.test? } 47 | @flamegraph_enabled = true 48 | @flamegraph_sample_rate = 0.5 49 | @logger = RailsMiniProfiler::Logger.new(Rails.logger) 50 | @skip_paths = [] 51 | @storage = Storage.new 52 | @tracers = %i[controller instantiation sequel view rmp] 53 | @ui = UserInterface.new 54 | @user_provider = proc { |env| Rack::Request.new(env).ip } 55 | end 56 | 57 | # Set the logger 58 | # 59 | # @param logger [Logger] 60 | # The logger to be used. If set to nil, the Rails default logger is used and the log level set to fatal 61 | def logger=(logger) 62 | if logger.nil? 63 | @logger.level = Logger::FATAL 64 | else 65 | @logger = logger 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/configuration/storage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | # Configure how profiling data is stored within your Rails app. 5 | # 6 | # @!attribute database 7 | # @return [Symbol] which database to connect to 8 | # @!attribute profiled_requests_table 9 | # @return [Symbol] where to store profiled requests 10 | # @!attribute traces_table 11 | # @return [Symbol] where to store traces 12 | # @!attribute flamegraphs_table 13 | # @return [Symbol] where to store flamegraphs 14 | class Storage 15 | class << self 16 | # Construct a new configuration instance 17 | # 18 | # @return [Storage] a new storage configuration 19 | def configuration 20 | @configuration ||= new 21 | end 22 | 23 | # Configure how profiling data is stored 24 | # 25 | # @yieldreturn [Storage] a new storage configuration object 26 | def configure 27 | yield(configuration) 28 | configuration 29 | end 30 | end 31 | 32 | attr_accessor :database, :profiled_requests_table, :traces_table, :flamegraphs_table 33 | 34 | def initialize(**kwargs) 35 | defaults! 36 | kwargs.each { |key, value| instance_variable_set("@#{key}", value) } 37 | end 38 | 39 | # Reset the configuration to default values 40 | def defaults! 41 | @database = nil 42 | @profiled_requests_table = 'rmp_profiled_requests' 43 | @flamegraphs_table = 'rmp_flamegraphs' 44 | @traces_table = 'rmp_traces' 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | # The Rails Mini Profiler engine 5 | # 6 | # Injects a a custom [Middleware] into an existing Rails app to record request profiling information. 7 | class Engine < ::Rails::Engine 8 | isolate_namespace RailsMiniProfiler 9 | 10 | initializer 'rails_mini_profiler.add_middleware' do |app| 11 | app.middleware.use(RailsMiniProfiler::Middleware) 12 | end 13 | 14 | config.generators do |g| 15 | g.test_framework :rspec 16 | end 17 | 18 | initializer 'rails_mini_profiler_add_static assets' do |app| 19 | app.middleware.insert_before(ActionDispatch::Static, ActionDispatch::Static, "#{root}/public") 20 | end 21 | 22 | # If sprockets is not being used then there is no need to hook into asset compilation. Calling config.assets 23 | # without Sprockets installed breaks compilation. 24 | if defined?(Sprockets::Rails) 25 | initializer 'rails_mini_profiler.assets.precompile', group: :all do |app| 26 | app.config.assets.precompile += %w[ 27 | rails_mini_profiler.js 28 | rails_mini_profiler/application.css 29 | vendor/assets/images 30 | ] 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/flamegraph_guard.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class FlamegraphGuard 5 | def initialize(request_context, configuration: RailsMiniProfiler.configuration) 6 | @request_context = request_context 7 | @request = request_context.request 8 | @configuration = configuration 9 | end 10 | 11 | def record(&block) 12 | return block.call unless enabled? 13 | 14 | if StackProf.running? 15 | RailsMiniProfiler.logger.error('Stackprof is already running, cannot record Flamegraph') 16 | return block.call 17 | end 18 | 19 | flamegraph, result = record_flamegraph(block) 20 | unless flamegraph 21 | RailsMiniProfiler.logger.error('Failed to record Flamegraph, possibly due to concurrent requests') 22 | return result 23 | end 24 | 25 | @request_context.flamegraph = flamegraph.to_json 26 | result 27 | end 28 | 29 | private 30 | 31 | def record_flamegraph(block) 32 | sample_rate = @configuration.flamegraph_sample_rate 33 | result = nil 34 | flamegraph = StackProf.run(mode: :wall, raw: true, aggregate: false, interval: (sample_rate * 1000).to_i) do 35 | result = block.call 36 | end 37 | [flamegraph, result] 38 | end 39 | 40 | def enabled? 41 | defined?(StackProf) && StackProf.respond_to?(:run) && config_enabled? 42 | end 43 | 44 | def config_enabled? 45 | params = CGI.parse(@request.query_string).transform_values(&:first).with_indifferent_access 46 | return params[:rmp_flamegraph] if params[:rmp_flamegraph] 47 | 48 | RailsMiniProfiler.configuration.flamegraph_enabled 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/guard.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | # Encapsulates guard conditions on whether or not to run certain parts of the profiler. 5 | class Guard 6 | # @param request_context [RequestContext] the current request context 7 | # @param configuration [Configuration] the current configuration 8 | def initialize(request_context, configuration: RailsMiniProfiler.configuration) 9 | @request_context = request_context 10 | @request = request_context.request 11 | @configuration = configuration 12 | end 13 | 14 | # Whether or not to profile 15 | # 16 | # Profiling is disabled the profiler has been flat out disabled in the configuration or if the current request path 17 | # matches on of the ignored paths. 18 | # 19 | # @return [Boolean] false if no profiling should be done 20 | def profile? 21 | return false unless enabled? 22 | 23 | return false if ignored_path? 24 | 25 | true 26 | end 27 | 28 | private 29 | 30 | # Is the path of the current request an ignored one? 31 | # 32 | # @return [Boolean] true if the path is ignored. Per default, paths going to the engine itself are ignored, as are 33 | # asset requests, and the paths the user has configured. 34 | def ignored_path? 35 | return true if /#{Engine.routes.find_script_name({})}/.match?(@request.path) 36 | 37 | return true if asset_path? 38 | 39 | return true if actioncable_request? 40 | 41 | ignored_paths = @configuration.skip_paths 42 | return true if Regexp.union(ignored_paths).match?(@request.path) 43 | 44 | false 45 | end 46 | 47 | # Is the current request an asset request, e.g. to webpacker packs or assets? 48 | # 49 | # @return [Boolean] if the request path matches packs or assets 50 | def asset_path? 51 | %r{^/packs}.match?(@request.path) || %r{^/assets}.match?(@request.path) 52 | end 53 | 54 | # Is the current request an actioncable ping 55 | # 56 | # @return [Boolean] if the request path matches the mount path of actioncable 57 | def actioncable_request? 58 | return false unless defined?(ActionCable) 59 | 60 | /#{ActionCable.server.config.mount_path}/.match?(@request.path) 61 | end 62 | 63 | # Is the profiler enabled? 64 | # 65 | # Takes into account the current request env to decide if the profiler is enabled. 66 | # 67 | # @return [Boolean] false if the profiler is disabled 68 | def enabled? 69 | enabled = @configuration.enabled 70 | return enabled unless enabled.respond_to?(:call) 71 | 72 | enabled.call(@request.env) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | # Construct a new custom logger to log from within the engine 5 | module Logger 6 | # Extends a logger with additional formatting 7 | # 8 | # @return [Logger] a customized logger 9 | def self.new(logger) 10 | logger = logger.dup 11 | 12 | logger.formatter = logger.formatter ? logger.formatter.dup : ActiveSupport::Logger::SimpleFormatter.new 13 | 14 | logger.formatter.extend Formatter 15 | logger.extend(self) 16 | end 17 | 18 | # Custom formatter to add a RailsMiniProfiler tag to log messages 19 | module Formatter 20 | def call(severity, timestamp, progname, msg) 21 | super(severity, timestamp, progname, "[RailsMiniProfiler] #{msg}") 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class Middleware 5 | def initialize(app) 6 | @app = app 7 | @config = RailsMiniProfiler.configuration 8 | @registry = Tracers::Registry.setup!(@config) 9 | Tracers::Subscriptions.setup!(@registry.tracers.keys) { |trace| track_trace(trace) } 10 | @trace_factory = Tracers::TraceFactory.new(@registry) 11 | end 12 | 13 | def call(env) 14 | request = RequestWrapper.new(env) 15 | request_context = RequestContext.new(request) 16 | return @app.call(env) unless Guard.new(request_context).profile? 17 | 18 | result = with_tracing(request_context) { profile(request_context) } 19 | return result unless request_context.authorized? 20 | 21 | status, headers, body = result 22 | request_context.response = ResponseWrapper.new(body, status, headers) 23 | complete!(request_context) 24 | request_context.saved? ? render_response(request_context).to_a : result 25 | ensure 26 | User.current_user = nil 27 | end 28 | 29 | def traces 30 | Thread.current[:rails_mini_profiler_traces] 31 | end 32 | 33 | def traces=(traces) 34 | Thread.current[:rails_mini_profiler_traces] = traces 35 | end 36 | 37 | def track_trace(event) 38 | return if traces.nil? 39 | 40 | trace = @trace_factory.create(event) 41 | traces.append(trace) unless trace.is_a?(RailsMiniProfiler::Tracers::NullTrace) 42 | end 43 | 44 | private 45 | 46 | def complete!(request_context) 47 | request_context.save_results! 48 | true 49 | rescue ActiveRecord::ActiveRecordError => e 50 | RailsMiniProfiler.logger.error("Could not save profile: #{e}") 51 | false 52 | end 53 | 54 | def render_response(request_context) 55 | redirect = Redirect.new(request_context).render 56 | return redirect if redirect 57 | 58 | Badge.new(request_context).render 59 | end 60 | 61 | def profile(request_context) 62 | ActiveSupport::Notifications.instrument('rails_mini_profiler.total_time') do 63 | request = request_context.request 64 | FlamegraphGuard.new(request_context).record { @app.call(request.env) } 65 | end 66 | end 67 | 68 | def with_tracing(request_context) 69 | self.traces = [] 70 | result = yield 71 | request_context.traces = traces 72 | self.traces = nil 73 | result 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/models/base_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | # Thin wrappers around request/response classes 5 | # 6 | # @api private 7 | module Models 8 | # A pseudo model to be used to wrap profiling information. We can't use regular models, as their connecting 9 | # to the database results in problems when profiling. 10 | class BaseModel 11 | include ActiveModel::Model 12 | 13 | def initialize(*_args, **attributes) 14 | super(attributes) 15 | end 16 | 17 | def to_h 18 | instance_variables 19 | .each_with_object({}) { |var, hash| hash[var.to_s.delete('@')] = instance_variable_get(var) } 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/redirect.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | # Renders a redirect response if the user should be redirected from the original request 5 | class Redirect 6 | include Engine.routes.url_helpers 7 | 8 | # @param request_context [RequestContext] the current request context 9 | def initialize(request_context) 10 | @request = request_context.request 11 | @profiled_request = request_context.profiled_request 12 | end 13 | 14 | # Renders a redirect to a specific resource under certain conditions 15 | # 16 | # When the user requests a Flamegraph using a parameter for a specific request, they are being served a redirect. 17 | # 18 | # @return [Boolean] false if no redirect happens 19 | # @return [Array] response with status 302 and the new location to redirect to 20 | def render 21 | params = CGI.parse(@request.query_string).transform_values(&:first).with_indifferent_access 22 | return redirect_to(flamegraph_path(@profiled_request.id)) if params[:rmp_flamegraph].present? 23 | 24 | false 25 | end 26 | 27 | private 28 | 29 | def redirect_to(location) 30 | [302, { 'Location' => location, 'Content-Type' => 'text/html' }, ['Moved Temporarily']] 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/request_wrapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | # A convenience wrapper extending {Rack::Request} 5 | # 6 | # @api private 7 | class RequestWrapper < Rack::Request 8 | # Convenience method to read the request body as String 9 | # 10 | # @return [String] the request body 11 | def body 12 | return '' unless super 13 | 14 | body = super.read 15 | super.rewind 16 | body 17 | end 18 | 19 | # The request headers 20 | # 21 | # @return [Hash] the request headers 22 | def headers 23 | env.select { |k, _v| k.start_with? 'HTTP_' } || {} 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/response_wrapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | # A convenience wrapper extending {Rack::Response} 5 | # 6 | # @api private 7 | class ResponseWrapper < Rack::Response 8 | # Return the response body as String 9 | # 10 | # Depending on preceding middleware, response bodies may be Strings, Arrays or literally anything else. This method 11 | # converts whatever it is to a string so we can store it later. 12 | # 13 | # @return [String] of the response body 14 | def body 15 | body = super 16 | case body 17 | when String 18 | body 19 | when Array 20 | body.join 21 | when ActionDispatch::Response::RackBody 22 | body.body 23 | else 24 | '' 25 | end 26 | end 27 | 28 | def json? 29 | media_type =~ %r{application/json} 30 | end 31 | 32 | def xml? 33 | media_type =~ %r{application/xml} 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_mini_profiler/tracers/registry' 4 | require 'rails_mini_profiler/tracers/subscriptions' 5 | require 'rails_mini_profiler/tracers/trace' 6 | require 'rails_mini_profiler/tracers/tracer' 7 | require 'rails_mini_profiler/tracers/controller_tracer' 8 | require 'rails_mini_profiler/tracers/instantiation_tracer' 9 | require 'rails_mini_profiler/tracers/sequel_tracker' 10 | require 'rails_mini_profiler/tracers/sequel_tracer' 11 | require 'rails_mini_profiler/tracers/view_tracer' 12 | require 'rails_mini_profiler/tracers/rmp_tracer' 13 | require 'rails_mini_profiler/tracers/null_trace' 14 | require 'rails_mini_profiler/tracers/trace_factory' 15 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/controller_tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | class ControllerTracer < Tracer 6 | class << self 7 | def subscribes_to 8 | 'process_action.action_controller' 9 | end 10 | 11 | def build_from(event) 12 | new(event).trace 13 | end 14 | 15 | def presents 16 | ControllerTracePresenter 17 | end 18 | end 19 | 20 | def trace 21 | @event[:payload] = @event[:payload] 22 | .slice(:view_runtime, :db_runtime) 23 | .reject { |_k, v| v.blank? } 24 | .transform_values { |value| value&.round(2) } 25 | super 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/instantiation_tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | class InstantiationTracer < Tracer 6 | class << self 7 | def subscribes_to 8 | 'instantiation.active_record' 9 | end 10 | 11 | def presents 12 | InstantiationTracePresenter 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/null_trace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | class NullTrace < Trace; end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | # This serves as a central store for tracers. Based on the configuration, indidual tracers are registered and are 6 | # then available to subscribe to events or render traced events. 7 | # 8 | # @api private 9 | class Registry 10 | class << self 11 | def setup!(config) 12 | new(config) 13 | end 14 | end 15 | 16 | def initialize(config) 17 | @config = config 18 | @config.tracers.each { |tracer| add(tracer) } 19 | end 20 | 21 | # Tracers that are available in the application, indexed by events they subscribe to. 22 | # 23 | # @return [Hash] a hash where keys are event names and values are the corresponding tracers 24 | def tracers 25 | @tracers ||= 26 | tracer_map 27 | .values 28 | .each_with_object({}) do |tracer, obj| 29 | subscriptions = wrap(tracer.subscribes_to) 30 | subscriptions.each { |subscription| obj[subscription] = tracer } 31 | end 32 | end 33 | 34 | # Presenters that are available in the application, indexed by events they should present. 35 | # 36 | # @return [Hash] a hash where keys are event names and values are the corresponding presenters 37 | def presenters 38 | @presenters ||= 39 | tracer_map 40 | .values 41 | .each_with_object({}) do |tracer, obj| 42 | presenters = tracer.presents 43 | if presenters.is_a?(Hash) 44 | obj.merge!(presenters) 45 | else 46 | obj[tracer.subscribes_to] = presenters 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def tracer_map 54 | @tracer_map ||= {} 55 | end 56 | 57 | def add(tracer) 58 | tracer = tracer.to_sym 59 | tracer = "#{tracer.to_s.camelize}Tracer" 60 | constant = "RailsMiniProfiler::Tracers::#{tracer}".safe_constantize 61 | 62 | tracer_map[tracer] = constant if constant 63 | end 64 | 65 | def wrap(object) 66 | if object.nil? 67 | [] 68 | elsif object.respond_to?(:to_ary) 69 | object.to_ary || [object] 70 | else 71 | [object] 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/rmp_tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | class RmpTracer < Tracer 6 | class << self 7 | def subscribes_to 8 | 'rails_mini_profiler.total_time' 9 | end 10 | 11 | def presents 12 | RmpTracePresenter 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/sequel_tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | class SequelTracer < Tracer 6 | class << self 7 | def subscribes_to 8 | 'sql.active_record' 9 | end 10 | 11 | def build_from(event) 12 | new(event).trace 13 | end 14 | 15 | def presents 16 | SequelTracePresenter 17 | end 18 | end 19 | 20 | def trace 21 | return NullTrace.new if ignore? 22 | 23 | payload = @event[:payload].slice(:name, :sql, :binds, :type_casted_binds) 24 | typecasted_binds = payload[:type_casted_binds] 25 | # Sometimes, typecasted binds are a proc. Not sure why. In those instances, we extract the typecasted 26 | # values from the proc by executing call. 27 | typecasted_binds = typecasted_binds.call if typecasted_binds.respond_to?(:call) 28 | payload[:binds] = transform_binds(payload[:binds], typecasted_binds) 29 | payload.delete(:type_casted_binds) 30 | payload.reject { |_k, v| v.blank? } 31 | @event[:payload] = payload 32 | super 33 | end 34 | 35 | private 36 | 37 | def transform_binds(binds, type_casted_binds) 38 | binds.each_with_object([]).with_index do |(binding, object), i| 39 | name = binding.name 40 | value = type_casted_binds[i] 41 | object << { name: name, value: value } 42 | end 43 | end 44 | 45 | def ignore? 46 | payload = @event[:payload] 47 | !SqlTracker.new(name: payload[:name], query: payload[:sql]).track? 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/sequel_tracker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | # Utility to clean up and simplify SQL Notification events. 6 | # 7 | # @api private 8 | class SqlTracker 9 | TRACKED_SQL_COMMANDS = %w[SELECT INSERT UPDATE DELETE].freeze 10 | UNTRACKED_NAMES = %w[SCHEMA].freeze 11 | UNTRACKED_TABLES = %w[ 12 | SCHEMA_MIGRATIONS 13 | SQLITE_MASTER 14 | ACTIVE_MONITORING_METRICS 15 | SQLITE_TEMP_MASTER 16 | SQLITE_VERSION 17 | AR_INTERNAL_METADATA 18 | ].freeze 19 | 20 | def initialize(query:, name:) 21 | @query = query.to_s.upcase 22 | @name = name.to_s.upcase 23 | end 24 | 25 | def track? 26 | query.start_with?(*TRACKED_SQL_COMMANDS) && 27 | !name.start_with?(*UNTRACKED_NAMES) && 28 | !untracked_tables? 29 | end 30 | 31 | private 32 | 33 | attr_reader :query, :name 34 | 35 | def untracked_tables? 36 | UNTRACKED_TABLES.any? { |table| query.include?(table) } 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/subscriptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | # Subscribe to application events. This is used during engine startup. 6 | # @api private 7 | class Subscriptions 8 | class << self 9 | # Subscribe to each individual active support event using a callback. 10 | def setup!(subscriptions, &callback) 11 | subscriptions.each do |event| 12 | subscribe(event, &callback) 13 | end 14 | end 15 | 16 | private 17 | 18 | def subscribe(*subscriptions, &callback) 19 | subscriptions.each do |subscription| 20 | ActiveSupport::Notifications.subscribe(subscription) do |event| 21 | callback.call(event) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/trace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | # A simplified representation of a trace. 6 | # 7 | # Is transformed into [RailsMiniProfiler::Trace] when recording has finished. 8 | # 9 | # @see https://guides.rubyonrails.org/active_support_instrumentation.html 10 | # 11 | # @!attribute id 12 | # @return [Integer] the trace ID 13 | # @!attribute name 14 | # @return [Integer] the trace type. 15 | # @!attribute start 16 | # @return [Integer] the trace start as microsecond timestamp 17 | # @!attribute finish 18 | # @return [Integer] the trace finish as microsecond timestamp 19 | # @!attribute duration 20 | # @return [Integer] the trace duration 21 | # @!attribute payload 22 | # @return [Hash] a subset of trace data 23 | # @!attribute backtrace 24 | # @return [String] the line where this trace was recorded 25 | # @!attribute allocations 26 | # @return [Integer] the number of allocations 27 | # @!attribute created_at 28 | # @return [DateTime] the creation date 29 | # @!attribute updated_at 30 | # @return [DateTime] the last updated date 31 | # 32 | # @api private 33 | class Trace < RailsMiniProfiler::Models::BaseModel 34 | attr_accessor :id, :name, :start, :finish, :duration, :payload, :backtrace, :allocations, :created_at, :updated_at 35 | 36 | def ignore? 37 | false 38 | end 39 | 40 | def transform! 41 | self 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/trace_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | # Creates traces based on the tracers registered with the application 6 | # 7 | # For example, an event with name 'sequel' will result in the lookup of a tracers that responds to that particular 8 | # event. The tracer itself will then build the trace from the event. 9 | # 10 | # @api private 11 | class TraceFactory 12 | def initialize(registry) 13 | @tracers = registry.tracers 14 | end 15 | 16 | # Create a new trace from an event 17 | # 18 | # @param event [ActiveSupport::Notifications::Event] an event from the application 19 | # 20 | # @return [Trace] a processed trace 21 | def create(event) 22 | tracer = @tracers[event.name] || Tracer 23 | tracer.build_from(event) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | class Tracer 6 | TIMESTAMP_MULTIPLIER = Rails::VERSION::MAJOR < 7 ? 100 : 1 7 | 8 | class << self 9 | def subscribes_to 10 | [] 11 | end 12 | 13 | def presents 14 | TracePresenter 15 | end 16 | 17 | def build_from(event) 18 | new(event).trace 19 | end 20 | end 21 | 22 | def initialize(event) 23 | @event = event_data(event) 24 | end 25 | 26 | def trace 27 | Trace.new(**@event) 28 | end 29 | 30 | private 31 | 32 | def event_data(event) 33 | # Rails 7 changes event timings and now uses CPU milliseconds as float for start and end. We multiply by 100 34 | # to convert the float with precision of 2 digits to integer, because integers are just easier to store and 35 | # process than floats. 36 | # 37 | # See https://github.com/rails/rails/commit/81d0dc90becfe0b8e7f7f26beb66c25d84b8ec7f 38 | start = (event.time.to_f * TIMESTAMP_MULTIPLIER).to_i 39 | finish = (event.end.to_f * TIMESTAMP_MULTIPLIER).to_i 40 | { 41 | name: event.name, 42 | start: start, 43 | finish: finish, 44 | duration: (event.duration.to_f * 100).to_i, 45 | allocations: event.allocations, 46 | backtrace: Rails.backtrace_cleaner.clean(caller), 47 | payload: event.payload 48 | } 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/tracers/view_tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | module Tracers 5 | class ViewTracer < Tracer 6 | class << self 7 | def subscribes_to 8 | %w[render_template.action_view render_partial.action_view] 9 | end 10 | 11 | def build_from(event) 12 | new(event).trace 13 | end 14 | 15 | def presents 16 | { 17 | 'render_template.action_view' => RenderTemplateTracePresenter, 18 | 'render_partial.action_view' => RenderPartialTracePresenter 19 | } 20 | end 21 | end 22 | 23 | def trace 24 | @event[:payload].slice!(:identifier, :count) 25 | super 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | class User 5 | class << self 6 | def current_user 7 | Thread.current[:rails_mini_profiler_current_user] 8 | end 9 | 10 | def get(env) 11 | new(Thread.current[:rails_mini_profiler_current_user], env).current_user 12 | end 13 | 14 | def authorize(user) 15 | Thread.current[:rails_mini_profiler_current_user] = user 16 | end 17 | 18 | def current_user=(user) 19 | Thread.current[:rails_mini_profiler_current_user] = user 20 | end 21 | end 22 | 23 | def initialize(current_user, env) 24 | @current_user = current_user 25 | @env = env 26 | end 27 | 28 | def current_user 29 | @current_user ||= find_current_user 30 | end 31 | 32 | def find_current_user 33 | return if Rails.env.production? 34 | 35 | user = RailsMiniProfiler.configuration.user_provider.call(@env) 36 | User.current_user = user 37 | user 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/rails_mini_profiler/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RailsMiniProfiler 4 | VERSION = '0.7.3' 5 | end 6 | -------------------------------------------------------------------------------- /lib/tasks/rails_mini_profiler_tasks.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :rails_mini_profiler do 4 | desc 'Install rails_mini_profiler' 5 | task :install do 6 | system('rails g rails_mini_profiler:install install') 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rails-mini-profiler/assets", 3 | "version": "0.7.3", 4 | "description": "Performance profiling for your Rails app, made simple", 5 | "main": "dist/rails-mini-profiler.js", 6 | "files": [ 7 | "dist/**/*" 8 | ], 9 | "scripts": { 10 | "test": "jest", 11 | "lint": "eslint --ignore-path .gitignore .", 12 | "lint:scss": "stylelint --ignore-path .gitignore **/*.scss", 13 | "lint:commit": "commitlint --from main", 14 | "watch": "browser-sync start --proxy localhost:3000 --files 'app/javascript/**/*,app/views/**/*' | rollup -c -w", 15 | "build": "NODE_ENV=production rollup -c" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/hschne/rails-mini-profiler.git" 20 | }, 21 | "author": "Hans Schnedlitz", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/hschne/rails-mini-profiler/issues" 25 | }, 26 | "homepage": "https://github.com/hschne/rails-mini-profiler#readme", 27 | "devDependencies": { 28 | "@babel/core": "^7.15.0", 29 | "@babel/eslint-parser": "^7.15.0", 30 | "@babel/preset-env": "^7.15.0", 31 | "@commitlint/cli": "^16.1.0", 32 | "@commitlint/config-conventional": "^16.0.0", 33 | "@rollup/plugin-babel": "^5.3.0", 34 | "@rollup/plugin-commonjs": "^20.0.0", 35 | "@rollup/plugin-eslint": "^8.0.1", 36 | "@rollup/plugin-node-resolve": "^13.0.4", 37 | "@rollup/plugin-replace": "^3.0.0", 38 | "autoprefixer": "^10.3.1", 39 | "browser-sync": "^3.0.2", 40 | "eslint": "^7.32.0", 41 | "eslint-config-prettier": "^8.3.0", 42 | "less": "^4.1.1", 43 | "node-sass": "^7.0.3", 44 | "postcss": "^8.3.6", 45 | "prettier": "2.3.2", 46 | "rollup": "^2.56.1", 47 | "rollup-plugin-copy": "^3.4.0", 48 | "rollup-plugin-postcss": "^4.0.0", 49 | "rollup-plugin-scss": "^3.0.0", 50 | "rollup-plugin-terser": "^7.0.2", 51 | "sass": "^1.37.5", 52 | "stylelint": "^13.13.1", 53 | "stylelint-config-idiomatic-order": "^8.1.0", 54 | "stylelint-config-standard": "^22.0.0", 55 | "stylelint-order": "^4.1.0", 56 | "stylelint-prettier": "^1.2.0", 57 | "stylelint-scss": "^3.20.1" 58 | }, 59 | "dependencies": { 60 | "@hotwired/stimulus": "^3.0.0", 61 | "tailwindcss-stimulus-components": "^3.0.0", 62 | "tippy.js": "^6.3.1" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/rails_mini_profiler/speedscope/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jamie Wong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/rails_mini_profiler/speedscope/favicon-16x16.f74b3187.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/public/rails_mini_profiler/speedscope/favicon-16x16.f74b3187.png -------------------------------------------------------------------------------- /public/rails_mini_profiler/speedscope/favicon-32x32.bc503437.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/public/rails_mini_profiler/speedscope/favicon-32x32.bc503437.png -------------------------------------------------------------------------------- /public/rails_mini_profiler/speedscope/index.html: -------------------------------------------------------------------------------- 1 | speedscope 2 | -------------------------------------------------------------------------------- /public/rails_mini_profiler/speedscope/release.txt: -------------------------------------------------------------------------------- 1 | speedscope@1.15.0 2 | Sat Oct 22 00:19:31 HKT 2022 3 | 81a6f29ad1eb632683429084bd6c5f497667fb5e 4 | -------------------------------------------------------------------------------- /public/rails_mini_profiler/speedscope/reset.8c46b7a1.css: -------------------------------------------------------------------------------- 1 | a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:initial}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{overflow:hidden}body,html{height:100%}body{overflow:auto} 2 | /*# sourceMappingURL=reset.8c46b7a1.css.map */ -------------------------------------------------------------------------------- /public/rails_mini_profiler/speedscope/reset.8c46b7a1.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["reset.css"],"names":[],"mappings":"AAIA,2ZAaC,QAAS,CACT,SAAU,CACV,QAAS,CACT,cAAe,CACf,YAAa,CACb,sBACD,CAEA,8EAEC,aACD,CACA,KACC,aACD,CACA,MACC,eACD,CACA,aACC,WACD,CACA,oDAEC,UAAW,CACX,YACD,CACA,MACC,wBAAyB,CACzB,gBACD,CAIA,KACI,eAEJ,CACA,UAFI,WAKJ,CAHA,KAEI,aACJ","file":"reset.8c46b7a1.css","sourceRoot":"../../assets","sourcesContent":["/* http://meyerweb.com/eric/tools/css/reset/\n v2.0 | 20110126\n License: none (public domain)\n*/\nhtml, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed,\nfigure, figcaption, footer, header, hgroup,\nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tfont-size: 100%;\n\tfont: inherit;\n\tvertical-align: baseline;\n}\n/* HTML5 display-role reset for older browsers */\narticle, aside, details, figcaption, figure,\nfooter, header, hgroup, menu, nav, section {\n\tdisplay: block;\n}\nbody {\n\tline-height: 1;\n}\nol, ul {\n\tlist-style: none;\n}\nblockquote, q {\n\tquotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n\tcontent: '';\n\tcontent: none;\n}\ntable {\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}\n\n/* Prevent overscrolling */\n/* https://stackoverflow.com/a/17899813 */\nhtml {\n overflow: hidden;\n height: 100%;\n}\nbody {\n height: 100%;\n overflow: auto;\n}"]} -------------------------------------------------------------------------------- /rails_mini_profiler.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/rails_mini_profiler/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'rails_mini_profiler' 7 | spec.version = RailsMiniProfiler::VERSION 8 | spec.authors = ['hschne'] 9 | spec.email = ['hans.schnedlitz@gmail.com'] 10 | 11 | spec.summary = 'Performance profiling for your Rails app, made simple' 12 | spec.description = 'Performance profiling for your Rails app, made simple' 13 | spec.homepage = 'https://github.com/hschne/rails-mini-profiler' 14 | spec.license = 'MIT' 15 | 16 | spec.metadata = { 17 | 'bug_tracker_uri' => "#{spec.homepage}/issues", 18 | 'changelog_uri' => "#{spec.homepage}/blob/main/CHANGELOG.md", 19 | 'documentation_uri' => spec.homepage.to_s, 20 | 'homepage_uri' => spec.homepage.to_s, 21 | 'source_code_uri' => spec.homepage.to_s, 22 | 'rubygems_mfa_required' => 'true' 23 | } 24 | 25 | spec.files = Dir['{app,config,db,lib,public}/**/*', 'vendor/assets/**/*', 'LICENSE', 'README.md'] 26 | 27 | spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0') 28 | 29 | spec.add_dependency 'inline_svg', '~> 1.9' 30 | spec.add_dependency 'jb', '~> 0.8' 31 | spec.add_dependency 'pagy', '>= 4.11' 32 | spec.add_dependency 'rails', '>= 6.0' 33 | end 34 | -------------------------------------------------------------------------------- /rakelib/speedscope.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :speedscope do 4 | desc 'Update Speedscope to the latest version' 5 | task :update do 6 | require_relative 'util/speedscope' 7 | 8 | Speedscope.update($stdout) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /rakelib/util/speedscope.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'httparty' 4 | require 'zip' 5 | 6 | class Speedscope 7 | REPOSITORY = 'jlfwong/speedscope' 8 | 9 | BASE_DIRECTORY = File.expand_path('../../public/rails_mini_profiler', File.dirname(__FILE__)) 10 | 11 | def self.update(stdout) 12 | new(stdout).update 13 | rescue StandardError => e 14 | stdout.puts("Error: #{e.message}") 15 | end 16 | 17 | def initialize(stdout) 18 | @stdout = stdout 19 | end 20 | 21 | def update 22 | return msg('No new version available') unless new_version_available? 23 | 24 | url = "https://github.com/#{REPOSITORY}/releases/download/v#{latest_version}/speedscope-#{latest_version}.zip" 25 | msg('Downloading latest...') 26 | body = HTTParty.get(url, follow_redirects: true).body 27 | msg('Overwriting existing files...') 28 | FileUtils.rm_f(Dir.glob(File.join(BASE_DIRECTORY, 'speedscope', '*'))) 29 | overwrite_speedscope(body) 30 | msg('Done!') 31 | end 32 | 33 | private 34 | 35 | def msg(*args) 36 | @stdout.puts(*args) 37 | end 38 | 39 | def new_version_available? 40 | latest_version != current_version 41 | end 42 | 43 | def latest_version 44 | @latest_version ||= latest_release[:name].sub('v', '') 45 | end 46 | 47 | def current_version 48 | @current_version ||= File.read(File.join(BASE_DIRECTORY, 'speedscope', 'release.txt')).split("\n")[0].split('@')[-1] 49 | end 50 | 51 | def latest_release 52 | @latest_release ||= 53 | begin 54 | response = HTTParty.get('https://api.github.com/repos/jlfwong/speedscope/releases/latest', format: :plain) 55 | JSON.parse(response, symbolize_names: true) 56 | end 57 | end 58 | 59 | def overwrite_speedscope(body) 60 | exclude_regex = /#{Regexp.union(%w[perf-vertx-stacks README])}/i 61 | Zip::File.open_buffer(StringIO.new(body)) do |zip_file| 62 | zip_file.each do |entry| 63 | next if entry.name =~ exclude_regex 64 | 65 | dest_path = File.join(BASE_DIRECTORY, entry.name) 66 | entry.extract(dest_path) { true } 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import eslint from '@rollup/plugin-eslint' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import commonjs from '@rollup/plugin-commonjs' 5 | import replace from '@rollup/plugin-replace' 6 | import { terser } from 'rollup-plugin-terser' 7 | import scss from 'rollup-plugin-scss' 8 | import postcss from 'rollup-plugin-postcss' 9 | import autoprefixer from 'autoprefixer' 10 | import copy from 'rollup-plugin-copy' 11 | 12 | import pkg from './package.json' 13 | 14 | const banner = 15 | `/* 16 | * Rails Mini Profiler 17 | * ${pkg.description} 18 | * ${pkg.repository.url} 19 | * v${pkg.version} 20 | * ${pkg.license} License 21 | */ 22 | ` 23 | 24 | const plugins = [ 25 | resolve(), 26 | commonjs(), 27 | eslint({ 28 | fix: true, 29 | exclude: ['./node_modules/**', './app/javascript/stylesheets/**'], 30 | }), 31 | babel({ 32 | exclude: 'node_modules/**', 33 | babelHelpers: 'bundled' 34 | }), 35 | replace({ 36 | 'ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 37 | 'process.env.NODE_ENV': JSON.stringify('production'), 38 | preventAssignment: true 39 | }), 40 | (process.env.NODE_ENV === 'production' && terser()), 41 | scss({ 42 | outputStyle: (process.env.NODE_ENV === 'production') ? 'compressed' : 'expanded', 43 | }), 44 | postcss({ 45 | plugins: [autoprefixer()], 46 | inject: false, 47 | extract: true, 48 | sourceMap: (process.env.NODE_ENV === 'production' ? false : 'inline'), 49 | minimize: (process.env.NODE_ENV === 'production') 50 | }), 51 | copy({ 52 | targets: [ 53 | { src: './app/javascript/images/**/*', dest: './dist/images' }, 54 | { src: './app/javascript/images/**/*', dest: './vendor/assets/images' } 55 | ] 56 | }) 57 | 58 | ] 59 | 60 | const input = 'app/javascript/packs/rails-mini-profiler.js' 61 | 62 | export default [ 63 | { 64 | input: input, 65 | context: 'window', 66 | output: { 67 | file: pkg.main, 68 | format: 'umd', 69 | name: 'RailsMiniProfiler', 70 | sourcemap: (process.env.NODE_ENV === 'production' ? false : 'inline'), 71 | banner: banner, 72 | }, 73 | plugins: plugins 74 | }, 75 | { 76 | input: input, 77 | context: 'window', 78 | output: { 79 | file: "vendor/assets/javascripts/rails-mini-profiler.js", 80 | format: 'umd', 81 | name: 'RailsMiniProfiler', 82 | sourcemap: (process.env.NODE_ENV === 'production' ? false : 'inline'), 83 | banner: banner, 84 | }, 85 | plugins: plugins 86 | } 87 | ] 88 | -------------------------------------------------------------------------------- /spec/adapters/rails_mini_profiler/database_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | RSpec.describe DatabaseAdapter, type: :model do 7 | describe 'cast column' do 8 | context 'default' do 9 | it 'returns original column' do 10 | expect(described_class.cast_to_text(:column)).to equal(:column) 11 | end 12 | end 13 | 14 | context 'postgres' do 15 | it 'returns original column' do 16 | expect(ActiveRecord::Base.connection).to receive(:adapter_name).and_return('PostgreSQL') 17 | 18 | expect(described_class.cast_to_text(:column)).to eq('column::text') 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative 'config/application' 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link rails_mini_profiler_manifest.js 4 | //= link rails_mini_profiler.js 5 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/movies.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/scaffold.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | color: #333; 4 | margin: 33px; 5 | } 6 | 7 | body, p, ol, ul, td { 8 | font-family: verdana, arial, helvetica, sans-serif; 9 | font-size: 13px; 10 | line-height: 18px; 11 | } 12 | 13 | pre { 14 | background-color: #eee; 15 | padding: 10px; 16 | font-size: 11px; 17 | } 18 | 19 | a { 20 | color: #000; 21 | } 22 | 23 | a:visited { 24 | color: #666; 25 | } 26 | 27 | a:hover { 28 | color: #fff; 29 | background-color: #000; 30 | } 31 | 32 | th { 33 | padding-bottom: 5px; 34 | } 35 | 36 | td { 37 | padding: 0 5px 7px; 38 | } 39 | 40 | div.field, 41 | div.actions { 42 | margin-bottom: 10px; 43 | } 44 | 45 | #notice { 46 | color: green; 47 | } 48 | 49 | .field_with_errors { 50 | padding: 2px; 51 | background-color: red; 52 | display: table; 53 | } 54 | 55 | #error_explanation { 56 | width: 450px; 57 | border: 2px solid red; 58 | padding: 7px 7px 0; 59 | margin-bottom: 20px; 60 | background-color: #f0f0f0; 61 | } 62 | 63 | #error_explanation h2 { 64 | text-align: left; 65 | font-weight: bold; 66 | padding: 5px 5px 5px 15px; 67 | font-size: 12px; 68 | margin: -7px -7px 0; 69 | background-color: #c00; 70 | color: #fff; 71 | } 72 | 73 | #error_explanation ul li { 74 | font-size: 12px; 75 | list-style: square; 76 | } 77 | 78 | label { 79 | display: block; 80 | } 81 | -------------------------------------------------------------------------------- /spec/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Channel < ActionCable::Channel::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Connection < ActionCable::Connection::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/controllers/movies_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MoviesController < ApplicationController 4 | before_action :set_movie, only: %i[show edit update destroy] 5 | 6 | def index 7 | @movies = Movie.all 8 | end 9 | 10 | def show; end 11 | 12 | def new 13 | @movie = Movie.new 14 | end 15 | 16 | def edit; end 17 | 18 | def create 19 | @movie = Movie.new(movie_params) 20 | 21 | if @movie.save 22 | redirect_to @movie, notice: 'Movie was successfully created.' 23 | else 24 | render :new 25 | end 26 | end 27 | 28 | def update 29 | if @movie.update(movie_params) 30 | redirect_to @movie, notice: 'Movie was successfully updated.' 31 | else 32 | render :edit 33 | end 34 | end 35 | 36 | def destroy 37 | @movie.destroy 38 | redirect_to movies_url, notice: 'Movie was successfully destroyed.' 39 | end 40 | 41 | private 42 | 43 | # Use callbacks to share common setup or constraints between actions. 44 | def set_movie 45 | @movie = Movie.find(params[:id]) 46 | end 47 | 48 | # Only allow a list of trusted parameters through. 49 | def movie_params 50 | params.require(:movie).permit(:title, :imdb_id, :popularity, :budget, :revenue, :runtime, :release_date) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/movies_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MoviesHelper 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require activestorage 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /spec/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | # Automatically retry jobs that encountered a deadlock 5 | # retry_on ActiveRecord::Deadlocked 6 | 7 | # Most jobs are safe to ignore if the underlying records are no longer available 8 | # discard_on ActiveJob::DeserializationError 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | default from: 'from@example.com' 5 | layout 'mailer' 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/movie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: movies 6 | # 7 | # id :integer not null, primary key 8 | # imdb_id :string 9 | # popularity :decimal(5, 2) 10 | # budget :decimal(, ) 11 | # revenue :decimal(, ) 12 | # title :string 13 | # runtime :decimal(3, ) 14 | # release_date :date 15 | # 16 | class Movie < ApplicationRecord 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag 'application', media: 'all' %> 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/movies/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with(model: movie) do |form| %> 2 | <% if movie.errors.any? %> 3 |
    4 |

    <%= pluralize(movie.errors.count, "error") %> prohibited this movie from being saved:

    5 | 6 |
      7 | <% movie.errors.each do |error| %> 8 |
    • <%= error.full_message %>
    • 9 | <% end %> 10 |
    11 |
    12 | <% end %> 13 | 14 |
    15 | <%= form.label :title %> 16 | <%= form.text_field :title %> 17 |
    18 | 19 |
    20 | <%= form.label :imdb_id %> 21 | <%= form.text_field :imdb_id %> 22 |
    23 | 24 |
    25 | <%= form.label :popularity %> 26 | <%= form.text_field :popularity %> 27 |
    28 | 29 |
    30 | <%= form.label :budget %> 31 | <%= form.text_field :budget %> 32 |
    33 | 34 |
    35 | <%= form.label :revenue %> 36 | <%= form.text_field :revenue %> 37 |
    38 | 39 |
    40 | <%= form.label :runtime %> 41 | <%= form.text_field :runtime %> 42 |
    43 | 44 |
    45 | <%= form.label :release_date %> 46 | <%= form.date_select :release_date %> 47 |
    48 | 49 |
    50 | <%= form.submit %> 51 |
    52 | <% end %> 53 | -------------------------------------------------------------------------------- /spec/dummy/app/views/movies/_movie.json.jb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | { 4 | id: movie.id, 5 | imdb_id: movie.imdb_id, 6 | popularity: movie.popularity, 7 | revenue: movie.revenue, 8 | title: movie.title, 9 | runtime: movie.runtime 10 | } 11 | -------------------------------------------------------------------------------- /spec/dummy/app/views/movies/edit.html.erb: -------------------------------------------------------------------------------- 1 |

    Editing Movie

    2 | 3 | <%= render 'form', movie: @movie %> 4 | 5 | <%= link_to 'Show', @movie %> | 6 | <%= link_to 'Back', movies_path %> 7 | -------------------------------------------------------------------------------- /spec/dummy/app/views/movies/index.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= notice %>

    2 | 3 |

    Movies

    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <% @movies.each do |movie| %> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | <% end %> 34 | 35 |
    TitleImdbPopularityBudgetRevenueRuntimeRelease date
    <%= movie.title %><%= movie.imdb_id %><%= movie.popularity %><%= movie.budget %><%= movie.revenue %><%= movie.runtime %><%= movie.release_date %><%= link_to 'Show', movie %><%= link_to 'Edit', edit_movie_path(movie) %><%= link_to 'Destroy', movie, method: :delete, data: { confirm: 'Are you sure?' } %>
    36 | 37 |
    38 | 39 | <%= link_to 'New Movie', new_movie_path %> 40 | -------------------------------------------------------------------------------- /spec/dummy/app/views/movies/index.json.jb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | render partial: 'movie', collection: @movies, as: :movie 4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/movies/new.html.erb: -------------------------------------------------------------------------------- 1 |

    New Movie

    2 | 3 | <%= render 'form', movie: @movie %> 4 | 5 | <%= link_to 'Back', movies_path %> 6 | -------------------------------------------------------------------------------- /spec/dummy/app/views/movies/show.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= notice %>

    2 | 3 |

    4 | Title: 5 | <%= @movie.title %> 6 |

    7 | 8 |

    9 | Imdb: 10 | <%= @movie.imdb_id %> 11 |

    12 | 13 |

    14 | Popularity: 15 | <%= @movie.popularity %> 16 |

    17 | 18 |

    19 | Budget: 20 | <%= @movie.budget %> 21 |

    22 | 23 |

    24 | Revenue: 25 | <%= @movie.revenue %> 26 |

    27 | 28 |

    29 | Runtime: 30 | <%= @movie.runtime %> 31 |

    32 | 33 |

    34 | Release date: 35 | <%= @movie.release_date %> 36 |

    37 | 38 | <%= link_to 'Edit', edit_movie_path(@movie) %> | 39 | <%= link_to 'Back', movies_path %> 40 | -------------------------------------------------------------------------------- /spec/dummy/app/views/movies/show.json.jb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | render 'movie', movie: @movie 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | APP_PATH = File.expand_path('../config/application', __dir__) 5 | require_relative '../config/boot' 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative '../config/boot' 5 | require 'rake' 6 | Rake.application.run 7 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'fileutils' 5 | 6 | # path to your application root. 7 | APP_ROOT = File.expand_path('..', __dir__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | FileUtils.chdir APP_ROOT do 14 | # This script is a way to set up or update your development environment automatically. 15 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 16 | # Add necessary setup steps to this file. 17 | 18 | puts '== Installing dependencies ==' 19 | system! 'gem install bundler --conservative' 20 | system('bundle check') || system!('bundle install') 21 | 22 | # puts "\n== Copying sample files ==" 23 | # unless File.exist?('config/database.yml') 24 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 25 | # end 26 | 27 | puts "\n== Preparing database ==" 28 | system! 'bin/rails db:prepare' 29 | 30 | puts "\n== Removing old logs and tempfiles ==" 31 | system! 'bin/rails log:clear tmp:clear' 32 | 33 | puts "\n== Restarting application server ==" 34 | system! 'bin/rails restart' 35 | end 36 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | Rails.application.load_server 9 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'boot' 4 | 5 | require 'rails/all' 6 | 7 | # Require the gems listed in Gemfile, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(*Rails.groups) 10 | require 'rails_mini_profiler' 11 | 12 | module Dummy 13 | class Application < Rails::Application 14 | config.load_defaults Rails::VERSION::MAJOR.to_f.to_s 15 | 16 | # Configuration for the application, engines, and railties goes here. 17 | # 18 | # These settings can be overridden in specific environments using the files 19 | # in config/environments, which are processed later. 20 | # 21 | config.time_zone = 'Europe/Vienna' 22 | config.active_record.default_timezone = :local 23 | # config.eager_load_paths << Rails.root.join("extras") 24 | 25 | config.database = ENV.fetch('DATABASE', :sqlite).to_sym 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 5 | 6 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 7 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 8 | -------------------------------------------------------------------------------- /spec/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /spec/dummy/config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | dAq9Q4G5eJXUFjT8fO+S3YP3U91ebX6yjoIOwOROCjcYHUTrjZLW8kNdmq/fFqj7SHyDsWPY1z3QalkFAlIfUQfLGE8q774JmUt6MkIyhcV4EwLru7OgDdLz81ugbtJFEEzBhXpLBNA6/RkoPjCGCSQv2aEAzoXKVl/jsbLlqqYMg95gUwY1rpxXgqCiZDoqY1lwPWJ3YzPk91GfDvyXOEidK7f/zYbDyKUIFJ4J8cIRpSXR1PMgLBgWmpJMkFPCEYephZRBudhXj7wfAoh771LUMm2flwM1rCIXtg8GIc1ZOvwcQGqFdwuiRk3EQZ5q1f7eAvfuiWe54LXl2yrZXHUsYj2viZhynZgEFufHXOgBoX+V/LxKTelmOBulIvkyqqKn8CwiQmN4bwb4DGY4vstQb8w0b+zZdtrk--4zK2itXWOmJuwe1V--F6DIE4BidiTTpoJiccoGNg== -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | 7 | <% if Rails.application.config.database == :postgres %> 8 | default: &default 9 | adapter: postgresql 10 | host: '127.0.0.1' 11 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 12 | username: dummy 13 | password: postgres 14 | development: 15 | <<: *default 16 | database: rmp_dummy_development 17 | test: 18 | <<: *default 19 | database: rmp_dummy_test 20 | production: 21 | <<: *default 22 | database: rmp_dummy_production 23 | <% else %> 24 | default: &default 25 | adapter: sqlite3 26 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 27 | timeout: 5000 28 | development: 29 | <<: *default 30 | database: db/development.sqlite3 31 | test: 32 | <<: *default 33 | database: db/test.sqlite3 34 | production: 35 | <<: *default 36 | database: db/production.sqlite3 37 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative 'application' 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | # The test environment is used exclusively to run your application's 6 | # test suite. You never need to work with it otherwise. Remember that 7 | # your test database is "scratch space" for the test suite and is wiped 8 | # and recreated between test runs. Don't rely on the data there! 9 | 10 | Rails.application.configure do 11 | # Settings specified here will take precedence over those in config/application.rb. 12 | 13 | config.cache_classes = true 14 | 15 | # Do not eager load code on boot. This avoids loading your whole application 16 | # just for the purpose of running a single test. If you are using a tool that 17 | # preloads Rails for running tests, you may have to set it to true. 18 | config.eager_load = false 19 | 20 | # Configure public file server for tests with Cache-Control for performance. 21 | config.public_file_server.enabled = true 22 | config.public_file_server.headers = { 23 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 24 | } 25 | 26 | # Show full error reports and disable caching. 27 | config.consider_all_requests_local = true 28 | config.action_controller.perform_caching = false 29 | config.cache_store = :null_store 30 | 31 | # Raise exceptions instead of rendering exception templates. 32 | config.action_dispatch.show_exceptions = false 33 | 34 | # Disable request forgery protection in test environment. 35 | config.action_controller.allow_forgery_protection = false 36 | 37 | # Store uploaded files on the local file system in a temporary directory. 38 | config.active_storage.service = :test 39 | 40 | config.action_mailer.perform_caching = false 41 | 42 | # Tell Action Mailer not to deliver emails to the real world. 43 | # The :test delivery method accumulates sent emails in the 44 | # ActionMailer::Base.deliveries array. 45 | config.action_mailer.delivery_method = :test 46 | 47 | # Print deprecation notices to the stderr. 48 | config.active_support.deprecation = :stderr 49 | 50 | # Raise exceptions for disallowed deprecations. 51 | config.active_support.disallowed_deprecation = :raise 52 | 53 | # Tell Active Support which deprecation messages to disallow. 54 | config.active_support.disallowed_deprecation_warnings = [] 55 | 56 | # Raises error for missing translations. 57 | # config.i18n.raise_on_missing_translations = true 58 | 59 | # Annotate rendered view with file names. 60 | # config.action_view.annotate_rendered_view_with_filenames = true 61 | end 62 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # ActiveSupport::Reloader.to_prepare do 5 | # ApplicationController.renderer.defaults.merge!( 6 | # http_host: 'example.org', 7 | # https: false 8 | # ) 9 | # end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = '1.0' 7 | 8 | # Add additional assets to the asset load path. 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 9 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 10 | Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE'] 11 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Define an application-wide content security policy 5 | # For further information see the following documentation 6 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 7 | 8 | # Rails.application.config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | 16 | # # Specify URI for violation reports 17 | # # policy.report_uri "/csp-violation-report-endpoint" 18 | # end 19 | 20 | # If you are using UJS then enable automatic nonce generation 21 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 22 | 23 | # Set the nonce only to specific directives 24 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 25 | 26 | # Report CSP violations to a specified URI 27 | # For further information see the following documentation: 28 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 29 | # Rails.application.config.content_security_policy_report_only = true 30 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Specify a serializer for the signed and encrypted cookie jars. 6 | # Valid options are :json, :marshal, and :hybrid. 7 | Rails.application.config.action_dispatch.cookies_serializer = :json 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure sensitive parameters which will be filtered from the log file. 6 | Rails.application.config.filter_parameters += %i[ 7 | passw secret token _key crypt salt certificate otp ssn 8 | ] 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new inflection rules using the following format. Inflections 5 | # are locale specific, and you may define rules for as many different 6 | # locales as you wish. All of these examples are active by default: 7 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 8 | # inflect.plural /^(ox)$/i, '\1en' 9 | # inflect.singular /^(ox)en/i, '\1' 10 | # inflect.irregular 'person', 'people' 11 | # inflect.uncountable %w( fish sheep ) 12 | # end 13 | 14 | # These inflection rules are supported but not enabled by default: 15 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 16 | # inflect.acronym 'RESTful' 17 | # end 18 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new mime types for use in respond_to blocks: 5 | # Mime::Type.register "text/richtext", :rtf 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Define an application-wide HTTP permissions policy. For further 3 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 4 | # 5 | # Rails.application.config.permissions_policy do |f| 6 | # f.camera :none 7 | # f.gyroscope :none 8 | # f.microphone :none 9 | # f.usb :none 10 | # f.fullscreen :self 11 | # f.payment :self, "https://secure.example.com" 12 | # end 13 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/rails_mini_profiler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RailsMiniProfiler.configure do |config| 4 | config.storage = RailsMiniProfiler::Storage.new 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] 11 | end 12 | 13 | # To enable root element in JSON for ActiveRecord objects. 14 | # ActiveSupport.on_load(:active_record) do 15 | # self.include_root_in_json = true 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /spec/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Puma can serve each request in a thread from an internal thread pool. 4 | # The `threads` method setting takes two numbers: a minimum and maximum. 5 | # Any libraries that use thread pools should be configured to match 6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 7 | # and maximum; this matches the default thread size of Active Record. 8 | # 9 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) 10 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } 11 | threads min_threads_count, max_threads_count 12 | 13 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 14 | # terminating a worker in development environments. 15 | # 16 | worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' 17 | 18 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 19 | # 20 | port ENV.fetch('PORT', 3000) 21 | 22 | # Specifies the `environment` that Puma will run in. 23 | # 24 | environment ENV.fetch('RAILS_ENV', 'development') 25 | 26 | # Specifies the `pidfile` that Puma will use. 27 | pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid') 28 | 29 | # Specifies the number of `workers` to boot in clustered mode. 30 | # Workers are forked web server processes. If using threads and workers together 31 | # the concurrency of the application would be max `threads` * `workers`. 32 | # Workers do not work on JRuby or Windows (both of which do not support 33 | # processes). 34 | # 35 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 36 | 37 | # Use the `preload_app!` method when specifying a `workers` number. 38 | # This directive tells Puma to first boot the application and load code 39 | # before forking the application. This takes advantage of Copy On Write 40 | # process behavior so workers use less memory. 41 | # 42 | # preload_app! 43 | 44 | # Allow puma to be restarted by `rails restart` command. 45 | plugin :tmp_restart 46 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | mount RailsMiniProfiler::Engine => '/rails_mini_profiler' 5 | resources :movies 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20210702114107_add_movies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddMovies < ActiveRecord::Migration[6.1] 4 | def change 5 | create_table :movies, force: :cascade do |t| 6 | t.string :imdb_id 7 | t.decimal :popularity, precision: 5, scale: 2 8 | t.decimal :budget 9 | t.decimal :revenue 10 | t.string :title 11 | t.decimal :runtime, precision: 3 12 | t.date :release_date 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | return if Movie.any? 4 | 5 | require 'csv' 6 | movies = CSV.read("#{File.dirname(__FILE__)}/movies.csv", headers: true).map(&:to_h) 7 | .each { |attrs| attrs['release_date'] = Date.strptime(attrs['release_date'], '%m/%d/%Y') } 8 | .map { |attrs| Movie.new(attrs) } 9 | 10 | Movie.import(movies) 11 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

    You may have mistyped the address or the page may have moved.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

    Maybe you tried to change something you didn't have access to.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    We're sorry, but something went wrong.

    62 |
    63 |

    If you are the application owner check the logs for more information.

    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /spec/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/public/apple-touch-icon.png -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummy/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/storage/.keep -------------------------------------------------------------------------------- /spec/dummy/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/tmp/.keep -------------------------------------------------------------------------------- /spec/dummy/tmp/development_secret.txt: -------------------------------------------------------------------------------- 1 | 03be70680939db04419ff891147d9c8ee15e80624edfe4a52c63314c2fcf35d2b3e3abbcc4100024422817e2e1a3625e59ce012712ad7a76a1ed2ee931630d76 -------------------------------------------------------------------------------- /spec/dummy/tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/tmp/pids/.keep -------------------------------------------------------------------------------- /spec/dummy/tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hschne/rails-mini-profiler/d5dbcbbf553073d12e541ebcb5e0d4b43dff2a77/spec/dummy/tmp/storage/.keep -------------------------------------------------------------------------------- /spec/helpers/rails_mini_profiler/profiled_request_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | RSpec.describe ProfiledRequestsHelper do 7 | describe 'formatted duration' do 8 | context 'less than 1 ms' do 9 | it 'shows decimal ' do 10 | expect(helper.formatted_duration(99)).to eq(0.99) 11 | end 12 | end 13 | 14 | context 'more than 1 ms' do 15 | it 'returns rounded ' do 16 | expect(helper.formatted_duration(111)).to eq(1) 17 | end 18 | end 19 | end 20 | 21 | describe 'formatted allocations' do 22 | context 'less than 1000' do 23 | it 'returns plain number' do 24 | expect(helper.formatted_allocations(100)).to eq('100') 25 | end 26 | end 27 | 28 | context 'with thousands' do 29 | it 'returns formatted number' do 30 | expect(helper.formatted_allocations(5_500)).to eq('5.5 k') 31 | end 32 | end 33 | 34 | context 'with millions' do 35 | it 'returns formatted number' do 36 | expect(helper.formatted_allocations(5_500_500)).to eq('5.5 M') 37 | end 38 | end 39 | 40 | context 'with billions' do 41 | it 'returns formatted number' do 42 | expect(helper.formatted_allocations(5_500_500_500)).to eq('5.5 B') 43 | end 44 | end 45 | 46 | context 'with trillions' do 47 | it 'returns formatted number' do 48 | expect(helper.formatted_allocations(5_500_500_500_500)).to eq('5.5 T') 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/badge_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | RSpec.describe Badge do 7 | describe 'render' do 8 | let(:env) { {} } 9 | let(:request) { RequestWrapper.new(env) } 10 | let(:response) { ResponseWrapper.new } 11 | let(:profiled_request) { ProfiledRequest.new(id: 1) } 12 | let(:configuration) { Configuration.new } 13 | let(:request_context) { RequestContext.new(request) } 14 | 15 | subject { Badge.new(request_context, configuration: configuration) } 16 | 17 | before(:each) do 18 | request_context.response = original_response 19 | request_context.profiled_request = profiled_request 20 | end 21 | 22 | context 'with non-html response' do 23 | let(:original_response) do 24 | ResponseWrapper.new( 25 | 'content', 26 | 200, 27 | { 'Content-Type' => 'application/json' } 28 | ) 29 | end 30 | 31 | it('should return original response') { expect(subject.render).to eq(original_response) } 32 | end 33 | 34 | context 'with html response' do 35 | let(:original_response) do 36 | ResponseWrapper.new( 37 | 'content', 38 | 200, 39 | { 'Content-Type' => 'text/html' } 40 | ) 41 | end 42 | 43 | context 'missing body' do 44 | it('should return original content') do 45 | new_body = subject.render.body 46 | expect(new_body).to eq('content') 47 | end 48 | end 49 | 50 | context 'with badge disabled' do 51 | let(:configuration) { Configuration.new(ui: UserInterface.new(badge_enabled: false)) } 52 | 53 | let(:original_response) do 54 | ResponseWrapper.new( 55 | 'content', 56 | 200, 57 | { 'Content-Type' => 'text/html' } 58 | ) 59 | end 60 | 61 | it 'should not inject badge' do 62 | new_body = subject.render.body 63 | expect(new_body).to eq('content') 64 | end 65 | end 66 | 67 | context 'with body tag' do 68 | let(:original_response) do 69 | ResponseWrapper.new( 70 | 'content', 71 | 200, 72 | { 'Content-Type' => 'text/html' } 73 | ) 74 | end 75 | 76 | it 'should inject badge' do 77 | new_body = subject.render.body 78 | expect(new_body).to match(/rails-mini-profiler-badge/) 79 | end 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/guard_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | RSpec.describe Guard do 7 | describe 'profile?' do 8 | let(:env) { {} } 9 | let(:request) { RequestWrapper.new(env) } 10 | let(:configuration) { Configuration.new } 11 | let(:request_context) { RequestContext.new(request) } 12 | 13 | subject { Guard.new(request_context, configuration: configuration) } 14 | 15 | context 'with path' do 16 | let(:env) { { 'PATH_INFO' => '/' } } 17 | 18 | it('should be true') { expect(subject.profile?).to be(true) } 19 | end 20 | 21 | context 'with no ignored path' do 22 | let(:env) { { 'PATH_INFO' => '/ignored' } } 23 | 24 | it('should be true') { expect(subject.profile?).to be(true) } 25 | end 26 | 27 | context 'with profiler mount path' do 28 | let(:env) { { 'PATH_INFO' => "/#{Engine.routes.find_script_name({})}/1" } } 29 | 30 | it('should be false') { expect(subject.profile?).to be(false) } 31 | end 32 | 33 | context 'with pack path' do 34 | let(:env) { { 'PATH_INFO' => '/packs/js/abc' } } 35 | 36 | it('should be false') { expect(subject.profile?).to be(false) } 37 | end 38 | 39 | context 'with asset path' do 40 | let(:env) { { 'PATH_INFO' => '/assets/images/abc' } } 41 | 42 | it('should be false') { expect(subject.profile?).to be(false) } 43 | end 44 | 45 | context 'with actioncable path' do 46 | let(:env) { { 'PATH_INFO' => ActionCable.server.config.mount_path } } 47 | 48 | it('should be false') { expect(subject.profile?).to be(false) } 49 | end 50 | 51 | context 'with ignored path match' do 52 | let(:env) { { 'PATH_INFO' => '/ignored' } } 53 | 54 | it('should be false') do 55 | configuration.skip_paths = [/ignored/] 56 | 57 | expect(subject.profile?).to be(false) 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/redirect_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | RSpec.describe Redirect do 7 | describe 'render' do 8 | let(:env) { {} } 9 | let(:request) { RequestWrapper.new(env) } 10 | let(:configuration) { Configuration.new } 11 | let(:profiled_request) { ProfiledRequest.new(id: 1) } 12 | let(:request_context) { RequestContext.new(request) } 13 | 14 | subject { Redirect.new(request_context) } 15 | 16 | before(:each) { request_context.profiled_request = profiled_request } 17 | 18 | context 'with flamegraph parameter' do 19 | let(:env) { { 'QUERY_STRING' => 'rmp_flamegraph=true' } } 20 | 21 | it('should redirect to flamegraph path') do 22 | expect(subject.render.first).to eq(302) 23 | expect(subject.render.last).to eq(['Moved Temporarily']) 24 | end 25 | end 26 | 27 | context 'without parameter' do 28 | it('should return false') do 29 | expect(subject.render).to be(false) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/tracers/controller_tracer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | module Tracers 7 | RSpec.describe ControllerTracer do 8 | describe 'trace' do 9 | let(:payload) { { view_runtime: 10, db_runtime: 5, ignore: 100 } } 10 | let(:event) { OpenStruct.new(payload: payload) } 11 | 12 | subject { ControllerTracer.new(event) } 13 | 14 | it('should remove payload fields') do 15 | trace = subject.trace 16 | expected = { view_runtime: 10, db_runtime: 5 } 17 | expect(trace.payload).to eq(expected) 18 | end 19 | 20 | context 'with blank values' do 21 | let(:payload) { { view_runtime: '', db_runtime: 5 } } 22 | 23 | it('should remove blank values') do 24 | trace = subject.trace 25 | expected = { db_runtime: 5 } 26 | expect(trace.payload).to eq(expected) 27 | end 28 | end 29 | 30 | context 'with numbers' do 31 | let(:payload) { { view_runtime: 10.105 } } 32 | 33 | it('should round to two digits') do 34 | trace = subject.trace 35 | expected = { view_runtime: 10.11 } 36 | expect(trace.payload).to eq(expected) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/tracers/registry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | module Tracers 7 | RSpec.describe Registry do 8 | let(:configuration) { Configuration.new } 9 | 10 | subject { described_class.new(configuration) } 11 | 12 | describe 'tracers' do 13 | it('should return a name to tracer class mapping') do 14 | result = { 15 | 'instantiation.active_record' => RailsMiniProfiler::Tracers::InstantiationTracer, 16 | 'process_action.action_controller' => RailsMiniProfiler::Tracers::ControllerTracer, 17 | 'render_partial.action_view' => RailsMiniProfiler::Tracers::ViewTracer, 18 | 'rails_mini_profiler.total_time' => RailsMiniProfiler::Tracers::RmpTracer, 19 | 'render_template.action_view' => RailsMiniProfiler::Tracers::ViewTracer, 20 | 'sql.active_record' => RailsMiniProfiler::Tracers::SequelTracer 21 | } 22 | expect(subject.tracers).to eq(result) 23 | end 24 | end 25 | 26 | describe 'presenters' do 27 | it('should return a name to presenter class mapping') do 28 | result = { 29 | 'instantiation.active_record' => RailsMiniProfiler::InstantiationTracePresenter, 30 | 'process_action.action_controller' => RailsMiniProfiler::ControllerTracePresenter, 31 | 'rails_mini_profiler.total_time' => RailsMiniProfiler::RmpTracePresenter, 32 | 'render_partial.action_view' => RailsMiniProfiler::RenderPartialTracePresenter, 33 | 'render_template.action_view' => RailsMiniProfiler::RenderTemplateTracePresenter, 34 | 'sql.active_record' => RailsMiniProfiler::SequelTracePresenter 35 | } 36 | 37 | expect(subject.presenters).to eq(result) 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/tracers/sequel_tracer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | module Tracers 7 | RSpec.describe SequelTracer do 8 | describe 'trace' do 9 | let(:payload) { { name: 'name', sql: 'select', binds: [], type_casted_binds: [], ignore: 'ignore' } } 10 | let(:event) { OpenStruct.new(payload: payload) } 11 | 12 | subject { SequelTracer.new(event) } 13 | 14 | context 'with invalid query' do 15 | let(:payload) { { name: 'name', sql: 'bad' } } 16 | 17 | it('should return null trace') do 18 | trace = subject.trace 19 | expect(trace).to be_a(NullTrace) 20 | end 21 | end 22 | 23 | it('should remove payload fields') do 24 | trace = subject.trace 25 | expected = { name: 'name', sql: 'select', binds: [] } 26 | expect(trace.payload).to eq(expected) 27 | end 28 | 29 | context('with callable binds') do 30 | let(:payload) do 31 | { 32 | name: 'name', 33 | sql: 'select', 34 | binds: [], 35 | type_casted_binds: -> { [] } 36 | } 37 | end 38 | 39 | it('should create binds') do 40 | trace = subject.trace 41 | expected = { name: 'name', sql: 'select', binds: [] } 42 | expect(trace.payload).to eq(expected) 43 | end 44 | end 45 | 46 | context('with binds') do 47 | let(:payload) do 48 | { 49 | name: 'name', 50 | sql: 'select', 51 | binds: [OpenStruct.new(name: 'id', value_before_cast: '1')], 52 | type_casted_binds: [1] 53 | } 54 | end 55 | 56 | it('should use type casted binds') do 57 | trace = subject.trace 58 | expected = { name: 'name', sql: 'select', binds: [{ name: 'id', value: 1 }] } 59 | expect(trace.payload).to eq(expected) 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/tracers/sequel_tracker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | module Tracers 7 | RSpec.describe SqlTracker do 8 | describe 'trace' do 9 | let(:query) { 'select' } 10 | let(:name) { 'name' } 11 | 12 | subject { SqlTracker.new(query: query, name: name) } 13 | 14 | context 'with untracked command' do 15 | let(:query) { 'ignore' } 16 | 17 | it('should not track') do 18 | expect(subject.track?).to eq(false) 19 | end 20 | end 21 | 22 | context 'with untracked name' do 23 | let(:name) { 'schema' } 24 | 25 | it('should not track') do 26 | expect(subject.track?).to eq(false) 27 | end 28 | end 29 | 30 | context 'with untracked table' do 31 | let(:query) { 'select * from sqlite_version' } 32 | 33 | it('should not track') do 34 | expect(subject.track?).to eq(false) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/tracers/trace_factory_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | module Tracers 7 | RSpec.describe TraceFactory do 8 | let(:configuration) { Configuration.new } 9 | 10 | let(:registry) { Registry.new(configuration) } 11 | 12 | subject { described_class.new(registry) } 13 | 14 | describe 'create' do 15 | it('should return a trace with unknown event') do 16 | event = OpenStruct.new(name: 'unknown') 17 | 18 | expect(subject.create(event)).to be_a(Trace) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/tracers/tracer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | module Tracers 7 | RSpec.describe Tracer do 8 | describe 'trace' do 9 | subject do 10 | ActiveSupport::Notifications.subscribe('wait') do |*args| 11 | @event = ActiveSupport::Notifications::Event.new(*args) 12 | end 13 | 14 | ActiveSupport::Notifications.instrument('wait') do 15 | sleep 0.001 16 | end 17 | 18 | Tracer.new(@event) 19 | end 20 | 21 | it('stores starts and ends as milliseconds') do 22 | expect(subject.trace.duration).to be_within(5).of(@event.duration * 100) 23 | end 24 | 25 | it('stores start and ends as microseconds') do 26 | expect(subject.trace.start).to be_within(100).of(@event.time.to_f * Tracer::TIMESTAMP_MULTIPLIER) 27 | expect(subject.trace.finish).to be_within(100).of(@event.end.to_f * Tracer::TIMESTAMP_MULTIPLIER) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/lib/rails_mini_profiler/tracers/view_tracer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | module Tracers 7 | RSpec.describe ViewTracer do 8 | describe 'trace' do 9 | let(:payload) { { identifier: 'id', count: 5, ignore: 100 } } 10 | let(:event) { OpenStruct.new(payload: payload) } 11 | 12 | subject { ViewTracer.new(event) } 13 | 14 | it('should remove payload fields') do 15 | trace = subject.trace 16 | expected = { identifier: 'id', count: 5 } 17 | expect(trace.payload).to eq(expected) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/rails_mini_profiler_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RailsMiniProfiler do 4 | it 'has a version number' do 5 | expect(RailsMiniProfiler::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/requests/rails_mini_profiler/flamegraph_request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | RSpec.describe 'Flamegraphs', type: :request do 7 | let(:user_id) { '127.0.0.1' } 8 | 9 | describe 'GET /show' do 10 | it 'returns http success' do 11 | profiled_request = ProfiledRequest.create(user_id: user_id) 12 | Flamegraph.create(profiled_request: profiled_request, data: { id: 1 }) 13 | 14 | get flamegraph_path(profiled_request.id) 15 | 16 | expect(response).to have_http_status(:success) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/routing/rails_mini_profiler/profiled_requests_routing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | RSpec.describe ProfiledRequestsController, type: :routing do 7 | describe 'routing' do 8 | it 'routes to #index' do 9 | expect(get: profiled_requests_path) 10 | .to route_to(controller: 'rails_mini_profiler/profiled_requests', action: 'index') 11 | end 12 | 13 | it 'routes to #show' do 14 | expect(get: profiled_request_path(1)) 15 | .to route_to(controller: 'rails_mini_profiler/profiled_requests', action: 'show', id: '1') 16 | end 17 | 18 | it 'routes to #destroy' do 19 | expect(delete: profiled_request_path(1)) 20 | .to route_to(controller: 'rails_mini_profiler/profiled_requests', action: 'destroy', id: '1') 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/search/rails_mini_profiler/trace_search_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | module RailsMiniProfiler 6 | RSpec.describe TraceSearch, type: :model do 7 | describe 'search' do 8 | let(:profiled_request) { ProfiledRequest.create(request_path: '/index') } 9 | 10 | context 'by name' do 11 | it 'returns traces with matching name' do 12 | trace = Trace.create(name: 'sql', profiled_request: profiled_request) 13 | Trace.create(name: 'other', profiled_request: profiled_request) 14 | 15 | results = described_class.results(scope: Trace.all, name: 'sql') 16 | 17 | expect(results).to eq([trace]) 18 | end 19 | end 20 | 21 | context ' by allocations' do 22 | it 'returns traces with greater allocation' do 23 | request = Trace.create(allocations: 101, profiled_request: profiled_request) 24 | Trace.create(allocations: 100, profiled_request: profiled_request) 25 | 26 | results = described_class.results(scope: Trace.all, allocations: 100) 27 | 28 | expect(results).to eq([request]) 29 | end 30 | end 31 | 32 | context 'by duration' do 33 | it 'returns traces with greater duration' do 34 | request = Trace.create(duration: 10_001, profiled_request: profiled_request) 35 | Trace.create(duration: 10_000, profiled_request: profiled_request) 36 | 37 | results = described_class.results(scope: Trace.all, duration: 10_000) 38 | 39 | expect(results).to eq([request]) 40 | end 41 | end 42 | 43 | context 'by payload' do 44 | it 'returns traces with matching payload' do 45 | profiled_request.traces.create(payload: { sample: 'one' }) 46 | trace = profiled_request.traces.create(payload: { sample: 'two here' }) 47 | 48 | results = described_class.results(scope: Trace.all, payload: 'two') 49 | 50 | expect(results).to eq([trace]) 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/support/spec_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SpecHelpers 4 | def response_json 5 | JSON.parse(response.body) 6 | end 7 | end 8 | --------------------------------------------------------------------------------