WIP N+1 detector gem Prosopite (replacing gem Bullet)#6744
WIP N+1 detector gem Prosopite (replacing gem Bullet)#6744
Conversation
There was a problem hiding this comment.
Pull request overview
Replaces Bullet with Prosopite for N+1 query detection, adding configuration for development/test (and optional production toggles) plus RSpec integration and an ignore mechanism to support gradual rollout.
Changes:
- Swap
bulletforprosopitein dependencies and environment config. - Add a Rails initializer to configure Prosopite (middleware + core settings).
- Add RSpec support to scan examples, pause during FactoryBot creation, and support an ignore list / TODO tracking.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/support/prosopite.rb | Adds RSpec + FactoryBot integration and ignore-list logic for Prosopite scanning |
| spec/.prosopite_ignore | Introduces ignore list file for incremental rollout |
| config/initializers/prosopite.rb | Configures Prosopite middleware/settings after Rails initialization |
| config/environments/test.rb | Enables Prosopite settings for test environment (replacing Bullet config) |
| config/environments/development.rb | Enables Prosopite settings for development environment (replacing Bullet config) |
| config/environments/production.rb | Adds env-driven Prosopite toggles for production |
| PROSOPITE_TODO.md | Captures detected N+1 locations for follow-up work |
| Gemfile.lock | Removes Bullet and adds Prosopite gem |
| Gemfile | Replaces Bullet with Prosopite dependency (dev/test group) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
config/initializers/prosopite.rb
Outdated
| # Only enable Rack middleware if Prosopite is configured on | ||
| if Rails.configuration.respond_to?(:prosopite_enabled) && Rails.configuration.prosopite_enabled | ||
| require "prosopite/middleware/rack" | ||
| Rails.configuration.middleware.use(Prosopite::Middleware::Rack) | ||
| end | ||
|
|
||
| Rails.application.config.after_initialize do | ||
| # Core settings | ||
| Prosopite.enabled = Rails.configuration.respond_to?(:prosopite_enabled) && | ||
| Rails.configuration.prosopite_enabled | ||
|
|
||
| # Minimum repeated queries to trigger detection (default: 2) | ||
| Prosopite.min_n_queries = Rails.configuration.respond_to?(:prosopite_min_n_queries) ? | ||
| Rails.configuration.prosopite_min_n_queries : 2 | ||
|
|
||
| # Logging options | ||
| Prosopite.rails_logger = true # Log to Rails.logger | ||
| Prosopite.prosopite_logger = Rails.env.development? # Log to log/prosopite.log | ||
| end |
There was a problem hiding this comment.
Prosopite is referenced unconditionally inside after_initialize, but the gem is only in the :development, :test group. In production (and any env without the gem), this initializer will raise NameError during boot. Wrap the entire initializer (including the after_initialize block) in return unless defined?(Prosopite) / if defined?(Prosopite) or move the gem to a group that is bundled in the environments where this initializer runs (and handle the case where prosopite_enabled is true but the gem isn't available).
| def use_prosopite?(example) | ||
| # Explicit metadata takes precedence | ||
| return false if example.metadata[:disable_prosopite] | ||
| return true if example.metadata[:enable_prosopite] | ||
|
|
||
| # Check against ignore list | ||
| PROSOPITE_IGNORE.none? do |path| | ||
| File.fnmatch?("./#{path}/*", example.metadata[:rerun_file_path].to_s) | ||
| end |
There was a problem hiding this comment.
use_prosopite? is defined at the top level, which adds a method to Object and can leak into other specs/helpers. Define it in a module (e.g., Support::Prosopite) and call it from the RSpec hook, or define it as a local lambda inside the RSpec.configure block to avoid global namespace pollution.
spec/support/prosopite.rb
Outdated
|
|
||
| # Optional: Load ignore list from file for gradual rollout | ||
| PROSOPITE_IGNORE = if File.exist?("spec/.prosopite_ignore") | ||
| File.read("spec/.prosopite_ignore").lines.map(&:chomp).reject(&:empty?) |
There was a problem hiding this comment.
The ignore list loader keeps comment lines (starting with #) and doesn’t strip surrounding whitespace. That makes it easy to accidentally add a path like spec/features that never matches, and the file header comments will always be processed. Consider filtering out comment lines and trimming whitespace before matching.
| File.read("spec/.prosopite_ignore").lines.map(&:chomp).reject(&:empty?) | |
| File.read("spec/.prosopite_ignore").lines | |
| .map { |line| line.strip } | |
| .reject { |line| line.empty? || line.start_with?("#") } |
- Set Prosopite.raise = true so N+1s fail specs instead of silently logging - Populate .prosopite_ignore with known-issue directories for gradual rollout - Scope initializer to development only; spec/support/prosopite.rb owns test config - Remove redundant prosopite settings from test.rb and production.rb - Replace fragile FactoryBot::Strategy::Create monkey-patch with SyntaxRunner hook - Filter comment lines in .prosopite_ignore parser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| group :development, :test, :production do | ||
| gem "brakeman" # Security inspection | ||
| gem "bullet" # Detect and fix N+1 queries | ||
| gem "prosopite" # N+1 query detection via SQL pattern analysis |
There was a problem hiding this comment.
Changing this group to include :production will also bundle/require a large set of dev/test-only gems (e.g., rspec-rails, factory_bot_rails, linters) in production, increasing boot time/memory and potentially introducing production-only dependency conflicts. Keep the group as :development, :test and, if Prosopite truly needs to be available in production, place only prosopite in its own appropriate group (or mark it require: false) instead of pulling the entire dev/test toolchain into production.
| PROSOPITE_IGNORE.none? do |path| | ||
| File.fnmatch?("./#{path}/*", example.metadata[:rerun_file_path].to_s) |
There was a problem hiding this comment.
The ignore matching here appears to use a nonstandard RSpec metadata key (:rerun_file_path), which is likely to be nil for most examples. That would prevent the ignore list from ever matching and cause Prosopite to run/raise in specs that are supposed to be ignored. Use the actual spec file path metadata key (e.g., :file_path/:absolute_file_path) when comparing against .prosopite_ignore.
| PROSOPITE_IGNORE.none? do |path| | |
| File.fnmatch?("./#{path}/*", example.metadata[:rerun_file_path].to_s) | |
| spec_path = example.metadata[:absolute_file_path] || | |
| example.metadata[:file_path] || | |
| example.metadata[:rerun_file_path] | |
| PROSOPITE_IGNORE.none? do |path| | |
| File.fnmatch?("./#{path}/*", spec_path.to_s) |
https://rubygems.org/gems/prosopite
https://github.com/flyerhzm/bullet