From ffaa889127d7e28587e380ac0b1dfc778acd11c8 Mon Sep 17 00:00:00 2001 From: Linda Goldstein Date: Mon, 27 Oct 2025 08:20:46 -0700 Subject: [PATCH 1/3] WIP replace bullet with prosopite for finding performance issues like db n+1 queries --- Gemfile | 5 +- Gemfile.lock | 11 ++++- app/controllers/application_controller.rb | 11 +++++ config/initializers/prosopite.rb | 12 +++++ spec/support/prosopite.rb | 56 +++++++++++++++++++++++ 5 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 config/initializers/prosopite.rb create mode 100644 spec/support/prosopite.rb 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..b811d07125 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -115,9 +115,12 @@ GEM bugsnag (6.28.0) concurrent-ruby (~> 1.0) builder (3.3.0) +<<<<<<< HEAD bullet (8.1.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) +======= +>>>>>>> c9fda4adf (WIP replace bullet with prosopite for finding performance issues like db n+1 queries) bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) @@ -421,6 +424,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 +666,10 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) +<<<<<<< HEAD uniform_notifier (1.18.0) +======= +>>>>>>> c9fda4adf (WIP replace bullet with prosopite for finding performance issues like db n+1 queries) useragent (0.16.11) view_component (3.22.0) activesupport (>= 5.2.0, < 8.1) @@ -707,7 +714,6 @@ DEPENDENCIES blueprinter brakeman bugsnag - bullet bundler-audit byebug capybara @@ -751,6 +757,7 @@ DEPENDENCIES pg_query pghero pretender + prosopite pry pry-byebug puma (= 7.0.4) @@ -775,7 +782,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/initializers/prosopite.rb b/config/initializers/prosopite.rb new file mode 100644 index 0000000000..51ba871150 --- /dev/null +++ b/config/initializers/prosopite.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +if Rails.configuration.application.prosopite_enabled + require "prosopite/middleware/rack" + Rails.configuration.middleware.use(Prosopite::Middleware::Rack) +end + +Rails.application.config.after_initialize do + Prosopite.enabled = Rails.configuration.application.prosopite_enabled + Prosopite.min_n_queries = Rails.configuration.application.prosopite_min_n_queries + Prosopite.rails_logger = true +end diff --git a/spec/support/prosopite.rb b/spec/support/prosopite.rb new file mode 100644 index 0000000000..085e8eda22 --- /dev/null +++ b/spec/support/prosopite.rb @@ -0,0 +1,56 @@ +# 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_PATHS = [ + "./spec/models/*" +].freeze + +# 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_enable = PROSOPITE_PATHS.any? { |pattern| + File.fnmatch?(pattern, example.metadata[:rerun_file_path] + ) + } + + if should_enable && !example.metadata[:disable_prosopite] + Prosopite.scan do + example.run + end + else + # Disable prosopite globally for this test (works across threads) + original_enabled = Prosopite.enabled? + Prosopite.enabled = false + example.run + Prosopite.enabled = original_enabled + end + end +end From bd762c64b2c8a825312200e4d623a8e1a3844779 Mon Sep 17 00:00:00 2001 From: Linda Goldstein Date: Tue, 28 Oct 2025 09:10:52 -0700 Subject: [PATCH 2/3] WIP --- spec/support/prosopite.rb | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/spec/support/prosopite.rb b/spec/support/prosopite.rb index 085e8eda22..6472bc0605 100644 --- a/spec/support/prosopite.rb +++ b/spec/support/prosopite.rb @@ -10,9 +10,7 @@ "shoulda/matchers/active_model/validate_inclusion_of_matcher.rb", ] -PROSOPITE_PATHS = [ - "./spec/models/*" -].freeze +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 @@ -36,21 +34,19 @@ def result(evaluation) RSpec.configure do |config| config.around do |example| - should_enable = PROSOPITE_PATHS.any? { |pattern| - File.fnmatch?(pattern, example.metadata[:rerun_file_path] - ) + should_ignore = PROSOPITE_IGNORE.any? { |path| + File.fnmatch?("./#{path}/*", example.metadata[:rerun_file_path]) } - - if should_enable && !example.metadata[:disable_prosopite] - Prosopite.scan do - example.run - end - else + 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 From 82c4afb39af12b5b1a411c8161e377c9c4a6dcd5 Mon Sep 17 00:00:00 2001 From: Linda Goldstein Date: Fri, 28 Nov 2025 21:38:48 -0500 Subject: [PATCH 3/3] fix: complete prosopite setup and remove bullet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves merge conflicts and completes the migration from bullet to prosopite for N+1 query detection. Changes: - Resolve Gemfile.lock merge conflicts (remove bullet/uniform_notifier) - Replace Bullet configuration with Prosopite in development.rb - Replace Bullet configuration with Prosopite in test.rb - Fix prosopite initializer to use config.x (Rails custom config) - Update rails_helper comment to reference prosopite - Add :disable_prosopite metadata to error handling test - Create .prosopite_ignore file for test exclusions Prosopite is now fully configured and working to detect N+1 queries in both development and test environments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .prosopite_ignore | 3 +++ Gemfile.lock | 10 ---------- config/environments/development.rb | 9 +++------ config/environments/test.rb | 11 ++++------- config/initializers/prosopite.rb | 6 +++--- spec/models/case_court_report_spec.rb | 2 +- spec/rails_helper.rb | 9 +++------ 7 files changed, 17 insertions(+), 33 deletions(-) create mode 100644 .prosopite_ignore 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.lock b/Gemfile.lock index b811d07125..cc1b9fece7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -115,12 +115,6 @@ GEM bugsnag (6.28.0) concurrent-ruby (~> 1.0) builder (3.3.0) -<<<<<<< HEAD - bullet (8.1.0) - activesupport (>= 3.0.0) - uniform_notifier (~> 1.11) -======= ->>>>>>> c9fda4adf (WIP replace bullet with prosopite for finding performance issues like db n+1 queries) bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) @@ -666,10 +660,6 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) -<<<<<<< HEAD - uniform_notifier (1.18.0) -======= ->>>>>>> c9fda4adf (WIP replace bullet with prosopite for finding performance issues like db n+1 queries) useragent (0.16.11) view_component (3.22.0) activesupport (>= 5.2.0, < 8.1) 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 index 51ba871150..8982eb9d4e 100644 --- a/config/initializers/prosopite.rb +++ b/config/initializers/prosopite.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -if Rails.configuration.application.prosopite_enabled +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.application.prosopite_enabled - Prosopite.min_n_queries = Rails.configuration.application.prosopite_min_n_queries + 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