# frozen_string_literal: true require 'active_support/core_ext/module/delegation' require 'set' require 'uniform_notifier' require 'bullet/ext/object' require 'bullet/ext/string' require 'bullet/dependency' require 'bullet/stack_trace_filter' module Bullet extend Dependency autoload :ActiveRecord, "bullet/#{active_record_version}" if active_record? autoload :Mongoid, "bullet/#{mongoid_version}" if mongoid? autoload :Rack, 'bullet/rack' autoload :ActiveJob, 'bullet/active_job' autoload :Notification, 'bullet/notification' autoload :Detector, 'bullet/detector' autoload :Registry, 'bullet/registry' autoload :NotificationCollector, 'bullet/notification_collector' if defined?(Rails::Railtie) class BulletRailtie < Rails::Railtie initializer 'bullet.configure_rails_initialization' do |app| app.middleware.use Bullet::Rack end end end class << self attr_writer :n_plus_one_query_enable, :unused_eager_loading_enable, :counter_cache_enable, :stacktrace_includes, :stacktrace_excludes, :skip_html_injection attr_reader :safelist attr_accessor :add_footer, :orm_patches_applied, :skip_http_headers available_notifiers = UniformNotifier::AVAILABLE_NOTIFIERS.select { |notifier| notifier != :raise }.map { |notifier| "#{notifier}=" } available_notifiers_options = { to: UniformNotifier } delegate(*available_notifiers, **available_notifiers_options) def raise=(should_raise) UniformNotifier.raise = (should_raise ? Notification::UnoptimizedQueryError : false) end DETECTORS = [ Bullet::Detector::NPlusOneQuery, Bullet::Detector::UnusedEagerLoading, Bullet::Detector::CounterCache ].freeze def enable=(enable) @enable = @n_plus_one_query_enable = @unused_eager_loading_enable = @counter_cache_enable = enable if enable? reset_safelist unless orm_patches_applied self.orm_patches_applied = true Bullet::Mongoid.enable if mongoid? Bullet::ActiveRecord.enable if active_record? end end end def enable? !!@enable end # Rails.root might be nil if `railties` is a dependency on a project that does not use Rails def app_root @app_root ||= (defined?(::Rails.root) && !::Rails.root.nil? ? Rails.root.to_s : Dir.pwd).to_s end def n_plus_one_query_enable? enable? && !!@n_plus_one_query_enable end def unused_eager_loading_enable? enable? && !!@unused_eager_loading_enable end def counter_cache_enable? enable? && !!@counter_cache_enable end def stacktrace_includes @stacktrace_includes ||= [] end def stacktrace_excludes @stacktrace_excludes ||= [] end def add_safelist(options) reset_safelist @safelist[options[:type]][options[:class_name]] ||= [] @safelist[options[:type]][options[:class_name]] << options[:association].to_sym end def delete_safelist(options) reset_safelist @safelist[options[:type]][options[:class_name]] ||= [] @safelist[options[:type]][options[:class_name]].delete(options[:association].to_sym) @safelist[options[:type]].delete_if { |_key, val| val.empty? } end def get_safelist_associations(type, class_name) Array(@safelist[type][class_name]) end def reset_safelist @safelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} } end def clear_safelist @safelist = nil end def add_whitelist(options) ActiveSupport::Deprecation.warn(<<~WARN.strip add_whitelist is deprecated in favor of add_safelist. It will be removed from the next major release. WARN ) add_safelist(options) end def delete_whitelist(options) ActiveSupport::Deprecation.warn(<<~WARN.strip delete_whitelist is deprecated in favor of delete_safelist. It will be removed from the next major release. WARN ) delete_safelist(options) end def get_whitelist_associations(type, class_name) ActiveSupport::Deprecation.warn(<<~WARN.strip get_whitelist_associations is deprecated in favor of get_safelist_associations. It will be removed from the next major release. WARN ) get_safelist_associations(type, class_name) end def reset_whitelist ActiveSupport::Deprecation.warn(<<~WARN.strip reset_whitelist is deprecated in favor of reset_safelist. It will be removed from the next major release. WARN ) reset_safelist end def clear_whitelist ActiveSupport::Deprecation.warn(<<~WARN.strip clear_whitelist is deprecated in favor of clear_safelist. It will be removed from the next major release. WARN ) clear_safelist end def bullet_logger=(active) if active require 'fileutils' FileUtils.mkdir_p(app_root + '/log') bullet_log_file = File.open("#{app_root}/log/bullet.log", 'a+') bullet_log_file.sync = true UniformNotifier.customized_logger = bullet_log_file end end def debug(title, message) puts "[Bullet][#{title}] #{message}" if ENV['BULLET_DEBUG'] == 'true' end def start_request Thread.current[:bullet_start] = true Thread.current[:bullet_notification_collector] = Bullet::NotificationCollector.new Thread.current[:bullet_object_associations] = Bullet::Registry::Base.new Thread.current[:bullet_call_object_associations] = Bullet::Registry::Base.new Thread.current[:bullet_possible_objects] = Bullet::Registry::Object.new Thread.current[:bullet_impossible_objects] = Bullet::Registry::Object.new Thread.current[:bullet_inversed_objects] = Bullet::Registry::Base.new Thread.current[:bullet_eager_loadings] = Bullet::Registry::Association.new Thread.current[:bullet_counter_possible_objects] ||= Bullet::Registry::Object.new Thread.current[:bullet_counter_impossible_objects] ||= Bullet::Registry::Object.new end def end_request Thread.current[:bullet_start] = nil Thread.current[:bullet_notification_collector] = nil Thread.current[:bullet_object_associations] = nil Thread.current[:bullet_call_object_associations] = nil Thread.current[:bullet_possible_objects] = nil Thread.current[:bullet_impossible_objects] = nil Thread.current[:bullet_inversed_objects] = nil Thread.current[:bullet_eager_loadings] = nil Thread.current[:bullet_counter_possible_objects] = nil Thread.current[:bullet_counter_impossible_objects] = nil end def start? enable? && Thread.current[:bullet_start] end def notification_collector Thread.current[:bullet_notification_collector] end def notification? return unless start? Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations notification_collector.notifications_present? end def gather_inline_notifications responses = [] for_each_active_notifier_with_notification { |notification| responses << notification.notify_inline } responses.join("\n") end def perform_out_of_channel_notifications(env = {}) request_uri = build_request_uri(env) for_each_active_notifier_with_notification do |notification| notification.url = request_uri notification.notify_out_of_channel end end def footer_info info = [] notification_collector.collection.each { |notification| info << notification.short_notice } info end def text_notifications info = [] notification_collector.collection.each do |notification| info << notification.notification_data.values.compact.join("\n") end info end def warnings notification_collector.collection.each_with_object({}) do |notification, warnings| warning_type = notification.class.to_s.split(':').last.tableize warnings[warning_type] ||= [] warnings[warning_type] << notification end end def profile return_value = nil if Bullet.enable? begin Bullet.start_request return_value = yield Bullet.perform_out_of_channel_notifications if Bullet.notification? ensure Bullet.end_request end else return_value = yield end return_value end def console_enabled? UniformNotifier.active_notifiers.include?(UniformNotifier::JavascriptConsole) end def inject_into_page? return false if defined?(@skip_html_injection) && @skip_html_injection console_enabled? || add_footer end private def for_each_active_notifier_with_notification UniformNotifier.active_notifiers.each do |notifier| notification_collector.collection.each do |notification| notification.notifier = notifier yield notification end end end def build_request_uri(env) return "#{env['REQUEST_METHOD']} #{env['REQUEST_URI']}" if env['REQUEST_URI'] if env['QUERY_STRING'].present? "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}?#{env['QUERY_STRING']}" else "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}" end end end end