bullet 5.8.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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