bullet 6.1.0 → 7.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +82 -0
  3. data/CHANGELOG.md +66 -0
  4. data/Gemfile.rails-6.0 +1 -1
  5. data/Gemfile.rails-6.1 +15 -0
  6. data/Gemfile.rails-7.0 +10 -0
  7. data/MIT-LICENSE +1 -1
  8. data/README.md +41 -27
  9. data/lib/bullet/active_job.rb +5 -1
  10. data/lib/bullet/active_record41.rb +1 -0
  11. data/lib/bullet/active_record42.rb +1 -0
  12. data/lib/bullet/active_record5.rb +10 -8
  13. data/lib/bullet/active_record52.rb +32 -25
  14. data/lib/bullet/active_record60.rb +30 -23
  15. data/lib/bullet/active_record61.rb +274 -0
  16. data/lib/bullet/active_record70.rb +284 -0
  17. data/lib/bullet/bullet_xhr.js +18 -17
  18. data/lib/bullet/dependency.rb +16 -0
  19. data/lib/bullet/detector/association.rb +8 -0
  20. data/lib/bullet/detector/base.rb +2 -1
  21. data/lib/bullet/detector/counter_cache.rb +2 -2
  22. data/lib/bullet/detector/n_plus_one_query.rb +24 -13
  23. data/lib/bullet/detector/unused_eager_loading.rb +3 -3
  24. data/lib/bullet/mongoid4x.rb +1 -1
  25. data/lib/bullet/mongoid5x.rb +1 -1
  26. data/lib/bullet/mongoid6x.rb +1 -1
  27. data/lib/bullet/mongoid7x.rb +32 -17
  28. data/lib/bullet/notification.rb +2 -1
  29. data/lib/bullet/rack.rb +64 -23
  30. data/lib/bullet/registry/call_stack.rb +12 -0
  31. data/lib/bullet/registry.rb +1 -0
  32. data/lib/bullet/stack_trace_filter.rb +15 -15
  33. data/lib/bullet/version.rb +1 -1
  34. data/lib/bullet.rb +35 -29
  35. data/lib/generators/bullet/install_generator.rb +22 -25
  36. data/perf/benchmark.rb +4 -1
  37. data/spec/bullet/detector/counter_cache_spec.rb +1 -1
  38. data/spec/bullet/detector/n_plus_one_query_spec.rb +1 -33
  39. data/spec/bullet/detector/unused_eager_loading_spec.rb +15 -10
  40. data/spec/bullet/ext/object_spec.rb +1 -1
  41. data/spec/bullet/notification/base_spec.rb +4 -4
  42. data/spec/bullet/notification/n_plus_one_query_spec.rb +1 -3
  43. data/spec/bullet/rack_spec.rb +152 -9
  44. data/spec/bullet/stack_trace_filter_spec.rb +26 -0
  45. data/spec/bullet_spec.rb +15 -15
  46. data/spec/integration/active_record/association_spec.rb +95 -12
  47. data/spec/integration/counter_cache_spec.rb +4 -4
  48. data/spec/integration/mongoid/association_spec.rb +4 -4
  49. data/spec/models/attachment.rb +5 -0
  50. data/spec/models/deal.rb +5 -0
  51. data/spec/models/folder.rb +2 -1
  52. data/spec/models/group.rb +2 -1
  53. data/spec/models/page.rb +2 -1
  54. data/spec/models/post.rb +2 -0
  55. data/spec/models/role.rb +7 -0
  56. data/spec/models/submission.rb +1 -0
  57. data/spec/models/user.rb +2 -0
  58. data/spec/models/writer.rb +2 -1
  59. data/spec/spec_helper.rb +0 -2
  60. data/spec/support/mongo_seed.rb +1 -0
  61. data/spec/support/sqlite_seed.rb +38 -0
  62. data/test.sh +2 -0
  63. metadata +20 -7
  64. data/.travis.yml +0 -33
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Bullet
4
4
  module Detector
5
- class Base; end
5
+ class Base
6
+ end
6
7
  end
7
8
  end
@@ -20,7 +20,7 @@ module Bullet
20
20
  return unless Bullet.start?
21
21
  return unless Bullet.counter_cache_enable?
22
22
 
23
- objects = Array(object_or_objects)
23
+ objects = Array.wrap(object_or_objects)
24
24
  return if objects.map(&:bullet_primary_key_value).compact.empty?
25
25
 
26
26
  Bullet.debug(
@@ -54,7 +54,7 @@ module Bullet
54
54
  private
55
55
 
56
56
  def create_notification(klazz, associations)
57
- notify_associations = Array(associations) - Bullet.get_whitelist_associations(:counter_cache, klazz)
57
+ notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:counter_cache, klazz)
58
58
 
59
59
  if notify_associations.present?
60
60
  notice = Bullet::Notification::CounterCache.new klazz, notify_associations
@@ -7,7 +7,7 @@ module Bullet
7
7
  extend StackTraceFilter
8
8
 
9
9
  class << self
10
- # executed when object.assocations is called.
10
+ # executed when object.associations is called.
11
11
  # first, it keeps this method call for object.association.
12
12
  # then, it checks if this associations call is unpreload.
13
13
  # if it is, keeps this unpreload associations and caller.
@@ -25,7 +25,7 @@ module Bullet
25
25
  )
26
26
  if !excluded_stacktrace_path? && conditions_met?(object, associations)
27
27
  Bullet.debug('detect n + 1 query', "object: #{object.bullet_key}, associations: #{associations}")
28
- create_notification caller_in_project, object.class.to_s, associations
28
+ create_notification caller_in_project(object.bullet_key), object.class.to_s, associations
29
29
  end
30
30
  end
31
31
 
@@ -33,14 +33,26 @@ module Bullet
33
33
  return unless Bullet.start?
34
34
  return unless Bullet.n_plus_one_query_enable?
35
35
 
36
- objects = Array(object_or_objects)
37
- return if objects.map(&:bullet_primary_key_value).compact.empty?
38
-
39
- Bullet.debug(
40
- 'Detector::NPlusOneQuery#add_possible_objects',
41
- "objects: #{objects.map(&:bullet_key).join(', ')}"
42
- )
43
- objects.each { |object| possible_objects.add object.bullet_key }
36
+ objects = Array.wrap(object_or_objects)
37
+ class_names_match_regex = true
38
+ primary_key_values_are_empty = true
39
+ keys_joined = ""
40
+ objects.each do |obj|
41
+ unless obj.class.name =~ /^HABTM_/
42
+ class_names_match_regex = false
43
+ end
44
+ unless obj.bullet_primary_key_value.nil?
45
+ primary_key_values_are_empty = false
46
+ end
47
+ keys_joined += "#{(keys_joined.empty?? '' : ', ')}#{obj.bullet_key}"
48
+ end
49
+ unless class_names_match_regex || primary_key_values_are_empty
50
+ Bullet.debug(
51
+ 'Detector::NPlusOneQuery#add_possible_objects',
52
+ "objects: #{keys_joined}"
53
+ )
54
+ objects.each { |object| possible_objects.add object.bullet_key }
55
+ end
44
56
  end
45
57
 
46
58
  def add_impossible_object(object)
@@ -84,8 +96,7 @@ module Bullet
84
96
  # associations == v comparison order is important here because
85
97
  # v variable might be a squeel node where :== method is redefined,
86
98
  # so it does not compare values at all and return unexpected results
87
- result =
88
- v.is_a?(Hash) ? v.key?(associations) : associations == v
99
+ result = v.is_a?(Hash) ? v.key?(associations) : associations == v
89
100
  return true if result
90
101
  end
91
102
 
@@ -95,7 +106,7 @@ module Bullet
95
106
  private
96
107
 
97
108
  def create_notification(callers, klazz, associations)
98
- notify_associations = Array(associations) - Bullet.get_whitelist_associations(:n_plus_one_query, klazz)
109
+ notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:n_plus_one_query, klazz)
99
110
 
100
111
  if notify_associations.present?
101
112
  notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, notify_associations)
@@ -10,7 +10,7 @@ module Bullet
10
10
  # check if there are unused preload associations.
11
11
  # get related_objects from eager_loadings associated with object and associations
12
12
  # get call_object_association from associations of call_object_associations whose object is in related_objects
13
- # if association not in call_object_association, then the object => association - call_object_association is ununsed preload assocations
13
+ # if association not in call_object_association, then the object => association - call_object_association is ununsed preload associations
14
14
  def check_unused_preload_associations
15
15
  return unless Bullet.start?
16
16
  return unless Bullet.unused_eager_loading_enable?
@@ -20,7 +20,7 @@ module Bullet
20
20
  next if object_association_diff.empty?
21
21
 
22
22
  Bullet.debug('detect unused preload', "object: #{bullet_key}, associations: #{object_association_diff}")
23
- create_notification(caller_in_project, bullet_key.bullet_class_name, object_association_diff)
23
+ create_notification(caller_in_project(bullet_key), bullet_key.bullet_class_name, object_association_diff)
24
24
  end
25
25
  end
26
26
 
@@ -65,7 +65,7 @@ module Bullet
65
65
  private
66
66
 
67
67
  def create_notification(callers, klazz, associations)
68
- notify_associations = Array(associations) - Bullet.get_whitelist_associations(:unused_eager_loading, klazz)
68
+ notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:unused_eager_loading, klazz)
69
69
 
70
70
  if notify_associations.present?
71
71
  notice = Bullet::Notification::UnusedEagerLoading.new(callers, klazz, notify_associations)
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -4,35 +4,50 @@ module Bullet
4
4
  module Mongoid
5
5
  def self.enable
6
6
  require 'mongoid'
7
+ require 'rubygems'
7
8
  ::Mongoid::Contextual::Mongo.class_eval do
8
9
  alias_method :origin_first, :first
9
10
  alias_method :origin_last, :last
10
11
  alias_method :origin_each, :each
11
12
  alias_method :origin_eager_load, :eager_load
12
13
 
13
- def first(opts = {})
14
- result = origin_first(opts)
15
- Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
16
- result
17
- end
18
-
19
- def last(opts = {})
20
- result = origin_last(opts)
21
- Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
22
- result
14
+ %i[first last].each do |context|
15
+ default = Gem::Version.new(::Mongoid::VERSION) >= Gem::Version.new('7.5') ? nil : {}
16
+ define_method(context) do |opts = default|
17
+ result = send(:"origin_#{context}", opts)
18
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
19
+ result
20
+ end
23
21
  end
24
22
 
25
23
  def each(&block)
26
24
  return to_enum unless block_given?
27
25
 
28
- records = []
29
- origin_each { |record| records << record }
30
- if records.length > 1
31
- Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
32
- elsif records.size == 1
33
- Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
26
+ first_document = nil
27
+ document_count = 0
28
+
29
+ origin_each do |document|
30
+ document_count += 1
31
+
32
+ if document_count == 1
33
+ first_document = document
34
+ elsif document_count == 2
35
+ Bullet::Detector::NPlusOneQuery.add_possible_objects([first_document, document])
36
+ yield(first_document)
37
+ first_document = nil
38
+ yield(document)
39
+ else
40
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(document)
41
+ yield(document)
42
+ end
34
43
  end
35
- records.each(&block)
44
+
45
+ if document_count == 1
46
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(first_document)
47
+ yield(first_document)
48
+ end
49
+
50
+ self
36
51
  end
37
52
 
38
53
  def eager_load(docs)
@@ -7,6 +7,7 @@ module Bullet
7
7
  autoload :NPlusOneQuery, 'bullet/notification/n_plus_one_query'
8
8
  autoload :CounterCache, 'bullet/notification/counter_cache'
9
9
 
10
- class UnoptimizedQueryError < StandardError; end
10
+ class UnoptimizedQueryError < StandardError
11
+ end
11
12
  end
12
13
  end
data/lib/bullet/rack.rb CHANGED
@@ -4,6 +4,8 @@ module Bullet
4
4
  class Rack
5
5
  include Dependency
6
6
 
7
+ NONCE_MATCHER = /script-src .*'nonce-(?<nonce>[A-Za-z0-9+\/]+={0,2})'/
8
+
7
9
  def initialize(app)
8
10
  @app = app
9
11
  end
@@ -17,14 +19,20 @@ module Bullet
17
19
  response_body = nil
18
20
 
19
21
  if Bullet.notification?
20
- if !Bullet.skip_html_injection? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
22
+ if Bullet.inject_into_page? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
21
23
  if html_request?(headers, response)
22
24
  response_body = response_body(response)
23
- response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
24
- response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
25
- response_body = append_to_html_body(response_body, xhr_script)
25
+
26
+ with_security_policy_nonce(headers) do |nonce|
27
+ response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
28
+ response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
29
+ if Bullet.add_footer && !Bullet.skip_http_headers
30
+ response_body = append_to_html_body(response_body, xhr_script(nonce))
31
+ end
32
+ end
33
+
26
34
  headers['Content-Length'] = response_body.bytesize.to_s
27
- else
35
+ elsif !Bullet.skip_http_headers
28
36
  set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
29
37
  set_header(headers, 'X-bullet-console-text', Bullet.text_notifications) if Bullet.console_enabled?
30
38
  end
@@ -40,12 +48,14 @@ module Bullet
40
48
  def empty?(response)
41
49
  # response may be ["Not Found"], ["Move Permanently"], etc, but
42
50
  # those should not happen if the status is 200
51
+ return true if !response.respond_to?(:body) && !response.respond_to?(:first)
43
52
  body = response_body(response)
44
53
  body.nil? || body.empty?
45
54
  end
46
55
 
47
56
  def append_to_html_body(response_body, content)
48
57
  body = response_body.dup
58
+ content = content.html_safe if content.respond_to?(:html_safe)
49
59
  if body.include?('</body>')
50
60
  position = body.rindex('</body>')
51
61
  body.insert(position, content)
@@ -55,14 +65,14 @@ module Bullet
55
65
  end
56
66
 
57
67
  def footer_note
58
- "<div #{footer_div_attributes}>" + footer_header + '<br>' + Bullet.footer_info.uniq.join('<br>') + '</div>'
68
+ "<details #{details_attributes}><summary #{summary_attributes}>Bullet Warnings</summary><div #{footer_content_attributes}>#{Bullet.footer_info.uniq.join('<br>')}#{footer_console_message}</div></details>"
59
69
  end
60
70
 
61
71
  def set_header(headers, header_name, header_array)
62
72
  # Many proxy applications such as Nginx and AWS ELB limit
63
73
  # the size a header to 8KB, so truncate the list of reports to
64
74
  # be under that limit
65
- header_array.pop while header_array.to_json.length > 8 * 1_024
75
+ header_array.pop while header_array.to_json.length > 8 * 1024
66
76
  headers[header_name] = header_array.to_json
67
77
  end
68
78
 
@@ -75,42 +85,73 @@ module Bullet
75
85
  end
76
86
 
77
87
  def html_request?(headers, response)
78
- headers['Content-Type']&.include?('text/html') && response_body(response).include?('<html')
88
+ headers['Content-Type']&.include?('text/html')
79
89
  end
80
90
 
81
91
  def response_body(response)
82
92
  if response.respond_to?(:body)
83
93
  Array === response.body ? response.body.first : response.body
84
- else
94
+ elsif response.respond_to?(:first)
85
95
  response.first
86
96
  end
87
97
  end
88
98
 
89
99
  private
90
100
 
91
- def footer_div_attributes
101
+ def details_attributes
102
+ <<~EOF
103
+ id="bullet-footer" data-is-bullet-footer
104
+ style="cursor: pointer; position: fixed; left: 0px; bottom: 0px; z-index: 9999; background: #fdf2f2; color: #9b1c1c; font-size: 12px; border-radius: 0px 8px 0px 0px; border: 1px solid #9b1c1c;"
105
+ EOF
106
+ end
107
+
108
+ def summary_attributes
109
+ <<~EOF
110
+ style="font-weight: 600; padding: 2px 8px"
111
+ EOF
112
+ end
113
+
114
+ def footer_content_attributes
92
115
  <<~EOF
93
- id="bullet-footer" data-is-bullet-footer ondblclick="this.parentNode.removeChild(this);" style="position: fixed; bottom: 0pt; left: 0pt; cursor: pointer; border-style: solid; border-color: rgb(153, 153, 153);
94
- -moz-border-top-colors: none; -moz-border-right-colors: none; -moz-border-bottom-colors: none;
95
- -moz-border-left-colors: none; -moz-border-image: none; border-width: 2pt 2pt 0px 0px;
96
- padding: 3px 5px; border-radius: 0pt 10pt 0pt 0px; background: none repeat scroll 0% 0% rgba(200, 200, 200, 0.8);
97
- color: rgb(119, 119, 119); font-size: 16px; font-family: 'Arial', sans-serif; z-index:9999;"
116
+ style="padding: 8px; border-top: 1px solid #9b1c1c;"
98
117
  EOF
99
118
  end
100
119
 
101
- def footer_header
102
- cancel_button =
103
- "<span onclick='this.parentNode.remove()' style='position:absolute; right: 10px; top: 0px; font-weight: bold; color: #333;'>&times;</span>"
120
+ def footer_console_message
104
121
  if Bullet.console_enabled?
105
- "<span>See 'Uniform Notifier' in JS Console for Stacktrace</span>#{cancel_button}"
106
- else
107
- cancel_button
122
+ "<br/><span style='font-style: italic;'>See 'Uniform Notifier' in JS Console for Stacktrace</span>"
108
123
  end
109
124
  end
110
125
 
111
126
  # Make footer work for XHR requests by appending data to the footer
112
- def xhr_script
113
- "<script type='text/javascript'>#{File.read("#{__dir__}/bullet_xhr.js")}</script>"
127
+ def xhr_script(nonce = nil)
128
+ script = File.read("#{__dir__}/bullet_xhr.js")
129
+
130
+ if nonce
131
+ "<script type='text/javascript' nonce='#{nonce}'>#{script}</script>"
132
+ else
133
+ "<script type='text/javascript'>#{script}</script>"
134
+ end
135
+ end
136
+
137
+ def with_security_policy_nonce(headers)
138
+ matched = (headers['Content-Security-Policy'] || '').match(NONCE_MATCHER)
139
+ nonce = matched[:nonce] if matched
140
+
141
+ if nonce
142
+ console_enabled = UniformNotifier.console
143
+ alert_enabled = UniformNotifier.alert
144
+
145
+ UniformNotifier.console = { attributes: { nonce: nonce } } if console_enabled
146
+ UniformNotifier.alert = { attributes: { nonce: nonce } } if alert_enabled
147
+
148
+ yield nonce
149
+
150
+ UniformNotifier.console = console_enabled
151
+ UniformNotifier.alert = alert_enabled
152
+ else
153
+ yield
154
+ end
114
155
  end
115
156
  end
116
157
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet
4
+ module Registry
5
+ class CallStack < Base
6
+ # remembers found association backtrace
7
+ def add(key)
8
+ @registry[key] = Thread.current.backtrace
9
+ end
10
+ end
11
+ end
12
+ end
@@ -5,5 +5,6 @@ module Bullet
5
5
  autoload :Base, 'bullet/registry/base'
6
6
  autoload :Object, 'bullet/registry/object'
7
7
  autoload :Association, 'bullet/registry/association'
8
+ autoload :CallStack, 'bullet/registry/call_stack'
8
9
  end
9
10
  end
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
+ require "bundler"
2
3
 
3
4
  module Bullet
4
5
  module StackTraceFilter
5
6
  VENDOR_PATH = '/vendor'
7
+ IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
6
8
 
7
- def caller_in_project
9
+ # @param bullet_key[String] - use this to get stored call stack from call_stacks object.
10
+ def caller_in_project(bullet_key = nil)
8
11
  vendor_root = Bullet.app_root + VENDOR_PATH
9
12
  bundler_path = Bundler.bundle_path.to_s
10
- select_caller_locations do |location|
13
+ select_caller_locations(bullet_key) do |location|
11
14
  caller_path = location_as_path(location)
12
15
  caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
13
- !caller_path.include?(bundler_path) ||
14
- Bullet.stacktrace_includes.any? { |include_pattern| pattern_matches?(location, include_pattern) }
16
+ !caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern|
17
+ pattern_matches?(location, include_pattern)
18
+ }
15
19
  end
16
20
  end
17
21
 
@@ -47,20 +51,16 @@ module Bullet
47
51
  end
48
52
 
49
53
  def location_as_path(location)
50
- ruby_19? ? location : location.absolute_path.to_s
51
- end
54
+ return location if location.is_a?(String)
52
55
 
53
- def select_caller_locations
54
- if ruby_19?
55
- caller.select { |caller_path| yield caller_path }
56
- else
57
- caller_locations.select { |location| yield location }
58
- end
56
+ IS_RUBY_19 ? location : location.absolute_path.to_s
59
57
  end
60
58
 
61
- def ruby_19?
62
- @ruby_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') if @ruby_19.nil?
63
- @ruby_19
59
+ def select_caller_locations(bullet_key = nil)
60
+ return caller.select { |caller_path| yield caller_path } if IS_RUBY_19
61
+
62
+ call_stack = bullet_key ? call_stacks[bullet_key] : caller_locations
63
+ call_stack.select { |location| yield location }
64
64
  end
65
65
  end
66
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '6.1.0'
4
+ VERSION = '7.0.7'
5
5
  end
data/lib/bullet.rb CHANGED
@@ -20,13 +20,14 @@ module Bullet
20
20
  autoload :Registry, 'bullet/registry'
21
21
  autoload :NotificationCollector, 'bullet/notification_collector'
22
22
 
23
- BULLET_DEBUG = 'BULLET_DEBUG'
24
- TRUE = 'true'
25
-
26
23
  if defined?(Rails::Railtie)
27
24
  class BulletRailtie < Rails::Railtie
28
25
  initializer 'bullet.configure_rails_initialization' do |app|
29
- app.middleware.use Bullet::Rack
26
+ if defined?(ActionDispatch::ContentSecurityPolicy::Middleware) && Rails.application.config.content_security_policy
27
+ app.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, Bullet::Rack
28
+ else
29
+ app.middleware.use Bullet::Rack
30
+ end
30
31
  end
31
32
  end
32
33
  end
@@ -38,10 +39,11 @@ module Bullet
38
39
  :stacktrace_includes,
39
40
  :stacktrace_excludes,
40
41
  :skip_html_injection
41
- attr_reader :whitelist
42
- attr_accessor :add_footer, :orm_patches_applied
42
+ attr_reader :safelist
43
+ attr_accessor :add_footer, :orm_patches_applied, :skip_http_headers
43
44
 
44
- available_notifiers = UniformNotifier::AVAILABLE_NOTIFIERS.map { |notifier| "#{notifier}=" }
45
+ available_notifiers =
46
+ UniformNotifier::AVAILABLE_NOTIFIERS.select { |notifier| notifier != :raise }.map { |notifier| "#{notifier}=" }
45
47
  available_notifiers_options = { to: UniformNotifier }
46
48
  delegate(*available_notifiers, **available_notifiers_options)
47
49
 
@@ -59,7 +61,7 @@ module Bullet
59
61
  @enable = @n_plus_one_query_enable = @unused_eager_loading_enable = @counter_cache_enable = enable
60
62
 
61
63
  if enable?
62
- reset_whitelist
64
+ reset_safelist
63
65
  unless orm_patches_applied
64
66
  self.orm_patches_applied = true
65
67
  Bullet::Mongoid.enable if mongoid?
@@ -72,8 +74,9 @@ module Bullet
72
74
  !!@enable
73
75
  end
74
76
 
77
+ # Rails.root might be nil if `railties` is a dependency on a project that does not use Rails
75
78
  def app_root
76
- (defined?(::Rails.root) ? Rails.root.to_s : Dir.pwd).to_s
79
+ @app_root ||= (defined?(::Rails.root) && !::Rails.root.nil? ? Rails.root.to_s : Dir.pwd).to_s
77
80
  end
78
81
 
79
82
  def n_plus_one_query_enable?
@@ -89,36 +92,36 @@ module Bullet
89
92
  end
90
93
 
91
94
  def stacktrace_includes
92
- @stacktrace_includes || []
95
+ @stacktrace_includes ||= []
93
96
  end
94
97
 
95
98
  def stacktrace_excludes
96
- @stacktrace_excludes || []
99
+ @stacktrace_excludes ||= []
97
100
  end
98
101
 
99
- def add_whitelist(options)
100
- reset_whitelist
101
- @whitelist[options[:type]][options[:class_name]] ||= []
102
- @whitelist[options[:type]][options[:class_name]] << options[:association].to_sym
102
+ def add_safelist(options)
103
+ reset_safelist
104
+ @safelist[options[:type]][options[:class_name]] ||= []
105
+ @safelist[options[:type]][options[:class_name]] << options[:association].to_sym
103
106
  end
104
107
 
105
- def delete_whitelist(options)
106
- reset_whitelist
107
- @whitelist[options[:type]][options[:class_name]] ||= []
108
- @whitelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
109
- @whitelist[options[:type]].delete_if { |_key, val| val.empty? }
108
+ def delete_safelist(options)
109
+ reset_safelist
110
+ @safelist[options[:type]][options[:class_name]] ||= []
111
+ @safelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
112
+ @safelist[options[:type]].delete_if { |_key, val| val.empty? }
110
113
  end
111
114
 
112
- def get_whitelist_associations(type, class_name)
113
- Array(@whitelist[type][class_name])
115
+ def get_safelist_associations(type, class_name)
116
+ Array.wrap(@safelist[type][class_name])
114
117
  end
115
118
 
116
- def reset_whitelist
117
- @whitelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
119
+ def reset_safelist
120
+ @safelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
118
121
  end
119
122
 
120
- def clear_whitelist
121
- @whitelist = nil
123
+ def clear_safelist
124
+ @safelist = nil
122
125
  end
123
126
 
124
127
  def bullet_logger=(active)
@@ -132,7 +135,7 @@ module Bullet
132
135
  end
133
136
 
134
137
  def debug(title, message)
135
- puts "[Bullet][#{title}] #{message}" if ENV[BULLET_DEBUG] == TRUE
138
+ puts "[Bullet][#{title}] #{message}" if ENV['BULLET_DEBUG'] == 'true'
136
139
  end
137
140
 
138
141
  def start_request
@@ -145,6 +148,7 @@ module Bullet
145
148
  Thread.current[:bullet_impossible_objects] = Bullet::Registry::Object.new
146
149
  Thread.current[:bullet_inversed_objects] = Bullet::Registry::Base.new
147
150
  Thread.current[:bullet_eager_loadings] = Bullet::Registry::Association.new
151
+ Thread.current[:bullet_call_stacks] = Bullet::Registry::CallStack.new
148
152
 
149
153
  Thread.current[:bullet_counter_possible_objects] ||= Bullet::Registry::Object.new
150
154
  Thread.current[:bullet_counter_impossible_objects] ||= Bullet::Registry::Object.new
@@ -240,8 +244,10 @@ module Bullet
240
244
  UniformNotifier.active_notifiers.include?(UniformNotifier::JavascriptConsole)
241
245
  end
242
246
 
243
- def skip_html_injection?
244
- @skip_html_injection || false
247
+ def inject_into_page?
248
+ return false if defined?(@skip_html_injection) && @skip_html_injection
249
+
250
+ console_enabled? || add_footer
245
251
  end
246
252
 
247
253
  private