bullet 6.1.0 → 7.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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