bullet 6.1.4 → 7.0.5
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 +4 -4
- data/.github/workflows/main.yml +82 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile.rails-7.0 +10 -0
- data/MIT-LICENSE +1 -1
- data/README.md +32 -26
- data/lib/bullet/active_record41.rb +1 -0
- data/lib/bullet/active_record42.rb +1 -0
- data/lib/bullet/active_record5.rb +10 -8
- data/lib/bullet/active_record52.rb +21 -25
- data/lib/bullet/active_record60.rb +20 -24
- data/lib/bullet/active_record61.rb +20 -24
- data/lib/bullet/active_record70.rb +284 -0
- data/lib/bullet/bullet_xhr.js +3 -2
- data/lib/bullet/dependency.rb +10 -0
- data/lib/bullet/detector/association.rb +8 -0
- data/lib/bullet/detector/base.rb +2 -1
- data/lib/bullet/detector/counter_cache.rb +2 -2
- data/lib/bullet/detector/n_plus_one_query.rb +24 -13
- data/lib/bullet/detector/unused_eager_loading.rb +3 -3
- data/lib/bullet/mongoid7x.rb +34 -19
- data/lib/bullet/notification.rb +2 -1
- data/lib/bullet/rack.rb +42 -7
- data/lib/bullet/registry/call_stack.rb +12 -0
- data/lib/bullet/registry.rb +1 -0
- data/lib/bullet/stack_trace_filter.rb +14 -10
- data/lib/bullet/version.rb +1 -1
- data/lib/bullet.rb +28 -24
- data/lib/generators/bullet/install_generator.rb +0 -1
- data/perf/benchmark.rb +4 -1
- data/spec/bullet/detector/n_plus_one_query_spec.rb +1 -33
- data/spec/bullet/detector/unused_eager_loading_spec.rb +11 -2
- data/spec/bullet/ext/object_spec.rb +1 -1
- data/spec/bullet/notification/base_spec.rb +4 -4
- data/spec/bullet/rack_spec.rb +50 -18
- data/spec/bullet/stack_trace_filter_spec.rb +26 -0
- data/spec/bullet_spec.rb +15 -15
- data/spec/integration/active_record/association_spec.rb +58 -10
- data/spec/integration/counter_cache_spec.rb +4 -4
- data/spec/integration/mongoid/association_spec.rb +1 -1
- data/spec/models/deal.rb +5 -0
- data/spec/models/folder.rb +2 -1
- data/spec/models/group.rb +2 -1
- data/spec/models/page.rb +2 -1
- data/spec/models/post.rb +2 -0
- data/spec/models/role.rb +7 -0
- data/spec/models/user.rb +1 -0
- data/spec/models/writer.rb +2 -1
- data/spec/spec_helper.rb +0 -2
- data/spec/support/mongo_seed.rb +1 -0
- data/spec/support/sqlite_seed.rb +30 -0
- data/test.sh +2 -0
- metadata +13 -4
- data/.travis.yml +0 -33
@@ -0,0 +1,284 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullet
|
4
|
+
module SaveWithBulletSupport
|
5
|
+
def _create_record(*)
|
6
|
+
super do
|
7
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
|
8
|
+
yield(self) if block_given?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ActiveRecord
|
14
|
+
def self.enable
|
15
|
+
require 'active_record'
|
16
|
+
::ActiveRecord::Base.extend(
|
17
|
+
Module.new do
|
18
|
+
def find_by_sql(sql, binds = [], preparable: nil, &block)
|
19
|
+
result = super
|
20
|
+
if Bullet.start?
|
21
|
+
if result.is_a? Array
|
22
|
+
if result.size > 1
|
23
|
+
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
|
24
|
+
Bullet::Detector::CounterCache.add_possible_objects(result)
|
25
|
+
elsif result.size == 1
|
26
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
|
27
|
+
Bullet::Detector::CounterCache.add_impossible_object(result.first)
|
28
|
+
end
|
29
|
+
elsif result.is_a? ::ActiveRecord::Base
|
30
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
|
31
|
+
Bullet::Detector::CounterCache.add_impossible_object(result)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
result
|
35
|
+
end
|
36
|
+
end
|
37
|
+
)
|
38
|
+
|
39
|
+
::ActiveRecord::Base.prepend(SaveWithBulletSupport)
|
40
|
+
|
41
|
+
::ActiveRecord::Relation.prepend(
|
42
|
+
Module.new do
|
43
|
+
# if select a collection of objects, then these objects have possible to cause N+1 query.
|
44
|
+
# if select only one object, then the only one object has impossible to cause N+1 query.
|
45
|
+
def records
|
46
|
+
result = super
|
47
|
+
if Bullet.start?
|
48
|
+
if result.first.class.name !~ /^HABTM_/
|
49
|
+
if result.size > 1
|
50
|
+
Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
|
51
|
+
Bullet::Detector::CounterCache.add_possible_objects(result)
|
52
|
+
elsif result.size == 1
|
53
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
|
54
|
+
Bullet::Detector::CounterCache.add_impossible_object(result.first)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
)
|
62
|
+
|
63
|
+
::ActiveRecord::Associations::Preloader::Batch.prepend(
|
64
|
+
Module.new do
|
65
|
+
def call
|
66
|
+
if Bullet.start?
|
67
|
+
@preloaders.each do |preloader|
|
68
|
+
preloader.records.each { |record| Bullet::Detector::Association.add_object_associations(record, preloader.associations) }
|
69
|
+
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(preloader.records, preloader.associations)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
)
|
76
|
+
|
77
|
+
::ActiveRecord::Associations::Preloader::Branch.prepend(
|
78
|
+
Module.new do
|
79
|
+
def preloaders_for_reflection(reflection, reflection_records)
|
80
|
+
if Bullet.start?
|
81
|
+
reflection_records.compact!
|
82
|
+
if reflection_records.first.class.name !~ /^HABTM_/
|
83
|
+
reflection_records.each { |record| Bullet::Detector::Association.add_object_associations(record, reflection.name) }
|
84
|
+
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(reflection_records, reflection.name)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
super
|
88
|
+
end
|
89
|
+
end
|
90
|
+
)
|
91
|
+
|
92
|
+
::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(
|
93
|
+
Module.new do
|
94
|
+
def preloaded_records
|
95
|
+
if Bullet.start? && !defined?(@preloaded_records)
|
96
|
+
source_preloaders.each do |source_preloader|
|
97
|
+
reflection_name = source_preloader.send(:reflection).name
|
98
|
+
source_preloader.send(:owners).each do |owner|
|
99
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
super
|
104
|
+
end
|
105
|
+
end
|
106
|
+
)
|
107
|
+
|
108
|
+
::ActiveRecord::Associations::JoinDependency.prepend(
|
109
|
+
Module.new do
|
110
|
+
def instantiate(result_set, strict_loading_value, &block)
|
111
|
+
@bullet_eager_loadings = {}
|
112
|
+
records = super
|
113
|
+
|
114
|
+
if Bullet.start?
|
115
|
+
@bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
|
116
|
+
objects = eager_loadings_hash.keys
|
117
|
+
Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
|
118
|
+
objects,
|
119
|
+
eager_loadings_hash[objects.first].to_a
|
120
|
+
)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
records
|
124
|
+
end
|
125
|
+
|
126
|
+
def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value)
|
127
|
+
if Bullet.start?
|
128
|
+
unless ar_parent.nil?
|
129
|
+
parent.children.each do |node|
|
130
|
+
key = aliases.column_alias(node, node.primary_key)
|
131
|
+
id = row[key]
|
132
|
+
next unless id.nil?
|
133
|
+
|
134
|
+
associations = node.reflection.name
|
135
|
+
Bullet::Detector::Association.add_object_associations(ar_parent, associations)
|
136
|
+
Bullet::Detector::NPlusOneQuery.call_association(ar_parent, associations)
|
137
|
+
@bullet_eager_loadings[ar_parent.class] ||= {}
|
138
|
+
@bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
|
139
|
+
@bullet_eager_loadings[ar_parent.class][ar_parent] << associations
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
super
|
145
|
+
end
|
146
|
+
|
147
|
+
# call join associations
|
148
|
+
def construct_model(record, node, row, model_cache, id, strict_loading_value)
|
149
|
+
result = super
|
150
|
+
|
151
|
+
if Bullet.start?
|
152
|
+
associations = node.reflection.name
|
153
|
+
Bullet::Detector::Association.add_object_associations(record, associations)
|
154
|
+
Bullet::Detector::NPlusOneQuery.call_association(record, associations)
|
155
|
+
@bullet_eager_loadings[record.class] ||= {}
|
156
|
+
@bullet_eager_loadings[record.class][record] ||= Set.new
|
157
|
+
@bullet_eager_loadings[record.class][record] << associations
|
158
|
+
end
|
159
|
+
|
160
|
+
result
|
161
|
+
end
|
162
|
+
end
|
163
|
+
)
|
164
|
+
|
165
|
+
::ActiveRecord::Associations::Association.prepend(
|
166
|
+
Module.new do
|
167
|
+
def inversed_from(record)
|
168
|
+
if Bullet.start?
|
169
|
+
Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
|
170
|
+
end
|
171
|
+
super
|
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
|
180
|
+
end
|
181
|
+
)
|
182
|
+
|
183
|
+
::ActiveRecord::Associations::CollectionAssociation.prepend(
|
184
|
+
Module.new do
|
185
|
+
def load_target
|
186
|
+
records = super
|
187
|
+
|
188
|
+
if Bullet.start?
|
189
|
+
if is_a? ::ActiveRecord::Associations::ThroughAssociation
|
190
|
+
association = owner.association(reflection.through_reflection.name)
|
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
|
196
|
+
|
197
|
+
if reflection.through_reflection != through_reflection
|
198
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
|
203
|
+
if records.first.class.name !~ /^HABTM_/
|
204
|
+
if records.size > 1
|
205
|
+
Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
|
206
|
+
Bullet::Detector::CounterCache.add_possible_objects(records)
|
207
|
+
elsif records.size == 1
|
208
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
|
209
|
+
Bullet::Detector::CounterCache.add_impossible_object(records.first)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
records
|
214
|
+
end
|
215
|
+
|
216
|
+
def empty?
|
217
|
+
if Bullet.start? && !reflection.has_cached_counter?
|
218
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
|
219
|
+
end
|
220
|
+
super
|
221
|
+
end
|
222
|
+
|
223
|
+
def include?(object)
|
224
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
|
225
|
+
super
|
226
|
+
end
|
227
|
+
end
|
228
|
+
)
|
229
|
+
|
230
|
+
::ActiveRecord::Associations::SingularAssociation.prepend(
|
231
|
+
Module.new do
|
232
|
+
# call has_one and belongs_to associations
|
233
|
+
def reader
|
234
|
+
result = super
|
235
|
+
|
236
|
+
if Bullet.start?
|
237
|
+
if owner.class.name !~ /^HABTM_/
|
238
|
+
if is_a? ::ActiveRecord::Associations::ThroughAssociation
|
239
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
|
240
|
+
association = owner.association(reflection.through_reflection.name)
|
241
|
+
Array.wrap(association.target).each do |through_record|
|
242
|
+
Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
|
243
|
+
end
|
244
|
+
|
245
|
+
if reflection.through_reflection != through_reflection
|
246
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
|
250
|
+
|
251
|
+
if Bullet::Detector::NPlusOneQuery.impossible?(owner)
|
252
|
+
Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
|
253
|
+
else
|
254
|
+
Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
result
|
259
|
+
end
|
260
|
+
end
|
261
|
+
)
|
262
|
+
|
263
|
+
::ActiveRecord::Associations::HasManyAssociation.prepend(
|
264
|
+
Module.new do
|
265
|
+
def empty?
|
266
|
+
result = super
|
267
|
+
if Bullet.start? && !reflection.has_cached_counter?
|
268
|
+
Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
|
269
|
+
end
|
270
|
+
result
|
271
|
+
end
|
272
|
+
|
273
|
+
def count_records
|
274
|
+
result = reflection.has_cached_counter?
|
275
|
+
if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
|
276
|
+
Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
|
277
|
+
end
|
278
|
+
super
|
279
|
+
end
|
280
|
+
end
|
281
|
+
)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
data/lib/bullet/bullet_xhr.js
CHANGED
@@ -20,6 +20,7 @@
|
|
20
20
|
if (this.onload) {
|
21
21
|
this._storedOnload = this.onload;
|
22
22
|
}
|
23
|
+
this.onload = null;
|
23
24
|
this.addEventListener("load", bulletXHROnload);
|
24
25
|
return Reflect.apply(oldSend, this, arguments);
|
25
26
|
}
|
@@ -30,7 +31,7 @@
|
|
30
31
|
) {
|
31
32
|
var bulletFooterText = this.getResponseHeader("X-bullet-footer-text");
|
32
33
|
if (bulletFooterText) {
|
33
|
-
setTimeout(function() {
|
34
|
+
setTimeout(function () {
|
34
35
|
var oldHtml = document.querySelector("#bullet-footer").innerHTML.split("<br>");
|
35
36
|
var header = oldHtml[0];
|
36
37
|
oldHtml = oldHtml.slice(1, oldHtml.length);
|
@@ -41,7 +42,7 @@
|
|
41
42
|
}
|
42
43
|
var bulletConsoleText = this.getResponseHeader("X-bullet-console-text");
|
43
44
|
if (bulletConsoleText && typeof console !== "undefined" && console.log) {
|
44
|
-
setTimeout(function() {
|
45
|
+
setTimeout(function () {
|
45
46
|
JSON.parse(bulletConsoleText).forEach((message) => {
|
46
47
|
if (console.groupCollapsed && console.groupEnd) {
|
47
48
|
console.groupCollapsed("Uniform Notifier");
|
data/lib/bullet/dependency.rb
CHANGED
@@ -29,6 +29,8 @@ module Bullet
|
|
29
29
|
'active_record60'
|
30
30
|
elsif active_record61?
|
31
31
|
'active_record61'
|
32
|
+
elsif active_record70?
|
33
|
+
'active_record70'
|
32
34
|
else
|
33
35
|
raise "Bullet does not support active_record #{::ActiveRecord::VERSION::STRING} yet"
|
34
36
|
end
|
@@ -64,6 +66,10 @@ module Bullet
|
|
64
66
|
active_record? && ::ActiveRecord::VERSION::MAJOR == 6
|
65
67
|
end
|
66
68
|
|
69
|
+
def active_record7?
|
70
|
+
active_record? && ::ActiveRecord::VERSION::MAJOR == 7
|
71
|
+
end
|
72
|
+
|
67
73
|
def active_record40?
|
68
74
|
active_record4? && ::ActiveRecord::VERSION::MINOR == 0
|
69
75
|
end
|
@@ -96,6 +102,10 @@ module Bullet
|
|
96
102
|
active_record6? && ::ActiveRecord::VERSION::MINOR == 1
|
97
103
|
end
|
98
104
|
|
105
|
+
def active_record70?
|
106
|
+
active_record7? && ::ActiveRecord::VERSION::MINOR == 0
|
107
|
+
end
|
108
|
+
|
99
109
|
def mongoid4x?
|
100
110
|
mongoid? && ::Mongoid::VERSION =~ /\A4/
|
101
111
|
end
|
@@ -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
|
data/lib/bullet/detector/base.rb
CHANGED
@@ -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.
|
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.
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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.
|
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
|
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.
|
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)
|
data/lib/bullet/mongoid7x.rb
CHANGED
@@ -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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
return to_enum unless
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
24
|
+
return to_enum unless block_given?
|
25
|
+
|
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
|
-
|
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)
|
data/lib/bullet/notification.rb
CHANGED
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,9 +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
|
-
|
24
|
-
|
25
|
-
|
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
35
|
elsif !Bullet.skip_http_headers
|
28
36
|
set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
|
@@ -40,6 +48,7 @@ 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
|
@@ -76,13 +85,13 @@ module Bullet
|
|
76
85
|
end
|
77
86
|
|
78
87
|
def html_request?(headers, response)
|
79
|
-
headers['Content-Type']&.include?('text/html')
|
88
|
+
headers['Content-Type']&.include?('text/html')
|
80
89
|
end
|
81
90
|
|
82
91
|
def response_body(response)
|
83
92
|
if response.respond_to?(:body)
|
84
93
|
Array === response.body ? response.body.first : response.body
|
85
|
-
|
94
|
+
elsif response.respond_to?(:first)
|
86
95
|
response.first
|
87
96
|
end
|
88
97
|
end
|
@@ -115,8 +124,34 @@ module Bullet
|
|
115
124
|
end
|
116
125
|
|
117
126
|
# Make footer work for XHR requests by appending data to the footer
|
118
|
-
def xhr_script
|
119
|
-
|
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
|
120
155
|
end
|
121
156
|
end
|
122
157
|
end
|