pax_global_header 0000666 0000000 0000000 00000000064 14644222732 0014520 g ustar 00root root 0000000 0000000 52 comment=9ea62db6797d5a3f4092931b40ab146f8126856b
bullet-7.2.0/ 0000775 0000000 0000000 00000000000 14644222732 0013015 5 ustar 00root root 0000000 0000000 bullet-7.2.0/.github/ 0000775 0000000 0000000 00000000000 14644222732 0014355 5 ustar 00root root 0000000 0000000 bullet-7.2.0/.github/dependabot.yml 0000664 0000000 0000000 00000000166 14644222732 0017210 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
bullet-7.2.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14644222732 0016412 5 ustar 00root root 0000000 0000000 bullet-7.2.0/.github/workflows/main.yml 0000664 0000000 0000000 00000005017 14644222732 0020064 0 ustar 00root root 0000000 0000000 # This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test_rails_4:
runs-on: ubuntu-latest
strategy:
matrix:
gemfile: ['Gemfile.rails-4.0', 'Gemfile.rails-4.1', 'Gemfile.rails-4.2']
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.3
bundler: 1
bundler-cache: true
- name: Run tests
run: bundle exec rake
test_rails_5:
runs-on: ubuntu-latest
strategy:
matrix:
gemfile: ['Gemfile.rails-5.0', 'Gemfile.rails-5.1', 'Gemfile.rails-5.2']
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.5
bundler: 1
bundler-cache: true
- name: Run tests
run: bundle exec rake
test_rails_6:
runs-on: ubuntu-latest
strategy:
matrix:
gemfile: ['Gemfile.rails-6.0', 'Gemfile.rails-6.1']
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
bundler-cache: true
- name: Run tests
run: bundle exec rake
test_rails_7:
runs-on: ubuntu-latest
strategy:
matrix:
gemfile: ['Gemfile.rails-7.0', 'Gemfile.rails-7.1', 'Gemfile.rails-7.2']
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1
bundler-cache: true
- name: Run tests
run: bundle exec rake
bullet-7.2.0/.gitignore 0000664 0000000 0000000 00000000254 14644222732 0015006 0 ustar 00root root 0000000 0000000 log/**
pkg/**
.DS_Store
lib/.DS_Store
.*.swp
coverage.data
tags
.bundle
*.gem
benchmark_profile*
/nbproject/private/
coverage/
.coveralls.yml
Gemfile*.lock
.idea/
.vscode/
bullet-7.2.0/.rspec 0000664 0000000 0000000 00000000033 14644222732 0014126 0 ustar 00root root 0000000 0000000 --colour
--format progress
bullet-7.2.0/CHANGELOG.md 0000664 0000000 0000000 00000017475 14644222732 0014644 0 ustar 00root root 0000000 0000000 ## Next Release
## 7.2.0 (07/12/2024)
* Support rails 7.2
* Fix count method signature for active_record5 and active_record60
## 7.1.6 (01/16/2024)
* Allow apps to not include the user in a notification
## 7.1.5 (01/05/2024)
* Fix mongoid8
## 7.1.4 (11/17/2023)
* Call association also on through reflection
## 7.1.3 (11/05/2023)
* Call NPlusOneQuery's call_association when calling count on collection association
## 7.1.2 (10/13/2023)
* Handle Rails 7.1 composite primary keys
## 7.1.1 (10/07/2023)
* Add support for `Content-Security-Policy-Report-Only` nonces
* Fix count method signature
## 7.1.0 (10/06/2023)
* Support rails 7.1
* Alias `Bullet.enable?` to `enabled?`, and `Bullet.enable=` to `enabled=`
* Added `always_append_html_body` option, so the html snippet is always included even if there are no notifications
* Added detection of n+1 count queries from `count` method
* Changed the counter cache notification title to recommend using `size`
## 7.0.7 (03/01/2023)
* Check `Rails.application.config.content_security_policy` before insert `Bullet::Rack`
## 7.0.6 (03/01/2023)
* Better way to check if `ActionDispatch::ContentSecurityPolicy::Middleware` exists
## 7.0.5 (01/01/2023)
* Fix n+1 false positives in AR 7.0
* Fix eager_load nested has_many :through false positives
* Respect Content-Security-Policy nonces
* Added CallStacks support for avoid eager loading
* Iterate fewer times over objects
## 7.0.4 (11/28/2022)
* Fix `eager_load` `has_many :through` false positives
* mongoid7x: add dynamic methods
## 7.0.3 (08/13/2022)
* Replace `Array()` with `Array.wrap()`
## 7.0.2 (05/31/2022)
* Drop growl support
* Do not check html tag in Bullet::Rack anymore
## 7.0.1 (01/15/2022)
* Get rid of *_whitelist methods
* Hack ActiveRecord::Associations::Preloader::Batch in rails 7
## 7.0.0 (12/18/2021)
* Support rails 7
* Fix Mongoid 7 view iteration
* Move CI from Travis to Github Actions
## 6.1.5 (08/16/2021)
* Rename whitelist to safelist
* Fix onload called twice
* Support Rack::Files::Iterator responses
* Ensure HABTM associations are not incorrectly labeled n+1
## 6.1.4 (02/26/2021)
* Added an option to stop adding HTTP headers to API requests
## 6.1.3 (01/21/2021)
* Consider ThroughAssociation at SingularAssociation like CollectionAssociation
* Add xhr_script only when add_footer is enabled
## 6.1.2 (12/12/2020)
* Revert "Make whitelist thread safe"
## 6.1.1 (12/12/2020)
* Add support Rails 6.1
* Make whitelist thread safe
## 6.1.0 (12/28/2019)
* Add skip_html_injection flag
* Remove writer hack in active_record6
* Use modern includes syntax in warnings
* Fix warning: The last argument is used as the keyword parameter
## 6.0.2 (08/20/2019)
* Fully support Rails 6.0
## 6.0.1 (06/26/2019)
* Add Bullet::ActiveJob
* Prevent "Maximum call stack exceeded" errors when used with Turbolinks
## 6.0.0 (04/25/2019)
* Add XHR support to Bullet
* Support Rails 6.0
* Handle case where ID is manually set on unpersisted record
## 5.9.0 (11/11/2018)
* Require Ruby 2.3+
* Support Mongo 7.x
## 5.8.0 (10/29/2018)
* Fix through reflection for rails 5.x
* Fix false positive in after_save/after_create callbacks
* Don't trigger a preload error on "manual" preloads
* Avoid Bullet from making extra queries in mongoid6
* Support option for #first and #last on mongoid6.x
* Fix duplicate logs in mongoid 4.x and 5.x version
* Use caller for ruby 1.9 while caller_locations for 2.0+
* Extend stacktrace matching for sub-file precision
* Exclude configured bundler path in addition to '/vendor'
* Fix `caller_path` in `excluded_stacktrace_path`
* Update `uniform_notifier` dependency to add Sentry support
* Integrate awesomecode.io and refactor code
## 5.7.0 (12/03/2017)
* Support rails 5.2
* Implement Bullet.delete_whitelist to delete a specific whitelist definition
* Fix caller_path in the case of nil
## 5.6.0 (07/16/2017)
* Migrate alias_method to Module#prepend
* Add install generator
* Stack trace filter
* Fix rails 5.1 compatibility
* Fix inverse_of for rails 5
* Fix detect file attachment for rack #319
## 5.5.0 (12/30/2016)
* Display http request method #311
* Add close button to footer
* Raise an error if bullet does not support AR or Mongoid
* Avoid double backtrace
* Fix false alert on counter cache when associations are already loaded #288
* Fix "false alert" in rails 5 #239
* Do not support ActiveRecord 3.x and Mongoid 3.x / 4.x anymore
## 5.4.0 (10/09/2016)
* Support rails 5.1
* Extract stack trace filtering into module
## 5.3.0 (15/08/2016)
* Fix false alert on through association with join sql #301
* Fix association.target in `through_association` can be singular #302
* Support `find_by_sql` #303
* Fix env `REQUEST_URI`
## 5.2.0 (07/26/2016)
* Fix `has_cached_counter?` is not defined in HABTM #297
* Fix false alert if preloaded association has no records #260
* Support Rails 5.0.0
## 5.1.0 (05/21/2016)
* Fix false alert when `empty?` used with `counter_cache`
* Fix `alias_method_chain` deprecation for rails 5
* Add response handling for non-Rails Rack responses
* Fix false alert when querying immediately after creation
* Fix UnusedEagerLoading bug when multiple eager loading query include same objects
## 5.0.0 (01/06/2016)
* Support Rails 5.0.0.beta1
* Fix `has_many :through` infinite loop issue
* Support mongoid 5.0.0
* Do not report association queries immediately after object creation to
require a preload
* Detect `counter_cache` for `has_many :through` association
* Compatible with `composite_primary_keys` gem
* Fix AR 4.2 SingularAssociation#reader result can be nil
* `perform_out_of_channel_notifications` should always be triggered
* Fix false positive with `belongs_to` -> `belongs_to` for active\_record 4.2
* Activate active\_record hacks only when Bullet already start
* Don't execute query when running `to_sql`
* Send backtrace to `uniform_notifier`
* Fix sse response check
* Dynamically delegate available notifiers to UniformNotifier
* Hotfix nil object when `add_impossible_object`
* Fix `has_one` then `has_many` associations in rails 4.2
* Append js and dom to html body in proper position
## 4.14.0 (10/03/2014)
* Support rails 4.2
* Polish notification output
* Fix warning: `*' interpreted as argument prefix
## 4.13.0 (07/19/2014)
* Support include? call on ar associations
## 4.12.0 (07/13/2014)
* Fix false n+1 queries caused by inversed objects.
* Replace .id with .primary_key_value
* Rename bullet_ar_key to bullet_key
* Fix rails sse detect
* Fix bullet using in test environment
* Memoize whoami
## 4.11.0 (06/24/2014)
* Support empty? call on ar associations
* Skip detecting if object is a new record
## 4.10.0 (06/06/2014)
* Handle join query smarter
* Support mongoid 4.0
* Thread safe
* Add debug mode
## 4.9.0 (04/30/2014)
* Add Bullet.stacktrace_includes option
* Applied keyword argument fixes on Ruby 2.2.0
* Add bugsnag notifier
* Support rails 4.1.0
## 4.8.0 (02/16/2014)
* Support rails 4.1.0.beta1
* Update specs to be RSpec 3.0 compatible
* Update latest minor version activerecord and mongoid on travis
## 4.7.0 (11/03/2013)
* Add coverall support
* Add helper to profile code outside a request
* Add activesupport dependency
* Add Bullet.raise notification
* Add Bullet.add_footer notification
* Fix activerecord4 warnings in test code
## 4.6.0 (04/18/2013)
* Fix Bullet::Rack to support sinatra
## 4.5.0 (03/24/2013)
* Add api way to access captured association
* Allow disable n_plus_one_query, unused_eager_loading and counter_cache respectively
* Add whitelist
## 4.4.0 (03/15/2013)
* Remove disable_browser_cache option
* Compatible with Rails 4.0.0.beta1
## 4.3.0 (12/28/2012)
* Fix content-length for non ascii html
* Add mongoid 2.5.x support
## 4.2.0 (09/29/2012)
* Add Bullet::Dependency to check AR and mongoid version
* Add Rails 4 support
* Add airbrake notifier support
## 4.1.0 (05/30/2012)
* Add mongoid 3 support
## 4.0.0 (05/09/2012)
* Add mongoid support
bullet-7.2.0/Gemfile 0000664 0000000 0000000 00000000763 14644222732 0014316 0 ustar 00root root 0000000 0000000 source 'https://rubygems.org'
git_source(:github) do |repo_name|
repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/')
"https://github.com/#{repo_name}.git"
end
gemspec
gem 'rails', github: 'rails'
gem 'sqlite3', platforms: [:ruby]
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem 'rspec'
gem 'guard'
gem 'guard-rspec'
gem 'coveralls', require: false
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.mongoid 0000664 0000000 0000000 00000000351 14644222732 0015742 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails'
gem 'sqlite3', platforms: [:ruby]
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'mongoid', github: 'mongoid/mongoid'
gem "rspec"
gem 'coveralls', require: false
bullet-7.2.0/Gemfile.mongoid-4.0 0000664 0000000 0000000 00000000426 14644222732 0016244 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 4.0.0'
gem 'sqlite3', platforms: [:ruby]
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'mongoid', '~> 4.0.0'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.mongoid-5.0 0000664 0000000 0000000 00000000426 14644222732 0016245 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 4.0.0'
gem 'sqlite3', platforms: [:ruby]
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'mongoid', '~> 5.1.0'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.mongoid-6.0 0000664 0000000 0000000 00000000426 14644222732 0016246 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 5.0.0'
gem 'sqlite3', platforms: [:ruby]
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'mongoid', '~> 6.0.0'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.mongoid-7.0 0000664 0000000 0000000 00000000424 14644222732 0016245 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 5.0'
gem 'sqlite3', platforms: [:ruby]
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'mongoid', '~> 7.0.0'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.mongoid-8.0 0000664 0000000 0000000 00000000422 14644222732 0016244 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 6.1'
gem 'sqlite3', platforms: [:ruby]
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'mongoid', '~> 8.0'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.rails-4.0 0000664 0000000 0000000 00000000520 14644222732 0015715 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 4.0.0'
gem 'sqlite3', '~> 1.3.6', platforms: [:ruby]
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem 'tins', '~> 1.6.0', platforms: [:ruby_19]
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.rails-4.1 0000664 0000000 0000000 00000000474 14644222732 0015726 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 4.1.0'
gem 'sqlite3', '~> 1.3.6'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem 'tins', '~> 1.6.0', platforms: [:ruby_19]
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.rails-4.2 0000664 0000000 0000000 00000000474 14644222732 0015727 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 4.2.0'
gem 'sqlite3', '~> 1.3.6'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem 'tins', '~> 1.6.0', platforms: [:ruby_19]
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.rails-5.0 0000664 0000000 0000000 00000000416 14644222732 0015722 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 5.0.0'
gem 'sqlite3', '~> 1.3.6'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.rails-5.1 0000664 0000000 0000000 00000000416 14644222732 0015723 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 5.1.0'
gem 'sqlite3', '~> 1.3.6'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.rails-5.2 0000664 0000000 0000000 00000000416 14644222732 0015724 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 5.2.0'
gem 'sqlite3', '~> 1.3.6'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.rails-6.0 0000664 0000000 0000000 00000000402 14644222732 0015716 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 6.0.0'
gem 'sqlite3'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.rails-6.1 0000664 0000000 0000000 00000000402 14644222732 0015717 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 6.1.0'
gem 'sqlite3'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem "rspec"
platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubinius-developer_tools'
end
bullet-7.2.0/Gemfile.rails-7.0 0000664 0000000 0000000 00000000273 14644222732 0015725 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 7.0.0'
gem 'sqlite3', '~> 1.4'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem "rspec"
bullet-7.2.0/Gemfile.rails-7.1 0000664 0000000 0000000 00000000273 14644222732 0015726 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem 'rails', '~> 7.1.0'
gem 'sqlite3', '~> 1.4'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem "rspec"
bullet-7.2.0/Gemfile.rails-7.2 0000664 0000000 0000000 00000000300 14644222732 0015716 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec
gem "rails", ">= 7.2.0.beta2", "< 7.3"
gem 'sqlite3'
gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby]
gem 'activerecord-import'
gem "rspec"
bullet-7.2.0/Guardfile 0000664 0000000 0000000 00000000516 14644222732 0014644 0 ustar 00root root 0000000 0000000 # A sample Guardfile
# More info at https://github.com/guard/guard#readme
guard 'rspec', version: 2, all_after_pass: false, all_on_start: false, cli: '--color --format nested --fail-fast' do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { 'spec' }
end
bullet-7.2.0/Hacking.md 0000664 0000000 0000000 00000006510 14644222732 0014705 0 ustar 00root root 0000000 0000000 # Bullet Overview for Developers
This file aims to give developers a quick tour of the bullet internals, making
it (hopefully) easier to extend or enhance the Bullet gem.
## General Control Flow aka. 10000 Meter View
When Rails is initialized, Bullet will extend ActiveRecord (and if you're using
Rails 2.x ActiveController too) with the relevant modules and methods found
in lib/bullet/active_recordX.rb and lib/bullet/action_controller2.rb. If you're
running Rails 3, Bullet will integrate itself as a middleware into the Rack
stack, so ActionController does not need to be extended.
The ActiveRecord extensions will call methods in a given detector class, when
certain methods are called.
Detector classes contain all the logic to recognize
a noteworthy event. If such an event is detected, an instance of the
corresponding Notification class is created and stored in a Set instance in the
main Bullet module (the 'notification collector').
Notification instances contain the message that will be displayed, and will
use a Presenter class to display their message to the user.
So the flow of a request goes like this:
1. Bullet.start_request is called, which resets all the detectors and empties
the notification collector
2. The request is handled by Rails, and the installed ActiveRecord extensions
trigger Detector callbacks
3. Detectors once called, will determine whether something noteworthy happened.
If yes, then a Notification is created and stored in the notification collector.
4. Rails finishes handling the request
5. For each notification in the collector, Bullet will iterate over each
Presenter and will try to generate an inline message that will be appended to
the generated response body.
6. The response is returned to the client.
7. Bullet will try to generate an out-of-channel message for each notification.
8. Bullet calls end_request for each detector.
9. Goto 1.
## Adding Notification Types
If you want to add more kinds of things that Bullet can detect, a little more
work is needed than if you were just adding a Presenter, but the concepts are
similar.
* Add the class to the DETECTORS constant in the main Bullet module
* Add (if needed) Rails monkey patches to Bullet.enable
* Add an autoload directive to lib/bullet/detector.rb
* Create a corresponding notification class in the Bullet::Notification namespace
* Add an autoload directive to lib/bullet/notification.rb
As a rule of thumb, you can assume that each Detector will have its own
Notification class. If you follow the principle of Separation of Concerns I
can't really think of an example where one would deviate from this rule.
Since the detection of pathological associations is a bit hairy, I'd recommend
having a look at the counter cache detector and associated notification to get
a feel for what is needed to get off the ground.
### Detectors
The only things you'll need to consider when building your Detector class is
that it will need to supply the .start_request, .end_request and .clear class
methods.
Simple implementations are provided by Bullet::Detector::Base for start_request
and end_request, you will have to supply your own clear method.
### Notifications
For notifications you will want to supply a #title and #body instance method,
and check to see if the #initialize and #full_notice methods in the
Bullet::Notification::Base class fit your needs.
bullet-7.2.0/MIT-LICENSE 0000664 0000000 0000000 00000002075 14644222732 0014455 0 ustar 00root root 0000000 0000000 Copyright (c) 2009 - 2022 Richard Huang (flyerhzm@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
bullet-7.2.0/README.md 0000664 0000000 0000000 00000036417 14644222732 0014307 0 ustar 00root root 0000000 0000000 # Bullet

[](http://badge.fury.io/rb/bullet)
[](https://awesomecode.io/repos/flyerhzm/bullet)
[](http://coderwall.com/flyerhzm)
The Bullet gem is designed to help you increase your application's performance by reducing the number of queries it makes. It will watch your queries while you develop your application and notify you when you should add eager loading (N+1 queries), when you're using eager loading that isn't necessary and when you should use counter cache.
Best practice is to use Bullet in development mode or custom mode (staging, profile, etc.). The last thing you want is your clients getting alerts about how lazy you are.
Bullet gem now supports **activerecord** >= 4.0 and **mongoid** >= 4.0.
If you use activerecord 2.x, please use bullet <= 4.5.0
If you use activerecord 3.x, please use bullet < 5.5.0
## External Introduction
* [http://railscasts.com/episodes/372-bullet](http://railscasts.com/episodes/372-bullet)
* [http://ruby5.envylabs.com/episodes/9-episode-8-september-8-2009](http://ruby5.envylabs.com/episodes/9-episode-8-september-8-2009)
* [http://railslab.newrelic.com/2009/10/23/episode-19-on-the-edge-part-1](http://railslab.newrelic.com/2009/10/23/episode-19-on-the-edge-part-1)
* [http://weblog.rubyonrails.org/2009/10/22/community-highlights](http://weblog.rubyonrails.org/2009/10/22/community-highlights)
## Install
You can install it as a gem:
```
gem install bullet
```
or add it into a Gemfile (Bundler):
```ruby
gem 'bullet', group: 'development'
```
enable the Bullet gem with generate command
```ruby
bundle exec rails g bullet:install
```
The generate command will auto generate the default configuration and may ask to include in the test environment as well. See below for custom configuration.
**Note**: make sure `bullet` gem is added after activerecord (rails) and
mongoid.
## Configuration
Bullet won't enable any notification systems unless you tell it to explicitly. Append to
`config/environments/development.rb` initializer with the following code:
```ruby
config.after_initialize do
Bullet.enable = true
Bullet.sentry = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.xmpp = { :account => 'bullets_account@jabber.org',
:password => 'bullets_password_for_jabber',
:receiver => 'your_account@jabber.org',
:show_online_status => true }
Bullet.rails_logger = true
Bullet.honeybadger = true
Bullet.bugsnag = true
Bullet.appsignal = true
Bullet.airbrake = true
Bullet.rollbar = true
Bullet.add_footer = true
Bullet.skip_html_injection = false
Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ]
Bullet.stacktrace_excludes = [ 'their_gem', 'their_middleware', ['my_file.rb', 'my_method'], ['my_file.rb', 16..20] ]
Bullet.slack = { webhook_url: 'http://some.slack.url', channel: '#default', username: 'notifier' }
end
```
The notifier of Bullet is a wrap of [uniform_notifier](https://github.com/flyerhzm/uniform_notifier)
The code above will enable all of the Bullet notification systems:
* `Bullet.enable`: enable Bullet gem, otherwise do nothing
* `Bullet.alert`: pop up a JavaScript alert in the browser
* `Bullet.bullet_logger`: log to the Bullet log file (Rails.root/log/bullet.log)
* `Bullet.console`: log warnings to your browser's console.log (Safari/Webkit browsers or Firefox w/Firebug installed)
* `Bullet.xmpp`: send XMPP/Jabber notifications to the receiver indicated. Note that the code will currently not handle the adding of contacts, so you will need to make both accounts indicated know each other manually before you will receive any notifications. If you restart the development server frequently, the 'coming online' sound for the Bullet account may start to annoy - in this case set :show_online_status to false; you will still get notifications, but the Bullet account won't announce it's online status anymore.
* `Bullet.rails_logger`: add warnings directly to the Rails log
* `Bullet.honeybadger`: add notifications to Honeybadger
* `Bullet.bugsnag`: add notifications to bugsnag
* `Bullet.airbrake`: add notifications to airbrake
* `Bullet.appsignal`: add notifications to AppSignal
* `Bullet.rollbar`: add notifications to rollbar
* `Bullet.sentry`: add notifications to sentry
* `Bullet.add_footer`: adds the details in the bottom left corner of the page. Double click the footer or use close button to hide footer.
* `Bullet.skip_html_injection`: prevents Bullet from injecting code into the returned HTML. This must be false for receiving alerts, showing the footer or console logging.
* `Bullet.skip_http_headers`: don't add headers to API requests, and remove the javascript that relies on them. Note that this prevents bullet from logging warnings to the browser console or updating the footer.
* `Bullet.stacktrace_includes`: include paths with any of these substrings in the stack trace, even if they are not in your main app
* `Bullet.stacktrace_excludes`: ignore paths with any of these substrings in the stack trace, even if they are not in your main app.
Each item can be a string (match substring), a regex, or an array where the first item is a path to match, and the second
item is a line number, a Range of line numbers, or a (bare) method name, to exclude only particular lines in a file.
* `Bullet.slack`: add notifications to slack
* `Bullet.raise`: raise errors, useful for making your specs fail unless they have optimized queries
* `Bullet.always_append_html_body`: always append the html body even if no notifications are present. Note: `console` or `add_footer` must also be true. Useful for Single Page Applications where the initial page load might not have any notifications present.
* `Bullet.skip_user_in_notification`: exclude the OS user (`whoami`) from notifications.
Bullet also allows you to disable any of its detectors.
```ruby
# Each of these settings defaults to true
# Detect N+1 queries
Bullet.n_plus_one_query_enable = false
# Detect eager-loaded associations which are not used
Bullet.unused_eager_loading_enable = false
# Detect unnecessary COUNT queries which could be avoided
# with a counter_cache
Bullet.counter_cache_enable = false
```
Note: When calling `Bullet.enable`, all other detectors are reset to their defaults (`true`) and need reconfiguring.
## Safe list
Sometimes Bullet may notify you of query problems you don't care to fix, or
which come from outside your code. You can add them to a safe list to ignore them:
```ruby
Bullet.add_safelist :type => :n_plus_one_query, :class_name => "Post", :association => :comments
Bullet.add_safelist :type => :unused_eager_loading, :class_name => "Post", :association => :comments
Bullet.add_safelist :type => :counter_cache, :class_name => "Country", :association => :cities
```
If you want to skip bullet in some specific controller actions, you can
do like
```ruby
class ApplicationController < ActionController::Base
around_action :skip_bullet, if: -> { defined?(Bullet) }
def skip_bullet
previous_value = Bullet.enable?
Bullet.enable = false
yield
ensure
Bullet.enable = previous_value
end
end
```
## Log
The Bullet log `log/bullet.log` will look something like this:
* N+1 Query:
```
2009-08-25 20:40:17[INFO] USE eager loading detected:
Post => [:comments]·
Add to your query: .includes([:comments])
2009-08-25 20:40:17[INFO] Call stack
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
/Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
```
The first log entry is a notification that N+1 queries have been encountered. The remaining entry is a stack trace so you can find exactly where the queries were invoked in your code, and fix them.
* Unused eager loading:
```
2009-08-25 20:53:56[INFO] AVOID eager loading detected
Post => [:comments]·
Remove from your query: .includes([:comments])
2009-08-25 20:53:56[INFO] Call stack
```
These lines are notifications that unused eager loadings have been encountered.
* Need counter cache:
```
2009-09-11 09:46:50[INFO] Need Counter Cache
Post => [:comments]
```
## XMPP/Jabber and Airbrake Support
see [https://github.com/flyerhzm/uniform_notifier](https://github.com/flyerhzm/uniform_notifier)
## Growl Support
Growl support is dropped from uniform_notifier 1.16.0, if you still want it, please use uniform_notifier 1.15.0.
## Important
If you find Bullet does not work for you, *please disable your browser's cache*.
## Advanced
### Work with ActiveJob
Include `Bullet::ActiveJob` in your `ApplicationJob`.
```ruby
class ApplicationJob < ActiveJob::Base
include Bullet::ActiveJob if Rails.env.development?
end
```
### Work with other background job solution
Use the Bullet.profile method.
```ruby
class ApplicationJob < ActiveJob::Base
around_perform do |_job, block|
Bullet.profile do
block.call
end
end
end
```
### Work with sinatra
Configure and use `Bullet::Rack`.
```ruby
configure :development do
Bullet.enable = true
Bullet.bullet_logger = true
use Bullet::Rack
end
```
If your application generates a Content-Security-Policy via a separate middleware, ensure that `Bullet::Rack` is loaded _before_ that middleware.
### Run in tests
First you need to enable Bullet in test environment.
```ruby
# config/environments/test.rb
config.after_initialize do
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.raise = true # raise an error if n+1 query occurs
end
```
Then wrap each test in Bullet api.
```ruby
# spec/rails_helper.rb
if Bullet.enable?
config.before(:each) do
Bullet.start_request
end
config.after(:each) do
Bullet.perform_out_of_channel_notifications if Bullet.notification?
Bullet.end_request
end
end
```
## Debug Mode
Bullet outputs some details info, to enable debug mode, set
`BULLET_DEBUG=true` env.
## Contributors
[https://github.com/flyerhzm/bullet/contributors](https://github.com/flyerhzm/bullet/contributors)
## Demo
Bullet is designed to function as you browse through your application in development. To see it in action,
you can follow these steps to create, detect, and fix example query problems.
1\. Create an example application
```
$ rails new test_bullet
$ cd test_bullet
$ rails g scaffold post name:string
$ rails g scaffold comment name:string post_id:integer
$ bundle exec rails db:migrate
```
2\. Change `app/models/post.rb` and `app/models/comment.rb`
```ruby
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
```
3\. Go to `rails c` and execute
```ruby
post1 = Post.create(:name => 'first')
post2 = Post.create(:name => 'second')
post1.comments.create(:name => 'first')
post1.comments.create(:name => 'second')
post2.comments.create(:name => 'third')
post2.comments.create(:name => 'fourth')
```
4\. Change the `app/views/posts/index.html.erb` to produce a N+1 query
```
<% @posts.each do |post| %>
| <%= post.name %> |
<%= post.comments.map(&:name) %> |
<%= link_to 'Show', post %> |
<%= link_to 'Edit', edit_post_path(post) %> |
<%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %> |
<% end %>
```
5\. Add the `bullet` gem to the `Gemfile`
```ruby
gem "bullet"
```
And run
```
bundle install
```
6\. enable the Bullet gem with generate command
```
bundle exec rails g bullet:install
```
7\. Start the server
```
$ rails s
```
8\. Visit `http://localhost:3000/posts` in browser, and you will see a popup alert box that says
```
The request has unused preload associations as follows:
None
The request has N+1 queries as follows:
model: Post => associations: [comment]
```
which means there is a N+1 query from the Post object to its Comment association.
In the meantime, there's a log appended into `log/bullet.log` file
```
2010-03-07 14:12:18[INFO] N+1 Query in /posts
Post => [:comments]
Add to your finder: :include => [:comments]
2010-03-07 14:12:18[INFO] N+1 Query method call stack
/home/flyerhzm/Downloads/test_bullet/app/views/posts/index.html.erb:14:in `_render_template__600522146_80203160_0'
/home/flyerhzm/Downloads/test_bullet/app/views/posts/index.html.erb:11:in `each'
/home/flyerhzm/Downloads/test_bullet/app/views/posts/index.html.erb:11:in `_render_template__600522146_80203160_0'
/home/flyerhzm/Downloads/test_bullet/app/controllers/posts_controller.rb:7:in `index'
```
The generated SQL is:
```
Post Load (1.0ms) SELECT * FROM "posts"
Comment Load (0.4ms) SELECT * FROM "comments" WHERE ("comments".post_id = 1)
Comment Load (0.3ms) SELECT * FROM "comments" WHERE ("comments".post_id = 2)
```
9\. To fix the N+1 query, change `app/controllers/posts_controller.rb` file
```ruby
def index
@posts = Post.includes(:comments)
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @posts }
end
end
```
10\. Refresh `http://localhost:3000/posts`. Now there's no alert box and nothing new in the log.
The generated SQL is:
```
Post Load (0.5ms) SELECT * FROM "posts"
Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE ("comments".post_id IN (1,2))
```
N+1 query fixed. Cool!
11\. Now simulate unused eager loading. Change
`app/controllers/posts_controller.rb` and
`app/views/posts/index.html.erb`
```ruby
def index
@posts = Post.includes(:comments)
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @posts }
end
end
```
```
<% @posts.each do |post| %>
| <%= post.name %> |
<%= link_to 'Show', post %> |
<%= link_to 'Edit', edit_post_path(post) %> |
<%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %> |
<% end %>
```
12\. Refresh `http://localhost:3000/posts`, and you will see a popup alert box that says
```
The request has unused preload associations as follows:
model: Post => associations: [comment]
The request has N+1 queries as follows:
None
```
Meanwhile, there's a line appended to `log/bullet.log`
```
2009-08-25 21:13:22[INFO] Unused preload associations: PATH_INFO: /posts; model: Post => associations: [comments]·
Remove from your finder: :include => [:comments]
```
13\. Simulate counter_cache. Change `app/controllers/posts_controller.rb`
and `app/views/posts/index.html.erb`
```ruby
def index
@posts = Post.all
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @posts }
end
end
```
```
<% @posts.each do |post| %>
| <%= post.name %> |
<%= post.comments.size %> |
<%= link_to 'Show', post %> |
<%= link_to 'Edit', edit_post_path(post) %> |
<%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %> |
<% end %>
```
14\. Refresh `http://localhost:3000/posts`, then you will see a popup alert box that says
```
Need counter cache
Post => [:comments]
```
Meanwhile, there's a line appended to `log/bullet.log`
```
2009-09-11 10:07:10[INFO] Need Counter Cache
Post => [:comments]
```
Copyright (c) 2009 - 2022 Richard Huang (flyerhzm@gmail.com), released under the MIT license
bullet-7.2.0/Rakefile 0000664 0000000 0000000 00000002242 14644222732 0014462 0 ustar 00root root 0000000 0000000 $LOAD_PATH.unshift File.expand_path('lib', __dir__)
require 'bundler'
Bundler.setup
require 'rake'
require 'rspec'
require 'rspec/core/rake_task'
require 'bullet/version'
task :build do
system 'gem build bullet.gemspec'
end
task install: :build do
system "sudo gem install bullet-#{Bullet::VERSION}.gem"
end
task release: :build do
puts "Tagging #{Bullet::VERSION}..."
system "git tag -a #{Bullet::VERSION} -m 'Tagging #{Bullet::VERSION}'"
puts 'Pushing to Github...'
system 'git push --tags'
puts 'Pushing to rubygems.org...'
system "gem push bullet-#{Bullet::VERSION}.gem"
end
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = 'spec/**/*_spec.rb'
end
RSpec::Core::RakeTask.new('spec:progress') do |spec|
spec.rspec_opts = %w[--format progress]
spec.pattern = 'spec/**/*_spec.rb'
end
begin
require 'rdoc/task'
desc 'Generate documentation for the plugin.'
Rake::RDocTask.new do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "bullet #{Bullet::VERSION}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
rescue LoadError
puts 'RDocTask is not supported for this platform'
end
task default: :spec
bullet-7.2.0/bullet.gemspec 0000664 0000000 0000000 00000002311 14644222732 0015646 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
require 'bullet/version'
Gem::Specification.new do |s|
s.name = 'bullet'
s.version = Bullet::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ['Richard Huang']
s.email = ['flyerhzm@gmail.com']
s.homepage = 'https://github.com/flyerhzm/bullet'
s.summary = 'help to kill N+1 queries and unused eager loading.'
s.description = 'help to kill N+1 queries and unused eager loading.'
s.metadata = {
'changelog_uri' => 'https://github.com/flyerhzm/bullet/blob/main/CHANGELOG.md',
'source_code_uri' => 'https://github.com/flyerhzm/bullet'
}
s.license = 'MIT'
s.required_ruby_version = '>= 2.3'
s.required_rubygems_version = '>= 1.3.6'
s.add_runtime_dependency 'activesupport', '>= 3.0.0'
s.add_runtime_dependency 'uniform_notifier', '~> 1.11'
s.files = Dir.chdir(__dir__) do
`git ls-files -z`.split("\x0").reject do |file|
file.start_with?(*%w[.git .rspec Gemfile Guardfile Hacking Rakefile
bullet.gemspec perf rails spec test.sh update.sh])
end
end
s.require_paths = ['lib']
end
bullet-7.2.0/lib/ 0000775 0000000 0000000 00000000000 14644222732 0013563 5 ustar 00root root 0000000 0000000 bullet-7.2.0/lib/bullet.rb 0000664 0000000 0000000 00000021000 14644222732 0015370 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'active_support/core_ext/module/delegation'
require 'set'
require 'uniform_notifier'
require 'bullet/ext/object'
require 'bullet/ext/string'
require 'bullet/dependency'
require 'bullet/stack_trace_filter'
module Bullet
extend Dependency
autoload :ActiveRecord, "bullet/#{active_record_version}" if active_record?
autoload :Mongoid, "bullet/#{mongoid_version}" if mongoid?
autoload :Rack, 'bullet/rack'
autoload :ActiveJob, 'bullet/active_job'
autoload :Notification, 'bullet/notification'
autoload :Detector, 'bullet/detector'
autoload :Registry, 'bullet/registry'
autoload :NotificationCollector, 'bullet/notification_collector'
if defined?(Rails::Railtie)
class BulletRailtie < Rails::Railtie
initializer 'bullet.configure_rails_initialization' do |app|
if defined?(ActionDispatch::ContentSecurityPolicy::Middleware) && Rails.application.config.content_security_policy
app.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, Bullet::Rack
else
app.middleware.use Bullet::Rack
end
end
end
end
class << self
attr_writer :n_plus_one_query_enable,
:unused_eager_loading_enable,
:counter_cache_enable,
:stacktrace_includes,
:stacktrace_excludes,
:skip_html_injection
attr_reader :safelist
attr_accessor :add_footer,
:orm_patches_applied,
:skip_http_headers,
:always_append_html_body,
:skip_user_in_notification
available_notifiers =
UniformNotifier::AVAILABLE_NOTIFIERS.select { |notifier| notifier != :raise }
.map { |notifier| "#{notifier}=" }
available_notifiers_options = { to: UniformNotifier }
delegate(*available_notifiers, **available_notifiers_options)
def raise=(should_raise)
UniformNotifier.raise = (should_raise ? Notification::UnoptimizedQueryError : false)
end
DETECTORS = [
Bullet::Detector::NPlusOneQuery,
Bullet::Detector::UnusedEagerLoading,
Bullet::Detector::CounterCache
].freeze
def enable=(enable)
@enable = @n_plus_one_query_enable = @unused_eager_loading_enable = @counter_cache_enable = enable
if enable?
reset_safelist
unless orm_patches_applied
self.orm_patches_applied = true
Bullet::Mongoid.enable if mongoid?
Bullet::ActiveRecord.enable if active_record?
end
end
end
alias enabled= enable=
def enable?
!!@enable
end
alias enabled? enable?
# Rails.root might be nil if `railties` is a dependency on a project that does not use Rails
def app_root
@app_root ||= (defined?(::Rails.root) && !::Rails.root.nil? ? Rails.root.to_s : Dir.pwd).to_s
end
def n_plus_one_query_enable?
enable? && !!@n_plus_one_query_enable
end
def unused_eager_loading_enable?
enable? && !!@unused_eager_loading_enable
end
def counter_cache_enable?
enable? && !!@counter_cache_enable
end
def stacktrace_includes
@stacktrace_includes ||= []
end
def stacktrace_excludes
@stacktrace_excludes ||= []
end
def add_safelist(options)
reset_safelist
@safelist[options[:type]][options[:class_name]] ||= []
@safelist[options[:type]][options[:class_name]] << options[:association].to_sym
end
def delete_safelist(options)
reset_safelist
@safelist[options[:type]][options[:class_name]] ||= []
@safelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
@safelist[options[:type]].delete_if { |_key, val| val.empty? }
end
def get_safelist_associations(type, class_name)
Array.wrap(@safelist[type][class_name])
end
def reset_safelist
@safelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
end
def clear_safelist
@safelist = nil
end
def bullet_logger=(active)
if active
require 'fileutils'
FileUtils.mkdir_p(app_root + '/log')
bullet_log_file = File.open("#{app_root}/log/bullet.log", 'a+')
bullet_log_file.sync = true
UniformNotifier.customized_logger = bullet_log_file
end
end
def debug(title, message)
puts "[Bullet][#{title}] #{message}" if ENV['BULLET_DEBUG'] == 'true'
end
def start_request
Thread.current[:bullet_start] = true
Thread.current[:bullet_notification_collector] = Bullet::NotificationCollector.new
Thread.current[:bullet_object_associations] = Bullet::Registry::Base.new
Thread.current[:bullet_call_object_associations] = Bullet::Registry::Base.new
Thread.current[:bullet_possible_objects] = Bullet::Registry::Object.new
Thread.current[:bullet_impossible_objects] = Bullet::Registry::Object.new
Thread.current[:bullet_inversed_objects] = Bullet::Registry::Base.new
Thread.current[:bullet_eager_loadings] = Bullet::Registry::Association.new
Thread.current[:bullet_call_stacks] = Bullet::Registry::CallStack.new
Thread.current[:bullet_counter_possible_objects] ||= Bullet::Registry::Object.new
Thread.current[:bullet_counter_impossible_objects] ||= Bullet::Registry::Object.new
end
def end_request
Thread.current[:bullet_start] = nil
Thread.current[:bullet_notification_collector] = nil
Thread.current[:bullet_object_associations] = nil
Thread.current[:bullet_call_object_associations] = nil
Thread.current[:bullet_possible_objects] = nil
Thread.current[:bullet_impossible_objects] = nil
Thread.current[:bullet_inversed_objects] = nil
Thread.current[:bullet_eager_loadings] = nil
Thread.current[:bullet_counter_possible_objects] = nil
Thread.current[:bullet_counter_impossible_objects] = nil
end
def start?
enable? && Thread.current[:bullet_start]
end
def notification_collector
Thread.current[:bullet_notification_collector]
end
def notification?
return unless start?
Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
notification_collector.notifications_present?
end
def gather_inline_notifications
responses = []
for_each_active_notifier_with_notification { |notification| responses << notification.notify_inline }
responses.join("\n")
end
def perform_out_of_channel_notifications(env = {})
request_uri = build_request_uri(env)
for_each_active_notifier_with_notification do |notification|
notification.url = request_uri
notification.notify_out_of_channel
end
end
def footer_info
info = []
notification_collector.collection.each { |notification| info << notification.short_notice }
info
end
def text_notifications
info = []
notification_collector.collection.each do |notification|
info << notification.notification_data.values.compact.join("\n")
end
info
end
def warnings
notification_collector.collection.each_with_object({}) do |notification, warnings|
warning_type = notification.class.to_s.split(':').last.tableize
warnings[warning_type] ||= []
warnings[warning_type] << notification
end
end
def profile
return_value = nil
if Bullet.enable?
begin
Bullet.start_request
return_value = yield
Bullet.perform_out_of_channel_notifications if Bullet.notification?
ensure
Bullet.end_request
end
else
return_value = yield
end
return_value
end
def console_enabled?
UniformNotifier.active_notifiers.include?(UniformNotifier::JavascriptConsole)
end
def inject_into_page?
return false if defined?(@skip_html_injection) && @skip_html_injection
console_enabled? || add_footer
end
private
def for_each_active_notifier_with_notification
UniformNotifier.active_notifiers.each do |notifier|
notification_collector.collection.each do |notification|
notification.notifier = notifier
yield notification
end
end
end
def build_request_uri(env)
return "#{env['REQUEST_METHOD']} #{env['REQUEST_URI']}" if env['REQUEST_URI']
if env['QUERY_STRING'].present?
"#{env['REQUEST_METHOD']} #{env['PATH_INFO']}?#{env['QUERY_STRING']}"
else
"#{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
end
end
end
end
bullet-7.2.0/lib/bullet/ 0000775 0000000 0000000 00000000000 14644222732 0015052 5 ustar 00root root 0000000 0000000 bullet-7.2.0/lib/bullet/active_job.rb 0000664 0000000 0000000 00000000355 14644222732 0017507 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module ActiveJob
def self.included(base)
base.class_eval do
around_perform do |_job, block|
Bullet.profile { block.call }
end
end
end
end
end
bullet-7.2.0/lib/bullet/active_record4.rb 0000664 0000000 0000000 00000016764 14644222732 0020312 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.class_eval do
class << self
alias_method :origin_find_by_sql, :find_by_sql
def find_by_sql(sql, binds = [])
result = origin_find_by_sql(sql, binds)
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
end
::ActiveRecord::Relation.class_eval do
alias_method :origin_to_a, :to_a
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def to_a
records = origin_to_a
if Bullet.start?
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
records
end
end
::ActiveRecord::Persistence.class_eval do
def _create_record_with_bullet(*args)
_create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.add_impossible_object(self) }
end
alias_method_chain :_create_record, :bullet
end
::ActiveRecord::Associations::Preloader.class_eval do
# include query for one to many associations.
# keep this eager loadings.
alias_method :origin_initialize, :initialize
def initialize(records, associations, preload_scope = nil)
origin_initialize(records, associations, preload_scope)
if Bullet.start?
records = [records].flatten.compact.uniq
return if records.empty?
records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations)
end
end
end
::ActiveRecord::FinderMethods.class_eval do
# add includes in scope
alias_method :origin_find_with_associations, :find_with_associations
def find_with_associations
records = origin_find_with_associations
if Bullet.start?
associations = (eager_load_values + includes_values).uniq
records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations)
end
records
end
end
::ActiveRecord::Associations::JoinDependency.class_eval do
alias_method :origin_instantiate, :instantiate
alias_method :origin_construct_association, :construct_association
def instantiate(rows)
@bullet_eager_loadings = {}
records = origin_instantiate(rows)
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(objects, eager_loadings_hash[objects.first].to_a)
end
end
records
end
# call join associations
def construct_association(record, join, row)
result = origin_construct_association(record, join, row)
if Bullet.start?
associations = [join.reflection.name]
if join.reflection.nested?
associations << join.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
::ActiveRecord::Associations::CollectionAssociation.class_eval do
# call one to many associations
alias_method :origin_load_target, :load_target
def load_target
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start?
origin_load_target
end
alias_method :origin_include?, :include?
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start?
origin_include?(object)
end
end
::ActiveRecord::Associations::HasManyAssociation.class_eval do
alias_method :origin_empty?, :empty?
def empty?
if Bullet.start? && !loaded? && !has_cached_counter?(@reflection)
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
end
origin_empty?
end
end
::ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
alias_method :origin_empty?, :empty?
def empty?
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start? && !loaded?
origin_empty?
end
end
::ActiveRecord::Associations::SingularAssociation.class_eval do
# call has_one and belongs_to associations
alias_method :origin_reader, :reader
def reader(force_reload = false)
result = origin_reader(force_reload)
if Bullet.start?
unless @inversed
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
end
end
result
end
end
::ActiveRecord::Associations::HasManyAssociation.class_eval do
alias_method :origin_has_cached_counter?, :has_cached_counter?
def has_cached_counter?(reflection = reflection())
result = origin_has_cached_counter?(reflection)
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name) if Bullet.start? && !result
result
end
end
::ActiveRecord::Associations::CollectionProxy.class_eval do
def count(column_name = nil, options = {})
if Bullet.start?
Bullet::Detector::CounterCache.add_counter_cache(proxy_association.owner, proxy_association.reflection.name)
Bullet::Detector::NPlusOneQuery.call_association(proxy_association.owner, proxy_association.reflection.name)
end
super(column_name, options)
end
end
end
end
end
bullet-7.2.0/lib/bullet/active_record41.rb 0000664 0000000 0000000 00000016344 14644222732 0020365 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.class_eval do
class << self
alias_method :origin_find_by_sql, :find_by_sql
def find_by_sql(sql, binds = [])
result = origin_find_by_sql(sql, binds)
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
end
::ActiveRecord::Relation.class_eval do
alias_method :origin_to_a, :to_a
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def to_a
records = origin_to_a
if Bullet.start?
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
end
::ActiveRecord::Persistence.class_eval do
def _create_record_with_bullet(*args)
_create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.add_impossible_object(self) }
end
alias_method_chain :_create_record, :bullet
end
::ActiveRecord::Associations::Preloader.class_eval do
alias_method :origin_preloaders_on, :preloaders_on
def preloaders_on(association, records, scope)
if Bullet.start?
records.compact!
if records.first.class.name !~ /^HABTM_/
records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association)
end
end
origin_preloaders_on(association, records, scope)
end
end
::ActiveRecord::FinderMethods.class_eval do
# add includes in scope
alias_method :origin_find_with_associations, :find_with_associations
def find_with_associations
return origin_find_with_associations { |r| yield r } if block_given?
records = origin_find_with_associations
if Bullet.start?
associations = (eager_load_values + includes_values).uniq
records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations)
end
records
end
end
::ActiveRecord::Associations::JoinDependency.class_eval do
alias_method :origin_instantiate, :instantiate
alias_method :origin_construct_model, :construct_model
def instantiate(result_set, aliases)
@bullet_eager_loadings = {}
records = origin_instantiate(result_set, aliases)
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(objects, eager_loadings_hash[objects.first].to_a)
end
end
records
end
# call join associations
def construct_model(record, node, row, model_cache, id, aliases)
result = origin_construct_model(record, node, row, model_cache, id, aliases)
if Bullet.start?
associations = [node.reflection.name]
if node.reflection.nested?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
::ActiveRecord::Associations::CollectionAssociation.class_eval do
# call one to many associations
alias_method :origin_load_target, :load_target
def load_target
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start? && !@inversed
origin_load_target
end
alias_method :origin_empty?, :empty?
def empty?
if Bullet.start? && !has_cached_counter?(@reflection)
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
end
origin_empty?
end
alias_method :origin_include?, :include?
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start?
origin_include?(object)
end
end
::ActiveRecord::Associations::SingularAssociation.class_eval do
# call has_one and belongs_to associations
alias_method :origin_reader, :reader
def reader(force_reload = false)
result = origin_reader(force_reload)
if Bullet.start?
if @owner.class.name !~ /^HABTM_/ && !@inversed
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
end
end
result
end
end
::ActiveRecord::Associations::HasManyAssociation.class_eval do
alias_method :origin_count_records, :count_records
def count_records
result = has_cached_counter?
Bullet::Detector::CounterCache.add_counter_cache(@owner, @reflection.name) if Bullet.start? && !result
origin_count_records
end
end
::ActiveRecord::Associations::CollectionProxy.class_eval do
def count(column_name = nil, options = {})
if Bullet.start?
Bullet::Detector::CounterCache.add_counter_cache(proxy_association.owner, proxy_association.reflection.name)
Bullet::Detector::NPlusOneQuery.call_association(proxy_association.owner, proxy_association.reflection.name)
end
super(column_name, options)
end
end
end
end
end
bullet-7.2.0/lib/bullet/active_record42.rb 0000664 0000000 0000000 00000024004 14644222732 0020356 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.class_eval do
class << self
alias_method :origin_find, :find
def find(*args)
result = origin_find(*args)
if Bullet.start?
if result.is_a? Array
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
alias_method :origin_find_by_sql, :find_by_sql
def find_by_sql(sql, binds = [])
result = origin_find_by_sql(sql, binds)
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
end
::ActiveRecord::Persistence.class_eval do
def _create_record_with_bullet(*args)
_create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.add_impossible_object(self) }
end
alias_method_chain :_create_record, :bullet
end
::ActiveRecord::Relation.class_eval do
alias_method :origin_to_a, :to_a
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def to_a
records = origin_to_a
if Bullet.start?
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
end
::ActiveRecord::Associations::Preloader.class_eval do
alias_method :origin_preloaders_on, :preloaders_on
def preloaders_on(association, records, scope)
if Bullet.start?
records.compact!
if records.first.class.name !~ /^HABTM_/
records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association)
end
end
origin_preloaders_on(association, records, scope)
end
end
::ActiveRecord::FinderMethods.class_eval do
# add includes in scope
alias_method :origin_find_with_associations, :find_with_associations
def find_with_associations
return origin_find_with_associations { |r| yield r } if block_given?
records = origin_find_with_associations
if Bullet.start?
associations = (eager_load_values + includes_values).uniq
records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations)
end
records
end
end
::ActiveRecord::Associations::JoinDependency.class_eval do
alias_method :origin_instantiate, :instantiate
alias_method :origin_construct, :construct
alias_method :origin_construct_model, :construct_model
def instantiate(result_set, aliases)
@bullet_eager_loadings = {}
records = origin_instantiate(result_set, aliases)
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(objects, eager_loadings_hash[objects.first].to_a)
end
end
records
end
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
if Bullet.start?
unless ar_parent.nil?
parent.children.each do |node|
key = aliases.column_alias(node, node.primary_key)
id = row[key]
next unless id.nil?
associations = [node.reflection.name]
if node.reflection.nested?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(ar_parent, association)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << association
end
end
end
end
origin_construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
end
# call join associations
def construct_model(record, node, row, model_cache, id, aliases)
result = origin_construct_model(record, node, row, model_cache, id, aliases)
if Bullet.start?
associations = [node.reflection.name]
if node.reflection.nested?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
::ActiveRecord::Associations::CollectionAssociation.class_eval do
# call one to many associations
alias_method :origin_load_target, :load_target
def load_target
records = origin_load_target
if Bullet.start?
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless @inversed
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
alias_method :origin_empty?, :empty?
def empty?
if Bullet.start? && !has_cached_counter?(@reflection)
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
end
origin_empty?
end
alias_method :origin_include?, :include?
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start?
origin_include?(object)
end
end
::ActiveRecord::Associations::SingularAssociation.class_eval do
# call has_one and belongs_to associations
alias_method :origin_reader, :reader
def reader(force_reload = false)
result = origin_reader(force_reload)
if Bullet.start?
if @owner.class.name !~ /^HABTM_/ && !@inversed
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
if Bullet::Detector::NPlusOneQuery.impossible?(@owner)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
else
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
end
end
end
result
end
end
::ActiveRecord::Associations::HasManyAssociation.class_eval do
alias_method :origin_many_empty?, :empty?
def empty?
result = origin_many_empty?
if Bullet.start? && !has_cached_counter?(@reflection)
Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
end
result
end
alias_method :origin_count_records, :count_records
def count_records
result = has_cached_counter?
Bullet::Detector::CounterCache.add_counter_cache(@owner, @reflection.name) if Bullet.start? && !result
origin_count_records
end
end
::ActiveRecord::Associations::CollectionProxy.class_eval do
def count(column_name = nil, options = {})
if Bullet.start?
Bullet::Detector::CounterCache.add_counter_cache(proxy_association.owner, proxy_association.reflection.name)
Bullet::Detector::NPlusOneQuery.call_association(proxy_association.owner, proxy_association.reflection.name)
end
super(column_name, options)
end
end
end
end
end
bullet-7.2.0/lib/bullet/active_record5.rb 0000664 0000000 0000000 00000025216 14644222732 0020303 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module SaveWithBulletSupport
def _create_record(*)
super do
Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
yield(self) if block_given?
end
end
end
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.extend(
Module.new do
def find_by_sql(sql, binds = [], preparable: nil, &block)
result = super
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
)
::ActiveRecord::Base.prepend(SaveWithBulletSupport)
::ActiveRecord::Relation.prepend(
Module.new do
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def records
result = super
if Bullet.start?
if result.first.class.name !~ /^HABTM_/
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
end
end
result
end
end
)
::ActiveRecord::Associations::Preloader.prepend(
Module.new do
def preloaders_for_one(association, records, scope)
if Bullet.start?
records.compact!
if records.first.class.name !~ /^HABTM_/
records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association)
end
end
super
end
end
)
::ActiveRecord::FinderMethods.prepend(
Module.new do
# add includes in scope
def find_with_associations
return super { |r| yield r } if block_given?
records = super
if Bullet.start?
associations = (eager_load_values + includes_values).uniq
records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations)
end
records
end
end
)
::ActiveRecord::Associations::JoinDependency.prepend(
Module.new do
if ::ActiveRecord::Associations::JoinDependency.instance_method(:instantiate).parameters.last[0] == :block
# ActiveRecord >= 5.1.5
def instantiate(result_set, &block)
@bullet_eager_loadings = {}
records = super
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
objects,
eager_loadings_hash[objects.first].to_a
)
end
end
records
end
else
# ActiveRecord <= 5.1.4
def instantiate(result_set, aliases)
@bullet_eager_loadings = {}
records = super
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
objects,
eager_loadings_hash[objects.first].to_a
)
end
end
records
end
end
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
if Bullet.start?
unless ar_parent.nil?
parent.children.each do |node|
key = aliases.column_alias(node, node.primary_key)
id = row[key]
next unless id.nil?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(ar_parent, association)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << association
end
end
end
end
super
end
# call join associations
def construct_model(record, node, row, model_cache, id, aliases)
result = super
if Bullet.start?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
)
::ActiveRecord::Associations::CollectionAssociation.prepend(
Module.new do
def load_target
records = super
if Bullet.start?
if is_a? ::ActiveRecord::Associations::ThroughAssociation
refl = reflection.through_reflection
association = owner.association(refl.name)
if association.loaded?
Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if refl.through_reflection?
refl = refl.through_reflection while refl.through_reflection?
Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
end
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
def empty?
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
super
end
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
super
end
end
)
::ActiveRecord::Associations::SingularAssociation.prepend(
Module.new do
# call has_one and belongs_to associations
def target
result = super()
if Bullet.start?
if owner.class.name !~ /^HABTM_/ && !@inversed
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if Bullet::Detector::NPlusOneQuery.impossible?(owner)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
else
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
end
end
end
result
end
end
)
::ActiveRecord::Associations::HasManyAssociation.prepend(
Module.new do
def empty?
result = super
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
result
end
def count_records
result = reflection.has_cached_counter?
if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionProxy.prepend(
Module.new do
def count(column_name = nil)
if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(
proxy_association.owner,
proxy_association.reflection.name
)
Bullet::Detector::NPlusOneQuery.call_association(
proxy_association.owner,
proxy_association.reflection.name
)
end
super(column_name)
end
end
)
end
end
end
bullet-7.2.0/lib/bullet/active_record52.rb 0000664 0000000 0000000 00000024321 14644222732 0020361 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module SaveWithBulletSupport
def _create_record(*)
super do
Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
yield(self) if block_given?
end
end
end
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.extend(
Module.new do
def find_by_sql(sql, binds = [], preparable: nil, &block)
result = super
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
)
::ActiveRecord::Base.prepend(SaveWithBulletSupport)
::ActiveRecord::Relation.prepend(
Module.new do
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def records
result = super
if Bullet.start?
if result.first.class.name !~ /^HABTM_/
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
end
end
result
end
end
)
::ActiveRecord::Associations::Preloader.prepend(
Module.new do
def preloaders_for_one(association, records, scope)
if Bullet.start?
records.compact!
if records.first.class.name !~ /^HABTM_/
records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association)
end
end
super
end
end
)
::ActiveRecord::Associations::JoinDependency.prepend(
Module.new do
def instantiate(result_set, &block)
@bullet_eager_loadings = {}
records = super
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
objects,
eager_loadings_hash[objects.first].to_a
)
end
end
records
end
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
if Bullet.start?
unless ar_parent.nil?
parent.children.each do |node|
key = aliases.column_alias(node, node.primary_key)
id = row[key]
next unless id.nil?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(ar_parent, association)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << association
end
end
end
end
super
end
# call join associations
def construct_model(record, node, row, model_cache, id, aliases)
result = super
if Bullet.start?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
)
::ActiveRecord::Associations::Association.prepend(
Module.new do
def inversed_from(record)
if Bullet.start?
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionAssociation.prepend(
Module.new do
def load_target
records = super
if Bullet.start?
if is_a? ::ActiveRecord::Associations::ThroughAssociation
association = owner.association(reflection.through_reflection.name)
if association.loaded?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
def empty?
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
super
end
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
super
end
end
)
::ActiveRecord::Associations::SingularAssociation.prepend(
Module.new do
# call has_one and belongs_to associations
def target
result = super()
if Bullet.start?
if owner.class.name !~ /^HABTM_/ && !@inversed
if is_a? ::ActiveRecord::Associations::ThroughAssociation
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
association = owner.association reflection.through_reflection.name
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if Bullet::Detector::NPlusOneQuery.impossible?(owner)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
else
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
end
end
end
result
end
end
)
::ActiveRecord::Associations::HasManyAssociation.prepend(
Module.new do
def empty?
result = super
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
result
end
def count_records
result = reflection.has_cached_counter?
if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionProxy.prepend(
Module.new do
def count(column_name = nil)
if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(
proxy_association.owner,
proxy_association.reflection.name
)
Bullet::Detector::NPlusOneQuery.call_association(
proxy_association.owner,
proxy_association.reflection.name
)
end
super(column_name)
end
end
)
end
end
end
bullet-7.2.0/lib/bullet/active_record60.rb 0000664 0000000 0000000 00000026356 14644222732 0020372 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module SaveWithBulletSupport
def _create_record(*)
super do
Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
yield(self) if block_given?
end
end
end
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.extend(
Module.new do
def find_by_sql(sql, binds = [], preparable: nil, &block)
result = super
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
)
::ActiveRecord::Base.prepend(SaveWithBulletSupport)
::ActiveRecord::Relation.prepend(
Module.new do
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def records
result = super
if Bullet.start?
if result.first.class.name !~ /^HABTM_/
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
end
end
result
end
end
)
::ActiveRecord::Associations::Preloader.prepend(
Module.new do
def preloaders_for_one(association, records, scope, polymorphic_parent)
if Bullet.start?
records.compact!
if records.first.class.name !~ /^HABTM_/
records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association)
end
end
super
end
def preloaders_for_reflection(reflection, records, scope)
if Bullet.start?
records.compact!
if records.first.class.name !~ /^HABTM_/
records.each { |record| Bullet::Detector::Association.add_object_associations(record, reflection.name) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, reflection.name)
end
end
super
end
end
)
::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(
Module.new do
def preloaded_records
if Bullet.start? && !defined?(@preloaded_records)
source_preloaders.each do |source_preloader|
reflection_name = source_preloader.send(:reflection).name
source_preloader.send(:owners).each do |owner|
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name)
end
end
end
super
end
end
)
::ActiveRecord::Associations::JoinDependency.prepend(
Module.new do
def instantiate(result_set, &block)
@bullet_eager_loadings = {}
records = super
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
objects,
eager_loadings_hash[objects.first].to_a
)
end
end
records
end
def construct(ar_parent, parent, row, seen, model_cache)
if Bullet.start?
unless ar_parent.nil?
parent.children.each do |node|
key = aliases.column_alias(node, node.primary_key)
id = row[key]
next unless id.nil?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(ar_parent, association)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << association
end
end
end
end
super
end
# call join associations
def construct_model(record, node, row, model_cache, id)
result = super
if Bullet.start?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
)
::ActiveRecord::Associations::Association.prepend(
Module.new do
def inversed_from(record)
if Bullet.start?
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionAssociation.prepend(
Module.new do
def load_target
records = super
if Bullet.start?
if is_a? ::ActiveRecord::Associations::ThroughAssociation
association = owner.association(reflection.through_reflection.name)
if association.loaded?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
def empty?
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
super
end
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
super
end
end
)
::ActiveRecord::Associations::SingularAssociation.prepend(
Module.new do
# call has_one and belongs_to associations
def target
result = super()
if Bullet.start?
if owner.class.name !~ /^HABTM_/ && !@inversed
if is_a? ::ActiveRecord::Associations::ThroughAssociation
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
association = owner.association(reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if Bullet::Detector::NPlusOneQuery.impossible?(owner)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
else
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
end
end
end
result
end
end
)
::ActiveRecord::Associations::HasManyAssociation.prepend(
Module.new do
def empty?
result = super
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
result
end
def count_records
result = reflection.has_cached_counter?
if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionProxy.prepend(
Module.new do
def count(column_name = nil)
if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(
proxy_association.owner,
proxy_association.reflection.name
)
Bullet::Detector::NPlusOneQuery.call_association(
proxy_association.owner,
proxy_association.reflection.name
)
end
super(column_name)
end
end
)
end
end
end
bullet-7.2.0/lib/bullet/active_record61.rb 0000664 0000000 0000000 00000026460 14644222732 0020367 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module SaveWithBulletSupport
def _create_record(*)
super do
Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
yield(self) if block_given?
end
end
end
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.extend(
Module.new do
def find_by_sql(sql, binds = [], preparable: nil, &block)
result = super
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
)
::ActiveRecord::Base.prepend(SaveWithBulletSupport)
::ActiveRecord::Relation.prepend(
Module.new do
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def records
result = super
if Bullet.start?
if result.first.class.name !~ /^HABTM_/
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
end
end
result
end
end
)
::ActiveRecord::Associations::Preloader.prepend(
Module.new do
def preloaders_for_one(association, records, scope, polymorphic_parent)
if Bullet.start?
records.compact!
if records.first.class.name !~ /^HABTM_/
records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association)
end
end
super
end
def preloaders_for_reflection(reflection, records, scope)
if Bullet.start?
records.compact!
if records.first.class.name !~ /^HABTM_/
records.each { |record| Bullet::Detector::Association.add_object_associations(record, reflection.name) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, reflection.name)
end
end
super
end
end
)
::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(
Module.new do
def preloaded_records
if Bullet.start? && !defined?(@preloaded_records)
source_preloaders.each do |source_preloader|
reflection_name = source_preloader.send(:reflection).name
source_preloader.send(:owners).each do |owner|
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name)
end
end
end
super
end
end
)
::ActiveRecord::Associations::JoinDependency.prepend(
Module.new do
def instantiate(result_set, strict_loading_value, &block)
@bullet_eager_loadings = {}
records = super
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
objects,
eager_loadings_hash[objects.first].to_a
)
end
end
records
end
def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value)
if Bullet.start?
unless ar_parent.nil?
parent.children.each do |node|
key = aliases.column_alias(node, node.primary_key)
id = row[key]
next unless id.nil?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(ar_parent, association)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << association
end
end
end
end
super
end
# call join associations
def construct_model(record, node, row, model_cache, id, strict_loading_value)
result = super
if Bullet.start?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
)
::ActiveRecord::Associations::Association.prepend(
Module.new do
def inversed_from(record)
if Bullet.start?
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionAssociation.prepend(
Module.new do
def load_target
records = super
if Bullet.start?
if is_a? ::ActiveRecord::Associations::ThroughAssociation
association = owner.association(reflection.through_reflection.name)
if association.loaded?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
def empty?
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
super
end
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
super
end
end
)
::ActiveRecord::Associations::SingularAssociation.prepend(
Module.new do
# call has_one and belongs_to associations
def target
result = super()
if Bullet.start?
if owner.class.name !~ /^HABTM_/ && !@inversed
if is_a? ::ActiveRecord::Associations::ThroughAssociation
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
association = owner.association(reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if Bullet::Detector::NPlusOneQuery.impossible?(owner)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
else
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
end
end
end
result
end
end
)
::ActiveRecord::Associations::HasManyAssociation.prepend(
Module.new do
def empty?
result = super
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
result
end
def count_records
result = reflection.has_cached_counter?
if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionProxy.prepend(
Module.new do
def count(column_name = nil)
if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(
proxy_association.owner,
proxy_association.reflection.name
)
Bullet::Detector::NPlusOneQuery.call_association(
proxy_association.owner,
proxy_association.reflection.name
)
end
super(column_name)
end
end
)
end
end
end
bullet-7.2.0/lib/bullet/active_record70.rb 0000664 0000000 0000000 00000027271 14644222732 0020370 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module SaveWithBulletSupport
def _create_record(*)
super do
Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
yield(self) if block_given?
end
end
end
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.extend(
Module.new do
def find_by_sql(sql, binds = [], preparable: nil, &block)
result = super
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
)
::ActiveRecord::Base.prepend(SaveWithBulletSupport)
::ActiveRecord::Relation.prepend(
Module.new do
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def records
result = super
if Bullet.start?
if result.first.class.name !~ /^HABTM_/
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
end
end
result
end
end
)
::ActiveRecord::Associations::Preloader::Batch.prepend(
Module.new do
def call
if Bullet.start?
@preloaders.each do |preloader|
preloader.records.each { |record|
Bullet::Detector::Association.add_object_associations(record, preloader.associations)
}
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(preloader.records, preloader.associations)
end
end
super
end
end
)
::ActiveRecord::Associations::Preloader::Branch.prepend(
Module.new do
def preloaders_for_reflection(reflection, reflection_records)
if Bullet.start?
reflection_records.compact!
if reflection_records.first.class.name !~ /^HABTM_/
reflection_records.each { |record|
Bullet::Detector::Association.add_object_associations(record, reflection.name)
}
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(reflection_records, reflection.name)
end
end
super
end
end
)
::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(
Module.new do
def source_preloaders
if Bullet.start? && !defined?(@source_preloaders)
preloaders = super
preloaders.each do |preloader|
reflection_name = preloader.send(:reflection).name
preloader.send(:owners).each do |owner|
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name)
end
end
else
super
end
end
end
)
::ActiveRecord::Associations::JoinDependency.prepend(
Module.new do
def instantiate(result_set, strict_loading_value, &block)
@bullet_eager_loadings = {}
records = super
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
objects,
eager_loadings_hash[objects.first].to_a
)
end
end
records
end
def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value)
if Bullet.start?
unless ar_parent.nil?
parent.children.each do |node|
key = aliases.column_alias(node, node.primary_key)
id = row[key]
next unless id.nil?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(ar_parent, association)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << association
end
end
end
end
super
end
# call join associations
def construct_model(record, node, row, model_cache, id, strict_loading_value)
result = super
if Bullet.start?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
)
::ActiveRecord::Associations::Association.prepend(
Module.new do
def inversed_from(record)
if Bullet.start?
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
end
super
end
def inversed_from_queries(record)
if Bullet.start? && inversable?(record)
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionAssociation.prepend(
Module.new do
def load_target
records = super
if Bullet.start?
if is_a? ::ActiveRecord::Associations::ThroughAssociation
association = owner.association(reflection.through_reflection.name)
if association.loaded?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
def empty?
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
super
end
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
super
end
end
)
::ActiveRecord::Associations::SingularAssociation.prepend(
Module.new do
# call has_one and belongs_to associations
def reader
result = super
if Bullet.start?
if owner.class.name !~ /^HABTM_/
if is_a? ::ActiveRecord::Associations::ThroughAssociation
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
association = owner.association(reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if Bullet::Detector::NPlusOneQuery.impossible?(owner)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
else
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
end
end
end
result
end
end
)
::ActiveRecord::Associations::HasManyAssociation.prepend(
Module.new do
def empty?
result = super
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
result
end
def count_records
result = reflection.has_cached_counter?
if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionProxy.prepend(
Module.new do
def count(column_name = nil)
if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(
proxy_association.owner,
proxy_association.reflection.name
)
Bullet::Detector::NPlusOneQuery.call_association(
proxy_association.owner,
proxy_association.reflection.name
)
end
super(column_name)
end
end
)
end
end
end
bullet-7.2.0/lib/bullet/active_record71.rb 0000664 0000000 0000000 00000027271 14644222732 0020371 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module SaveWithBulletSupport
def _create_record(*)
super do
Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
yield(self) if block_given?
end
end
end
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.extend(
Module.new do
def find_by_sql(sql, binds = [], preparable: nil, &block)
result = super
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
)
::ActiveRecord::Base.prepend(SaveWithBulletSupport)
::ActiveRecord::Relation.prepend(
Module.new do
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def records
result = super
if Bullet.start?
if result.first.class.name !~ /^HABTM_/
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
end
end
result
end
end
)
::ActiveRecord::Associations::Preloader::Batch.prepend(
Module.new do
def call
if Bullet.start?
@preloaders.each do |preloader|
preloader.records.each { |record|
Bullet::Detector::Association.add_object_associations(record, preloader.associations)
}
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(preloader.records, preloader.associations)
end
end
super
end
end
)
::ActiveRecord::Associations::Preloader::Branch.prepend(
Module.new do
def preloaders_for_reflection(reflection, reflection_records)
if Bullet.start?
reflection_records.compact!
if reflection_records.first.class.name !~ /^HABTM_/
reflection_records.each { |record|
Bullet::Detector::Association.add_object_associations(record, reflection.name)
}
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(reflection_records, reflection.name)
end
end
super
end
end
)
::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(
Module.new do
def source_preloaders
if Bullet.start? && !defined?(@source_preloaders)
preloaders = super
preloaders.each do |preloader|
reflection_name = preloader.send(:reflection).name
preloader.send(:owners).each do |owner|
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name)
end
end
else
super
end
end
end
)
::ActiveRecord::Associations::JoinDependency.prepend(
Module.new do
def instantiate(result_set, strict_loading_value, &block)
@bullet_eager_loadings = {}
records = super
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
objects,
eager_loadings_hash[objects.first].to_a
)
end
end
records
end
def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value)
if Bullet.start?
unless ar_parent.nil?
parent.children.each do |node|
key = aliases.column_alias(node, node.primary_key)
id = row[key]
next unless id.nil?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(ar_parent, association)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << association
end
end
end
end
super
end
# call join associations
def construct_model(record, node, row, model_cache, id, strict_loading_value)
result = super
if Bullet.start?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
)
::ActiveRecord::Associations::Association.prepend(
Module.new do
def inversed_from(record)
if Bullet.start?
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
end
super
end
def inversed_from_queries(record)
if Bullet.start? && inversable?(record)
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionAssociation.prepend(
Module.new do
def load_target
records = super
if Bullet.start?
if is_a? ::ActiveRecord::Associations::ThroughAssociation
association = owner.association(reflection.through_reflection.name)
if association.loaded?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
def empty?
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
super
end
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
super
end
end
)
::ActiveRecord::Associations::SingularAssociation.prepend(
Module.new do
# call has_one and belongs_to associations
def reader
result = super
if Bullet.start?
if owner.class.name !~ /^HABTM_/
if is_a? ::ActiveRecord::Associations::ThroughAssociation
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
association = owner.association(reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if Bullet::Detector::NPlusOneQuery.impossible?(owner)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
else
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
end
end
end
result
end
end
)
::ActiveRecord::Associations::HasManyAssociation.prepend(
Module.new do
def empty?
result = super
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
result
end
def count_records
result = reflection.has_cached_counter?
if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionProxy.prepend(
Module.new do
def count(column_name = nil)
if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(
proxy_association.owner,
proxy_association.reflection.name
)
Bullet::Detector::NPlusOneQuery.call_association(
proxy_association.owner,
proxy_association.reflection.name
)
end
super(column_name)
end
end
)
end
end
end
bullet-7.2.0/lib/bullet/active_record72.rb 0000664 0000000 0000000 00000027315 14644222732 0020371 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module SaveWithBulletSupport
def _create_record(*)
super do
Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
yield(self) if block_given?
end
end
end
module ActiveRecord
def self.enable
require 'active_record'
::ActiveRecord::Base.extend(
Module.new do
def find_by_sql(sql, binds = [], preparable: nil, allow_retry: false, &block)
result = super
if Bullet.start?
if result.is_a? Array
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
elsif result.is_a? ::ActiveRecord::Base
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
Bullet::Detector::CounterCache.add_impossible_object(result)
end
end
result
end
end
)
::ActiveRecord::Base.prepend(SaveWithBulletSupport)
::ActiveRecord::Relation.prepend(
Module.new do
# if select a collection of objects, then these objects have possible to cause N+1 query.
# if select only one object, then the only one object has impossible to cause N+1 query.
def records
result = super
if Bullet.start?
if result.first.class.name !~ /^HABTM_/
if result.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
Bullet::Detector::CounterCache.add_possible_objects(result)
elsif result.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
Bullet::Detector::CounterCache.add_impossible_object(result.first)
end
end
end
result
end
end
)
::ActiveRecord::Associations::Preloader::Batch.prepend(
Module.new do
def call
if Bullet.start?
@preloaders.each do |preloader|
preloader.records.each { |record|
Bullet::Detector::Association.add_object_associations(record, preloader.associations)
}
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(preloader.records, preloader.associations)
end
end
super
end
end
)
::ActiveRecord::Associations::Preloader::Branch.prepend(
Module.new do
def preloaders_for_reflection(reflection, reflection_records)
if Bullet.start?
reflection_records.compact!
if reflection_records.first.class.name !~ /^HABTM_/
reflection_records.each { |record|
Bullet::Detector::Association.add_object_associations(record, reflection.name)
}
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(reflection_records, reflection.name)
end
end
super
end
end
)
::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(
Module.new do
def source_preloaders
if Bullet.start? && !defined?(@source_preloaders)
preloaders = super
preloaders.each do |preloader|
reflection_name = preloader.send(:reflection).name
preloader.send(:owners).each do |owner|
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name)
end
end
else
super
end
end
end
)
::ActiveRecord::Associations::JoinDependency.prepend(
Module.new do
def instantiate(result_set, strict_loading_value, &block)
@bullet_eager_loadings = {}
records = super
if Bullet.start?
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
objects = eager_loadings_hash.keys
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
objects,
eager_loadings_hash[objects.first].to_a
)
end
end
records
end
def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value)
if Bullet.start?
unless ar_parent.nil?
parent.children.each do |node|
key = aliases.column_alias(node, node.primary_key)
id = row[key]
next unless id.nil?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(ar_parent, association)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << association
end
end
end
end
super
end
# call join associations
def construct_model(record, node, row, model_cache, id, strict_loading_value)
result = super
if Bullet.start?
associations = [node.reflection.name]
if node.reflection.through_reflection?
associations << node.reflection.through_reflection.name
end
associations.each do |association|
Bullet::Detector::Association.add_object_associations(record, association)
Bullet::Detector::NPlusOneQuery.call_association(record, association)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << association
end
end
result
end
end
)
::ActiveRecord::Associations::Association.prepend(
Module.new do
def inversed_from(record)
if Bullet.start?
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
end
super
end
def inversed_from_queries(record)
if Bullet.start? && inversable?(record)
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionAssociation.prepend(
Module.new do
def load_target
records = super
if Bullet.start?
if is_a? ::ActiveRecord::Associations::ThroughAssociation
association = owner.association(reflection.through_reflection.name)
if association.loaded?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if records.first.class.name !~ /^HABTM_/
if records.size > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
Bullet::Detector::CounterCache.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
Bullet::Detector::CounterCache.add_impossible_object(records.first)
end
end
end
records
end
def empty?
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
super
end
def include?(object)
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
super
end
end
)
::ActiveRecord::Associations::SingularAssociation.prepend(
Module.new do
# call has_one and belongs_to associations
def reader
result = super
if Bullet.start?
if owner.class.name !~ /^HABTM_/
if is_a? ::ActiveRecord::Associations::ThroughAssociation
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
association = owner.association(reflection.through_reflection.name)
Array.wrap(association.target).each do |through_record|
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
end
if reflection.through_reflection != through_reflection
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
end
end
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
if Bullet::Detector::NPlusOneQuery.impossible?(owner)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
else
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
end
end
end
result
end
end
)
::ActiveRecord::Associations::HasManyAssociation.prepend(
Module.new do
def empty?
result = super
if Bullet.start? && !reflection.has_cached_counter?
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
end
result
end
def count_records
result = reflection.has_cached_counter?
if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
end
super
end
end
)
::ActiveRecord::Associations::CollectionProxy.prepend(
Module.new do
def count(column_name = nil)
if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation)
Bullet::Detector::CounterCache.add_counter_cache(
proxy_association.owner,
proxy_association.reflection.name
)
Bullet::Detector::NPlusOneQuery.call_association(
proxy_association.owner,
proxy_association.reflection.name
)
end
super(column_name)
end
end
)
end
end
end
bullet-7.2.0/lib/bullet/bullet_xhr.js 0000664 0000000 0000000 00000004603 14644222732 0017563 0 ustar 00root root 0000000 0000000 (function () {
var oldOpen = window.XMLHttpRequest.prototype.open;
var oldSend = window.XMLHttpRequest.prototype.send;
/**
* Return early if we've already extended prototype. This prevents
* "maximum call stack exceeded" errors when used with Turbolinks.
* See https://github.com/flyerhzm/bullet/issues/454
*/
if (isBulletInitiated()) return;
function isBulletInitiated() {
return oldOpen.name == "bulletXHROpen" && oldSend.name == "bulletXHRSend";
}
function bulletXHROpen(_, url) {
this._storedUrl = url;
return Reflect.apply(oldOpen, this, arguments);
}
function bulletXHRSend() {
if (this.onload) {
this._storedOnload = this.onload;
}
this.onload = null;
this.addEventListener("load", bulletXHROnload);
return Reflect.apply(oldSend, this, arguments);
}
function bulletXHROnload() {
if (
this._storedUrl.startsWith(window.location.protocol + "//" + window.location.host) ||
!this._storedUrl.startsWith("http") // For relative paths
) {
var bulletFooterText = this.getResponseHeader("X-bullet-footer-text");
if (bulletFooterText) {
setTimeout(function () {
var oldHtml = document.querySelector("#bullet-footer").innerHTML.split("
");
var header = oldHtml[0];
oldHtml = oldHtml.slice(1, oldHtml.length);
var newHtml = oldHtml.concat(JSON.parse(bulletFooterText));
newHtml = newHtml.slice(newHtml.length - 10, newHtml.length); // rotate through 10 most recent
document.querySelector("#bullet-footer").innerHTML = `${header}
${newHtml.join("
")}`;
}, 0);
}
var bulletConsoleText = this.getResponseHeader("X-bullet-console-text");
if (bulletConsoleText && typeof console !== "undefined" && console.log) {
setTimeout(function () {
JSON.parse(bulletConsoleText).forEach((message) => {
if (console.groupCollapsed && console.groupEnd) {
console.groupCollapsed("Uniform Notifier");
console.log(message);
console.groupEnd();
} else {
console.log(message);
}
});
}, 0);
}
}
if (this._storedOnload) {
return Reflect.apply(this._storedOnload, this, arguments);
}
}
window.XMLHttpRequest.prototype.open = bulletXHROpen;
window.XMLHttpRequest.prototype.send = bulletXHRSend;
})();
bullet-7.2.0/lib/bullet/dependency.rb 0000664 0000000 0000000 00000006444 14644222732 0017525 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Dependency
def mongoid?
@mongoid ||= defined?(::Mongoid)
end
def active_record?
@active_record ||= defined?(::ActiveRecord)
end
def active_record_version
@active_record_version ||=
begin
if active_record40?
'active_record4'
elsif active_record41?
'active_record41'
elsif active_record42?
'active_record42'
elsif active_record50?
'active_record5'
elsif active_record51?
'active_record5'
elsif active_record52?
'active_record52'
elsif active_record60?
'active_record60'
elsif active_record61?
'active_record61'
elsif active_record70?
'active_record70'
elsif active_record71?
'active_record71'
elsif active_record72?
'active_record72'
else
raise "Bullet does not support active_record #{::ActiveRecord::VERSION::STRING} yet"
end
end
end
def mongoid_version
@mongoid_version ||=
begin
if mongoid4x?
'mongoid4x'
elsif mongoid5x?
'mongoid5x'
elsif mongoid6x?
'mongoid6x'
elsif mongoid7x?
'mongoid7x'
elsif mongoid8x?
'mongoid8x'
else
raise "Bullet does not support mongoid #{::Mongoid::VERSION} yet"
end
end
end
def active_record4?
active_record? && ::ActiveRecord::VERSION::MAJOR == 4
end
def active_record5?
active_record? && ::ActiveRecord::VERSION::MAJOR == 5
end
def active_record6?
active_record? && ::ActiveRecord::VERSION::MAJOR == 6
end
def active_record7?
active_record? && ::ActiveRecord::VERSION::MAJOR == 7
end
def active_record40?
active_record4? && ::ActiveRecord::VERSION::MINOR == 0
end
def active_record41?
active_record4? && ::ActiveRecord::VERSION::MINOR == 1
end
def active_record42?
active_record4? && ::ActiveRecord::VERSION::MINOR == 2
end
def active_record50?
active_record5? && ::ActiveRecord::VERSION::MINOR == 0
end
def active_record51?
active_record5? && ::ActiveRecord::VERSION::MINOR == 1
end
def active_record52?
active_record5? && ::ActiveRecord::VERSION::MINOR == 2
end
def active_record60?
active_record6? && ::ActiveRecord::VERSION::MINOR == 0
end
def active_record61?
active_record6? && ::ActiveRecord::VERSION::MINOR == 1
end
def active_record70?
active_record7? && ::ActiveRecord::VERSION::MINOR == 0
end
def active_record71?
active_record7? && ::ActiveRecord::VERSION::MINOR == 1
end
def active_record72?
active_record7? && ::ActiveRecord::VERSION::MINOR == 2
end
def mongoid4x?
mongoid? && ::Mongoid::VERSION =~ /\A4/
end
def mongoid5x?
mongoid? && ::Mongoid::VERSION =~ /\A5/
end
def mongoid6x?
mongoid? && ::Mongoid::VERSION =~ /\A6/
end
def mongoid7x?
mongoid? && ::Mongoid::VERSION =~ /\A7/
end
def mongoid8x?
mongoid? && ::Mongoid::VERSION =~ /\A8/
end
end
end
bullet-7.2.0/lib/bullet/detector.rb 0000664 0000000 0000000 00000000562 14644222732 0017213 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Detector
autoload :Base, 'bullet/detector/base'
autoload :Association, 'bullet/detector/association'
autoload :NPlusOneQuery, 'bullet/detector/n_plus_one_query'
autoload :UnusedEagerLoading, 'bullet/detector/unused_eager_loading'
autoload :CounterCache, 'bullet/detector/counter_cache'
end
end
bullet-7.2.0/lib/bullet/detector/ 0000775 0000000 0000000 00000000000 14644222732 0016663 5 ustar 00root root 0000000 0000000 bullet-7.2.0/lib/bullet/detector/association.rb 0000664 0000000 0000000 00000006511 14644222732 0021527 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Detector
class Association < Base
class << self
def add_object_associations(object, associations)
return unless Bullet.start?
return if !Bullet.n_plus_one_query_enable? && !Bullet.unused_eager_loading_enable?
return unless object.bullet_primary_key_value
Bullet.debug(
'Detector::Association#add_object_associations',
"object: #{object.bullet_key}, associations: #{associations}"
)
call_stacks.add(object.bullet_key)
object_associations.add(object.bullet_key, associations)
end
def add_call_object_associations(object, associations)
return unless Bullet.start?
return if !Bullet.n_plus_one_query_enable? && !Bullet.unused_eager_loading_enable?
return unless object.bullet_primary_key_value
Bullet.debug(
'Detector::Association#add_call_object_associations',
"object: #{object.bullet_key}, associations: #{associations}"
)
call_stacks.add(object.bullet_key)
call_object_associations.add(object.bullet_key, associations)
end
# possible_objects keep the class to object relationships
# that the objects may cause N+1 query.
# e.g. { Post => ["Post:1", "Post:2"] }
def possible_objects
Thread.current[:bullet_possible_objects]
end
# impossible_objects keep the class to objects relationships
# that the objects may not cause N+1 query.
# e.g. { Post => ["Post:1", "Post:2"] }
# if find collection returns only one object, then the object is impossible object,
# impossible_objects are used to avoid treating 1+1 query to N+1 query.
def impossible_objects
Thread.current[:bullet_impossible_objects]
end
private
# object_associations keep the object relationships
# that the object has many associations.
# e.g. { "Post:1" => [:comments] }
# the object_associations keep all associations that may be or may no be
# unpreload associations or unused preload associations.
def object_associations
Thread.current[:bullet_object_associations]
end
# call_object_associations keep the object relationships
# that object.associations is called.
# e.g. { "Post:1" => [:comments] }
# they are used to detect unused preload associations.
def call_object_associations
Thread.current[:bullet_call_object_associations]
end
# inversed_objects keeps object relationships
# that association is inversed.
# e.g. { "Comment:1" => ["post"] }
def inversed_objects
Thread.current[:bullet_inversed_objects]
end
# eager_loadings keep the object relationships
# that the associations are preloaded by find :include.
# e.g. { ["Post:1", "Post:2"] => [:comments, :user] }
def eager_loadings
Thread.current[:bullet_eager_loadings]
end
# cal_stacks keeps stacktraces where querie-objects were called from.
# e.g. { 'Object:111' => [SomeProject/app/controllers/...] }
def call_stacks
Thread.current[:bullet_call_stacks]
end
end
end
end
end
bullet-7.2.0/lib/bullet/detector/base.rb 0000664 0000000 0000000 00000000140 14644222732 0020115 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Detector
class Base
end
end
end
bullet-7.2.0/lib/bullet/detector/counter_cache.rb 0000664 0000000 0000000 00000004332 14644222732 0022014 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Detector
class CounterCache < Base
class << self
def add_counter_cache(object, associations)
return unless Bullet.start?
return unless Bullet.counter_cache_enable?
return unless object.bullet_primary_key_value
Bullet.debug(
'Detector::CounterCache#add_counter_cache',
"object: #{object.bullet_key}, associations: #{associations}"
)
create_notification object.class.to_s, associations if conditions_met?(object, associations)
end
def add_possible_objects(object_or_objects)
return unless Bullet.start?
return unless Bullet.counter_cache_enable?
objects = Array.wrap(object_or_objects)
return if objects.map(&:bullet_primary_key_value).compact.empty?
Bullet.debug(
'Detector::CounterCache#add_possible_objects',
"objects: #{objects.map(&:bullet_key).join(', ')}"
)
objects.each { |object| possible_objects.add object.bullet_key }
end
def add_impossible_object(object)
return unless Bullet.start?
return unless Bullet.counter_cache_enable?
return unless object.bullet_primary_key_value
Bullet.debug('Detector::CounterCache#add_impossible_object', "object: #{object.bullet_key}")
impossible_objects.add object.bullet_key
end
def conditions_met?(object, _associations)
possible_objects.include?(object.bullet_key) && !impossible_objects.include?(object.bullet_key)
end
def possible_objects
Thread.current[:bullet_counter_possible_objects]
end
def impossible_objects
Thread.current[:bullet_counter_impossible_objects]
end
private
def create_notification(klazz, associations)
notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:counter_cache, klazz)
if notify_associations.present?
notice = Bullet::Notification::CounterCache.new klazz, notify_associations
Bullet.notification_collector.add notice
end
end
end
end
end
end
bullet-7.2.0/lib/bullet/detector/n_plus_one_query.rb 0000664 0000000 0000000 00000010610 14644222732 0022574 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Detector
class NPlusOneQuery < Association
extend Dependency
extend StackTraceFilter
class << self
# executed when object.associations is called.
# first, it keeps this method call for object.association.
# then, it checks if this associations call is unpreload.
# if it is, keeps this unpreload associations and caller.
def call_association(object, associations)
return unless Bullet.start?
return unless Bullet.n_plus_one_query_enable?
return unless object.bullet_primary_key_value
return if inversed_objects.include?(object.bullet_key, associations)
add_call_object_associations(object, associations)
Bullet.debug(
'Detector::NPlusOneQuery#call_association',
"object: #{object.bullet_key}, associations: #{associations}"
)
if !excluded_stacktrace_path? && conditions_met?(object, associations)
Bullet.debug('detect n + 1 query', "object: #{object.bullet_key}, associations: #{associations}")
create_notification caller_in_project(object.bullet_key), object.class.to_s, associations
end
end
def add_possible_objects(object_or_objects)
return unless Bullet.start?
return unless Bullet.n_plus_one_query_enable?
objects = Array.wrap(object_or_objects)
class_names_match_regex = true
primary_key_values_are_empty = true
keys_joined = ""
objects.each do |obj|
unless obj.class.name =~ /^HABTM_/
class_names_match_regex = false
end
unless obj.bullet_primary_key_value.nil?
primary_key_values_are_empty = false
end
keys_joined += "#{(keys_joined.empty? ? '' : ', ')}#{obj.bullet_key}"
end
unless class_names_match_regex || primary_key_values_are_empty
Bullet.debug('Detector::NPlusOneQuery#add_possible_objects', "objects: #{keys_joined}")
objects.each { |object| possible_objects.add object.bullet_key }
end
end
def add_impossible_object(object)
return unless Bullet.start?
return unless Bullet.n_plus_one_query_enable?
return unless object.bullet_primary_key_value
Bullet.debug('Detector::NPlusOneQuery#add_impossible_object', "object: #{object.bullet_key}")
impossible_objects.add object.bullet_key
end
def add_inversed_object(object, association)
return unless Bullet.start?
return unless Bullet.n_plus_one_query_enable?
return unless object.bullet_primary_key_value
Bullet.debug(
'Detector::NPlusOneQuery#add_inversed_object',
"object: #{object.bullet_key}, association: #{association}"
)
inversed_objects.add object.bullet_key, association
end
# decide whether the object.associations is unpreloaded or not.
def conditions_met?(object, associations)
possible?(object) && !impossible?(object) && !association?(object, associations)
end
def possible?(object)
possible_objects.include? object.bullet_key
end
def impossible?(object)
impossible_objects.include? object.bullet_key
end
# check if object => associations already exists in object_associations.
def association?(object, associations)
value = object_associations[object.bullet_key]
value&.each do |v|
# associations == v comparison order is important here because
# v variable might be a squeel node where :== method is redefined,
# so it does not compare values at all and return unexpected results
result = v.is_a?(Hash) ? v.key?(associations) : associations == v
return true if result
end
false
end
private
def create_notification(callers, klazz, associations)
notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:n_plus_one_query, klazz)
if notify_associations.present?
notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, notify_associations)
Bullet.notification_collector.add(notice)
end
end
end
end
end
end
bullet-7.2.0/lib/bullet/detector/unused_eager_loading.rb 0000664 0000000 0000000 00000007100 14644222732 0023351 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Detector
class UnusedEagerLoading < Association
extend Dependency
extend StackTraceFilter
class << self
# check if there are unused preload associations.
# get related_objects from eager_loadings associated with object and associations
# get call_object_association from associations of call_object_associations whose object is in related_objects
# if association not in call_object_association, then the object => association - call_object_association is unused preload associations
def check_unused_preload_associations
return unless Bullet.start?
return unless Bullet.unused_eager_loading_enable?
object_associations.each do |bullet_key, associations|
object_association_diff = diff_object_associations bullet_key, associations
next if object_association_diff.empty?
Bullet.debug('detect unused preload', "object: #{bullet_key}, associations: #{object_association_diff}")
create_notification(caller_in_project(bullet_key), bullet_key.bullet_class_name, object_association_diff)
end
end
def add_eager_loadings(objects, associations)
return unless Bullet.start?
return unless Bullet.unused_eager_loading_enable?
return if objects.map(&:bullet_primary_key_value).compact.empty?
Bullet.debug(
'Detector::UnusedEagerLoading#add_eager_loadings',
"objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}"
)
bullet_keys = objects.map(&:bullet_key)
to_add = []
to_merge = []
to_delete = []
eager_loadings.each do |k, _v|
key_objects_overlap = k & bullet_keys
next if key_objects_overlap.empty?
bullet_keys -= k
if key_objects_overlap == k
to_add << [k, associations]
else
to_merge << [key_objects_overlap, (eager_loadings[k].dup << associations)]
keys_without_objects = k - key_objects_overlap
to_merge << [keys_without_objects, eager_loadings[k]]
to_delete << k
end
end
to_add.each { |k, val| eager_loadings.add k, val }
to_merge.each { |k, val| eager_loadings.merge k, val }
to_delete.each { |k| eager_loadings.delete k }
eager_loadings.add bullet_keys, associations unless bullet_keys.empty?
end
private
def create_notification(callers, klazz, associations)
notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(
:unused_eager_loading,
klazz
)
if notify_associations.present?
notice = Bullet::Notification::UnusedEagerLoading.new(callers, klazz, notify_associations)
Bullet.notification_collector.add(notice)
end
end
def call_associations(bullet_key, associations)
all = Set.new
eager_loadings.similarly_associated(bullet_key, associations).each do |related_bullet_key|
coa = call_object_associations[related_bullet_key]
next if coa.nil?
all.merge coa
end
all.to_a
end
def diff_object_associations(bullet_key, associations)
potential_associations = associations - call_associations(bullet_key, associations)
potential_associations.reject { |a| a.is_a?(Hash) }
end
end
end
end
end
bullet-7.2.0/lib/bullet/ext/ 0000775 0000000 0000000 00000000000 14644222732 0015652 5 ustar 00root root 0000000 0000000 bullet-7.2.0/lib/bullet/ext/object.rb 0000664 0000000 0000000 00000001415 14644222732 0017446 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
class Object
def bullet_key
"#{self.class}:#{bullet_primary_key_value}"
end
def bullet_primary_key_value
return if respond_to?(:persisted?) && !persisted?
if self.class.respond_to?(:primary_keys) && self.class.primary_keys
primary_key = self.class.primary_keys
elsif self.class.respond_to?(:primary_key) && self.class.primary_key
primary_key = self.class.primary_key
else
primary_key = :id
end
bullet_join_potential_composite_primary_key(primary_key)
end
private
def bullet_join_potential_composite_primary_key(primary_keys)
return send(primary_keys) unless primary_keys.is_a?(Enumerable)
primary_keys.map { |primary_key| send primary_key }
.join(',')
end
end
bullet-7.2.0/lib/bullet/ext/string.rb 0000664 0000000 0000000 00000000146 14644222732 0017506 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
class String
def bullet_class_name
sub(/:[^:]*?$/, '')
end
end
bullet-7.2.0/lib/bullet/mongoid4x.rb 0000664 0000000 0000000 00000003424 14644222732 0017312 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Mongoid
def self.enable
require 'mongoid'
::Mongoid::Contextual::Mongo.class_eval do
alias_method :origin_first, :first
alias_method :origin_last, :last
alias_method :origin_each, :each
alias_method :origin_eager_load, :eager_load
def first
result = origin_first
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
result
end
def last
result = origin_last
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
result
end
def each(&block)
return to_enum unless block
records = []
origin_each { |record| records << record }
if records.length > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
end
records.each(&block)
end
def eager_load(docs)
associations = criteria.inclusions.map(&:name)
docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
origin_eager_load(docs)
end
end
::Mongoid::Relations::Accessors.class_eval do
alias_method :origin_get_relation, :get_relation
def get_relation(name, metadata, object, reload = false)
result = origin_get_relation(name, metadata, object, reload)
Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
result
end
end
end
end
end
bullet-7.2.0/lib/bullet/mongoid5x.rb 0000664 0000000 0000000 00000003424 14644222732 0017313 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Mongoid
def self.enable
require 'mongoid'
::Mongoid::Contextual::Mongo.class_eval do
alias_method :origin_first, :first
alias_method :origin_last, :last
alias_method :origin_each, :each
alias_method :origin_eager_load, :eager_load
def first
result = origin_first
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
result
end
def last
result = origin_last
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
result
end
def each(&block)
return to_enum unless block
records = []
origin_each { |record| records << record }
if records.length > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
end
records.each(&block)
end
def eager_load(docs)
associations = criteria.inclusions.map(&:name)
docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
origin_eager_load(docs)
end
end
::Mongoid::Relations::Accessors.class_eval do
alias_method :origin_get_relation, :get_relation
def get_relation(name, metadata, object, reload = false)
result = origin_get_relation(name, metadata, object, reload)
Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
result
end
end
end
end
end
bullet-7.2.0/lib/bullet/mongoid6x.rb 0000664 0000000 0000000 00000003462 14644222732 0017316 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Mongoid
def self.enable
require 'mongoid'
::Mongoid::Contextual::Mongo.class_eval do
alias_method :origin_first, :first
alias_method :origin_last, :last
alias_method :origin_each, :each
alias_method :origin_eager_load, :eager_load
def first(opt = {})
result = origin_first(opt)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
result
end
def last(opt = {})
result = origin_last(opt)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
result
end
def each(&block)
return to_enum unless block
records = []
origin_each { |record| records << record }
if records.length > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
end
records.each(&block)
end
def eager_load(docs)
associations = criteria.inclusions.map(&:name)
docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
origin_eager_load(docs)
end
end
::Mongoid::Relations::Accessors.class_eval do
alias_method :origin_get_relation, :get_relation
def get_relation(name, metadata, object, reload = false)
result = origin_get_relation(name, metadata, object, reload)
Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
result
end
end
end
end
end
bullet-7.2.0/lib/bullet/mongoid7x.rb 0000664 0000000 0000000 00000004436 14644222732 0017321 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Mongoid
def self.enable
require 'mongoid'
require 'rubygems'
::Mongoid::Contextual::Mongo.class_eval do
alias_method :origin_first, :first
alias_method :origin_last, :last
alias_method :origin_each, :each
alias_method :origin_eager_load, :eager_load
%i[first last].each do |context|
default = Gem::Version.new(::Mongoid::VERSION) >= Gem::Version.new('7.5') ? nil : {}
define_method(context) do |opts = default|
result = send(:"origin_#{context}", opts)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
result
end
end
def each(&block)
return to_enum unless block_given?
first_document = nil
document_count = 0
origin_each do |document|
document_count += 1
if document_count == 1
first_document = document
elsif document_count == 2
Bullet::Detector::NPlusOneQuery.add_possible_objects([first_document, document])
yield(first_document)
first_document = nil
yield(document)
else
Bullet::Detector::NPlusOneQuery.add_possible_objects(document)
yield(document)
end
end
if document_count == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(first_document)
yield(first_document)
end
self
end
def eager_load(docs)
associations = criteria.inclusions.map(&:name)
docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
origin_eager_load(docs)
end
end
::Mongoid::Association::Accessors.class_eval do
alias_method :origin_get_relation, :get_relation
def get_relation(name, association, object, reload = false)
result = origin_get_relation(name, association, object, reload)
Bullet::Detector::NPlusOneQuery.call_association(self, name) unless association.embedded?
result
end
end
end
end
end
bullet-7.2.0/lib/bullet/mongoid8x.rb 0000664 0000000 0000000 00000003537 14644222732 0017323 0 ustar 00root root 0000000 0000000 module Bullet
module Mongoid
def self.enable
require 'mongoid'
::Mongoid::Contextual::Mongo.class_eval do
alias_method :origin_first, :first
alias_method :origin_last, :last
alias_method :origin_each, :each
alias_method :origin_eager_load, :eager_load
def first(limit = nil)
result = origin_first(limit)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
result
end
def last(limit = nil)
result = origin_last(limit)
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
result
end
def each(&block)
return to_enum unless block_given?
records = []
origin_each { |record| records << record }
if records.length > 1
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
elsif records.size == 1
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
end
records.each(&block)
end
def eager_load(docs)
associations = criteria.inclusions.map(&:name)
docs.each do |doc|
Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
end
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
origin_eager_load(docs)
end
end
::Mongoid::Association::Accessors.class_eval do
alias_method :origin_get_relation, :get_relation
def get_relation(name, association, object, reload = false)
result = origin_get_relation(name, association, object, reload)
unless association.embedded?
Bullet::Detector::NPlusOneQuery.call_association(self, name)
end
result
end
end
end
end
end
bullet-7.2.0/lib/bullet/notification.rb 0000664 0000000 0000000 00000000606 14644222732 0020067 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Notification
autoload :Base, 'bullet/notification/base'
autoload :UnusedEagerLoading, 'bullet/notification/unused_eager_loading'
autoload :NPlusOneQuery, 'bullet/notification/n_plus_one_query'
autoload :CounterCache, 'bullet/notification/counter_cache'
class UnoptimizedQueryError < StandardError
end
end
end
bullet-7.2.0/lib/bullet/notification/ 0000775 0000000 0000000 00000000000 14644222732 0017540 5 ustar 00root root 0000000 0000000 bullet-7.2.0/lib/bullet/notification/base.rb 0000664 0000000 0000000 00000004104 14644222732 0020776 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Notification
class Base
attr_accessor :notifier, :url
attr_reader :base_class, :associations, :path
def initialize(base_class, association_or_associations, path = nil)
@base_class = base_class
@associations =
association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations]
@path = path
end
def title
raise NoMethodError, 'no method title defined'
end
def body
raise NoMethodError, 'no method body defined'
end
def call_stack_messages
''
end
def whoami
@user ||=
ENV['USER'].presence ||
(
begin
`whoami`.chomp
rescue StandardError
''
end
)
@user.present? ? "user: #{@user}" : ''
end
def body_with_caller
"#{body}\n#{call_stack_messages}\n"
end
def notify_inline
notifier.inline_notify(notification_data)
end
def notify_out_of_channel
notifier.out_of_channel_notify(notification_data)
end
def short_notice
parts = []
parts << whoami.presence unless Bullet.skip_user_in_notification
parts << url
parts << title
parts << body
parts.compact.join(' ')
end
def notification_data
hash = {}
hash[:user] = whoami unless Bullet.skip_user_in_notification
hash[:url] = url
hash[:title] = title
hash[:body] = body_with_caller
hash
end
def eql?(other)
self.class == other.class && klazz_associations_str == other.klazz_associations_str
end
def hash
[self.class, klazz_associations_str].hash
end
protected
def klazz_associations_str
" #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]"
end
def associations_str
".includes(#{@associations.map { |a| a.to_s.to_sym }
.inspect})"
end
end
end
end
bullet-7.2.0/lib/bullet/notification/counter_cache.rb 0000664 0000000 0000000 00000000373 14644222732 0022672 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Notification
class CounterCache < Base
def body
klazz_associations_str
end
def title
'Need Counter Cache with Active Record size'
end
end
end
end
bullet-7.2.0/lib/bullet/notification/n_plus_one_query.rb 0000664 0000000 0000000 00000001206 14644222732 0023452 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Notification
class NPlusOneQuery < Base
def initialize(callers, base_class, associations, path = nil)
super(base_class, associations, path)
@callers = callers
end
def body
"#{klazz_associations_str}\n Add to your query: #{associations_str}"
end
def title
"USE eager loading #{@path ? "in #{@path}" : 'detected'}"
end
def notification_data
super.merge(backtrace: [])
end
protected
def call_stack_messages
(['Call stack'] + @callers).join("\n ")
end
end
end
end
bullet-7.2.0/lib/bullet/notification/unused_eager_loading.rb 0000664 0000000 0000000 00000001222 14644222732 0024225 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Notification
class UnusedEagerLoading < Base
def initialize(callers, base_class, associations, path = nil)
super(base_class, associations, path)
@callers = callers
end
def body
"#{klazz_associations_str}\n Remove from your query: #{associations_str}"
end
def title
"AVOID eager loading #{@path ? "in #{@path}" : 'detected'}"
end
def notification_data
super.merge(backtrace: [])
end
protected
def call_stack_messages
(['Call stack'] + @callers).join("\n ")
end
end
end
end
bullet-7.2.0/lib/bullet/notification_collector.rb 0000664 0000000 0000000 00000000524 14644222732 0022134 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'set'
module Bullet
class NotificationCollector
attr_reader :collection
def initialize
reset
end
def reset
@collection = Set.new
end
def add(value)
@collection << value
end
def notifications_present?
!@collection.empty?
end
end
end
bullet-7.2.0/lib/bullet/rack.rb 0000664 0000000 0000000 00000012103 14644222732 0016314 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
class Rack
include Dependency
NONCE_MATCHER = /script-src .*'nonce-(?[A-Za-z0-9+\/]+={0,2})'/
def initialize(app)
@app = app
end
def call(env)
return @app.call(env) unless Bullet.enable?
Bullet.start_request
status, headers, response = @app.call(env)
response_body = nil
if Bullet.notification? || Bullet.always_append_html_body
if Bullet.inject_into_page? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
if html_request?(headers, response)
response_body = response_body(response)
with_security_policy_nonce(headers) do |nonce|
response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
if Bullet.add_footer && !Bullet.skip_http_headers
response_body = append_to_html_body(response_body, xhr_script(nonce))
end
end
headers['Content-Length'] = response_body.bytesize.to_s
elsif !Bullet.skip_http_headers
set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
set_header(headers, 'X-bullet-console-text', Bullet.text_notifications) if Bullet.console_enabled?
end
end
Bullet.perform_out_of_channel_notifications(env)
end
[status, headers, response_body ? [response_body] : response]
ensure
Bullet.end_request
end
# fix issue if response's body is a Proc
def empty?(response)
# response may be ["Not Found"], ["Move Permanently"], etc, but
# those should not happen if the status is 200
return true if !response.respond_to?(:body) && !response.respond_to?(:first)
body = response_body(response)
body.nil? || body.empty?
end
def append_to_html_body(response_body, content)
body = response_body.dup
content = content.html_safe if content.respond_to?(:html_safe)
if body.include?('')
position = body.rindex('')
body.insert(position, content)
else
body << content
end
end
def footer_note
"Bullet Warnings
#{Bullet.footer_info.uniq.join('
')}#{footer_console_message}
"
end
def set_header(headers, header_name, header_array)
# Many proxy applications such as Nginx and AWS ELB limit
# the size a header to 8KB, so truncate the list of reports to
# be under that limit
header_array.pop while header_array.to_json.length > 8 * 1024
headers[header_name] = header_array.to_json
end
def file?(headers)
headers['Content-Transfer-Encoding'] == 'binary' || headers['Content-Disposition']
end
def sse?(headers)
headers['Content-Type'] == 'text/event-stream'
end
def html_request?(headers, response)
headers['Content-Type']&.include?('text/html')
end
def response_body(response)
if response.respond_to?(:body)
Array === response.body ? response.body.first : response.body
elsif response.respond_to?(:first)
response.first
end
end
private
def details_attributes
<<~EOF
id="bullet-footer" data-is-bullet-footer
style="cursor: pointer; position: fixed; left: 0px; bottom: 0px; z-index: 9999; background: #fdf2f2; color: #9b1c1c; font-size: 12px; border-radius: 0px 8px 0px 0px; border: 1px solid #9b1c1c;"
EOF
end
def summary_attributes
<<~EOF
style="font-weight: 600; padding: 2px 8px"
EOF
end
def footer_content_attributes
<<~EOF
style="padding: 8px; border-top: 1px solid #9b1c1c;"
EOF
end
def footer_console_message
if Bullet.console_enabled?
"
See 'Uniform Notifier' in JS Console for Stacktrace"
end
end
# Make footer work for XHR requests by appending data to the footer
def xhr_script(nonce = nil)
script = File.read("#{__dir__}/bullet_xhr.js")
if nonce
""
else
""
end
end
def with_security_policy_nonce(headers)
csp = headers['Content-Security-Policy'] || headers['Content-Security-Policy-Report-Only'] || ''
matched = csp.match(NONCE_MATCHER)
nonce = matched[:nonce] if matched
if nonce
console_enabled = UniformNotifier.console
alert_enabled = UniformNotifier.alert
UniformNotifier.console = { attributes: { nonce: nonce } } if console_enabled
UniformNotifier.alert = { attributes: { nonce: nonce } } if alert_enabled
yield nonce
UniformNotifier.console = console_enabled
UniformNotifier.alert = alert_enabled
else
yield
end
end
end
end
bullet-7.2.0/lib/bullet/registry.rb 0000664 0000000 0000000 00000000422 14644222732 0017245 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Registry
autoload :Base, 'bullet/registry/base'
autoload :Object, 'bullet/registry/object'
autoload :Association, 'bullet/registry/association'
autoload :CallStack, 'bullet/registry/call_stack'
end
end
bullet-7.2.0/lib/bullet/registry/ 0000775 0000000 0000000 00000000000 14644222732 0016722 5 ustar 00root root 0000000 0000000 bullet-7.2.0/lib/bullet/registry/association.rb 0000664 0000000 0000000 00000000612 14644222732 0021562 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Registry
class Association < Base
def merge(base, associations)
@registry.merge!(base => associations)
end
def similarly_associated(base, associations)
@registry.select { |key, value| key.include?(base) && value == associations }
.collect(&:first).flatten
end
end
end
end
bullet-7.2.0/lib/bullet/registry/base.rb 0000664 0000000 0000000 00000001347 14644222732 0020166 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Registry
class Base
attr_reader :registry
def initialize
@registry = {}
end
def [](key)
@registry[key]
end
def each(&block)
@registry.each(&block)
end
def delete(base)
@registry.delete(base)
end
def select(*args, &block)
@registry.select(*args, &block)
end
def add(key, value)
@registry[key] ||= Set.new
if value.is_a? Array
@registry[key] += value
else
@registry[key] << value
end
end
def include?(key, value)
!@registry[key].nil? && @registry[key].include?(value)
end
end
end
end
bullet-7.2.0/lib/bullet/registry/call_stack.rb 0000664 0000000 0000000 00000000351 14644222732 0021346 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Registry
class CallStack < Base
# remembers found association backtrace
def add(key)
@registry[key] = Thread.current.backtrace
end
end
end
end
bullet-7.2.0/lib/bullet/registry/object.rb 0000664 0000000 0000000 00000000447 14644222732 0020522 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Registry
class Object < Base
def add(bullet_key)
super(bullet_key.bullet_class_name, bullet_key)
end
def include?(bullet_key)
super(bullet_key.bullet_class_name, bullet_key)
end
end
end
end
bullet-7.2.0/lib/bullet/stack_trace_filter.rb 0000664 0000000 0000000 00000004071 14644222732 0021231 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require "bundler"
module Bullet
module StackTraceFilter
VENDOR_PATH = '/vendor'
IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
# @param bullet_key[String] - use this to get stored call stack from call_stacks object.
def caller_in_project(bullet_key = nil)
vendor_root = Bullet.app_root + VENDOR_PATH
bundler_path = Bundler.bundle_path.to_s
select_caller_locations(bullet_key) do |location|
caller_path = location_as_path(location)
caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
!caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern|
pattern_matches?(location, include_pattern)
}
end
end
def excluded_stacktrace_path?
Bullet.stacktrace_excludes.any? do |exclude_pattern|
caller_in_project.any? { |location| pattern_matches?(location, exclude_pattern) }
end
end
private
def pattern_matches?(location, pattern)
path = location_as_path(location)
case pattern
when Array
pattern_path = pattern.first
filter = pattern.last
return false unless pattern_matches?(location, pattern_path)
case filter
when Range
filter.include?(location.lineno)
when Integer
filter == location.lineno
when String
filter == location.base_label
end
when String
path.include?(pattern)
when Regexp
path =~ pattern
end
end
def location_as_path(location)
return location if location.is_a?(String)
IS_RUBY_19 ? location : location.absolute_path.to_s
end
def select_caller_locations(bullet_key = nil)
return caller.select { |caller_path| yield caller_path } if IS_RUBY_19
call_stack = bullet_key ? call_stacks[bullet_key] : caller_locations
call_stack.select { |location| yield location }
end
end
end
bullet-7.2.0/lib/bullet/version.rb 0000664 0000000 0000000 00000000105 14644222732 0017060 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
VERSION = '7.2.0'
end
bullet-7.2.0/lib/generators/ 0000775 0000000 0000000 00000000000 14644222732 0015734 5 ustar 00root root 0000000 0000000 bullet-7.2.0/lib/generators/bullet/ 0000775 0000000 0000000 00000000000 14644222732 0017223 5 ustar 00root root 0000000 0000000 bullet-7.2.0/lib/generators/bullet/install_generator.rb 0000664 0000000 0000000 00000002355 14644222732 0023271 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
module Generators
class InstallGenerator < ::Rails::Generators::Base
desc <<~DESC
Description:
Enable bullet in development/test for your application.
DESC
def enable_in_development
environment(nil, env: 'development') do
<<~FILE
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.rails_logger = true
Bullet.add_footer = true
end
FILE
end
say 'Enabled bullet in config/environments/development.rb'
end
def enable_in_test
return unless yes?('Would you like to enable bullet in test environment? (y/n)')
environment(nil, env: 'test') do
<<~FILE
config.after_initialize do
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.raise = true # raise an error if n+1 query occurs
end
FILE
end
say 'Enabled bullet in config/environments/test.rb'
end
end
end
end
bullet-7.2.0/perf/ 0000775 0000000 0000000 00000000000 14644222732 0013751 5 ustar 00root root 0000000 0000000 bullet-7.2.0/perf/benchmark.rb 0000664 0000000 0000000 00000006360 14644222732 0016235 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
$LOAD_PATH << 'lib'
require 'benchmark'
require 'rails'
require 'active_record'
require 'activerecord-import'
require 'bullet'
begin
require 'perftools'
rescue LoadError
puts "Could not load perftools.rb, profiling won't be possible"
end
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
class User < ActiveRecord::Base
has_many :posts
has_many :comments
end
# create database bullet_benchmark;
ActiveRecord::Base.establish_connection(
adapter: 'mysql2',
database: 'bullet_benchmark',
server: '/tmp/mysql.socket',
username: 'root'
)
ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
ActiveRecord::Schema.define(version: 1) do
create_table :posts do |t|
t.column :title, :string
t.column :body, :string
t.column :user_id, :integer
end
create_table :comments do |t|
t.column :body, :string
t.column :post_id, :integer
t.column :user_id, :integer
end
create_table :users do |t|
t.column :name, :string
end
end
users_size = 100
posts_size = 1_000
comments_size = 10_000
users = []
users_size.times { |i| users << User.new(name: "user#{i}") }
User.import users
users = User.all
posts = []
posts_size.times { |i| posts << Post.new(title: "Title #{i}", body: "Body #{i}", user: users[i % 100]) }
Post.import posts
posts = Post.all
comments = []
comments_size.times { |i| comments << Comment.new(body: "Comment #{i}", post: posts[i % 1_000], user: users[i % 100]) }
Comment.import comments
puts 'Start benchmarking...'
Bullet.enable = true
Benchmark.bm(70) do |bm|
bm.report("Querying & Iterating #{posts_size} Posts with #{comments_size} Comments and #{users_size} Users") do
10.times do
Bullet.start_request
Post.select('SQL_NO_CACHE *').includes(:user, comments: :user).each do |p|
p.title
p.user.name
p.comments.each do |c|
c.body
c.user.name
end
end
Bullet.end_request
end
end
end
puts 'End benchmarking...'
# Run benchmark with bundler
#
# bundle exec ruby perf/benchmark.rb
#
# bullet 2.3.0 with rails 3.2.2
# user system total real
# Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 16.460000 0.190000 16.650000 ( 16.968246)
#
# bullet 2.3.0 with rails 3.1.4
# user system total real
# Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 14.600000 0.130000 14.730000 ( 14.937590)
#
# bullet 2.3.0 with rails 3.0.12
# user system total real
# Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 26.120000 0.430000 26.550000 ( 27.179304)
#
#
# bullet 2.2.1 with rails 3.0.12
# user system total real
# Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 29.970000 0.270000 30.240000 ( 30.452083)
bullet-7.2.0/rails/ 0000775 0000000 0000000 00000000000 14644222732 0014127 5 ustar 00root root 0000000 0000000 bullet-7.2.0/rails/init.rb 0000664 0000000 0000000 00000000060 14644222732 0015413 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'bullet'
bullet-7.2.0/spec/ 0000775 0000000 0000000 00000000000 14644222732 0013747 5 ustar 00root root 0000000 0000000 bullet-7.2.0/spec/bullet/ 0000775 0000000 0000000 00000000000 14644222732 0015236 5 ustar 00root root 0000000 0000000 bullet-7.2.0/spec/bullet/detector/ 0000775 0000000 0000000 00000000000 14644222732 0017047 5 ustar 00root root 0000000 0000000 bullet-7.2.0/spec/bullet/detector/association_spec.rb 0000664 0000000 0000000 00000001471 14644222732 0022725 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
module Detector
describe Association do
before :all do
@post1 = Post.first
@post2 = Post.last
end
context '.add_object_association' do
it 'should add object, associations pair' do
Association.add_object_associations(@post1, :associations)
expect(Association.send(:object_associations)).to be_include(@post1.bullet_key, :associations)
end
end
context '.add_call_object_associations' do
it 'should add call object, associations pair' do
Association.add_call_object_associations(@post1, :associations)
expect(Association.send(:call_object_associations)).to be_include(@post1.bullet_key, :associations)
end
end
end
end
end
bullet-7.2.0/spec/bullet/detector/base_spec.rb 0000664 0000000 0000000 00000000175 14644222732 0021323 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
module Detector
describe Base do
end
end
end
bullet-7.2.0/spec/bullet/detector/counter_cache_spec.rb 0000664 0000000 0000000 00000004127 14644222732 0023214 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
module Detector
describe CounterCache do
before :all do
@post1 = Post.first
@post2 = Post.last
end
context '.add_counter_cache' do
it 'should create notification if conditions met' do
expect(CounterCache).to receive(:conditions_met?).with(@post1, %i[comments]).and_return(true)
expect(CounterCache).to receive(:create_notification).with('Post', %i[comments])
CounterCache.add_counter_cache(@post1, %i[comments])
end
it 'should not create notification if conditions not met' do
expect(CounterCache).to receive(:conditions_met?).with(@post1, %i[comments]).and_return(false)
expect(CounterCache).to receive(:create_notification).never
CounterCache.add_counter_cache(@post1, %i[comments])
end
end
context '.add_possible_objects' do
it 'should add possible objects' do
CounterCache.add_possible_objects([@post1, @post2])
expect(CounterCache.possible_objects).to be_include(@post1.bullet_key)
expect(CounterCache.possible_objects).to be_include(@post2.bullet_key)
end
it 'should add impossible object' do
CounterCache.add_impossible_object(@post1)
expect(CounterCache.impossible_objects).to be_include(@post1.bullet_key)
end
end
context '.conditions_met?' do
it 'should be true when object is possible, not impossible' do
CounterCache.add_possible_objects(@post1)
expect(CounterCache.conditions_met?(@post1, :associations)).to eq true
end
it 'should be false when object is not possible' do
expect(CounterCache.conditions_met?(@post1, :associations)).to eq false
end
it 'should be false when object is possible, and impossible' do
CounterCache.add_possible_objects(@post1)
CounterCache.add_impossible_object(@post1)
expect(CounterCache.conditions_met?(@post1, :associations)).to eq false
end
end
end
end
end
bullet-7.2.0/spec/bullet/detector/n_plus_one_query_spec.rb 0000664 0000000 0000000 00000016313 14644222732 0024000 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
module Detector
describe NPlusOneQuery do
before(:all) do
@post = Post.first
@post2 = Post.last
end
context '.call_association' do
it 'should add call_object_associations' do
expect(NPlusOneQuery).to receive(:add_call_object_associations).with(@post, :associations)
NPlusOneQuery.call_association(@post, :associations)
end
end
context '.possible?' do
it 'should be true if possible_objects contain' do
NPlusOneQuery.add_possible_objects(@post)
expect(NPlusOneQuery.possible?(@post)).to eq true
end
end
context '.impossible?' do
it 'should be true if impossible_objects contain' do
NPlusOneQuery.add_impossible_object(@post)
expect(NPlusOneQuery.impossible?(@post)).to eq true
end
end
context '.association?' do
it 'should be true if object, associations pair is already existed' do
NPlusOneQuery.add_object_associations(@post, :association)
expect(NPlusOneQuery.association?(@post, :association)).to eq true
end
it 'should be false if object, association pair is not existed' do
NPlusOneQuery.add_object_associations(@post, :association1)
expect(NPlusOneQuery.association?(@post, :association2)).to eq false
end
end
context '.conditions_met?' do
it 'should be true if object is possible, not impossible and object, associations pair is not already existed' do
allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq true
end
it 'should be false if object is not possible, not impossible and object, associations pair is not already existed' do
allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(false)
allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
end
it 'should be false if object is possible, but impossible and object, associations pair is not already existed' do
allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(true)
allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
end
it 'should be false if object is possible, not impossible and object, associations pair is already existed' do
allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(true)
expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
end
end
context '.call_association' do
it 'should create notification if conditions met' do
expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
expect(NPlusOneQuery).to receive(:caller_in_project).and_return(%w[caller])
expect(NPlusOneQuery).to receive(:create_notification).with(%w[caller], 'Post', :association)
NPlusOneQuery.call_association(@post, :association)
end
it 'should not create notification if conditions not met' do
expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(false)
expect(NPlusOneQuery).not_to receive(:caller_in_project!)
expect(NPlusOneQuery).not_to receive(:create_notification).with('Post', :association)
NPlusOneQuery.call_association(@post, :association)
end
context 'stacktrace_excludes' do
before { Bullet.stacktrace_excludes = [/def/] }
after { Bullet.stacktrace_excludes = nil }
it 'should not create notification when stacktrace contains paths that are in the exclude list' do
in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
included_path = OpenStruct.new(absolute_path: '/ghi/ghi.rb')
excluded_path = OpenStruct.new(absolute_path: '/def/def.rb')
expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, included_path, excluded_path])
expect(NPlusOneQuery).to_not receive(:create_notification)
NPlusOneQuery.call_association(@post, :association)
end
# just a sanity spec to make sure the following spec works correctly
it "should create notification when stacktrace contains methods that aren't in the exclude list" do
method = NPlusOneQuery.method(:excluded_stacktrace_path?).source_location
in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
excluded_path = OpenStruct.new(absolute_path: method.first, lineno: method.last)
expect(NPlusOneQuery).to receive(:caller_locations).at_least(1).and_return([in_project, excluded_path])
expect(NPlusOneQuery).to receive(:conditions_met?).and_return(true)
expect(NPlusOneQuery).to receive(:create_notification)
NPlusOneQuery.call_association(@post, :association)
end
it 'should not create notification when stacktrace contains methods that are in the exclude list' do
method = NPlusOneQuery.method(:excluded_stacktrace_path?).source_location
Bullet.stacktrace_excludes = [method]
in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
excluded_path = OpenStruct.new(absolute_path: method.first, lineno: method.last)
expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, excluded_path])
expect(NPlusOneQuery).to_not receive(:create_notification)
NPlusOneQuery.call_association(@post, :association)
end
end
end
context '.add_possible_objects' do
it 'should add possible objects' do
NPlusOneQuery.add_possible_objects([@post, @post2])
expect(NPlusOneQuery.possible_objects).to be_include(@post.bullet_key)
expect(NPlusOneQuery.possible_objects).to be_include(@post2.bullet_key)
end
it 'should not raise error if object is nil' do
expect { NPlusOneQuery.add_possible_objects(nil) }
.not_to raise_error
end
end
context '.add_impossible_object' do
it 'should add impossible object' do
NPlusOneQuery.add_impossible_object(@post)
expect(NPlusOneQuery.impossible_objects).to be_include(@post.bullet_key)
end
end
end
end
end
bullet-7.2.0/spec/bullet/detector/unused_eager_loading_spec.rb 0000664 0000000 0000000 00000014254 14644222732 0024557 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
module Detector
describe UnusedEagerLoading do
before(:all) do
@post = Post.first
@post2 = Post.all[1]
@post3 = Post.last
end
context '.call_associations' do
it 'should get empty array if eager_loadings' do
expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to be_empty
end
it 'should get call associations if object and association are both in eager_loadings and call_object_associations' do
UnusedEagerLoading.add_eager_loadings([@post], :association)
UnusedEagerLoading.add_call_object_associations(@post, :association)
expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to eq(
[:association]
)
end
it 'should not get call associations if not exist in call_object_associations' do
UnusedEagerLoading.add_eager_loadings([@post], :association)
expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to be_empty
end
end
context '.diff_object_associations' do
it 'should return associations not exist in call_association' do
expect(UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))).to eq(
[:association]
)
end
it 'should return empty if associations exist in call_association' do
UnusedEagerLoading.add_eager_loadings([@post], :association)
UnusedEagerLoading.add_call_object_associations(@post, :association)
expect(
UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))
).to be_empty
end
end
context '.check_unused_preload_associations' do
let(:paths) { %w[/dir1 /dir1/subdir] }
it 'should create notification if object_association_diff is not empty' do
UnusedEagerLoading.add_object_associations(@post, :association)
allow(UnusedEagerLoading).to receive(:caller_in_project).and_return(paths)
expect(UnusedEagerLoading).to receive(:create_notification).with(paths, 'Post', [:association])
UnusedEagerLoading.check_unused_preload_associations
end
it 'should not create notification if object_association_diff is empty' do
UnusedEagerLoading.add_object_associations(@post, :association)
UnusedEagerLoading.add_eager_loadings([@post], :association)
UnusedEagerLoading.add_call_object_associations(@post, :association)
expect(
UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))
).to be_empty
expect(UnusedEagerLoading).not_to receive(:create_notification).with('Post', [:association])
UnusedEagerLoading.check_unused_preload_associations
end
it 'should create call stack for notification' do
UnusedEagerLoading.add_object_associations(@post, :association)
expect(UnusedEagerLoading.send(:call_stacks).registry).not_to be_empty
end
end
context '.add_eager_loadings' do
it 'should add objects, associations pair when eager_loadings are empty' do
UnusedEagerLoading.add_eager_loadings([@post, @post2], :associations)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include(
[@post.bullet_key, @post2.bullet_key],
:associations
)
end
it 'should add objects, associations pair for existing eager_loadings' do
UnusedEagerLoading.add_eager_loadings([@post, @post2], :association1)
UnusedEagerLoading.add_eager_loadings([@post, @post2], :association2)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include(
[@post.bullet_key, @post2.bullet_key],
:association1
)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include(
[@post.bullet_key, @post2.bullet_key],
:association2
)
end
it 'should merge objects, associations pair for existing eager_loadings' do
UnusedEagerLoading.add_eager_loadings([@post], :association1)
UnusedEagerLoading.add_eager_loadings([@post, @post2], :association2)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association1)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association2)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post2.bullet_key], :association2)
end
it 'should vmerge objects recursively, associations pair for existing eager_loadings' do
UnusedEagerLoading.add_eager_loadings([@post, @post2], :association1)
UnusedEagerLoading.add_eager_loadings([@post, @post3], :association1)
UnusedEagerLoading.add_eager_loadings([@post, @post3], :association2)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association1)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association2)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post2.bullet_key], :association1)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post3.bullet_key], :association1)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post3.bullet_key], :association2)
end
it 'should delete objects, associations pair for existing eager_loadings' do
UnusedEagerLoading.add_eager_loadings([@post, @post2], :association1)
UnusedEagerLoading.add_eager_loadings([@post], :association2)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association1)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association2)
expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post2.bullet_key], :association1)
end
end
end
end
end
bullet-7.2.0/spec/bullet/ext/ 0000775 0000000 0000000 00000000000 14644222732 0016036 5 ustar 00root root 0000000 0000000 bullet-7.2.0/spec/bullet/ext/object_spec.rb 0000664 0000000 0000000 00000003021 14644222732 0020637 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
describe Object do
context 'bullet_key' do
it 'should return class and id composition' do
post = Post.first
expect(post.bullet_key).to eq("Post:#{post.id}")
end
if mongoid?
it 'should return class with namespace and id composition' do
post = Mongoid::Post.first
expect(post.bullet_key).to eq("Mongoid::Post:#{post.id}")
end
end
end
context 'bullet_primary_key_value' do
it 'should return id' do
post = Post.first
expect(post.bullet_primary_key_value).to eq(post.id)
end
it 'should return primary key value' do
post = Post.first
Post.primary_key = 'name'
expect(post.bullet_primary_key_value).to eq(post.name)
Post.primary_key = 'id'
end
it 'should return value for multiple primary keys from the composite_primary_key gem' do
post = Post.first
allow(Post).to receive(:primary_keys).and_return(%i[category_id writer_id])
expect(post.bullet_primary_key_value).to eq("#{post.category_id},#{post.writer_id}")
end
it 'should return value for multiple primary keys from ActiveRecord 7.1' do
post = Post.first
allow(Post).to receive(:primary_key).and_return(%i[category_id writer_id])
expect(post.bullet_primary_key_value).to eq("#{post.category_id},#{post.writer_id}")
end
it 'it should return nil for unpersisted records' do
post = Post.new(id: 123)
expect(post.bullet_primary_key_value).to be_nil
end
end
end
bullet-7.2.0/spec/bullet/ext/string_spec.rb 0000664 0000000 0000000 00000000554 14644222732 0020707 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
describe String do
context 'bullet_class_name' do
it 'should only return class name' do
expect('Post:1'.bullet_class_name).to eq('Post')
end
it 'should return class name with namespace' do
expect('Mongoid::Post:1234567890'.bullet_class_name).to eq('Mongoid::Post')
end
end
end
bullet-7.2.0/spec/bullet/notification/ 0000775 0000000 0000000 00000000000 14644222732 0017724 5 ustar 00root root 0000000 0000000 bullet-7.2.0/spec/bullet/notification/base_spec.rb 0000664 0000000 0000000 00000007251 14644222732 0022202 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
module Notification
describe Base do
subject { Base.new(Post, %i[comments votes]) }
context '#title' do
it 'should raise NoMethodError' do
expect { subject.title }
.to raise_error(NoMethodError)
end
end
context '#body' do
it 'should raise NoMethodError' do
expect { subject.body }
.to raise_error(NoMethodError)
end
end
context '#whoami' do
it 'should display user name' do
user = `whoami`.chomp
expect(subject.whoami).to eq("user: #{user}")
end
it 'should leverage ENV parameter' do
temp_env_variable('USER', 'bogus') { expect(subject.whoami).to eq('user: bogus') }
end
it 'should return blank if no user available' do
temp_env_variable('USER', '') do
expect(subject).to receive(:`).with('whoami').and_return('')
expect(subject.whoami).to eq('')
end
end
it 'should return blank if whoami is not available' do
temp_env_variable('USER', '') do
expect(subject).to receive(:`).with('whoami').and_raise(Errno::ENOENT)
expect(subject.whoami).to eq('')
end
end
def temp_env_variable(name, value)
old_value = ENV[name]
ENV[name] = value
yield
ensure
ENV[name] = old_value
end
end
context '#body_with_caller' do
it 'should return body' do
allow(subject).to receive(:body).and_return('body')
allow(subject).to receive(:call_stack_messages).and_return('call_stack_messages')
expect(subject.body_with_caller).to eq("body\ncall_stack_messages\n")
end
end
context '#notification_data' do
it 'should return notification data' do
allow(subject).to receive(:whoami).and_return('whoami')
allow(subject).to receive(:url).and_return('url')
allow(subject).to receive(:title).and_return('title')
allow(subject).to receive(:body_with_caller).and_return('body_with_caller')
expect(subject.notification_data).to eq(user: 'whoami', url: 'url', title: 'title', body: 'body_with_caller')
end
context 'when skip_user_in_notification is true' do
before { allow(Bullet).to receive(:skip_user_in_notification).and_return(true) }
it 'should return notification data without user' do
allow(subject).to receive(:url).and_return('url')
allow(subject).to receive(:title).and_return('title')
allow(subject).to receive(:body_with_caller).and_return('body_with_caller')
expect(subject.notification_data).to eq(url: 'url', title: 'title', body: 'body_with_caller')
end
end
end
context '#notify_inline' do
it 'should send full_notice to notifier' do
notifier = double
allow(subject).to receive(:notifier).and_return(notifier)
allow(subject).to receive(:notification_data).and_return({ foo: :bar })
expect(notifier).to receive(:inline_notify).with({ foo: :bar })
subject.notify_inline
end
end
context '#notify_out_of_channel' do
it 'should send full_out_of_channel to notifier' do
notifier = double
allow(subject).to receive(:notifier).and_return(notifier)
allow(subject).to receive(:notification_data).and_return({ foo: :bar })
expect(notifier).to receive(:out_of_channel_notify).with({ foo: :bar })
subject.notify_out_of_channel
end
end
end
end
end
bullet-7.2.0/spec/bullet/notification/counter_cache_spec.rb 0000664 0000000 0000000 00000000547 14644222732 0024073 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
module Notification
describe CounterCache do
subject { CounterCache.new(Post, %i[comments votes]) }
it { expect(subject.body).to eq(' Post => [:comments, :votes]') }
it { expect(subject.title).to eq('Need Counter Cache with Active Record size') }
end
end
end
bullet-7.2.0/spec/bullet/notification/n_plus_one_query_spec.rb 0000664 0000000 0000000 00000002037 14644222732 0024653 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
module Notification
describe NPlusOneQuery do
subject { NPlusOneQuery.new([%w[caller1 caller2]], Post, %i[comments votes], 'path') }
it do
expect(subject.body_with_caller).to eq(
" Post => [:comments, :votes]\n Add to your query: .includes([:comments, :votes])\nCall stack\n caller1\n caller2\n"
)
end
it do
expect([subject.body_with_caller, subject.body_with_caller]).to eq(
[
" Post => [:comments, :votes]\n Add to your query: .includes([:comments, :votes])\nCall stack\n caller1\n caller2\n",
" Post => [:comments, :votes]\n Add to your query: .includes([:comments, :votes])\nCall stack\n caller1\n caller2\n"
]
)
end
it do
expect(subject.body).to eq(" Post => [:comments, :votes]\n Add to your query: .includes([:comments, :votes])")
end
it { expect(subject.title).to eq('USE eager loading in path') }
end
end
end
bullet-7.2.0/spec/bullet/notification/unused_eager_loading_spec.rb 0000664 0000000 0000000 00000000721 14644222732 0025426 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
module Notification
describe UnusedEagerLoading do
subject { UnusedEagerLoading.new([''], Post, %i[comments votes], 'path') }
it do
expect(subject.body).to eq(
" Post => [:comments, :votes]\n Remove from your query: .includes([:comments, :votes])"
)
end
it { expect(subject.title).to eq('AVOID eager loading in path') }
end
end
end
bullet-7.2.0/spec/bullet/notification_collector_spec.rb 0000664 0000000 0000000 00000001465 14644222732 0023337 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
describe NotificationCollector do
subject { NotificationCollector.new.tap { |collector| collector.add('value') } }
context '#add' do
it 'should add a value' do
subject.add('value1')
expect(subject.collection).to be_include('value1')
end
end
context '#reset' do
it 'should reset collector' do
subject.reset
expect(subject.collection).to be_empty
end
end
context '#notifications_present?' do
it 'should be true if collection is not empty' do
expect(subject).to be_notifications_present
end
it 'should be false if collection is empty' do
subject.reset
expect(subject).not_to be_notifications_present
end
end
end
end
bullet-7.2.0/spec/bullet/rack_spec.rb 0000664 0000000 0000000 00000036014 14644222732 0017521 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
module Bullet
describe Rack do
let(:middleware) { Bullet::Rack.new app }
let(:app) { Support::AppDouble.new }
context '#html_request?' do
it 'should be true if Content-Type is text/html and http body contains html tag' do
headers = { 'Content-Type' => 'text/html' }
response = double(body: '