diff --git a/.prosopite_ignore b/.prosopite_ignore new file mode 100644 index 0000000000..29b1f8aafe --- /dev/null +++ b/.prosopite_ignore @@ -0,0 +1,3 @@ +# This file contains paths to ignore for Prosopite N+1 query detection +# Add spec file paths (one per line) that should be excluded from N+1 detection +# Example: spec/system/some_feature diff --git a/Gemfile b/Gemfile index bfba58b670..1c990ba6eb 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,7 @@ gem "rswag-api" gem "rswag-ui" gem "sablon" # Word document templating tool for Case Court Reports gem "scout_apm" +gem "scout_apm_logging" # production metrics around speed gem "sprockets-rails" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] gem "stimulus-rails" gem "strong_migrations" @@ -63,13 +64,13 @@ gem "flipper-ui" gem "pghero" gem "pg_query" group :development, :test do - gem "bullet" # Detect and fix N+1 queries gem "byebug", platforms: %i[mri mingw x64_mingw] # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem "dotenv-rails" gem "factory_bot_rails" gem "parallel_tests" gem "pry" gem "pry-byebug" + gem "prosopite" # check for performance issues like N+1 queries gem "rspec_junit_formatter" gem "rspec-rails" gem "rswag-specs" @@ -110,5 +111,3 @@ group :test do gem "simplecov", require: false gem "webmock" # HTTP request stubber end - -gem "scout_apm_logging", "~> 2.1" diff --git a/Gemfile.lock b/Gemfile.lock index d181a0c640..cc1b9fece7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -115,9 +115,6 @@ GEM bugsnag (6.28.0) concurrent-ruby (~> 1.0) builder (3.3.0) - bullet (8.1.0) - activesupport (>= 3.0.0) - uniform_notifier (~> 1.11) bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) @@ -421,6 +418,7 @@ GEM actionpack (>= 7.1) prettyprint (0.2.0) prism (1.5.1) + prosopite (2.1.2) pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) @@ -662,7 +660,6 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) - uniform_notifier (1.18.0) useragent (0.16.11) view_component (3.22.0) activesupport (>= 5.2.0, < 8.1) @@ -707,7 +704,6 @@ DEPENDENCIES blueprinter brakeman bugsnag - bullet bundler-audit byebug capybara @@ -751,6 +747,7 @@ DEPENDENCIES pg_query pghero pretender + prosopite pry pry-byebug puma (= 7.0.4) @@ -775,7 +772,7 @@ DEPENDENCIES rubocop-rspec_rails sablon scout_apm - scout_apm_logging (~> 2.1) + scout_apm_logging selenium-webdriver shoulda-matchers simplecov diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 024dd07985..3665b2612c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,17 @@ class ApplicationController < ActionController::Base include Organizational include Users::TimeZone + unless Rails.env.production? + around_action :n_plus_one_detection + + def n_plus_one_detection + Prosopite.scan + yield + ensure + Prosopite.finish + end + end + protect_from_forgery before_action :store_user_location!, if: :storable_location? before_action :authenticate_user! diff --git a/config/environments/development.rb b/config/environments/development.rb index 0ffa5ba8ac..375f6934a3 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -68,12 +68,9 @@ # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true - config.after_initialize do - Bullet.enable = true - Bullet.console = true - Bullet.rails_logger = true - Bullet.bullet_logger = true - end + # Prosopite configuration for N+1 query detection + config.x.prosopite_enabled = true + config.x.prosopite_min_n_queries = 2 # Annotate rendered view with file names. config.action_view.annotate_rendered_view_with_filenames = true diff --git a/config/environments/test.rb b/config/environments/test.rb index 3111e9657b..fd39f8f307 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -63,13 +63,10 @@ # Raises error for missing translations. config.i18n.raise_on_missing_translations = true - config.after_initialize do - Bullet.enable = true - Bullet.console = true - Bullet.bullet_logger = true - Bullet.rails_logger = true - # Bullet.raise = true # TODO https://github.com/rubyforgood/casa/issues/2441 - end + # Prosopite configuration for N+1 query detection + # Detailed configuration is in spec/support/prosopite.rb + config.x.prosopite_enabled = false # Managed by spec/support/prosopite.rb + config.x.prosopite_min_n_queries = 2 # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true diff --git a/config/initializers/prosopite.rb b/config/initializers/prosopite.rb new file mode 100644 index 0000000000..8982eb9d4e --- /dev/null +++ b/config/initializers/prosopite.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +if Rails.configuration.x.prosopite_enabled + require "prosopite/middleware/rack" + Rails.configuration.middleware.use(Prosopite::Middleware::Rack) +end + +Rails.application.config.after_initialize do + Prosopite.enabled = Rails.configuration.x.prosopite_enabled + Prosopite.min_n_queries = Rails.configuration.x.prosopite_min_n_queries + Prosopite.rails_logger = true +end diff --git a/spec/models/case_court_report_spec.rb b/spec/models/case_court_report_spec.rb index ca6185300c..a375592cb0 100644 --- a/spec/models/case_court_report_spec.rb +++ b/spec/models/case_court_report_spec.rb @@ -386,7 +386,7 @@ let(:casa_case_with_contacts) { volunteer.casa_cases.first } let(:nonexistent_path) { "app/documents/templates/nonexisitent_report_template.docx" } - it "raises Zip::Error when generating report" do + it "raises Zip::Error when generating report", :disable_prosopite do args = { case_id: casa_case_with_contacts.id, volunteer_id: volunteer.id, diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 005c9d840d..cdb9669e77 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -121,12 +121,9 @@ raise StandardError.new "\"#{example.full_description}\" in #{example.location} timed out." end - # NOTE: not applicable currently, left to show how to skip bullet errrors - # config.around :each, :disable_bullet do |example| - # Bullet.raise = false - # example.run - # Bullet.raise = true - # end + # NOTE: not applicable currently, left to show how to skip prosopite errors + # You can use the :disable_prosopite metadata tag on specific examples + # See spec/support/prosopite.rb for configuration config.around do |example| Capybara.server_port = 7654 + ENV["TEST_ENV_NUMBER"].to_i diff --git a/spec/support/prosopite.rb b/spec/support/prosopite.rb new file mode 100644 index 0000000000..6472bc0605 --- /dev/null +++ b/spec/support/prosopite.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +Prosopite.enabled = true +Prosopite.raise = true # Fail specs on N+1 detection +Prosopite.rails_logger = true +Prosopite.prosopite_logger = true +Prosopite.allow_stack_paths = [ + "shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb", + "shoulda/matchers/active_model/validate_presence_of_matcher.rb", + "shoulda/matchers/active_model/validate_inclusion_of_matcher.rb", +] + +PROSOPITE_IGNORE = File.read(".prosopite_ignore").lines.map(&:chomp) + +# Monkey-patch FactoryBot to pause Prosopite during factory creation +# This prevents N+1 detection in factory callbacks, focusing on actual test code +module FactoryBot + module Strategy + class Create + alias_method :original_result, :result + + def result(evaluation) + if defined?(Prosopite) && Prosopite.enabled? + Prosopite.pause do + original_result(evaluation) + end + else + original_result(evaluation) + end + end + end + end +end + +RSpec.configure do |config| + config.around do |example| + should_ignore = PROSOPITE_IGNORE.any? { |path| + File.fnmatch?("./#{path}/*", example.metadata[:rerun_file_path]) + } + if should_ignore || example.metadata[:disable_prosopite] + # Disable prosopite globally for this test (works across threads) + original_enabled = Prosopite.enabled? + Prosopite.enabled = false + example.run + Prosopite.enabled = original_enabled + else + Prosopite.scan do + example.run + end + end + end +end