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
data/lib/bullet/registry.rb
CHANGED
@@ -1,18 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "bundler"
|
2
3
|
|
3
4
|
module Bullet
|
4
5
|
module StackTraceFilter
|
5
6
|
VENDOR_PATH = '/vendor'
|
6
7
|
IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
|
7
8
|
|
8
|
-
|
9
|
+
# @param bullet_key[String] - use this to get stored call stack from call_stacks object.
|
10
|
+
def caller_in_project(bullet_key = nil)
|
9
11
|
vendor_root = Bullet.app_root + VENDOR_PATH
|
10
12
|
bundler_path = Bundler.bundle_path.to_s
|
11
|
-
select_caller_locations do |location|
|
13
|
+
select_caller_locations(bullet_key) do |location|
|
12
14
|
caller_path = location_as_path(location)
|
13
15
|
caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
|
14
|
-
!caller_path.include?(bundler_path) ||
|
15
|
-
|
16
|
+
!caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern|
|
17
|
+
pattern_matches?(location, include_pattern)
|
18
|
+
}
|
16
19
|
end
|
17
20
|
end
|
18
21
|
|
@@ -48,15 +51,16 @@ module Bullet
|
|
48
51
|
end
|
49
52
|
|
50
53
|
def location_as_path(location)
|
54
|
+
return location if location.is_a?(String)
|
55
|
+
|
51
56
|
IS_RUBY_19 ? location : location.absolute_path.to_s
|
52
57
|
end
|
53
58
|
|
54
|
-
def select_caller_locations
|
55
|
-
if IS_RUBY_19
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
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 }
|
60
64
|
end
|
61
65
|
end
|
62
66
|
end
|
data/lib/bullet/version.rb
CHANGED
data/lib/bullet.rb
CHANGED
@@ -20,13 +20,14 @@ module Bullet
|
|
20
20
|
autoload :Registry, 'bullet/registry'
|
21
21
|
autoload :NotificationCollector, 'bullet/notification_collector'
|
22
22
|
|
23
|
-
BULLET_DEBUG = 'BULLET_DEBUG'
|
24
|
-
TRUE = 'true'
|
25
|
-
|
26
23
|
if defined?(Rails::Railtie)
|
27
24
|
class BulletRailtie < Rails::Railtie
|
28
25
|
initializer 'bullet.configure_rails_initialization' do |app|
|
29
|
-
|
26
|
+
if defined?(ActionDispatch::ContentSecurityPolicy::Middleware)
|
27
|
+
app.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, Bullet::Rack
|
28
|
+
else
|
29
|
+
app.middleware.use Bullet::Rack
|
30
|
+
end
|
30
31
|
end
|
31
32
|
end
|
32
33
|
end
|
@@ -38,10 +39,11 @@ module Bullet
|
|
38
39
|
:stacktrace_includes,
|
39
40
|
:stacktrace_excludes,
|
40
41
|
:skip_html_injection
|
41
|
-
attr_reader :
|
42
|
+
attr_reader :safelist
|
42
43
|
attr_accessor :add_footer, :orm_patches_applied, :skip_http_headers
|
43
44
|
|
44
|
-
available_notifiers =
|
45
|
+
available_notifiers =
|
46
|
+
UniformNotifier::AVAILABLE_NOTIFIERS.select { |notifier| notifier != :raise }.map { |notifier| "#{notifier}=" }
|
45
47
|
available_notifiers_options = { to: UniformNotifier }
|
46
48
|
delegate(*available_notifiers, **available_notifiers_options)
|
47
49
|
|
@@ -59,7 +61,7 @@ module Bullet
|
|
59
61
|
@enable = @n_plus_one_query_enable = @unused_eager_loading_enable = @counter_cache_enable = enable
|
60
62
|
|
61
63
|
if enable?
|
62
|
-
|
64
|
+
reset_safelist
|
63
65
|
unless orm_patches_applied
|
64
66
|
self.orm_patches_applied = true
|
65
67
|
Bullet::Mongoid.enable if mongoid?
|
@@ -72,8 +74,9 @@ module Bullet
|
|
72
74
|
!!@enable
|
73
75
|
end
|
74
76
|
|
77
|
+
# Rails.root might be nil if `railties` is a dependency on a project that does not use Rails
|
75
78
|
def app_root
|
76
|
-
@app_root ||= (defined?(::Rails.root) ? Rails.root.to_s : Dir.pwd).to_s
|
79
|
+
@app_root ||= (defined?(::Rails.root) && !::Rails.root.nil? ? Rails.root.to_s : Dir.pwd).to_s
|
77
80
|
end
|
78
81
|
|
79
82
|
def n_plus_one_query_enable?
|
@@ -96,29 +99,29 @@ module Bullet
|
|
96
99
|
@stacktrace_excludes ||= []
|
97
100
|
end
|
98
101
|
|
99
|
-
def
|
100
|
-
|
101
|
-
@
|
102
|
-
@
|
102
|
+
def add_safelist(options)
|
103
|
+
reset_safelist
|
104
|
+
@safelist[options[:type]][options[:class_name]] ||= []
|
105
|
+
@safelist[options[:type]][options[:class_name]] << options[:association].to_sym
|
103
106
|
end
|
104
107
|
|
105
|
-
def
|
106
|
-
|
107
|
-
@
|
108
|
-
@
|
109
|
-
@
|
108
|
+
def delete_safelist(options)
|
109
|
+
reset_safelist
|
110
|
+
@safelist[options[:type]][options[:class_name]] ||= []
|
111
|
+
@safelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
|
112
|
+
@safelist[options[:type]].delete_if { |_key, val| val.empty? }
|
110
113
|
end
|
111
114
|
|
112
|
-
def
|
113
|
-
Array(@
|
115
|
+
def get_safelist_associations(type, class_name)
|
116
|
+
Array.wrap(@safelist[type][class_name])
|
114
117
|
end
|
115
118
|
|
116
|
-
def
|
117
|
-
@
|
119
|
+
def reset_safelist
|
120
|
+
@safelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
|
118
121
|
end
|
119
122
|
|
120
|
-
def
|
121
|
-
@
|
123
|
+
def clear_safelist
|
124
|
+
@safelist = nil
|
122
125
|
end
|
123
126
|
|
124
127
|
def bullet_logger=(active)
|
@@ -132,7 +135,7 @@ module Bullet
|
|
132
135
|
end
|
133
136
|
|
134
137
|
def debug(title, message)
|
135
|
-
puts "[Bullet][#{title}] #{message}" if ENV[BULLET_DEBUG] ==
|
138
|
+
puts "[Bullet][#{title}] #{message}" if ENV['BULLET_DEBUG'] == 'true'
|
136
139
|
end
|
137
140
|
|
138
141
|
def start_request
|
@@ -145,6 +148,7 @@ module Bullet
|
|
145
148
|
Thread.current[:bullet_impossible_objects] = Bullet::Registry::Object.new
|
146
149
|
Thread.current[:bullet_inversed_objects] = Bullet::Registry::Base.new
|
147
150
|
Thread.current[:bullet_eager_loadings] = Bullet::Registry::Association.new
|
151
|
+
Thread.current[:bullet_call_stacks] = Bullet::Registry::CallStack.new
|
148
152
|
|
149
153
|
Thread.current[:bullet_counter_possible_objects] ||= Bullet::Registry::Object.new
|
150
154
|
Thread.current[:bullet_counter_impossible_objects] ||= Bullet::Registry::Object.new
|
data/perf/benchmark.rb
CHANGED
@@ -30,7 +30,10 @@ end
|
|
30
30
|
|
31
31
|
# create database bullet_benchmark;
|
32
32
|
ActiveRecord::Base.establish_connection(
|
33
|
-
adapter: 'mysql2',
|
33
|
+
adapter: 'mysql2',
|
34
|
+
database: 'bullet_benchmark',
|
35
|
+
server: '/tmp/mysql.socket',
|
36
|
+
username: 'root'
|
34
37
|
)
|
35
38
|
|
36
39
|
ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
|
@@ -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, :
|
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])
|
@@ -19,7 +19,9 @@ module Bullet
|
|
19
19
|
it 'should get call associations if object and association are both in eager_loadings and call_object_associations' do
|
20
20
|
UnusedEagerLoading.add_eager_loadings([@post], :association)
|
21
21
|
UnusedEagerLoading.add_call_object_associations(@post, :association)
|
22
|
-
expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to eq(
|
22
|
+
expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to eq(
|
23
|
+
[:association]
|
24
|
+
)
|
23
25
|
end
|
24
26
|
|
25
27
|
it 'should not get call associations if not exist in call_object_associations' do
|
@@ -30,7 +32,9 @@ module Bullet
|
|
30
32
|
|
31
33
|
context '.diff_object_associations' do
|
32
34
|
it 'should return associations not exist in call_association' do
|
33
|
-
expect(UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))).to eq(
|
35
|
+
expect(UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))).to eq(
|
36
|
+
[:association]
|
37
|
+
)
|
34
38
|
end
|
35
39
|
|
36
40
|
it 'should return empty if associations exist in call_association' do
|
@@ -61,6 +65,11 @@ module Bullet
|
|
61
65
|
expect(UnusedEagerLoading).not_to receive(:create_notification).with('Post', [:association])
|
62
66
|
UnusedEagerLoading.check_unused_preload_associations
|
63
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
|
64
73
|
end
|
65
74
|
|
66
75
|
context '.add_eager_loadings' do
|
@@ -10,7 +10,7 @@ describe Object do
|
|
10
10
|
end
|
11
11
|
|
12
12
|
if mongoid?
|
13
|
-
it 'should return class with
|
13
|
+
it 'should return class with namespace and id composition' do
|
14
14
|
post = Mongoid::Post.first
|
15
15
|
expect(post.bullet_key).to eq("Mongoid::Post:#{post.id}")
|
16
16
|
end
|
@@ -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
|
data/spec/bullet/rack_spec.rb
CHANGED
@@ -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
|
@@ -54,6 +48,11 @@ module Bullet
|
|
54
48
|
response = double(body: '')
|
55
49
|
expect(middleware).to be_empty(response)
|
56
50
|
end
|
51
|
+
|
52
|
+
it 'should be true if no response body' do
|
53
|
+
response = double
|
54
|
+
expect(middleware).to be_empty(response)
|
55
|
+
end
|
57
56
|
end
|
58
57
|
|
59
58
|
context '#call' do
|
@@ -105,9 +104,10 @@ module Bullet
|
|
105
104
|
|
106
105
|
it 'should change response body for html safe string if add_footer is true' do
|
107
106
|
expect(Bullet).to receive(:add_footer).exactly(3).times.and_return(true)
|
108
|
-
app.response =
|
109
|
-
|
110
|
-
|
107
|
+
app.response =
|
108
|
+
Support::ResponseDouble.new.tap do |response|
|
109
|
+
response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
|
110
|
+
end
|
111
111
|
_, headers, response = middleware.call('Content-Type' => 'text/html')
|
112
112
|
|
113
113
|
expect(headers['Content-Length']).to eq((73 + middleware.send(:footer_note).length).to_s)
|
@@ -117,7 +117,7 @@ module Bullet
|
|
117
117
|
it 'should add the footer-text header for non-html requests when add_footer is true' do
|
118
118
|
allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
|
119
119
|
allow(Bullet).to receive(:footer_info).and_return(['footer text'])
|
120
|
-
app.headers = {'Content-Type' => 'application/json'}
|
120
|
+
app.headers = { 'Content-Type' => 'application/json' }
|
121
121
|
_, headers, _response = middleware.call({})
|
122
122
|
expect(headers).to include('X-bullet-footer-text' => '["footer text"]')
|
123
123
|
end
|
@@ -129,11 +129,29 @@ module Bullet
|
|
129
129
|
expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
|
130
130
|
end
|
131
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
|
+
|
132
149
|
it 'should change response body for html safe string if console_enabled is true' do
|
133
150
|
expect(Bullet).to receive(:console_enabled?).and_return(true)
|
134
|
-
app.response =
|
135
|
-
|
136
|
-
|
151
|
+
app.response =
|
152
|
+
Support::ResponseDouble.new.tap do |response|
|
153
|
+
response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
|
154
|
+
end
|
137
155
|
_, headers, response = middleware.call('Content-Type' => 'text/html')
|
138
156
|
expect(headers['Content-Length']).to eq('56')
|
139
157
|
expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
|
@@ -142,7 +160,7 @@ module Bullet
|
|
142
160
|
it 'should add headers for non-html requests when console_enabled is true' do
|
143
161
|
allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true)
|
144
162
|
allow(Bullet).to receive(:text_notifications).and_return(['text notifications'])
|
145
|
-
app.headers = {'Content-Type' => 'application/json'}
|
163
|
+
app.headers = { 'Content-Type' => 'application/json' }
|
146
164
|
_, headers, _response = middleware.call({})
|
147
165
|
expect(headers).to include('X-bullet-console-text' => '["text notifications"]')
|
148
166
|
end
|
@@ -155,13 +173,13 @@ module Bullet
|
|
155
173
|
end
|
156
174
|
|
157
175
|
it "shouldn't add headers unnecessarily" do
|
158
|
-
app.headers = {'Content-Type' => 'application/json'}
|
176
|
+
app.headers = { 'Content-Type' => 'application/json' }
|
159
177
|
_, headers, _response = middleware.call({})
|
160
178
|
expect(headers).not_to include('X-bullet-footer-text')
|
161
179
|
expect(headers).not_to include('X-bullet-console-text')
|
162
180
|
end
|
163
181
|
|
164
|
-
context
|
182
|
+
context 'when skip_http_headers is enabled' do
|
165
183
|
before do
|
166
184
|
allow(Bullet).to receive(:skip_http_headers).and_return(true)
|
167
185
|
end
|
@@ -183,14 +201,14 @@ module Bullet
|
|
183
201
|
|
184
202
|
it 'should not add the footer-text header for non-html requests when add_footer is true' do
|
185
203
|
allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
|
186
|
-
app.headers = {'Content-Type' => 'application/json'}
|
204
|
+
app.headers = { 'Content-Type' => 'application/json' }
|
187
205
|
_, headers, _response = middleware.call({})
|
188
206
|
expect(headers).not_to include('X-bullet-footer-text')
|
189
207
|
end
|
190
208
|
|
191
209
|
it 'should not add headers for non-html requests when console_enabled is true' do
|
192
210
|
allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true)
|
193
|
-
app.headers = {'Content-Type' => 'application/json'}
|
211
|
+
app.headers = { 'Content-Type' => 'application/json' }
|
194
212
|
_, headers, _response = middleware.call({})
|
195
213
|
expect(headers).not_to include('X-bullet-console-text')
|
196
214
|
end
|
@@ -259,6 +277,20 @@ module Bullet
|
|
259
277
|
expect(middleware.response_body(response)).to eq body_string
|
260
278
|
end
|
261
279
|
end
|
280
|
+
|
281
|
+
begin
|
282
|
+
require 'rack/files'
|
283
|
+
|
284
|
+
context 'when `response` is a Rack::Files::Iterator' do
|
285
|
+
let(:response) { instance_double(::Rack::Files::Iterator) }
|
286
|
+
before { allow(response).to receive(:is_a?).with(::Rack::Files::Iterator) { true } }
|
287
|
+
|
288
|
+
it 'should return nil' do
|
289
|
+
expect(middleware.response_body(response)).to be_nil
|
290
|
+
end
|
291
|
+
end
|
292
|
+
rescue LoadError
|
293
|
+
end
|
262
294
|
end
|
263
295
|
end
|
264
296
|
end
|
@@ -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
|
data/spec/bullet_spec.rb
CHANGED
@@ -74,31 +74,31 @@ describe Bullet, focused: true do
|
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
77
|
-
describe '#
|
77
|
+
describe '#add_safelist' do
|
78
78
|
context "for 'special' class names" do
|
79
|
-
it 'is added to the
|
80
|
-
Bullet.
|
81
|
-
expect(Bullet.
|
79
|
+
it 'is added to the safelist successfully' do
|
80
|
+
Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
|
81
|
+
expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
|
82
82
|
end
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
86
|
-
describe '#
|
86
|
+
describe '#delete_safelist' do
|
87
87
|
context "for 'special' class names" do
|
88
|
-
it 'is deleted from the
|
89
|
-
Bullet.
|
90
|
-
Bullet.
|
91
|
-
expect(Bullet.
|
88
|
+
it 'is deleted from the safelist successfully' do
|
89
|
+
Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
|
90
|
+
Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
|
91
|
+
expect(Bullet.safelist[:n_plus_one_query]).to eq({})
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
95
95
|
context 'when exists multiple definitions' do
|
96
|
-
it 'is deleted from the
|
97
|
-
Bullet.
|
98
|
-
Bullet.
|
99
|
-
Bullet.
|
100
|
-
expect(Bullet.
|
101
|
-
expect(Bullet.
|
96
|
+
it 'is deleted from the safelist successfully' do
|
97
|
+
Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
|
98
|
+
Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
|
99
|
+
Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
|
100
|
+
expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
|
101
|
+
expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to_not include :team
|
102
102
|
end
|
103
103
|
end
|
104
104
|
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
|
@@ -129,7 +142,7 @@ if active_record?
|
|
129
142
|
expect(Bullet::Detector::Association).to be_completely_preloading_associations
|
130
143
|
end
|
131
144
|
|
132
|
-
it 'should detect unused preload with post =>
|
145
|
+
it 'should detect unused preload with post => comments, no category => posts' do
|
133
146
|
Category.includes(posts: :comments).each { |category| category.posts.map(&:name) }
|
134
147
|
Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
|
135
148
|
expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Post, :comments)
|
@@ -202,7 +215,7 @@ if active_record?
|
|
202
215
|
expect(Bullet::Detector::Association).to be_completely_preloading_associations
|
203
216
|
end
|
204
217
|
|
205
|
-
it 'should detect preload with post =>
|
218
|
+
it 'should detect preload with post => comments' do
|
206
219
|
Post.first.comments.map(&:name)
|
207
220
|
Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
|
208
221
|
expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
|
@@ -401,6 +414,15 @@ if active_record?
|
|
401
414
|
end
|
402
415
|
|
403
416
|
describe Bullet::Detector::Association, 'has_and_belongs_to_many' do
|
417
|
+
context 'posts <=> deals' do
|
418
|
+
it 'should detect preload associations with join tables that have identifier' do
|
419
|
+
Post.includes(:deals).each { |post| post.deals.map(&:name) }
|
420
|
+
Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
|
421
|
+
expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
|
422
|
+
|
423
|
+
expect(Bullet::Detector::Association).to be_completely_preloading_associations
|
424
|
+
end
|
425
|
+
end
|
404
426
|
context 'students <=> teachers' do
|
405
427
|
it 'should detect non preload associations' do
|
406
428
|
Student.all.each { |student| student.teachers.map(&:name) }
|
@@ -442,6 +464,16 @@ if active_record?
|
|
442
464
|
expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Student, :teachers)
|
443
465
|
end
|
444
466
|
end
|
467
|
+
|
468
|
+
context 'user => roles' do
|
469
|
+
it 'should detect preload associations' do
|
470
|
+
User.first.roles.includes(:resource).each { |role| role.resource }
|
471
|
+
Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
|
472
|
+
expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
|
473
|
+
|
474
|
+
expect(Bullet::Detector::Association).to be_completely_preloading_associations
|
475
|
+
end
|
476
|
+
end
|
445
477
|
end
|
446
478
|
|
447
479
|
describe Bullet::Detector::Association, 'has_many :through' do
|
@@ -455,7 +487,15 @@ if active_record?
|
|
455
487
|
end
|
456
488
|
|
457
489
|
it 'should detect preload associations' do
|
458
|
-
Firm.
|
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) }
|
459
499
|
Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
|
460
500
|
expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
|
461
501
|
|
@@ -489,7 +529,15 @@ if active_record?
|
|
489
529
|
end
|
490
530
|
|
491
531
|
it 'should detect preload associations' do
|
492
|
-
Firm.
|
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) }
|
493
541
|
Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
|
494
542
|
expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
|
495
543
|
|
@@ -729,9 +777,9 @@ if active_record?
|
|
729
777
|
end
|
730
778
|
end
|
731
779
|
|
732
|
-
context '
|
733
|
-
before { Bullet.
|
734
|
-
after { Bullet.
|
780
|
+
context 'add n plus one query to safelist' do
|
781
|
+
before { Bullet.add_safelist type: :n_plus_one_query, class_name: 'Post', association: :comments }
|
782
|
+
after { Bullet.clear_safelist }
|
735
783
|
|
736
784
|
it 'should not detect n plus one query' do
|
737
785
|
Post.all.each { |post| post.comments.map(&:name) }
|
@@ -750,9 +798,9 @@ if active_record?
|
|
750
798
|
end
|
751
799
|
end
|
752
800
|
|
753
|
-
context '
|
754
|
-
before { Bullet.
|
755
|
-
after { Bullet.
|
801
|
+
context 'add unused eager loading to safelist' do
|
802
|
+
before { Bullet.add_safelist type: :unused_eager_loading, class_name: 'Post', association: :comments }
|
803
|
+
after { Bullet.clear_safelist }
|
756
804
|
|
757
805
|
it 'should not detect unused eager loading' do
|
758
806
|
Post.includes(:comments).map(&:name)
|