bullet 5.8.1 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +22 -1
  3. data/CHANGELOG.md +24 -1
  4. data/Gemfile.mongoid-7.0 +15 -0
  5. data/Gemfile.rails-4.0 +1 -1
  6. data/Gemfile.rails-4.1 +1 -1
  7. data/Gemfile.rails-4.2 +1 -1
  8. data/Gemfile.rails-5.0 +1 -1
  9. data/Gemfile.rails-5.1 +1 -1
  10. data/Gemfile.rails-5.2 +1 -1
  11. data/Gemfile.rails-6.0 +15 -0
  12. data/README.md +24 -10
  13. data/lib/bullet.rb +42 -17
  14. data/lib/bullet/active_job.rb +9 -0
  15. data/lib/bullet/active_record4.rb +9 -32
  16. data/lib/bullet/active_record41.rb +7 -27
  17. data/lib/bullet/active_record42.rb +8 -24
  18. data/lib/bullet/active_record5.rb +188 -179
  19. data/lib/bullet/active_record52.rb +176 -168
  20. data/lib/bullet/active_record60.rb +267 -0
  21. data/lib/bullet/bullet_xhr.js +63 -0
  22. data/lib/bullet/dependency.rb +48 -34
  23. data/lib/bullet/detector/association.rb +26 -20
  24. data/lib/bullet/detector/base.rb +1 -2
  25. data/lib/bullet/detector/counter_cache.rb +13 -9
  26. data/lib/bullet/detector/n_plus_one_query.rb +22 -12
  27. data/lib/bullet/detector/unused_eager_loading.rb +6 -3
  28. data/lib/bullet/ext/object.rb +4 -2
  29. data/lib/bullet/mongoid4x.rb +2 -6
  30. data/lib/bullet/mongoid5x.rb +2 -6
  31. data/lib/bullet/mongoid6x.rb +2 -6
  32. data/lib/bullet/mongoid7x.rb +57 -0
  33. data/lib/bullet/notification/base.rb +14 -18
  34. data/lib/bullet/notification/n_plus_one_query.rb +2 -4
  35. data/lib/bullet/notification/unused_eager_loading.rb +2 -4
  36. data/lib/bullet/rack.rb +39 -20
  37. data/lib/bullet/stack_trace_filter.rb +6 -12
  38. data/lib/bullet/version.rb +1 -1
  39. data/lib/generators/bullet/install_generator.rb +4 -2
  40. data/perf/benchmark.rb +8 -14
  41. data/spec/bullet/detector/counter_cache_spec.rb +5 -5
  42. data/spec/bullet/detector/n_plus_one_query_spec.rb +7 -3
  43. data/spec/bullet/detector/unused_eager_loading_spec.rb +29 -12
  44. data/spec/bullet/ext/object_spec.rb +9 -4
  45. data/spec/bullet/notification/base_spec.rb +1 -3
  46. data/spec/bullet/notification/n_plus_one_query_spec.rb +18 -3
  47. data/spec/bullet/notification/unused_eager_loading_spec.rb +5 -1
  48. data/spec/bullet/rack_spec.rb +30 -6
  49. data/spec/bullet/registry/association_spec.rb +2 -2
  50. data/spec/bullet/registry/base_spec.rb +1 -1
  51. data/spec/bullet_spec.rb +10 -29
  52. data/spec/integration/active_record/association_spec.rb +45 -136
  53. data/spec/integration/counter_cache_spec.rb +11 -31
  54. data/spec/integration/mongoid/association_spec.rb +18 -32
  55. data/spec/models/folder.rb +1 -2
  56. data/spec/models/group.rb +1 -2
  57. data/spec/models/page.rb +1 -2
  58. data/spec/models/writer.rb +1 -2
  59. data/spec/spec_helper.rb +6 -10
  60. data/spec/support/bullet_ext.rb +8 -9
  61. data/spec/support/mongo_seed.rb +2 -16
  62. data/test.sh +2 -0
  63. data/update.sh +1 -0
  64. metadata +9 -4
@@ -37,9 +37,7 @@ module Bullet
37
37
 
38
38
  def eager_load(docs)
39
39
  associations = criteria.inclusions.map(&:name)
40
- docs.each do |doc|
41
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
42
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
43
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
44
42
  origin_eager_load(docs)
45
43
  end
@@ -50,9 +48,7 @@ module Bullet
50
48
 
51
49
  def get_relation(name, metadata, object, reload = false)
52
50
  result = origin_get_relation(name, metadata, object, reload)
53
- if metadata.macro !~ /embed/
54
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
55
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
56
52
  result
57
53
  end
58
54
  end
@@ -37,9 +37,7 @@ module Bullet
37
37
 
38
38
  def eager_load(docs)
39
39
  associations = criteria.inclusions.map(&:name)
40
- docs.each do |doc|
41
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
42
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
43
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
44
42
  origin_eager_load(docs)
45
43
  end
@@ -50,9 +48,7 @@ module Bullet
50
48
 
51
49
  def get_relation(name, metadata, object, reload = false)
52
50
  result = origin_get_relation(name, metadata, object, reload)
53
- if metadata.macro !~ /embed/
54
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
55
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
56
52
  result
57
53
  end
58
54
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet
4
+ module Mongoid
5
+ def self.enable
6
+ require 'mongoid'
7
+ ::Mongoid::Contextual::Mongo.class_eval do
8
+ alias_method :origin_first, :first
9
+ alias_method :origin_last, :last
10
+ alias_method :origin_each, :each
11
+ alias_method :origin_eager_load, :eager_load
12
+
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
23
+ end
24
+
25
+ def each(&block)
26
+ return to_enum unless block_given?
27
+
28
+ records = []
29
+ origin_each { |record| records << record }
30
+ if records.length > 1
31
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
32
+ elsif records.size == 1
33
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
34
+ end
35
+ records.each(&block)
36
+ end
37
+
38
+ def eager_load(docs)
39
+ associations = criteria.inclusions.map(&:name)
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
41
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
42
+ origin_eager_load(docs)
43
+ end
44
+ end
45
+
46
+ ::Mongoid::Association::Accessors.class_eval do
47
+ alias_method :origin_get_relation, :get_relation
48
+
49
+ def get_relation(name, association, object, reload = false)
50
+ result = origin_get_relation(name, association, object, reload)
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) unless association.embedded?
52
+ result
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -8,7 +8,8 @@ module Bullet
8
8
 
9
9
  def initialize(base_class, association_or_associations, path = nil)
10
10
  @base_class = base_class
11
- @associations = association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations]
11
+ @associations =
12
+ association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations]
12
13
  @path = path
13
14
  end
14
15
 
@@ -25,16 +26,16 @@ module Bullet
25
26
  end
26
27
 
27
28
  def whoami
28
- @user ||= ENV['USER'].presence || (begin
29
- `whoami`.chomp
30
- rescue StandardError
31
- ''
32
- end)
33
- if @user.present?
34
- "user: #{@user}"
35
- else
36
- ''
37
- end
29
+ @user ||=
30
+ ENV['USER'].presence ||
31
+ (
32
+ begin
33
+ `whoami`.chomp
34
+ rescue StandardError
35
+ ''
36
+ end
37
+ )
38
+ @user.present? ? "user: #{@user}" : ''
38
39
  end
39
40
 
40
41
  def body_with_caller
@@ -54,12 +55,7 @@ module Bullet
54
55
  end
55
56
 
56
57
  def notification_data
57
- {
58
- user: whoami,
59
- url: url,
60
- title: title,
61
- body: body_with_caller
62
- }
58
+ { user: whoami, url: url, title: title, body: body_with_caller }
63
59
  end
64
60
 
65
61
  def eql?(other)
@@ -77,7 +73,7 @@ module Bullet
77
73
  end
78
74
 
79
75
  def associations_str
80
- ":includes => #{@associations.map { |a| a.to_s.to_sym unless a.is_a? Hash }.inspect}"
76
+ ".includes(#{@associations.map { |a| a.to_s.to_sym }.inspect})"
81
77
  end
82
78
  end
83
79
  end
@@ -10,7 +10,7 @@ module Bullet
10
10
  end
11
11
 
12
12
  def body
13
- "#{klazz_associations_str}\n Add to your finder: #{associations_str}"
13
+ "#{klazz_associations_str}\n Add to your query: #{associations_str}"
14
14
  end
15
15
 
16
16
  def title
@@ -18,9 +18,7 @@ module Bullet
18
18
  end
19
19
 
20
20
  def notification_data
21
- super.merge(
22
- backtrace: []
23
- )
21
+ super.merge(backtrace: [])
24
22
  end
25
23
 
26
24
  protected
@@ -10,7 +10,7 @@ module Bullet
10
10
  end
11
11
 
12
12
  def body
13
- "#{klazz_associations_str}\n Remove from your finder: #{associations_str}"
13
+ "#{klazz_associations_str}\n Remove from your query: #{associations_str}"
14
14
  end
15
15
 
16
16
  def title
@@ -18,9 +18,7 @@ module Bullet
18
18
  end
19
19
 
20
20
  def notification_data
21
- super.merge(
22
- backtrace: []
23
- )
21
+ super.merge(backtrace: [])
24
22
  end
25
23
 
26
24
  protected
@@ -15,13 +15,19 @@ module Bullet
15
15
  status, headers, response = @app.call(env)
16
16
 
17
17
  response_body = nil
18
+
18
19
  if Bullet.notification?
19
- if !file?(headers) && !sse?(headers) && !empty?(response) &&
20
- status == 200 && html_request?(headers, response)
21
- response_body = response_body(response)
22
- response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
23
- response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
24
- headers['Content-Length'] = response_body.bytesize.to_s
20
+ if !Bullet.skip_html_injection? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
21
+ if html_request?(headers, response)
22
+ response_body = response_body(response)
23
+ response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
24
+ response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
25
+ response_body = append_to_html_body(response_body, xhr_script)
26
+ headers['Content-Length'] = response_body.bytesize.to_s
27
+ else
28
+ set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
29
+ set_header(headers, 'X-bullet-console-text', Bullet.text_notifications) if Bullet.console_enabled?
30
+ end
25
31
  end
26
32
  Bullet.perform_out_of_channel_notifications(env)
27
33
  end
@@ -32,16 +38,10 @@ module Bullet
32
38
 
33
39
  # fix issue if response's body is a Proc
34
40
  def empty?(response)
35
- # response may be ["Not Found"], ["Move Permanently"], etc.
36
- if rails?
37
- (response.is_a?(Array) && response.size <= 1) ||
38
- !response.respond_to?(:body) ||
39
- !response_body(response).respond_to?(:empty?) ||
40
- response_body(response).empty?
41
- else
42
- body = response_body(response)
43
- body.nil? || body.empty?
44
- end
41
+ # response may be ["Not Found"], ["Move Permanently"], etc, but
42
+ # those should not happen if the status is 200
43
+ body = response_body(response)
44
+ body.nil? || body.empty?
45
45
  end
46
46
 
47
47
  def append_to_html_body(response_body, content)
@@ -55,7 +55,15 @@ module Bullet
55
55
  end
56
56
 
57
57
  def footer_note
58
- "<div #{footer_div_attributes}>" + footer_close_button + Bullet.footer_info.uniq.join('<br>') + '</div>'
58
+ "<div #{footer_div_attributes}>" + footer_header + '<br>' + Bullet.footer_info.uniq.join('<br>') + '</div>'
59
+ end
60
+
61
+ def set_header(headers, header_name, header_array)
62
+ # Many proxy applications such as Nginx and AWS ELB limit
63
+ # the size a header to 8KB, so truncate the list of reports to
64
+ # be under that limit
65
+ header_array.pop while header_array.to_json.length > 8 * 1_024
66
+ headers[header_name] = header_array.to_json
59
67
  end
60
68
 
61
69
  def file?(headers)
@@ -82,7 +90,7 @@ module Bullet
82
90
 
83
91
  def footer_div_attributes
84
92
  <<~EOF
85
- data-is-bullet-footer ondblclick="this.parentNode.removeChild(this);" style="position: fixed; bottom: 0pt; left: 0pt; cursor: pointer; border-style: solid; border-color: rgb(153, 153, 153);
93
+ id="bullet-footer" data-is-bullet-footer ondblclick="this.parentNode.removeChild(this);" style="position: fixed; bottom: 0pt; left: 0pt; cursor: pointer; border-style: solid; border-color: rgb(153, 153, 153);
86
94
  -moz-border-top-colors: none; -moz-border-right-colors: none; -moz-border-bottom-colors: none;
87
95
  -moz-border-left-colors: none; -moz-border-image: none; border-width: 2pt 2pt 0px 0px;
88
96
  padding: 3px 5px; border-radius: 0pt 10pt 0pt 0px; background: none repeat scroll 0% 0% rgba(200, 200, 200, 0.8);
@@ -90,8 +98,19 @@ module Bullet
90
98
  EOF
91
99
  end
92
100
 
93
- def footer_close_button
94
- "<span onclick='this.parentNode.remove()' style='position:absolute; right: 10px; top: 0px; font-weight: bold; color: #333;'>&times;</span>"
101
+ def footer_header
102
+ cancel_button =
103
+ "<span onclick='this.parentNode.remove()' style='position:absolute; right: 10px; top: 0px; font-weight: bold; color: #333;'>&times;</span>"
104
+ if Bullet.console_enabled?
105
+ "<span>See 'Uniform Notifier' in JS Console for Stacktrace</span>#{cancel_button}"
106
+ else
107
+ cancel_button
108
+ end
109
+ end
110
+
111
+ # Make footer work for XHR requests by appending data to the footer
112
+ def xhr_script
113
+ "<script type='text/javascript'>#{File.read("#{__dir__}/bullet_xhr.js")}</script>"
95
114
  end
96
115
  end
97
116
  end
@@ -5,12 +5,12 @@ module Bullet
5
5
  VENDOR_PATH = '/vendor'
6
6
 
7
7
  def caller_in_project
8
- app_root = rails? ? Rails.root.to_s : Dir.pwd
9
- vendor_root = app_root + VENDOR_PATH
8
+ vendor_root = Bullet.app_root + VENDOR_PATH
10
9
  bundler_path = Bundler.bundle_path.to_s
11
10
  select_caller_locations do |location|
12
11
  caller_path = location_as_path(location)
13
- caller_path.include?(app_root) && !caller_path.include?(vendor_root) && !caller_path.include?(bundler_path) ||
12
+ caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
13
+ !caller_path.include?(bundler_path) ||
14
14
  Bullet.stacktrace_includes.any? { |include_pattern| pattern_matches?(location, include_pattern) }
15
15
  end
16
16
  end
@@ -52,20 +52,14 @@ module Bullet
52
52
 
53
53
  def select_caller_locations
54
54
  if ruby_19?
55
- caller.select do |caller_path|
56
- yield caller_path
57
- end
55
+ caller.select { |caller_path| yield caller_path }
58
56
  else
59
- caller_locations.select do |location|
60
- yield location
61
- end
57
+ caller_locations.select { |location| yield location }
62
58
  end
63
59
  end
64
60
 
65
61
  def ruby_19?
66
- if @ruby_19.nil?
67
- @ruby_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
68
- end
62
+ @ruby_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') if @ruby_19.nil?
69
63
  @ruby_19
70
64
  end
71
65
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '5.8.1'
4
+ VERSION = '6.1.0'
5
5
  end
@@ -10,7 +10,7 @@ module Bullet
10
10
 
11
11
  def enable_in_development
12
12
  environment(nil, env: 'development') do
13
- <<-"FILE".strip
13
+ <<-"FILE"
14
14
 
15
15
  config.after_initialize do
16
16
  Bullet.enable = true
@@ -22,6 +22,7 @@ module Bullet
22
22
  Bullet.add_footer = true
23
23
  end
24
24
  FILE
25
+ .strip
25
26
  end
26
27
 
27
28
  say 'Enabled bullet in config/environments/development.rb'
@@ -30,7 +31,7 @@ module Bullet
30
31
  def enable_in_test
31
32
  if yes?('Would you like to enable bullet in test environment? (y/n)')
32
33
  environment(nil, env: 'test') do
33
- <<-"FILE".strip
34
+ <<-"FILE"
34
35
 
35
36
  config.after_initialize do
36
37
  Bullet.enable = true
@@ -38,6 +39,7 @@ module Bullet
38
39
  Bullet.raise = true # raise an error if n+1 query occurs
39
40
  end
40
41
  FILE
42
+ .strip
41
43
  end
42
44
 
43
45
  say 'Enabled bullet in config/environments/test.rb'
@@ -29,11 +29,11 @@ class User < ActiveRecord::Base
29
29
  end
30
30
 
31
31
  # create database bullet_benchmark;
32
- ActiveRecord::Base.establish_connection(adapter: 'mysql2', database: 'bullet_benchmark', server: '/tmp/mysql.socket', username: 'root')
32
+ ActiveRecord::Base.establish_connection(
33
+ adapter: 'mysql2', database: 'bullet_benchmark', server: '/tmp/mysql.socket', username: 'root'
34
+ )
33
35
 
34
- ActiveRecord::Base.connection.tables.each do |table|
35
- ActiveRecord::Base.connection.drop_table(table)
36
- end
36
+ ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
37
37
 
38
38
  ActiveRecord::Schema.define(version: 1) do
39
39
  create_table :posts do |t|
@@ -54,26 +54,20 @@ ActiveRecord::Schema.define(version: 1) do
54
54
  end
55
55
 
56
56
  users_size = 100
57
- posts_size = 1000
57
+ posts_size = 1_000
58
58
  comments_size = 10_000
59
59
  users = []
60
- users_size.times do |i|
61
- users << User.new(name: "user#{i}")
62
- end
60
+ users_size.times { |i| users << User.new(name: "user#{i}") }
63
61
  User.import users
64
62
  users = User.all
65
63
 
66
64
  posts = []
67
- posts_size.times do |i|
68
- posts << Post.new(title: "Title #{i}", body: "Body #{i}", user: users[i % 100])
69
- end
65
+ posts_size.times { |i| posts << Post.new(title: "Title #{i}", body: "Body #{i}", user: users[i % 100]) }
70
66
  Post.import posts
71
67
  posts = Post.all
72
68
 
73
69
  comments = []
74
- comments_size.times do |i|
75
- comments << Comment.new(body: "Comment #{i}", post: posts[i % 1000], user: users[i % 100])
76
- end
70
+ comments_size.times { |i| comments << Comment.new(body: "Comment #{i}", post: posts[i % 1_000], user: users[i % 100]) }
77
71
  Comment.import comments
78
72
 
79
73
  puts 'Start benchmarking...'
@@ -12,15 +12,15 @@ module Bullet
12
12
 
13
13
  context '.add_counter_cache' do
14
14
  it 'should create notification if conditions met' do
15
- expect(CounterCache).to receive(:conditions_met?).with(@post1, [:comments]).and_return(true)
16
- expect(CounterCache).to receive(:create_notification).with('Post', [:comments])
17
- CounterCache.add_counter_cache(@post1, [:comments])
15
+ expect(CounterCache).to receive(:conditions_met?).with(@post1, %i[comments]).and_return(true)
16
+ expect(CounterCache).to receive(:create_notification).with('Post', %i[comments])
17
+ CounterCache.add_counter_cache(@post1, %i[comments])
18
18
  end
19
19
 
20
20
  it 'should not create notification if conditions not met' do
21
- expect(CounterCache).to receive(:conditions_met?).with(@post1, [:comments]).and_return(false)
21
+ expect(CounterCache).to receive(:conditions_met?).with(@post1, %i[comments]).and_return(false)
22
22
  expect(CounterCache).to receive(:create_notification).never
23
- CounterCache.add_counter_cache(@post1, [:comments])
23
+ CounterCache.add_counter_cache(@post1, %i[comments])
24
24
  end
25
25
  end
26
26
 
@@ -76,8 +76,8 @@ module Bullet
76
76
  context '.call_association' do
77
77
  it 'should create notification if conditions met' do
78
78
  expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
79
- expect(NPlusOneQuery).to receive(:caller_in_project).and_return(['caller'])
80
- expect(NPlusOneQuery).to receive(:create_notification).with(['caller'], 'Post', :association)
79
+ expect(NPlusOneQuery).to receive(:caller_in_project).and_return(%w[caller])
80
+ expect(NPlusOneQuery).to receive(:create_notification).with(%w[caller], 'Post', :association)
81
81
  NPlusOneQuery.call_association(@post, :association)
82
82
  end
83
83
 
@@ -149,7 +149,11 @@ module Bullet
149
149
 
150
150
  expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, *included_gems, excluded_gem])
151
151
  expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
152
- expect(NPlusOneQuery).to receive(:create_notification).with([in_project, *included_gems], 'Post', :association)
152
+ expect(NPlusOneQuery).to receive(:create_notification).with(
153
+ [in_project, *included_gems],
154
+ 'Post',
155
+ :association
156
+ )
153
157
  NPlusOneQuery.call_association(@post, :association)
154
158
  end
155
159
  end