bullet 7.0.1 → 7.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 823bcc1af2be934ec4f10df71ea2951223834d3146e9c323afada99ba4ae4254
4
- data.tar.gz: 4cda048b45bd219d62bee22e0041027561c943d283ee262ea71686a15e6e4345
3
+ metadata.gz: 0f47e147f5df074217ee7a65123f54a1918f98bde162c7d4bf1f76bffc9f15a6
4
+ data.tar.gz: e05e48b96a5e68bfbcac63f4dce4601d717ec1a32ee83a5c45aad738e1bfc48b
5
5
  SHA512:
6
- metadata.gz: 9a2398eb23da7201bb8ab3fb1dbea3d48089d6c51d4a0e473d25009841aab41c68a5b0c27fb6ac6b77084e2465a836a5b848ffda63a2ea3b457a9571b04d428f
7
- data.tar.gz: ab3debd03dc9cafbbd8ef9237f747c67d14bf3d8bf116b7dbed4e145176cf3f10045862b32641036e9bb00caca6eee841c75f9b3defad83ca681d0d70a5c6f0d
6
+ metadata.gz: 87bf095754befedb8f1137ae039191807baa1f1bcadb09c923b4f1a6abd385e73e4143e50077358a8f7155e3bf367edb1b405e3b0b260c8de5dd572699b7bafc
7
+ data.tar.gz: fd692ae67a1695ee4598b24d3cd030a7f091deb42f30df0f92085b3c54f765960c0b5eabaace7b306fadf8539b514fe77076cff825c27a292c5e36f56916646b
@@ -9,9 +9,9 @@ name: CI
9
9
 
10
10
  on:
11
11
  push:
12
- branches: [ master ]
12
+ branches: [ main ]
13
13
  pull_request:
14
- branches: [ master ]
14
+ branches: [ main ]
15
15
 
16
16
  jobs:
17
17
  test_rails_4:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  ## Next Release
2
2
 
3
+ ## 7.0.7 (03/01/2023)
4
+
5
+ * Check `Rails.application.config.content_security_policy` before insert `Bullet::Rack`
6
+
7
+ ## 7.0.6 (03/01/2023)
8
+
9
+ * Better way to check if `ActionDispatch::ContentSecurityPolicy::Middleware` exists
10
+
11
+ ## 7.0.5 (01/01/2023)
12
+
13
+ * Fix n+1 false positives in AR 7.0
14
+ * Fix eager_load nested has_many :through false positives
15
+ * Respect Content-Security-Policy nonces
16
+ * Added CallStacks support for avoid eager loading
17
+ * Iterate fewer times over objects
18
+
19
+ ## 7.0.4 (11/28/2022)
20
+
21
+ * Fix `eager_load` `has_many :through` false positives
22
+ * mongoid7x: add dynamic methods
23
+
24
+ ## 7.0.3 (08/13/2022)
25
+
26
+ * Replace `Array()` with `Array.wrap()`
27
+
28
+ ## 7.0.2 (05/31/2022)
29
+
30
+ * Drop growl support
31
+ * Do not check html tag in Bullet::Rack anymore
32
+
3
33
  ## 7.0.1 (01/15/2022)
4
34
 
5
35
  * Get rid of *_whitelist methods
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 - 2010 Richard Huang (flyerhzm@gmail.com)
1
+ Copyright (c) 2009 - 2022 Richard Huang (flyerhzm@gmail.com)
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -49,7 +49,7 @@ mongoid.
49
49
 
50
50
  ## Configuration
51
51
 
52
- Bullet won't do ANYTHING unless you tell it to explicitly. Append to
52
+ Bullet won't enable any notification systems unless you tell it to explicitly. Append to
53
53
  `config/environments/development.rb` initializer with the following code:
54
54
 
55
55
  ```ruby
@@ -59,7 +59,6 @@ config.after_initialize do
59
59
  Bullet.alert = true
60
60
  Bullet.bullet_logger = true
61
61
  Bullet.console = true
62
- Bullet.growl = true
63
62
  Bullet.xmpp = { :account => 'bullets_account@jabber.org',
64
63
  :password => 'bullets_password_for_jabber',
65
64
  :receiver => 'your_account@jabber.org',
@@ -85,7 +84,6 @@ The code above will enable all of the Bullet notification systems:
85
84
  * `Bullet.alert`: pop up a JavaScript alert in the browser
86
85
  * `Bullet.bullet_logger`: log to the Bullet log file (Rails.root/log/bullet.log)
87
86
  * `Bullet.console`: log warnings to your browser's console.log (Safari/Webkit browsers or Firefox w/Firebug installed)
88
- * `Bullet.growl`: pop up Growl warnings if your system has Growl installed. Requires a little bit of configuration
89
87
  * `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.
90
88
  * `Bullet.rails_logger`: add warnings directly to the Rails log
91
89
  * `Bullet.honeybadger`: add notifications to Honeybadger
@@ -156,25 +154,26 @@ The Bullet log `log/bullet.log` will look something like this:
156
154
  * N+1 Query:
157
155
 
158
156
  ```
159
- 2009-08-25 20:40:17[INFO] N+1 Query: PATH_INFO: /posts; model: Post => associations: [comments]·
160
- Add to your finder: :include => [:comments]
161
- 2009-08-25 20:40:17[INFO] N+1 Query: method call stack:·
162
- /Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb'
163
- /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
164
- /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
165
- /Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
157
+ 2009-08-25 20:40:17[INFO] USE eager loading detected:
158
+ Post => [:comments]·
159
+ Add to your query: .includes([:comments])
160
+ 2009-08-25 20:40:17[INFO] Call stack
161
+ /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
162
+ /Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
166
163
  ```
167
164
 
168
- 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.
165
+ The first log entry is a notification that N+1 queries have been encountered. The remaining entry is a stack trace so you can find exactly where the queries were invoked in your code, and fix them.
169
166
 
170
167
  * Unused eager loading:
171
168
 
172
169
  ```
173
- 2009-08-25 20:53:56[INFO] Unused eager loadings: PATH_INFO: /posts; model: Post => associations: [comments]·
174
- Remove from your finder: :include => [:comments]
170
+ 2009-08-25 20:53:56[INFO] AVOID eager loading detected
171
+ Post => [:comments]·
172
+ Remove from your query: .includes([:comments])
173
+ 2009-08-25 20:53:56[INFO] Call stack
175
174
  ```
176
175
 
177
- These two lines are notifications that unused eager loadings have been encountered.
176
+ These lines are notifications that unused eager loadings have been encountered.
178
177
 
179
178
  * Need counter cache:
180
179
 
@@ -183,10 +182,14 @@ These two lines are notifications that unused eager loadings have been encounter
183
182
  Post => [:comments]
184
183
  ```
185
184
 
186
- ## Growl, XMPP/Jabber and Airbrake Support
185
+ ## XMPP/Jabber and Airbrake Support
187
186
 
188
187
  see [https://github.com/flyerhzm/uniform_notifier](https://github.com/flyerhzm/uniform_notifier)
189
188
 
189
+ ## Growl Support
190
+
191
+ Growl support is dropped from uniform_notifier 1.16.0, if you still want it, please use uniform_notifier 1.15.0.
192
+
190
193
  ## Important
191
194
 
192
195
  If you find Bullet does not work for you, *please disable your browser's cache*.
@@ -219,7 +222,7 @@ end
219
222
 
220
223
  ### Work with sinatra
221
224
 
222
- Configure and use `Bullet::Rack`
225
+ Configure and use `Bullet::Rack`.
223
226
 
224
227
  ```ruby
225
228
  configure :development do
@@ -229,6 +232,8 @@ configure :development do
229
232
  end
230
233
  ```
231
234
 
235
+ If your application generates a Content-Security-Policy via a separate middleware, ensure that `Bullet::Rack` is loaded _before_ that middleware.
236
+
232
237
  ### Run in tests
233
238
 
234
239
  First you need to enable Bullet in test environment.
@@ -282,7 +287,7 @@ $ rails g scaffold comment name:string post_id:integer
282
287
  $ bundle exec rake db:migrate
283
288
  ```
284
289
 
285
- 2\. Change `app/model/post.rb` and `app/model/comment.rb`
290
+ 2\. Change `app/models/post.rb` and `app/models/comment.rb`
286
291
 
287
292
  ```ruby
288
293
  class Post < ActiveRecord::Base
@@ -177,16 +177,18 @@ module Bullet
177
177
  if Bullet.start?
178
178
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
179
179
  refl = reflection.through_reflection
180
- Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
181
- association = owner.association refl.name
182
- Array(association.target).each do |through_record|
183
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
184
- end
180
+ association = owner.association(refl.name)
181
+ if association.loaded?
182
+ Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
183
+ Array.wrap(association.target).each do |through_record|
184
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
185
+ end
185
186
 
186
- if refl.through_reflection?
187
- refl = refl.through_reflection while refl.through_reflection?
187
+ if refl.through_reflection?
188
+ refl = refl.through_reflection while refl.through_reflection?
188
189
 
189
- Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
190
+ Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
191
+ end
190
192
  end
191
193
  end
192
194
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -150,14 +150,16 @@ module Bullet
150
150
 
151
151
  if Bullet.start?
152
152
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
153
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
154
- association = owner.association reflection.through_reflection.name
155
- Array(association.target).each do |through_record|
156
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
157
- end
153
+ association = owner.association(reflection.through_reflection.name)
154
+ if association.loaded?
155
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
156
+ Array.wrap(association.target).each do |through_record|
157
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
158
+ end
158
159
 
159
- if reflection.through_reflection != through_reflection
160
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
160
+ if reflection.through_reflection != through_reflection
161
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
162
+ end
161
163
  end
162
164
  end
163
165
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -199,7 +201,7 @@ module Bullet
199
201
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
200
202
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
201
203
  association = owner.association reflection.through_reflection.name
202
- Array(association.target).each do |through_record|
204
+ Array.wrap(association.target).each do |through_record|
203
205
  Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
204
206
  end
205
207
 
@@ -177,14 +177,16 @@ module Bullet
177
177
 
178
178
  if Bullet.start?
179
179
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
180
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
181
180
  association = owner.association(reflection.through_reflection.name)
182
- Array(association.target).each do |through_record|
183
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
184
- end
181
+ if association.loaded?
182
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
183
+ Array.wrap(association.target).each do |through_record|
184
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
185
+ end
185
186
 
186
- if reflection.through_reflection != through_reflection
187
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
187
+ if reflection.through_reflection != through_reflection
188
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
189
+ end
188
190
  end
189
191
  end
190
192
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -226,7 +228,7 @@ module Bullet
226
228
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
227
229
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
228
230
  association = owner.association(reflection.through_reflection.name)
229
- Array(association.target).each do |through_record|
231
+ Array.wrap(association.target).each do |through_record|
230
232
  Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
231
233
  end
232
234
 
@@ -177,14 +177,16 @@ module Bullet
177
177
 
178
178
  if Bullet.start?
179
179
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
180
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
181
180
  association = owner.association(reflection.through_reflection.name)
182
- Array(association.target).each do |through_record|
183
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
184
- end
181
+ if association.loaded?
182
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
183
+ Array.wrap(association.target).each do |through_record|
184
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
185
+ end
185
186
 
186
- if reflection.through_reflection != through_reflection
187
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
187
+ if reflection.through_reflection != through_reflection
188
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
189
+ end
188
190
  end
189
191
  end
190
192
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -226,7 +228,7 @@ module Bullet
226
228
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
227
229
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
228
230
  association = owner.association(reflection.through_reflection.name)
229
- Array(association.target).each do |through_record|
231
+ Array.wrap(association.target).each do |through_record|
230
232
  Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
231
233
  end
232
234
 
@@ -170,6 +170,13 @@ module Bullet
170
170
  end
171
171
  super
172
172
  end
173
+
174
+ def inversed_from_queries(record)
175
+ if Bullet.start? && inversable?(record)
176
+ Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
177
+ end
178
+ super
179
+ end
173
180
  end
174
181
  )
175
182
 
@@ -180,14 +187,16 @@ module Bullet
180
187
 
181
188
  if Bullet.start?
182
189
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
183
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
184
190
  association = owner.association(reflection.through_reflection.name)
185
- Array(association.target).each do |through_record|
186
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
187
- end
191
+ if association.loaded?
192
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
193
+ Array.wrap(association.target).each do |through_record|
194
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
195
+ end
188
196
 
189
- if reflection.through_reflection != through_reflection
190
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
197
+ if reflection.through_reflection != through_reflection
198
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
199
+ end
191
200
  end
192
201
  end
193
202
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
@@ -229,7 +238,7 @@ module Bullet
229
238
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
230
239
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
231
240
  association = owner.association(reflection.through_reflection.name)
232
- Array(association.target).each do |through_record|
241
+ Array.wrap(association.target).each do |through_record|
233
242
  Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
234
243
  end
235
244
 
@@ -20,7 +20,7 @@
20
20
  if (this.onload) {
21
21
  this._storedOnload = this.onload;
22
22
  }
23
- this.onload = null
23
+ this.onload = null;
24
24
  this.addEventListener("load", bulletXHROnload);
25
25
  return Reflect.apply(oldSend, this, arguments);
26
26
  }
@@ -31,7 +31,7 @@
31
31
  ) {
32
32
  var bulletFooterText = this.getResponseHeader("X-bullet-footer-text");
33
33
  if (bulletFooterText) {
34
- setTimeout(function() {
34
+ setTimeout(function () {
35
35
  var oldHtml = document.querySelector("#bullet-footer").innerHTML.split("<br>");
36
36
  var header = oldHtml[0];
37
37
  oldHtml = oldHtml.slice(1, oldHtml.length);
@@ -42,7 +42,7 @@
42
42
  }
43
43
  var bulletConsoleText = this.getResponseHeader("X-bullet-console-text");
44
44
  if (bulletConsoleText && typeof console !== "undefined" && console.log) {
45
- setTimeout(function() {
45
+ setTimeout(function () {
46
46
  JSON.parse(bulletConsoleText).forEach((message) => {
47
47
  if (console.groupCollapsed && console.groupEnd) {
48
48
  console.groupCollapsed("Uniform Notifier");
@@ -13,6 +13,7 @@ module Bullet
13
13
  'Detector::Association#add_object_associations',
14
14
  "object: #{object.bullet_key}, associations: #{associations}"
15
15
  )
16
+ call_stacks.add(object.bullet_key)
16
17
  object_associations.add(object.bullet_key, associations)
17
18
  end
18
19
 
@@ -25,6 +26,7 @@ module Bullet
25
26
  'Detector::Association#add_call_object_associations',
26
27
  "object: #{object.bullet_key}, associations: #{associations}"
27
28
  )
29
+ call_stacks.add(object.bullet_key)
28
30
  call_object_associations.add(object.bullet_key, associations)
29
31
  end
30
32
 
@@ -76,6 +78,12 @@ module Bullet
76
78
  def eager_loadings
77
79
  Thread.current[:bullet_eager_loadings]
78
80
  end
81
+
82
+ # cal_stacks keeps stacktraces where querie-objects were called from.
83
+ # e.g. { 'Object:111' => [SomeProject/app/controllers/...] }
84
+ def call_stacks
85
+ Thread.current[:bullet_call_stacks]
86
+ end
79
87
  end
80
88
  end
81
89
  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_safelist_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,15 +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
- return if objects.all? { |obj| obj.class.name =~ /^HABTM_/ }
39
-
40
- Bullet.debug(
41
- 'Detector::NPlusOneQuery#add_possible_objects',
42
- "objects: #{objects.map(&:bullet_key).join(', ')}"
43
- )
44
- 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
45
56
  end
46
57
 
47
58
  def add_impossible_object(object)
@@ -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_safelist_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_safelist_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)
@@ -4,22 +4,20 @@ 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)
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
@@ -20,11 +22,15 @@ module Bullet
20
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
- if Bullet.add_footer && !Bullet.skip_http_headers
26
- 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
27
32
  end
33
+
28
34
  headers['Content-Length'] = response_body.bytesize.to_s
29
35
  elsif !Bullet.skip_http_headers
30
36
  set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
@@ -79,7 +85,7 @@ module Bullet
79
85
  end
80
86
 
81
87
  def html_request?(headers, response)
82
- headers['Content-Type']&.include?('text/html') && response_body(response).include?('<html')
88
+ headers['Content-Type']&.include?('text/html')
83
89
  end
84
90
 
85
91
  def response_body(response)
@@ -118,8 +124,34 @@ module Bullet
118
124
  end
119
125
 
120
126
  # Make footer work for XHR requests by appending data to the footer
121
- def xhr_script
122
- "<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
123
155
  end
124
156
  end
125
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
@@ -6,10 +6,11 @@ module Bullet
6
6
  VENDOR_PATH = '/vendor'
7
7
  IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
8
8
 
9
- 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)
10
11
  vendor_root = Bullet.app_root + VENDOR_PATH
11
12
  bundler_path = Bundler.bundle_path.to_s
12
- select_caller_locations do |location|
13
+ select_caller_locations(bullet_key) do |location|
13
14
  caller_path = location_as_path(location)
14
15
  caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
15
16
  !caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern|
@@ -50,15 +51,16 @@ module Bullet
50
51
  end
51
52
 
52
53
  def location_as_path(location)
54
+ return location if location.is_a?(String)
55
+
53
56
  IS_RUBY_19 ? location : location.absolute_path.to_s
54
57
  end
55
58
 
56
- def select_caller_locations
57
- if IS_RUBY_19
58
- caller.select { |caller_path| yield caller_path }
59
- else
60
- caller_locations.select { |location| yield location }
61
- end
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 }
62
64
  end
63
65
  end
64
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '7.0.1'
4
+ VERSION = '7.0.7'
5
5
  end
data/lib/bullet.rb CHANGED
@@ -23,7 +23,11 @@ module Bullet
23
23
  if defined?(Rails::Railtie)
24
24
  class BulletRailtie < Rails::Railtie
25
25
  initializer 'bullet.configure_rails_initialization' do |app|
26
- 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
27
31
  end
28
32
  end
29
33
  end
@@ -109,7 +113,7 @@ module Bullet
109
113
  end
110
114
 
111
115
  def get_safelist_associations(type, class_name)
112
- Array(@safelist[type][class_name])
116
+ Array.wrap(@safelist[type][class_name])
113
117
  end
114
118
 
115
119
  def reset_safelist
@@ -144,6 +148,7 @@ module Bullet
144
148
  Thread.current[:bullet_impossible_objects] = Bullet::Registry::Object.new
145
149
  Thread.current[:bullet_inversed_objects] = Bullet::Registry::Base.new
146
150
  Thread.current[:bullet_eager_loadings] = Bullet::Registry::Association.new
151
+ Thread.current[:bullet_call_stacks] = Bullet::Registry::CallStack.new
147
152
 
148
153
  Thread.current[:bullet_counter_possible_objects] ||= Bullet::Registry::Object.new
149
154
  Thread.current[:bullet_counter_impossible_objects] ||= Bullet::Registry::Object.new
@@ -16,7 +16,6 @@ module Bullet
16
16
  Bullet.alert = true
17
17
  Bullet.bullet_logger = true
18
18
  Bullet.console = true
19
- # Bullet.growl = true
20
19
  Bullet.rails_logger = true
21
20
  Bullet.add_footer = true
22
21
  end
@@ -39,7 +39,7 @@ module Bullet
39
39
 
40
40
  it 'should be false if object, association pair is not existed' do
41
41
  NPlusOneQuery.add_object_associations(@post, :association1)
42
- expect(NPlusOneQuery.association?(@post, :associatio2)).to eq false
42
+ expect(NPlusOneQuery.association?(@post, :association2)).to eq false
43
43
  end
44
44
  end
45
45
 
@@ -127,38 +127,6 @@ module Bullet
127
127
  end
128
128
  end
129
129
 
130
- context '.caller_in_project' do
131
- it 'should include only paths that are in the project' do
132
- in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
133
- not_in_project = OpenStruct.new(absolute_path: '/def/def.rb')
134
-
135
- expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, not_in_project])
136
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
137
- expect(NPlusOneQuery).to receive(:create_notification).with([in_project], 'Post', :association)
138
- NPlusOneQuery.call_association(@post, :association)
139
- end
140
-
141
- context 'stacktrace_includes' do
142
- before { Bullet.stacktrace_includes = ['def', /xyz/] }
143
- after { Bullet.stacktrace_includes = nil }
144
-
145
- it 'should include paths that are in the stacktrace_include list' do
146
- in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
147
- included_gems = [OpenStruct.new(absolute_path: '/def/def.rb'), OpenStruct.new(absolute_path: 'xyz/xyz.rb')]
148
- excluded_gem = OpenStruct.new(absolute_path: '/ghi/ghi.rb')
149
-
150
- expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, *included_gems, excluded_gem])
151
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
152
- expect(NPlusOneQuery).to receive(:create_notification).with(
153
- [in_project, *included_gems],
154
- 'Post',
155
- :association
156
- )
157
- NPlusOneQuery.call_association(@post, :association)
158
- end
159
- end
160
- end
161
-
162
130
  context '.add_possible_objects' do
163
131
  it 'should add possible objects' do
164
132
  NPlusOneQuery.add_possible_objects([@post, @post2])
@@ -65,6 +65,11 @@ module Bullet
65
65
  expect(UnusedEagerLoading).not_to receive(:create_notification).with('Post', [:association])
66
66
  UnusedEagerLoading.check_unused_preload_associations
67
67
  end
68
+
69
+ it 'should create call stack for notification' do
70
+ UnusedEagerLoading.add_object_associations(@post, :association)
71
+ expect(UnusedEagerLoading.send(:call_stacks).registry).not_to be_empty
72
+ end
68
73
  end
69
74
 
70
75
  context '.add_eager_loadings' do
@@ -74,8 +74,8 @@ module Bullet
74
74
  it 'should send full_notice to notifier' do
75
75
  notifier = double
76
76
  allow(subject).to receive(:notifier).and_return(notifier)
77
- allow(subject).to receive(:notification_data).and_return(foo: :bar)
78
- expect(notifier).to receive(:inline_notify).with(foo: :bar)
77
+ allow(subject).to receive(:notification_data).and_return({ foo: :bar })
78
+ expect(notifier).to receive(:inline_notify).with({ foo: :bar })
79
79
  subject.notify_inline
80
80
  end
81
81
  end
@@ -84,8 +84,8 @@ module Bullet
84
84
  it 'should send full_out_of_channel to notifier' do
85
85
  notifier = double
86
86
  allow(subject).to receive(:notifier).and_return(notifier)
87
- allow(subject).to receive(:notification_data).and_return(foo: :bar)
88
- expect(notifier).to receive(:out_of_channel_notify).with(foo: :bar)
87
+ allow(subject).to receive(:notification_data).and_return({ foo: :bar })
88
+ expect(notifier).to receive(:out_of_channel_notify).with({ foo: :bar })
89
89
  subject.notify_out_of_channel
90
90
  end
91
91
  end
@@ -31,12 +31,6 @@ module Bullet
31
31
  response = double(body: '<html><head></head><body></body></html>')
32
32
  expect(middleware).not_to be_html_request(headers, response)
33
33
  end
34
-
35
- it "should be false if response body doesn't contain html tag" do
36
- headers = { 'Content-Type' => 'text/html' }
37
- response = double(body: '<div>Partial</div>')
38
- expect(middleware).not_to be_html_request(headers, response)
39
- end
40
34
  end
41
35
 
42
36
  context 'empty?' do
@@ -56,7 +50,7 @@ module Bullet
56
50
  end
57
51
 
58
52
  it 'should be true if no response body' do
59
- response = double()
53
+ response = double
60
54
  expect(middleware).to be_empty(response)
61
55
  end
62
56
  end
@@ -135,6 +129,23 @@ module Bullet
135
129
  expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
136
130
  end
137
131
 
132
+ it 'should include CSP nonce in inline script if console_enabled and a CSP is applied' do
133
+ allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
134
+ expect(Bullet).to receive(:console_enabled?).and_return(true)
135
+ allow(middleware).to receive(:xhr_script).and_call_original
136
+
137
+ nonce = '+t9/wTlgG6xbHxXYUaDNzQ=='
138
+ app.headers = {
139
+ 'Content-Type' => 'text/html',
140
+ 'Content-Security-Policy' => "default-src 'self' https:; script-src 'self' https: 'nonce-#{nonce}'"
141
+ }
142
+
143
+ _, headers, response = middleware.call('Content-Type' => 'text/html')
144
+
145
+ size = 56 + middleware.send(:footer_note).length + middleware.send(:xhr_script, nonce).length
146
+ expect(headers['Content-Length']).to eq(size.to_s)
147
+ end
148
+
138
149
  it 'should change response body for html safe string if console_enabled is true' do
139
150
  expect(Bullet).to receive(:console_enabled?).and_return(true)
140
151
  app.response =
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module Bullet
6
+ RSpec.describe StackTraceFilter do
7
+ let(:dummy_class) { Class.new { extend StackTraceFilter } }
8
+ let(:root_path) { Dir.pwd }
9
+ let(:bundler_path) { Bundler.bundle_path }
10
+
11
+ describe '#caller_in_project' do
12
+ it 'gets the caller in the project' do
13
+ expect(dummy_class).to receive(:call_stacks).and_return({
14
+ 'Post:1' => [
15
+ File.join(root_path, 'lib/bullet.rb'),
16
+ File.join(root_path, 'vendor/uniform_notifier.rb'),
17
+ File.join(bundler_path, 'rack.rb')
18
+ ]
19
+ })
20
+ expect(dummy_class.caller_in_project('Post:1')).to eq([
21
+ File.join(root_path, 'lib/bullet.rb')
22
+ ])
23
+ end
24
+ end
25
+ end
26
+ end
@@ -58,6 +58,19 @@ if active_record?
58
58
  expect(Bullet::Detector::Association).to be_completely_preloading_associations
59
59
  end
60
60
 
61
+ it 'should detect non preload comment => post with inverse_of from a query' do
62
+ Post.first.comments.find_each do |comment|
63
+ comment.name
64
+ comment.post.name
65
+ end
66
+
67
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
68
+ expect(Post.first.comments.count).not_to eq(0)
69
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
70
+
71
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
72
+ end
73
+
61
74
  it 'should detect non preload post => comments with empty?' do
62
75
  Post.all.each { |post| post.comments.empty? }
63
76
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
@@ -474,7 +487,15 @@ if active_record?
474
487
  end
475
488
 
476
489
  it 'should detect preload associations' do
477
- Firm.includes(:clients).each { |firm| firm.clients.map(&:name) }
490
+ Firm.preload(:clients).each { |firm| firm.clients.map(&:name) }
491
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
492
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
493
+
494
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
495
+ end
496
+
497
+ it 'should detect eager load association' do
498
+ Firm.eager_load(:clients).each { |firm| firm.clients.map(&:name) }
478
499
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
479
500
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
480
501
 
@@ -508,7 +529,15 @@ if active_record?
508
529
  end
509
530
 
510
531
  it 'should detect preload associations' do
511
- Firm.includes(:groups).each { |firm| firm.groups.map(&:name) }
532
+ Firm.preload(:groups).each { |firm| firm.groups.map(&:name) }
533
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
534
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
535
+
536
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
537
+ end
538
+
539
+ it 'should detect eager load associations' do
540
+ Firm.eager_load(:groups).each { |firm| firm.groups.map(&:name) }
512
541
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
513
542
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
514
543
 
@@ -92,7 +92,7 @@ module Support
92
92
  page3 = Page.create(name: 'page3', parent_id: folder2.id, author_id: author2.id)
93
93
  page4 = Page.create(name: 'page4', parent_id: folder2.id, author_id: author2.id)
94
94
 
95
- role1 = Role.create(name: 'Amdin')
95
+ role1 = Role.create(name: 'Admin')
96
96
  role2 = Role.create(name: 'User')
97
97
 
98
98
  user1 = User.create(name: 'user1', category: category1)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.1
4
+ version: 7.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-15 00:00:00.000000000 Z
11
+ date: 2023-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -104,6 +104,7 @@ files:
104
104
  - lib/bullet/registry.rb
105
105
  - lib/bullet/registry/association.rb
106
106
  - lib/bullet/registry/base.rb
107
+ - lib/bullet/registry/call_stack.rb
107
108
  - lib/bullet/registry/object.rb
108
109
  - lib/bullet/stack_trace_filter.rb
109
110
  - lib/bullet/version.rb
@@ -126,6 +127,7 @@ files:
126
127
  - spec/bullet/registry/association_spec.rb
127
128
  - spec/bullet/registry/base_spec.rb
128
129
  - spec/bullet/registry/object_spec.rb
130
+ - spec/bullet/stack_trace_filter_spec.rb
129
131
  - spec/bullet_spec.rb
130
132
  - spec/integration/active_record/association_spec.rb
131
133
  - spec/integration/counter_cache_spec.rb
@@ -195,7 +197,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
197
  - !ruby/object:Gem::Version
196
198
  version: 1.3.6
197
199
  requirements: []
198
- rubygems_version: 3.2.32
200
+ rubygems_version: 3.4.1
199
201
  signing_key:
200
202
  specification_version: 4
201
203
  summary: help to kill N+1 queries and unused eager loading.
@@ -216,6 +218,7 @@ test_files:
216
218
  - spec/bullet/registry/association_spec.rb
217
219
  - spec/bullet/registry/base_spec.rb
218
220
  - spec/bullet/registry/object_spec.rb
221
+ - spec/bullet/stack_trace_filter_spec.rb
219
222
  - spec/bullet_spec.rb
220
223
  - spec/integration/active_record/association_spec.rb
221
224
  - spec/integration/counter_cache_spec.rb