bullet-6.1.0/ 0000755 0001750 0001750 00000000000 13704275356 014241 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/Gemfile.mongoid-4.0 0000644 0001750 0001750 00000000426 13704275356 017470 0 ustar debbiecocoa debbiecocoa 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-6.1.0/Guardfile 0000644 0001750 0001750 00000000516 13704275356 016070 0 ustar debbiecocoa debbiecocoa # 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-6.1.0/README.md 0000644 0001750 0001750 00000034740 13704275356 015530 0 ustar debbiecocoa debbiecocoa # Bullet [](http://badge.fury.io/rb/bullet) [](http://travis-ci.org/flyerhzm/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' ``` **Note**: make sure `bullet` gem is added after activerecord (rails) and mongoid. ## Configuration Bullet won't do ANYTHING 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.growl = 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.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.growl`: pop up Growl warnings if your system has Growl installed. Requires a little bit of configuration * `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.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 XHR into the returned HTML. This must be false for receiving alerts or console logging. * `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 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 ``` ## Whitelist Sometimes Bullet may notify you of query problems you don't care to fix, or which come from outside your code. You can whitelist these to ignore them: ```ruby Bullet.add_whitelist :type => :n_plus_one_query, :class_name => "Post", :association => :comments Bullet.add_whitelist :type => :unused_eager_loading, :class_name => "Post", :association => :comments Bullet.add_whitelist :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] N+1 Query: PATH_INFO: /posts; model: Post => associations: [comments]· Add to your finder: :include => [:comments] 2009-08-25 20:40:17[INFO] N+1 Query: method call stack:· /Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb' /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each' /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb' /Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index' ``` The first two lines are notifications that N+1 queries have been encountered. The remaining lines are stack traces 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] Unused eager loadings: PATH_INFO: /posts; model: Post => associations: [comments]· Remove from your finder: :include => [:comments] ``` These two 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] ``` ## Growl, XMPP/Jabber and Airbrake Support see [https://github.com/flyerhzm/uniform_notifier](https://github.com/flyerhzm/uniform_notifier) ## 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 ``` ### 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 visit [https://github.com/flyerhzm/bullet_test](https://github.com/flyerhzm/bullet_test) or 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 rake db:migrate ``` 2\. Change `app/model/post.rb` and `app/model/comment.rb` ```ruby class Post < ActiveRecord::Base has_many :comments end class Comment < ActiveRecord::Base 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 "
'
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 * 1_024
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') && response_body(response).include?('See 'Uniform Notifier' in JS Console for Stacktrace#{cancel_button}"
else
cancel_button
end
end
# Make footer work for XHR requests by appending data to the footer
def xhr_script
""
end
end
end
bullet-6.1.0/lib/bullet/ext/ 0000755 0001750 0001750 00000000000 13704275356 017076 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/lib/bullet/ext/object.rb 0000644 0001750 0001750 00000000760 13704275356 020674 0 ustar debbiecocoa debbiecocoa # 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
self.class.primary_keys.map { |primary_key| send primary_key }.join(',')
elsif self.class.respond_to?(:primary_key) && self.class.primary_key
send self.class.primary_key
else
id
end
end
end
bullet-6.1.0/lib/bullet/ext/string.rb 0000644 0001750 0001750 00000000146 13704275356 020732 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class String
def bullet_class_name
sub(/:[^:]*?$/, '')
end
end
bullet-6.1.0/lib/bullet/version.rb 0000644 0001750 0001750 00000000105 13704275356 020304 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
module Bullet
VERSION = '6.1.0'
end
bullet-6.1.0/lib/bullet/detector.rb 0000644 0001750 0001750 00000000562 13704275356 020437 0 ustar debbiecocoa debbiecocoa # 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-6.1.0/lib/bullet/notification.rb 0000644 0001750 0001750 00000000603 13704275356 021310 0 ustar debbiecocoa debbiecocoa # 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-6.1.0/lib/bullet/notification/ 0000755 0001750 0001750 00000000000 13704275356 020764 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/lib/bullet/notification/unused_eager_loading.rb 0000644 0001750 0001750 00000001222 13704275356 025451 0 ustar debbiecocoa debbiecocoa # 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-6.1.0/lib/bullet/notification/base.rb 0000644 0001750 0001750 00000003530 13704275356 022224 0 ustar debbiecocoa debbiecocoa # 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
[whoami.presence, url, title, body].compact.join(' ')
end
def notification_data
{ user: whoami, url: url, title: title, body: body_with_caller }
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-6.1.0/lib/bullet/notification/counter_cache.rb 0000644 0001750 0001750 00000000343 13704275356 024113 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
module Bullet
module Notification
class CounterCache < Base
def body
klazz_associations_str
end
def title
'Need Counter Cache'
end
end
end
end
bullet-6.1.0/lib/bullet/notification/n_plus_one_query.rb 0000644 0001750 0001750 00000001206 13704275356 024676 0 ustar debbiecocoa debbiecocoa # 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-6.1.0/lib/bullet/active_job.rb 0000644 0001750 0001750 00000000307 13704275356 020730 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
module Bullet
module ActiveJob
def self.included(base)
base.class_eval { around_perform { |_job, block| Bullet.profile { block.call } } }
end
end
end
bullet-6.1.0/lib/bullet/registry/ 0000755 0001750 0001750 00000000000 13704275356 020146 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/lib/bullet/registry/association.rb 0000644 0001750 0001750 00000000570 13704275356 023011 0 ustar debbiecocoa debbiecocoa # 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-6.1.0/lib/bullet/registry/base.rb 0000644 0001750 0001750 00000001347 13704275356 021412 0 ustar debbiecocoa debbiecocoa # 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-6.1.0/lib/bullet/registry/object.rb 0000644 0001750 0001750 00000000447 13704275356 021746 0 ustar debbiecocoa debbiecocoa # 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-6.1.0/lib/bullet/active_record41.rb 0000644 0001750 0001750 00000015121 13704275356 021601 0 ustar debbiecocoa debbiecocoa # 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
Bullet::Detector::Association.add_object_associations(record, associations)
Bullet::Detector::NPlusOneQuery.call_association(record, associations)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << associations
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
end
end
end
bullet-6.1.0/lib/bullet/active_record42.rb 0000644 0001750 0001750 00000022230 13704275356 021601 0 ustar debbiecocoa debbiecocoa # 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
Bullet::Detector::Association.add_object_associations(ar_parent, associations)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, associations)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << associations
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
Bullet::Detector::Association.add_object_associations(record, associations)
Bullet::Detector::NPlusOneQuery.call_association(record, associations)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << associations
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
end
end
end
bullet-6.1.0/lib/bullet/mongoid6x.rb 0000644 0001750 0001750 00000003471 13704275356 020542 0 ustar debbiecocoa debbiecocoa # 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_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 { |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-6.1.0/lib/bullet/active_record5.rb 0000644 0001750 0001750 00000022746 13704275356 021534 0 ustar debbiecocoa debbiecocoa # 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
Bullet::Detector::Association.add_object_associations(ar_parent, associations)
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, associations)
@bullet_eager_loadings[ar_parent.class] ||= {}
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
@bullet_eager_loadings[ar_parent.class][ar_parent] << associations
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
Bullet::Detector::Association.add_object_associations(record, associations)
Bullet::Detector::NPlusOneQuery.call_association(record, associations)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << associations
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
Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
association = owner.association refl.name
Array(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
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
)
end
end
end
bullet-6.1.0/lib/bullet/detector/ 0000755 0001750 0001750 00000000000 13704275356 020107 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/lib/bullet/detector/unused_eager_loading.rb 0000644 0001750 0001750 00000007014 13704275356 024601 0 ustar debbiecocoa debbiecocoa # 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 ununsed preload assocations
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_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(associations) - Bullet.get_whitelist_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-6.1.0/lib/bullet/detector/association.rb 0000644 0001750 0001750 00000006011 13704275356 022746 0 ustar debbiecocoa debbiecocoa # 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}"
)
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_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
end
end
end
end
bullet-6.1.0/lib/bullet/detector/base.rb 0000644 0001750 0001750 00000000135 13704275356 021345 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
module Bullet
module Detector
class Base; end
end
end
bullet-6.1.0/lib/bullet/detector/counter_cache.rb 0000644 0001750 0001750 00000004321 13704275356 023236 0 ustar debbiecocoa debbiecocoa # 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(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(associations) - Bullet.get_whitelist_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-6.1.0/lib/bullet/detector/n_plus_one_query.rb 0000644 0001750 0001750 00000007720 13704275356 024030 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
module Bullet
module Detector
class NPlusOneQuery < Association
extend Dependency
extend StackTraceFilter
class << self
# executed when object.assocations 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.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(object_or_objects)
return if objects.map(&:bullet_primary_key_value).compact.empty?
Bullet.debug(
'Detector::NPlusOneQuery#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.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(associations) - Bullet.get_whitelist_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-6.1.0/lib/bullet/active_record4.rb 0000644 0001750 0001750 00000015542 13704275356 021527 0 ustar debbiecocoa debbiecocoa # 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
Bullet::Detector::Association.add_object_associations(record, associations)
Bullet::Detector::NPlusOneQuery.call_association(record, associations)
@bullet_eager_loadings[record.class] ||= {}
@bullet_eager_loadings[record.class][record] ||= Set.new
@bullet_eager_loadings[record.class][record] << associations
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
end
end
end
bullet-6.1.0/lib/bullet/mongoid4x.rb 0000644 0001750 0001750 00000003433 13704275356 020536 0 ustar debbiecocoa debbiecocoa # 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_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 { |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-6.1.0/lib/bullet/bullet_xhr.js 0000644 0001750 0001750 00000004501 13704275356 021004 0 ustar debbiecocoa debbiecocoa (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 oldOpen.apply(this, arguments);
}
function bulletXHRSend() {
if (this.onload) {
this._storedOnload = this.onload;
}
this.addEventListener('load', bulletXHROnload);
return oldSend.apply(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(() => {
var oldHtml = document.getElementById('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.getElementById('bullet-footer').innerHTML = `${header}
${newHtml.join('
')}`;
}, 0);
}
var bulletConsoleText = this.getResponseHeader('X-bullet-console-text');
if (bulletConsoleText && typeof console !== 'undefined' && console.log) {
setTimeout(() => {
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 this._storedOnload.apply(this, arguments);
}
}
window.XMLHttpRequest.prototype.open = bulletXHROpen;
window.XMLHttpRequest.prototype.send = bulletXHRSend;
})();
bullet-6.1.0/lib/generators/ 0000755 0001750 0001750 00000000000 13704275356 017160 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/lib/generators/bullet/ 0000755 0001750 0001750 00000000000 13704275356 020447 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/lib/generators/bullet/install_generator.rb 0000644 0001750 0001750 00000002301 13704275356 024504 0 ustar debbiecocoa debbiecocoa # 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.growl = true
Bullet.rails_logger = true
Bullet.add_footer = true
end
FILE
.strip
end
say 'Enabled bullet in config/environments/development.rb'
end
def enable_in_test
if 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
.strip
end
say 'Enabled bullet in config/environments/test.rb'
end
end
end
end
end
bullet-6.1.0/Gemfile 0000644 0001750 0001750 00000000763 13704275356 015542 0 ustar debbiecocoa debbiecocoa 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-6.1.0/Gemfile.rails-4.1 0000644 0001750 0001750 00000000474 13704275356 017152 0 ustar debbiecocoa debbiecocoa 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-6.1.0/Gemfile.rails-4.0 0000644 0001750 0001750 00000000520 13704275356 017141 0 ustar debbiecocoa debbiecocoa 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-6.1.0/Gemfile.mongoid-5.0 0000644 0001750 0001750 00000000426 13704275356 017471 0 ustar debbiecocoa debbiecocoa 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-6.1.0/Gemfile.rails-5.0 0000644 0001750 0001750 00000000416 13704275356 017146 0 ustar debbiecocoa debbiecocoa 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-6.1.0/Gemfile.mongoid-6.0 0000644 0001750 0001750 00000000426 13704275356 017472 0 ustar debbiecocoa debbiecocoa 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-6.1.0/Gemfile.mongoid-7.0 0000644 0001750 0001750 00000000424 13704275356 017471 0 ustar debbiecocoa debbiecocoa 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-6.1.0/Rakefile 0000644 0001750 0001750 00000002242 13704275356 015706 0 ustar debbiecocoa debbiecocoa $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-6.1.0/tasks/ 0000755 0001750 0001750 00000000000 13704275356 015366 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/tasks/bullet_tasks.rake 0000644 0001750 0001750 00000000335 13704275356 020727 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
namespace :bullet do
namespace :log do
desc 'Truncates the bullet log file to zero bytes'
task :clear do
f = File.open('log/bullet.log', 'w')
f.close
end
end
end
bullet-6.1.0/update.sh 0000755 0001750 0001750 00000000736 13704275356 016070 0 ustar debbiecocoa debbiecocoa BUNDLE_GEMFILE=Gemfile.rails-5.2 bundle update
BUNDLE_GEMFILE=Gemfile.rails-5.1 bundle update
BUNDLE_GEMFILE=Gemfile.rails-5.0 bundle update
BUNDLE_GEMFILE=Gemfile.rails-4.2 bundle update
BUNDLE_GEMFILE=Gemfile.rails-4.1 bundle update
BUNDLE_GEMFILE=Gemfile.rails-4.0 bundle update
BUNDLE_GEMFILE=Gemfile.mongoid-7.0 bundle update
BUNDLE_GEMFILE=Gemfile.mongoid-6.0 bundle update
BUNDLE_GEMFILE=Gemfile.mongoid-5.0 bundle update
BUNDLE_GEMFILE=Gemfile.mongoid-4.0 bundle update
bullet-6.1.0/MIT-LICENSE 0000644 0001750 0001750 00000002075 13704275356 015701 0 ustar debbiecocoa debbiecocoa Copyright (c) 2009 - 2010 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-6.1.0/spec/ 0000755 0001750 0001750 00000000000 13704275356 015173 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/spec/spec_helper.rb 0000644 0001750 0001750 00000004310 13704275356 020007 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
require 'rspec'
begin
require 'active_record'
rescue LoadError
end
begin
require 'mongoid'
rescue LoadError
end
module Rails
class << self
def root
File.expand_path(__FILE__).split('/')[0..-3].join('/')
end
def env
'test'
end
end
end
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
require 'bullet'
extend Bullet::Dependency
Bullet.enable = true
MODELS = File.join(File.dirname(__FILE__), 'models')
$LOAD_PATH.unshift(MODELS)
SUPPORT = File.join(File.dirname(__FILE__), 'support')
Dir[File.join(SUPPORT, '*.rb')].reject { |filename| filename =~ /_seed.rb$/ }.sort.each { |file| require file }
RSpec.configure do |config|
config.extend Bullet::Dependency
config.filter_run focus: true
config.run_all_when_everything_filtered = true
end
if active_record?
ActiveRecord::Migration.verbose = false
# Autoload every active_record model for the test suite that sits in spec/models.
Dir[File.join(MODELS, '*.rb')].sort.each do |filename|
name = File.basename(filename, '.rb')
autoload name.camelize.to_sym, name
end
require File.join(SUPPORT, 'sqlite_seed.rb')
RSpec.configure do |config|
config.before(:suite) do
Support::SqliteSeed.setup_db
Support::SqliteSeed.seed_db
end
config.before(:example) do
Bullet.start_request
Bullet.enable = true
end
config.after(:example) { Bullet.end_request }
end
if ENV['BULLET_LOG']
require 'logger'
ActiveRecord::Base.logger = Logger.new(STDOUT)
end
end
if mongoid?
# Autoload every mongoid model for the test suite that sits in spec/models.
Dir[File.join(MODELS, 'mongoid', '*.rb')].sort.each { |file| require file }
require File.join(SUPPORT, 'mongo_seed.rb')
RSpec.configure do |config|
config.before(:suite) do
Support::MongoSeed.setup_db
Support::MongoSeed.seed_db
end
config.after(:suite) do
Support::MongoSeed.setup_db
Support::MongoSeed.teardown_db
end
config.before(:each) { Bullet.start_request }
config.after(:each) { Bullet.end_request }
end
if ENV['BULLET_LOG']
Mongoid.logger = Logger.new(STDOUT)
Moped.logger = Logger.new(STDOUT)
end
end
bullet-6.1.0/spec/models/ 0000755 0001750 0001750 00000000000 13704275356 016456 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/spec/models/relationship.rb 0000644 0001750 0001750 00000000163 13704275356 021504 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Relationship < ActiveRecord::Base
belongs_to :firm
belongs_to :client
end
bullet-6.1.0/spec/models/client.rb 0000644 0001750 0001750 00000000237 13704275356 020263 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Client < ActiveRecord::Base
belongs_to :group
has_many :relationships
has_many :firms, through: :relationships
end
bullet-6.1.0/spec/models/address.rb 0000644 0001750 0001750 00000000134 13704275356 020426 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Address < ActiveRecord::Base
belongs_to :company
end
bullet-6.1.0/spec/models/reply.rb 0000644 0001750 0001750 00000000135 13704275356 020135 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Reply < ActiveRecord::Base
belongs_to :submission
end
bullet-6.1.0/spec/models/newspaper.rb 0000644 0001750 0001750 00000000164 13704275356 021010 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Newspaper < ActiveRecord::Base
has_many :writers, class_name: 'BaseUser'
end
bullet-6.1.0/spec/models/country.rb 0000644 0001750 0001750 00000000131 13704275356 020501 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Country < ActiveRecord::Base
has_many :cities
end
bullet-6.1.0/spec/models/author.rb 0000644 0001750 0001750 00000000133 13704275356 020302 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Author < ActiveRecord::Base
has_many :documents
end
bullet-6.1.0/spec/models/post.rb 0000644 0001750 0001750 00000001604 13704275356 017771 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Post < ActiveRecord::Base
belongs_to :category, inverse_of: :posts
belongs_to :writer
has_many :comments, inverse_of: :post
validates :category, presence: true
scope :preload_comments, -> { includes(:comments) }
scope :in_category_name, ->(name) { where(['categories.name = ?', name]).includes(:category) }
scope :draft, -> { where(active: false) }
def link=(*)
comments.new
end
# see association_spec.rb 'should not detect newly assigned object in an after_save'
attr_accessor :trigger_after_save
after_save do
next unless trigger_after_save
temp_comment = Comment.new(post: self)
# this triggers self to be "possible", even though it's
# not saved yet
temp_comment.post
# category should NOT whine about not being pre-loaded, because
# it's obviously attached to a new object
category
end
end
bullet-6.1.0/spec/models/document.rb 0000644 0001750 0001750 00000000352 13704275356 020621 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Document < ActiveRecord::Base
has_many :children, class_name: 'Document', foreign_key: 'parent_id'
belongs_to :parent, class_name: 'Document', foreign_key: 'parent_id'
belongs_to :author
end
bullet-6.1.0/spec/models/teacher.rb 0000644 0001750 0001750 00000000152 13704275356 020414 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Teacher < ActiveRecord::Base
has_and_belongs_to_many :students
end
bullet-6.1.0/spec/models/comment.rb 0000644 0001750 0001750 00000000300 13704275356 020436 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Comment < ActiveRecord::Base
belongs_to :post, inverse_of: :comments
belongs_to :author, class_name: 'BaseUser'
validates :post, presence: true
end
bullet-6.1.0/spec/models/page.rb 0000644 0001750 0001750 00000000072 13704275356 017716 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Page < Document; end
bullet-6.1.0/spec/models/category.rb 0000644 0001750 0001750 00000000317 13704275356 020621 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Category < ActiveRecord::Base
has_many :posts, inverse_of: :category
has_many :entries
has_many :users
def draft_post
posts.draft.first_or_create
end
end
bullet-6.1.0/spec/models/pet.rb 0000644 0001750 0001750 00000000154 13704275356 017573 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Pet < ActiveRecord::Base
belongs_to :person, counter_cache: true
end
bullet-6.1.0/spec/models/student.rb 0000644 0001750 0001750 00000000152 13704275356 020467 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Student < ActiveRecord::Base
has_and_belongs_to_many :teachers
end
bullet-6.1.0/spec/models/base_user.rb 0000644 0001750 0001750 00000000206 13704275356 020751 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class BaseUser < ActiveRecord::Base
has_many :comments
has_many :posts
belongs_to :newspaper
end
bullet-6.1.0/spec/models/entry.rb 0000644 0001750 0001750 00000000133 13704275356 020141 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Entry < ActiveRecord::Base
belongs_to :category
end
bullet-6.1.0/spec/models/folder.rb 0000644 0001750 0001750 00000000074 13704275356 020257 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Folder < Document; end
bullet-6.1.0/spec/models/group.rb 0000644 0001750 0001750 00000000105 13704275356 020133 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Group < ActiveRecord::Base; end
bullet-6.1.0/spec/models/submission.rb 0000644 0001750 0001750 00000000160 13704275356 021173 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Submission < ActiveRecord::Base
belongs_to :user
has_many :replies
end
bullet-6.1.0/spec/models/person.rb 0000644 0001750 0001750 00000000126 13704275356 020310 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Person < ActiveRecord::Base
has_many :pets
end
bullet-6.1.0/spec/models/city.rb 0000644 0001750 0001750 00000000131 13704275356 017746 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class City < ActiveRecord::Base
belongs_to :country
end
bullet-6.1.0/spec/models/writer.rb 0000644 0001750 0001750 00000000074 13704275356 020320 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Writer < BaseUser; end
bullet-6.1.0/spec/models/mongoid/ 0000755 0001750 0001750 00000000000 13704275356 020112 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/spec/models/mongoid/address.rb 0000644 0001750 0001750 00000000234 13704275356 022063 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Mongoid::Address
include Mongoid::Document
field :name
belongs_to :company, class_name: 'Mongoid::Company'
end
bullet-6.1.0/spec/models/mongoid/post.rb 0000644 0001750 0001750 00000000472 13704275356 021427 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Mongoid::Post
include Mongoid::Document
field :name
has_many :comments, class_name: 'Mongoid::Comment'
belongs_to :category, class_name: 'Mongoid::Category'
embeds_many :users, class_name: 'Mongoid::User'
scope :preload_comments, -> { includes(:comments) }
end
bullet-6.1.0/spec/models/mongoid/comment.rb 0000644 0001750 0001750 00000000226 13704275356 022101 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Mongoid::Comment
include Mongoid::Document
field :name
belongs_to :post, class_name: 'Mongoid::Post'
end
bullet-6.1.0/spec/models/mongoid/category.rb 0000644 0001750 0001750 00000000310 13704275356 022246 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Mongoid::Category
include Mongoid::Document
field :name
has_many :posts, class_name: 'Mongoid::Post'
has_many :entries, class_name: 'Mongoid::Entry'
end
bullet-6.1.0/spec/models/mongoid/entry.rb 0000644 0001750 0001750 00000000234 13704275356 021577 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Mongoid::Entry
include Mongoid::Document
field :name
belongs_to :category, class_name: 'Mongoid::Category'
end
bullet-6.1.0/spec/models/mongoid/company.rb 0000644 0001750 0001750 00000000231 13704275356 022101 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Mongoid::Company
include Mongoid::Document
field :name
has_one :address, class_name: 'Mongoid::Address'
end
bullet-6.1.0/spec/models/mongoid/user.rb 0000644 0001750 0001750 00000000142 13704275356 021412 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Mongoid::User
include Mongoid::Document
field :name
end
bullet-6.1.0/spec/models/firm.rb 0000644 0001750 0001750 00000000260 13704275356 017736 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Firm < ActiveRecord::Base
has_many :relationships
has_many :clients, through: :relationships
has_many :groups, through: :clients
end
bullet-6.1.0/spec/models/company.rb 0000644 0001750 0001750 00000000131 13704275356 020444 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class Company < ActiveRecord::Base
has_one :address
end
bullet-6.1.0/spec/models/user.rb 0000644 0001750 0001750 00000000160 13704275356 017756 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
class User < ActiveRecord::Base
has_one :submission
belongs_to :category
end
bullet-6.1.0/spec/bullet_spec.rb 0000644 0001750 0001750 00000010513 13704275356 020021 0 ustar debbiecocoa debbiecocoa # frozen_string_literal: true
require 'spec_helper'
describe Bullet, focused: true do
subject { Bullet }
describe '#enable' do
context 'enable Bullet' do
before do
# Bullet.enable
# Do nothing. Bullet has already been enabled for the whole test suite.
end
it 'should be enabled' do
expect(subject).to be_enable
end
context 'disable Bullet' do
before { Bullet.enable = false }
it 'should be disabled' do
expect(subject).to_not be_enable
end
context 'enable Bullet again without patching again the orms' do
before do
expect(Bullet::Mongoid).not_to receive(:enable) if defined?(Bullet::Mongoid)
expect(Bullet::ActiveRecord).not_to receive(:enable) if defined?(Bullet::ActiveRecord)
Bullet.enable = true
end
it 'should be enabled again' do
expect(subject).to be_enable
end
end
end
end
end
describe '#start?' do
context 'when bullet is disabled' do
before(:each) { Bullet.enable = false }
it 'should not be started' do
expect(Bullet).not_to be_start
end
end
end
describe '#debug' do
before(:each) { $stdout = StringIO.new }
after(:each) { $stdout = STDOUT }
context 'when debug is enabled' do
before(:each) { ENV['BULLET_DEBUG'] = 'true' }
after(:each) { ENV['BULLET_DEBUG'] = 'false' }
it 'should output debug information' do
Bullet.debug('debug_message', 'this is helpful information')
expect($stdout.string).to eq("[Bullet][debug_message] this is helpful information\n")
end
end
context 'when debug is disabled' do
it 'should output debug information' do
Bullet.debug('debug_message', 'this is helpful information')
expect($stdout.string).to be_empty
end
end
end
describe '#add_whitelist' do
context "for 'special' class names" do
it 'is added to the whitelist successfully' do
Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
expect(Bullet.get_whitelist_associations(:n_plus_one_query, 'Klass')).to include :department
end
end
end
describe '#delete_whitelist' do
context "for 'special' class names" do
it 'is deleted from the whitelist successfully' do
Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
Bullet.delete_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
expect(Bullet.whitelist[:n_plus_one_query]).to eq({})
end
end
context 'when exists multiple definitions' do
it 'is deleted from the whitelist successfully' do
Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
Bullet.delete_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
expect(Bullet.get_whitelist_associations(:n_plus_one_query, 'Klass')).to include :department
expect(Bullet.get_whitelist_associations(:n_plus_one_query, 'Klass')).to_not include :team
end
end
end
describe '#perform_out_of_channel_notifications' do
let(:notification) { double }
before do
allow(Bullet).to receive(:for_each_active_notifier_with_notification).and_yield(notification)
allow(notification).to receive(:notify_out_of_channel)
end
context 'when called with Rack environment hash' do
let(:env) { { 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/path', 'QUERY_STRING' => 'foo=bar' } }
context "when env['REQUEST_URI'] is nil" do
before { env['REQUEST_URI'] = nil }
it 'should notification.url is built' do
expect(notification).to receive(:url=).with('GET /path?foo=bar')
Bullet.perform_out_of_channel_notifications(env)
end
end
context "when env['REQUEST_URI'] is present" do
before { env['REQUEST_URI'] = 'http://example.com/path' }
it "should notification.url is env['REQUEST_URI']" do
expect(notification).to receive(:url=).with('GET http://example.com/path')
Bullet.perform_out_of_channel_notifications(env)
end
end
end
end
end
bullet-6.1.0/spec/bullet/ 0000755 0001750 0001750 00000000000 13704275356 016462 5 ustar debbiecocoa debbiecocoa bullet-6.1.0/spec/bullet/notification_collector_spec.rb 0000644 0001750 0001750 00000001465 13704275356 024563 0 ustar debbiecocoa debbiecocoa # 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-6.1.0/spec/bullet/rack_spec.rb 0000644 0001750 0001750 00000014056 13704275356 020747 0 ustar debbiecocoa debbiecocoa # 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: '