bullet 8.0.3 → 8.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b32cc08a4de57560f18996d4ff4c96cb7a8cbc36cfb6dc285f612ac8ccd8f2e
4
- data.tar.gz: 1d5b9e6f885039b6a3208d546b2ebad0a44e8063c4e1fffc7d74790ba79c1f71
3
+ metadata.gz: ca6723ad41e0d2c1303ee57eda90abd7973379fff9760a4d94ccc54765af56d5
4
+ data.tar.gz: a96d3fab59f09fa013794cdb4fdf1ecc1e033a94650005e2a42e089ba590742d
5
5
  SHA512:
6
- metadata.gz: fb317b0bab9154eb40be0a315f1fe767ff8c227f631ee6b25e270c31422fb6aa7b4857ae7cb33b9155eb8a2300a1c52d6097fef899ad02b7cae42c8c7082e2a6
7
- data.tar.gz: ba13a423de83f43a6a5a2d75a576e4612011c21cf835f2204cda2b02114680f03cf397b7d28f476eefd1c8e933da2a7e0ab9e860ee28f9e0dbbb1ecbf5abeef7
6
+ metadata.gz: 60ad7aab73a64546410654d8bf1bc6b07671efd9b6d3860c0192a8287ef7446f287fea34f0011d1d30f3f5a74e6fb5391a8826bca9f86b45813772395795415b
7
+ data.tar.gz: f9076576155195baee90fbee13217bf5e4b6a83faff5e11bfadfe91ec8a9f9db3e06df1014c78549893abb767139fdde904a1c097b1867976784f9cba77cf9c0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  ## Next Release
2
2
 
3
+ ## 8.1.0 (10/23/2025)
4
+
5
+ * Make `get_relation` private
6
+ * Support Rails 8.1
7
+
8
+ ## 8.0.8 (05/30/2025)
9
+
10
+ * Add middleware after initializers
11
+ * Fix bullet composite primary key retrieval
12
+
13
+ ## 8.0.7 (05/15/2025)
14
+
15
+ * Try to insert `Bullet::Rack` properly
16
+
17
+ ## 8.0.6 (05/07/2025)
18
+
19
+ * Add CSP nonce for footer styles as well
20
+ * Add support for OpenTelemetry reporting
21
+
22
+ ## 8.0.5 (04/21/2025)
23
+
24
+ * Properly insert ContentSecurityPolicy middleware
25
+ * Properly parse query string
26
+
27
+ ## 8.0.4 (04/18/2025)
28
+
29
+ * Insert bullet middleware before `ContentSecurityPolicy`
30
+ * Support url query `skip_html_injection=true`
31
+ * Mark object as impossible after updating inversed
32
+
3
33
  ## 8.0.3 (04/04/2025)
4
34
 
5
35
  * Update non persisted `inversed_objects`
data/README.md CHANGED
@@ -71,9 +71,14 @@ config.after_initialize do
71
71
  Bullet.rollbar = true
72
72
  Bullet.add_footer = true
73
73
  Bullet.skip_html_injection = false
74
+ Bullet.skip_http_headers = false
74
75
  Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ]
75
76
  Bullet.stacktrace_excludes = [ 'their_gem', 'their_middleware', ['my_file.rb', 'my_method'], ['my_file.rb', 16..20] ]
76
77
  Bullet.slack = { webhook_url: 'http://some.slack.url', channel: '#default', username: 'notifier' }
78
+ Bullet.opentelemetry = true
79
+ Bullet.raise = false
80
+ Bullet.always_append_html_body = false
81
+ Bullet.skip_user_in_notification = false
77
82
  end
78
83
  ```
79
84
 
@@ -81,6 +86,7 @@ The notifier of Bullet is a wrap of [uniform_notifier](https://github.com/flyerh
81
86
 
82
87
  The code above will enable all of the Bullet notification systems:
83
88
  * `Bullet.enable`: enable Bullet gem, otherwise do nothing
89
+ * `Bullet.sentry`: add notifications to sentry
84
90
  * `Bullet.alert`: pop up a JavaScript alert in the browser
85
91
  * `Bullet.bullet_logger`: log to the Bullet log file (Rails.root/log/bullet.log)
86
92
  * `Bullet.console`: log warnings to your browser's console.log (Safari/Webkit browsers or Firefox w/Firebug installed)
@@ -88,10 +94,9 @@ The code above will enable all of the Bullet notification systems:
88
94
  * `Bullet.rails_logger`: add warnings directly to the Rails log
89
95
  * `Bullet.honeybadger`: add notifications to Honeybadger
90
96
  * `Bullet.bugsnag`: add notifications to bugsnag
91
- * `Bullet.airbrake`: add notifications to airbrake
92
97
  * `Bullet.appsignal`: add notifications to AppSignal
98
+ * `Bullet.airbrake`: add notifications to airbrake
93
99
  * `Bullet.rollbar`: add notifications to rollbar
94
- * `Bullet.sentry`: add notifications to sentry
95
100
  * `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.
96
101
  * `Bullet.skip_html_injection`: prevents Bullet from injecting code into the returned HTML. This must be false for receiving alerts, showing the footer or console logging.
97
102
  * `Bullet.skip_http_headers`: don't add headers to API requests, and remove the javascript that relies on them. Note that this prevents bullet from logging warnings to the browser console or updating the footer.
@@ -100,6 +105,7 @@ The code above will enable all of the Bullet notification systems:
100
105
  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
101
106
  item is a line number, a Range of line numbers, or a (bare) method name, to exclude only particular lines in a file.
102
107
  * `Bullet.slack`: add notifications to slack
108
+ * `Bullet.opentelemetry`: add notifications to OpenTelemetry
103
109
  * `Bullet.raise`: raise errors, useful for making your specs fail unless they have optimized queries
104
110
  * `Bullet.always_append_html_body`: always append the html body even if no notifications are present. Note: `console` or `add_footer` must also be true. Useful for Single Page Applications where the initial page load might not have any notifications present.
105
111
  * `Bullet.skip_user_in_notification`: exclude the OS user (`whoami`) from notifications.
@@ -192,6 +198,11 @@ see [https://github.com/flyerhzm/uniform_notifier](https://github.com/flyerhzm/u
192
198
 
193
199
  Growl support is dropped from uniform_notifier 1.16.0, if you still want it, please use uniform_notifier 1.15.0.
194
200
 
201
+ ## URL query control
202
+
203
+ You can add the URL query parameter `skip_html_injection` to make the current HTML request behave as if `Bullet.skip_html_injection` is enabled,
204
+ e.g. `http://localhost:3000/posts?skip_html_injection=true`
205
+
195
206
  ## Important
196
207
 
197
208
  If you find Bullet does not work for you, *please disable your browser's cache*.
@@ -49,7 +49,10 @@ module Bullet
49
49
 
50
50
  ::ActiveRecord::Persistence.class_eval do
51
51
  def _create_record_with_bullet(*args)
52
- _create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.update_inversed_object(self) }
52
+ _create_record_without_bullet(*args).tap do
53
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
54
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
55
+ end
53
56
  end
54
57
  alias_method_chain :_create_record, :bullet
55
58
  end
@@ -52,7 +52,10 @@ module Bullet
52
52
 
53
53
  ::ActiveRecord::Persistence.class_eval do
54
54
  def _create_record_with_bullet(*args)
55
- _create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.update_inversed_object(self) }
55
+ _create_record_without_bullet(*args).tap do
56
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
57
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
58
+ end
56
59
  end
57
60
  alias_method_chain :_create_record, :bullet
58
61
  end
@@ -45,7 +45,10 @@ module Bullet
45
45
 
46
46
  ::ActiveRecord::Persistence.class_eval do
47
47
  def _create_record_with_bullet(*args)
48
- _create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.update_inversed_object(self) }
48
+ _create_record_without_bullet(*args).tap do
49
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
50
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
51
+ end
49
52
  end
50
53
  alias_method_chain :_create_record, :bullet
51
54
  end
@@ -5,6 +5,7 @@ module Bullet
5
5
  def _create_record(*)
6
6
  super do
7
7
  Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
8
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
10
11
  end
@@ -5,6 +5,7 @@ module Bullet
5
5
  def _create_record(*)
6
6
  super do
7
7
  Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
8
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
10
11
  end
@@ -5,6 +5,7 @@ module Bullet
5
5
  def _create_record(*)
6
6
  super do
7
7
  Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
8
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
10
11
  end
@@ -5,6 +5,7 @@ module Bullet
5
5
  def _create_record(*)
6
6
  super do
7
7
  Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
8
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
10
11
  end
@@ -5,6 +5,7 @@ module Bullet
5
5
  def _create_record(*)
6
6
  super do
7
7
  Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
8
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
10
11
  end
@@ -5,6 +5,7 @@ module Bullet
5
5
  def _create_record(*)
6
6
  super do
7
7
  Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
8
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
10
11
  end
@@ -5,6 +5,7 @@ module Bullet
5
5
  def _create_record(*)
6
6
  super do
7
7
  Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
8
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
10
11
  end
@@ -5,6 +5,7 @@ module Bullet
5
5
  def _create_record(*)
6
6
  super do
7
7
  Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
8
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
10
11
  end
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet
4
+ module SaveWithBulletSupport
5
+ def _create_record(*)
6
+ super do
7
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
8
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
9
+ yield(self) if block_given?
10
+ end
11
+ end
12
+ end
13
+
14
+ module ActiveRecord
15
+ def self.enable
16
+ require 'active_record'
17
+ ::ActiveRecord::Base.extend(
18
+ Module.new do
19
+ def find_by_sql(sql, binds = [], preparable: nil, allow_retry: false, &block)
20
+ result = super
21
+ if Bullet.start?
22
+ if result.is_a? Array
23
+ if result.size > 1
24
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
25
+ Bullet::Detector::CounterCache.add_possible_objects(result)
26
+ elsif result.size == 1
27
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
28
+ Bullet::Detector::CounterCache.add_impossible_object(result.first)
29
+ end
30
+ elsif result.is_a? ::ActiveRecord::Base
31
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
32
+ Bullet::Detector::CounterCache.add_impossible_object(result)
33
+ end
34
+ end
35
+ result
36
+ end
37
+ end
38
+ )
39
+
40
+ ::ActiveRecord::Base.prepend(SaveWithBulletSupport)
41
+
42
+ ::ActiveRecord::Relation.prepend(
43
+ Module.new do
44
+ # if select a collection of objects, then these objects have possible to cause N+1 query.
45
+ # if select only one object, then the only one object has impossible to cause N+1 query.
46
+ def records
47
+ result = super
48
+ if Bullet.start?
49
+ if result.first.class.name !~ /^HABTM_/
50
+ if result.size > 1
51
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
52
+ Bullet::Detector::CounterCache.add_possible_objects(result)
53
+ elsif result.size == 1
54
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
55
+ Bullet::Detector::CounterCache.add_impossible_object(result.first)
56
+ end
57
+ end
58
+ end
59
+ result
60
+ end
61
+ end
62
+ )
63
+
64
+ ::ActiveRecord::Associations::Preloader::Batch.prepend(
65
+ Module.new do
66
+ def call
67
+ if Bullet.start?
68
+ @preloaders.each do |preloader|
69
+ preloader.records.each { |record|
70
+ Bullet::Detector::Association.add_object_associations(record, preloader.associations)
71
+ }
72
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(preloader.records, preloader.associations)
73
+ end
74
+ end
75
+ super
76
+ end
77
+ end
78
+ )
79
+
80
+ ::ActiveRecord::Associations::Preloader::Branch.prepend(
81
+ Module.new do
82
+ def preloaders_for_reflection(reflection, reflection_records)
83
+ if Bullet.start?
84
+ reflection_records.compact!
85
+ if reflection_records.first.class.name !~ /^HABTM_/
86
+ reflection_records.each { |record|
87
+ Bullet::Detector::Association.add_object_associations(record, reflection.name)
88
+ }
89
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(reflection_records, reflection.name)
90
+ end
91
+ end
92
+ super
93
+ end
94
+ end
95
+ )
96
+
97
+ ::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(
98
+ Module.new do
99
+ def source_preloaders
100
+ if Bullet.start? && !defined?(@source_preloaders)
101
+ preloaders = super
102
+ preloaders.each do |preloader|
103
+ reflection_name = preloader.send(:reflection).name
104
+ preloader.send(:owners).each do |owner|
105
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name)
106
+ end
107
+ end
108
+ else
109
+ super
110
+ end
111
+ end
112
+ end
113
+ )
114
+
115
+ ::ActiveRecord::Associations::JoinDependency.prepend(
116
+ Module.new do
117
+ def instantiate(result_set, strict_loading_value, &block)
118
+ @bullet_eager_loadings = {}
119
+ records = super
120
+
121
+ if Bullet.start?
122
+ @bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
123
+ objects = eager_loadings_hash.keys
124
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
125
+ objects,
126
+ eager_loadings_hash[objects.first].to_a
127
+ )
128
+ end
129
+ end
130
+ records
131
+ end
132
+
133
+ def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value)
134
+ if Bullet.start?
135
+ unless ar_parent.nil?
136
+ parent.children.each do |node|
137
+ key = aliases.column_alias(node, node.primary_key)
138
+ id = row[key]
139
+ next unless id.nil?
140
+
141
+ associations = [node.reflection.name]
142
+ if node.reflection.through_reflection?
143
+ associations << node.reflection.through_reflection.name
144
+ end
145
+ associations.each do |association|
146
+ Bullet::Detector::Association.add_object_associations(ar_parent, association)
147
+ Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association)
148
+ @bullet_eager_loadings[ar_parent.class] ||= {}
149
+ @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
150
+ @bullet_eager_loadings[ar_parent.class][ar_parent] << association
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ super
157
+ end
158
+
159
+ # call join associations
160
+ def construct_model(record, node, row, model_cache, id, strict_loading_value)
161
+ result = super
162
+
163
+ if Bullet.start?
164
+ associations = [node.reflection.name]
165
+ if node.reflection.through_reflection?
166
+ associations << node.reflection.through_reflection.name
167
+ end
168
+ associations.each do |association|
169
+ Bullet::Detector::Association.add_object_associations(record, association)
170
+ Bullet::Detector::NPlusOneQuery.call_association(record, association)
171
+ @bullet_eager_loadings[record.class] ||= {}
172
+ @bullet_eager_loadings[record.class][record] ||= Set.new
173
+ @bullet_eager_loadings[record.class][record] << association
174
+ end
175
+ end
176
+
177
+ result
178
+ end
179
+ end
180
+ )
181
+
182
+ ::ActiveRecord::Associations::Association.prepend(
183
+ Module.new do
184
+ def inversed_from(record)
185
+ if Bullet.start?
186
+ Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
187
+ end
188
+ super
189
+ end
190
+
191
+ def inversed_from_queries(record)
192
+ if Bullet.start? && inversable?(record)
193
+ Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
194
+ end
195
+ super
196
+ end
197
+ end
198
+ )
199
+
200
+ ::ActiveRecord::Associations::CollectionAssociation.prepend(
201
+ Module.new do
202
+ def load_target
203
+ records = super
204
+
205
+ if Bullet.start?
206
+ if is_a? ::ActiveRecord::Associations::ThroughAssociation
207
+ association = owner.association(reflection.through_reflection.name)
208
+ if association.loaded?
209
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
210
+ Array.wrap(association.target).each do |through_record|
211
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
212
+ end
213
+
214
+ if reflection.through_reflection != through_reflection
215
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
216
+ end
217
+ end
218
+ end
219
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
220
+ if records.first.class.name !~ /^HABTM_/
221
+ if records.size > 1
222
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
223
+ Bullet::Detector::CounterCache.add_possible_objects(records)
224
+ elsif records.size == 1
225
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
226
+ Bullet::Detector::CounterCache.add_impossible_object(records.first)
227
+ end
228
+ end
229
+ end
230
+ records
231
+ end
232
+
233
+ def empty?
234
+ if Bullet.start? && !reflection.has_cached_counter?
235
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
236
+ end
237
+ super
238
+ end
239
+
240
+ def include?(object)
241
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
242
+ super
243
+ end
244
+ end
245
+ )
246
+
247
+ ::ActiveRecord::Associations::SingularAssociation.prepend(
248
+ Module.new do
249
+ # call has_one and belongs_to associations
250
+ def reader
251
+ result = super
252
+
253
+ if Bullet.start?
254
+ if owner.class.name !~ /^HABTM_/
255
+ if is_a? ::ActiveRecord::Associations::ThroughAssociation
256
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
257
+ association = owner.association(reflection.through_reflection.name)
258
+ Array.wrap(association.target).each do |through_record|
259
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
260
+ end
261
+
262
+ if reflection.through_reflection != through_reflection
263
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
264
+ end
265
+ end
266
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
267
+
268
+ if Bullet::Detector::NPlusOneQuery.impossible?(owner)
269
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
270
+ else
271
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
272
+ end
273
+ end
274
+ end
275
+ result
276
+ end
277
+ end
278
+ )
279
+
280
+ ::ActiveRecord::Associations::HasManyAssociation.prepend(
281
+ Module.new do
282
+ def empty?
283
+ result = super
284
+ if Bullet.start? && !reflection.has_cached_counter?
285
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
286
+ end
287
+ result
288
+ end
289
+
290
+ def count_records
291
+ result = reflection.has_cached_counter?
292
+ if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
293
+ Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
294
+ end
295
+ super
296
+ end
297
+ end
298
+ )
299
+
300
+ ::ActiveRecord::Associations::CollectionProxy.prepend(
301
+ Module.new do
302
+ def count(column_name = nil)
303
+ if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation)
304
+ Bullet::Detector::CounterCache.add_counter_cache(
305
+ proxy_association.owner,
306
+ proxy_association.reflection.name
307
+ )
308
+ Bullet::Detector::NPlusOneQuery.call_association(
309
+ proxy_association.owner,
310
+ proxy_association.reflection.name
311
+ )
312
+ end
313
+ super(column_name)
314
+ end
315
+ end
316
+ )
317
+ end
318
+ end
319
+ end
@@ -37,6 +37,8 @@ module Bullet
37
37
  'active_record72'
38
38
  elsif active_record80?
39
39
  'active_record80'
40
+ elsif active_record81?
41
+ 'active_record81'
40
42
  else
41
43
  raise "Bullet does not support active_record #{::ActiveRecord::VERSION::STRING} yet"
42
44
  end
@@ -132,6 +134,10 @@ module Bullet
132
134
  active_record8? && ::ActiveRecord::VERSION::MINOR == 0
133
135
  end
134
136
 
137
+ def active_record81?
138
+ active_record8? && ::ActiveRecord::VERSION::MINOR == 1
139
+ end
140
+
135
141
  def mongoid4x?
136
142
  mongoid? && ::Mongoid::VERSION =~ /\A4/
137
143
  end
@@ -26,10 +26,10 @@ module Bullet
26
26
  private
27
27
 
28
28
  def bullet_join_potential_composite_primary_key(primary_keys)
29
- return send(primary_keys) unless primary_keys.is_a?(Enumerable)
29
+ return read_attribute(primary_keys) unless primary_keys.is_a?(Enumerable)
30
30
 
31
- primary_keys.map { |primary_key| send primary_key }
32
- .join(',')
31
+ primary_keys.map { |primary_key| read_attribute primary_key }
32
+ .compact.join(',')
33
33
  end
34
34
  end
35
35
  end
@@ -46,6 +46,8 @@ module Bullet
46
46
  ::Mongoid::Relations::Accessors.class_eval do
47
47
  alias_method :origin_get_relation, :get_relation
48
48
 
49
+ private
50
+
49
51
  def get_relation(name, metadata, object, reload = false)
50
52
  result = origin_get_relation(name, metadata, object, reload)
51
53
  Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
@@ -46,6 +46,8 @@ module Bullet
46
46
  ::Mongoid::Relations::Accessors.class_eval do
47
47
  alias_method :origin_get_relation, :get_relation
48
48
 
49
+ private
50
+
49
51
  def get_relation(name, metadata, object, reload = false)
50
52
  result = origin_get_relation(name, metadata, object, reload)
51
53
  Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
@@ -46,6 +46,8 @@ module Bullet
46
46
  ::Mongoid::Relations::Accessors.class_eval do
47
47
  alias_method :origin_get_relation, :get_relation
48
48
 
49
+ private
50
+
49
51
  def get_relation(name, metadata, object, reload = false)
50
52
  result = origin_get_relation(name, metadata, object, reload)
51
53
  Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
@@ -61,6 +61,8 @@ module Bullet
61
61
  ::Mongoid::Association::Accessors.class_eval do
62
62
  alias_method :origin_get_relation, :get_relation
63
63
 
64
+ private
65
+
64
66
  def get_relation(name, association, object, reload = false)
65
67
  result = origin_get_relation(name, association, object, reload)
66
68
  Bullet::Detector::NPlusOneQuery.call_association(self, name) unless association.embedded?
@@ -46,6 +46,8 @@ module Bullet
46
46
  ::Mongoid::Association::Accessors.class_eval do
47
47
  alias_method :origin_get_relation, :get_relation
48
48
 
49
+ private
50
+
49
51
  def get_relation(name, association, object, reload = false)
50
52
  result = origin_get_relation(name, association, object, reload)
51
53
  unless association.embedded?
@@ -61,6 +61,8 @@ module Bullet
61
61
  ::Mongoid::Association::Accessors.class_eval do
62
62
  alias_method :origin_get_relation, :get_relation
63
63
 
64
+ private
65
+
64
66
  def get_relation(name, association, object, reload = false)
65
67
  result = origin_get_relation(name, association, object, reload)
66
68
  Bullet::Detector::NPlusOneQuery.call_association(self, name) unless association.embedded?
data/lib/bullet/rack.rb CHANGED
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rack/request'
4
+ require 'json'
5
+ require 'cgi'
6
+
3
7
  module Bullet
4
8
  class Rack
5
9
  include Dependency
6
10
 
7
- NONCE_MATCHER = /script-src .*'nonce-(?<nonce>[A-Za-z0-9+\/]+={0,2})'/
11
+ NONCE_MATCHER = /(script|style)-src .*'nonce-(?<nonce>[A-Za-z0-9+\/]+={0,2})'/
8
12
 
9
13
  def initialize(app)
10
14
  @app = app
@@ -19,12 +23,13 @@ module Bullet
19
23
  response_body = nil
20
24
 
21
25
  if Bullet.notification? || Bullet.always_append_html_body
22
- if Bullet.inject_into_page? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
26
+ request = ::Rack::Request.new(env)
27
+ if Bullet.inject_into_page? && !skip_html_injection?(request) && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
23
28
  if html_request?(headers, response)
24
29
  response_body = response_body(response)
25
30
 
26
31
  with_security_policy_nonce(headers) do |nonce|
27
- response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
32
+ response_body = append_to_html_body(response_body, footer_note(nonce)) if Bullet.add_footer
28
33
  response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
29
34
  if Bullet.add_footer && !Bullet.skip_http_headers
30
35
  response_body = append_to_html_body(response_body, xhr_script(nonce))
@@ -65,16 +70,48 @@ module Bullet
65
70
  end
66
71
  end
67
72
 
68
- def footer_note
69
- "<details #{details_attributes}><summary #{summary_attributes}>Bullet Warnings</summary><div #{footer_content_attributes}>#{Bullet.footer_info.uniq.join('<br>')}#{footer_console_message}</div></details>"
73
+ def footer_note(nonce = nil)
74
+ %(<details id="bullet-footer" data-is-bullet-footer><summary>Bullet Warnings</summary><div>#{Bullet.footer_info.uniq.join('<br>')}#{footer_console_message(nonce)}</div>#{footer_style(nonce)}</details>)
75
+ end
76
+
77
+ # Make footer styles work with ContentSecurityPolicy style-src as self
78
+ def footer_style(nonce = nil)
79
+ css = <<~CSS
80
+ details#bullet-footer {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;}
81
+ details#bullet-footer summary {font-weight: 600; padding: 2px 8px;}
82
+ details#bullet-footer div {padding: 8px; border-top: 1px solid #9b1c1c;}
83
+ CSS
84
+ if nonce
85
+ %(<style type="text/css" nonce="#{nonce}">#{css}</style>)
86
+ else
87
+ %(<style type="text/css">#{css}</style>)
88
+ end
70
89
  end
71
90
 
72
91
  def set_header(headers, header_name, header_array)
73
92
  # Many proxy applications such as Nginx and AWS ELB limit
74
93
  # the size a header to 8KB, so truncate the list of reports to
75
94
  # be under that limit
76
- header_array.pop while header_array.to_json.length > 8 * 1024
77
- headers[header_name] = header_array.to_json
95
+ header_array.pop while JSON.generate(header_array).length > 8 * 1024
96
+ headers[header_name] = JSON.generate(header_array)
97
+ end
98
+
99
+ def skip_html_injection?(request)
100
+ query_string = request.env['QUERY_STRING']
101
+ return false if query_string.nil? || query_string.empty?
102
+
103
+ params = simple_parse_query_string(query_string)
104
+ params['skip_html_injection'] == 'true'
105
+ end
106
+
107
+ # Simple query string parser
108
+ def simple_parse_query_string(query_string)
109
+ params = {}
110
+ query_string.split('&').each do |pair|
111
+ key, value = pair.split('=', 2).map { |s| CGI.unescape(s) }
112
+ params[key] = value if key && !key.empty?
113
+ end
114
+ params
78
115
  end
79
116
 
80
117
  def file?(headers)
@@ -99,28 +136,18 @@ module Bullet
99
136
 
100
137
  private
101
138
 
102
- def details_attributes
103
- <<~EOF
104
- id="bullet-footer" data-is-bullet-footer
105
- 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;"
106
- EOF
107
- end
108
-
109
- def summary_attributes
110
- <<~EOF
111
- style="font-weight: 600; padding: 2px 8px"
112
- EOF
113
- end
114
-
115
- def footer_content_attributes
116
- <<~EOF
117
- style="padding: 8px; border-top: 1px solid #9b1c1c;"
118
- EOF
119
- end
120
-
121
- def footer_console_message
139
+ def footer_console_message(nonce = nil)
122
140
  if Bullet.console_enabled?
123
- "<br/><span style='font-style: italic;'>See 'Uniform Notifier' in JS Console for Stacktrace</span>"
141
+ footer = %(<br/><span id="console-message">See 'Uniform Notifier' in JS Console for Stacktrace</span>)
142
+ css = "details#bullet-footer #console-message {font-style: italic;}"
143
+ style =
144
+ if nonce
145
+ %(<style type="text/css" nonce="#{nonce}">#{css}</style>)
146
+ else
147
+ %(<style type="text/css">#{css}</style>)
148
+ end
149
+
150
+ footer + style
124
151
  end
125
152
  end
126
153
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '8.0.3'
4
+ VERSION = '8.1.0'
5
5
  end
data/lib/bullet.rb CHANGED
@@ -23,8 +23,8 @@ module Bullet
23
23
 
24
24
  if defined?(Rails::Railtie)
25
25
  class BulletRailtie < Rails::Railtie
26
- initializer 'bullet.configure_rails_initialization' do |app|
27
- if defined?(ActionDispatch::ContentSecurityPolicy::Middleware) && Rails.application.config.content_security_policy
26
+ initializer 'bullet.add_middleware', after: :load_config_initializers do |app|
27
+ if defined?(ActionDispatch::ContentSecurityPolicy::Middleware) && Rails.application.config.content_security_policy && !app.config.api_only
28
28
  app.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, Bullet::Rack
29
29
  else
30
30
  app.middleware.use Bullet::Rack
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.3
4
+ version: 8.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-04 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -60,6 +60,7 @@ files:
60
60
  - lib/bullet/active_record71.rb
61
61
  - lib/bullet/active_record72.rb
62
62
  - lib/bullet/active_record80.rb
63
+ - lib/bullet/active_record81.rb
63
64
  - lib/bullet/bullet_xhr.js
64
65
  - lib/bullet/dependency.rb
65
66
  - lib/bullet/detector.rb
@@ -112,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
113
  - !ruby/object:Gem::Version
113
114
  version: 1.3.6
114
115
  requirements: []
115
- rubygems_version: 3.6.2
116
+ rubygems_version: 3.6.9
116
117
  specification_version: 4
117
118
  summary: help to kill N+1 queries and unused eager loading.
118
119
  test_files: []