pax_global_header 0000666 0000000 0000000 00000000064 14525652253 0014523 g ustar 00root root 0000000 0000000 52 comment=5de18679c269931622b44892d68cbd7ecdad0aa6 bullet-7.1.4/ 0000775 0000000 0000000 00000000000 14525652253 0013023 5 ustar 00root root 0000000 0000000 bullet-7.1.4/.github/ 0000775 0000000 0000000 00000000000 14525652253 0014363 5 ustar 00root root 0000000 0000000 bullet-7.1.4/.github/dependabot.yml 0000664 0000000 0000000 00000000166 14525652253 0017216 0 ustar 00root root 0000000 0000000 version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" bullet-7.1.4/.github/workflows/ 0000775 0000000 0000000 00000000000 14525652253 0016420 5 ustar 00root root 0000000 0000000 bullet-7.1.4/.github/workflows/main.yml 0000664 0000000 0000000 00000004772 14525652253 0020101 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'] 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.1.4/.gitignore 0000664 0000000 0000000 00000000243 14525652253 0015012 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/ bullet-7.1.4/.rspec 0000664 0000000 0000000 00000000033 14525652253 0014134 0 ustar 00root root 0000000 0000000 --colour --format progress bullet-7.1.4/CHANGELOG.md 0000664 0000000 0000000 00000017127 14525652253 0014644 0 ustar 00root root 0000000 0000000 ## Next Release ## 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 assocation ## 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 associatioin * 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.1.4/Gemfile 0000664 0000000 0000000 00000000763 14525652253 0014324 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.1.4/Gemfile.mongoid 0000664 0000000 0000000 00000000351 14525652253 0015750 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.1.4/Gemfile.mongoid-4.0 0000664 0000000 0000000 00000000426 14525652253 0016252 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.1.4/Gemfile.mongoid-5.0 0000664 0000000 0000000 00000000426 14525652253 0016253 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.1.4/Gemfile.mongoid-6.0 0000664 0000000 0000000 00000000426 14525652253 0016254 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.1.4/Gemfile.mongoid-7.0 0000664 0000000 0000000 00000000424 14525652253 0016253 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.1.4/Gemfile.mongoid-8.0 0000664 0000000 0000000 00000000422 14525652253 0016252 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.1.4/Gemfile.rails-4.0 0000664 0000000 0000000 00000000520 14525652253 0015723 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.1.4/Gemfile.rails-4.1 0000664 0000000 0000000 00000000474 14525652253 0015734 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.1.4/Gemfile.rails-4.2 0000664 0000000 0000000 00000000474 14525652253 0015735 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.1.4/Gemfile.rails-5.0 0000664 0000000 0000000 00000000416 14525652253 0015730 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.1.4/Gemfile.rails-5.1 0000664 0000000 0000000 00000000416 14525652253 0015731 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.1.4/Gemfile.rails-5.2 0000664 0000000 0000000 00000000416 14525652253 0015732 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.1.4/Gemfile.rails-6.0 0000664 0000000 0000000 00000000402 14525652253 0015724 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.1.4/Gemfile.rails-6.1 0000664 0000000 0000000 00000000402 14525652253 0015725 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.1.4/Gemfile.rails-7.0 0000664 0000000 0000000 00000000261 14525652253 0015730 0 ustar 00root root 0000000 0000000 source "https://rubygems.org" gemspec gem 'rails', '~> 7.0.0' gem 'sqlite3' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem "rspec" bullet-7.1.4/Gemfile.rails-7.1 0000664 0000000 0000000 00000000261 14525652253 0015731 0 ustar 00root root 0000000 0000000 source "https://rubygems.org" gemspec gem 'rails', '~> 7.1.0' gem 'sqlite3' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem "rspec" bullet-7.1.4/Guardfile 0000664 0000000 0000000 00000000516 14525652253 0014652 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.1.4/Hacking.md 0000664 0000000 0000000 00000006510 14525652253 0014713 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.1.4/MIT-LICENSE 0000664 0000000 0000000 00000002075 14525652253 0014463 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.1.4/README.md 0000664 0000000 0000000 00000036266 14525652253 0014317 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 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| %>
')
position = body.rindex('')
body.insert(position, content)
else
body << content
end
end
def footer_note
"Bullet Warnings
')}#{footer_console_message}
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.1.4/lib/bullet/registry.rb 0000664 0000000 0000000 00000000422 14525652253 0017253 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.1.4/lib/bullet/registry/ 0000775 0000000 0000000 00000000000 14525652253 0016730 5 ustar 00root root 0000000 0000000 bullet-7.1.4/lib/bullet/registry/association.rb 0000664 0000000 0000000 00000000612 14525652253 0021570 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.1.4/lib/bullet/registry/base.rb 0000664 0000000 0000000 00000001347 14525652253 0020174 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.1.4/lib/bullet/registry/call_stack.rb 0000664 0000000 0000000 00000000351 14525652253 0021354 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.1.4/lib/bullet/registry/object.rb 0000664 0000000 0000000 00000000447 14525652253 0020530 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.1.4/lib/bullet/stack_trace_filter.rb 0000664 0000000 0000000 00000004071 14525652253 0021237 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.1.4/lib/bullet/version.rb 0000664 0000000 0000000 00000000105 14525652253 0017066 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module Bullet
VERSION = '7.1.4'
end
bullet-7.1.4/lib/generators/ 0000775 0000000 0000000 00000000000 14525652253 0015742 5 ustar 00root root 0000000 0000000 bullet-7.1.4/lib/generators/bullet/ 0000775 0000000 0000000 00000000000 14525652253 0017231 5 ustar 00root root 0000000 0000000 bullet-7.1.4/lib/generators/bullet/install_generator.rb 0000664 0000000 0000000 00000002355 14525652253 0023277 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.1.4/perf/ 0000775 0000000 0000000 00000000000 14525652253 0013757 5 ustar 00root root 0000000 0000000 bullet-7.1.4/perf/benchmark.rb 0000664 0000000 0000000 00000006360 14525652253 0016243 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.1.4/rails/ 0000775 0000000 0000000 00000000000 14525652253 0014135 5 ustar 00root root 0000000 0000000 bullet-7.1.4/rails/init.rb 0000664 0000000 0000000 00000000060 14525652253 0015421 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'bullet'
bullet-7.1.4/spec/ 0000775 0000000 0000000 00000000000 14525652253 0013755 5 ustar 00root root 0000000 0000000 bullet-7.1.4/spec/bullet/ 0000775 0000000 0000000 00000000000 14525652253 0015244 5 ustar 00root root 0000000 0000000 bullet-7.1.4/spec/bullet/detector/ 0000775 0000000 0000000 00000000000 14525652253 0017055 5 ustar 00root root 0000000 0000000 bullet-7.1.4/spec/bullet/detector/association_spec.rb 0000664 0000000 0000000 00000001471 14525652253 0022733 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.1.4/spec/bullet/detector/base_spec.rb 0000664 0000000 0000000 00000000175 14525652253 0021331 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.1.4/spec/bullet/detector/counter_cache_spec.rb 0000664 0000000 0000000 00000004127 14525652253 0023222 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.1.4/spec/bullet/detector/n_plus_one_query_spec.rb 0000664 0000000 0000000 00000016313 14525652253 0024006 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.1.4/spec/bullet/detector/unused_eager_loading_spec.rb 0000664 0000000 0000000 00000014254 14525652253 0024565 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.1.4/spec/bullet/ext/ 0000775 0000000 0000000 00000000000 14525652253 0016044 5 ustar 00root root 0000000 0000000 bullet-7.1.4/spec/bullet/ext/object_spec.rb 0000664 0000000 0000000 00000003021 14525652253 0020645 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.1.4/spec/bullet/ext/string_spec.rb 0000664 0000000 0000000 00000000554 14525652253 0020715 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.1.4/spec/bullet/notification/ 0000775 0000000 0000000 00000000000 14525652253 0017732 5 ustar 00root root 0000000 0000000 bullet-7.1.4/spec/bullet/notification/base_spec.rb 0000664 0000000 0000000 00000006164 14525652253 0022212 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
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.1.4/spec/bullet/notification/counter_cache_spec.rb 0000664 0000000 0000000 00000000547 14525652253 0024101 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.1.4/spec/bullet/notification/n_plus_one_query_spec.rb 0000664 0000000 0000000 00000002037 14525652253 0024661 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.1.4/spec/bullet/notification/unused_eager_loading_spec.rb 0000664 0000000 0000000 00000000721 14525652253 0025434 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.1.4/spec/bullet/notification_collector_spec.rb 0000664 0000000 0000000 00000001465 14525652253 0023345 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.1.4/spec/bullet/rack_spec.rb 0000664 0000000 0000000 00000036014 14525652253 0017527 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: '