bullet 5.7.5 → 6.1.4

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 (76) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +22 -1
  3. data/CHANGELOG.md +49 -12
  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 +2 -2
  11. data/Gemfile.rails-6.0 +15 -0
  12. data/Gemfile.rails-6.1 +15 -0
  13. data/README.md +38 -13
  14. data/Rakefile +1 -1
  15. data/bullet.gemspec +8 -3
  16. data/lib/bullet.rb +50 -22
  17. data/lib/bullet/active_job.rb +13 -0
  18. data/lib/bullet/active_record4.rb +12 -35
  19. data/lib/bullet/active_record41.rb +10 -30
  20. data/lib/bullet/active_record42.rb +12 -27
  21. data/lib/bullet/active_record5.rb +197 -177
  22. data/lib/bullet/active_record52.rb +191 -166
  23. data/lib/bullet/active_record60.rb +278 -0
  24. data/lib/bullet/active_record61.rb +278 -0
  25. data/lib/bullet/bullet_xhr.js +63 -0
  26. data/lib/bullet/dependency.rb +54 -34
  27. data/lib/bullet/detector/association.rb +26 -20
  28. data/lib/bullet/detector/base.rb +1 -2
  29. data/lib/bullet/detector/counter_cache.rb +14 -9
  30. data/lib/bullet/detector/n_plus_one_query.rb +27 -17
  31. data/lib/bullet/detector/unused_eager_loading.rb +7 -3
  32. data/lib/bullet/ext/object.rb +5 -3
  33. data/lib/bullet/ext/string.rb +1 -1
  34. data/lib/bullet/mongoid4x.rb +4 -7
  35. data/lib/bullet/mongoid5x.rb +4 -7
  36. data/lib/bullet/mongoid6x.rb +8 -11
  37. data/lib/bullet/mongoid7x.rb +57 -0
  38. data/lib/bullet/notification/base.rb +15 -19
  39. data/lib/bullet/notification/n_plus_one_query.rb +2 -4
  40. data/lib/bullet/notification/unused_eager_loading.rb +2 -4
  41. data/lib/bullet/rack.rb +54 -28
  42. data/lib/bullet/stack_trace_filter.rb +39 -30
  43. data/lib/bullet/version.rb +1 -1
  44. data/lib/generators/bullet/install_generator.rb +26 -26
  45. data/perf/benchmark.rb +8 -14
  46. data/spec/bullet/detector/counter_cache_spec.rb +6 -6
  47. data/spec/bullet/detector/n_plus_one_query_spec.rb +30 -3
  48. data/spec/bullet/detector/unused_eager_loading_spec.rb +19 -6
  49. data/spec/bullet/ext/object_spec.rb +9 -4
  50. data/spec/bullet/notification/base_spec.rb +1 -3
  51. data/spec/bullet/notification/n_plus_one_query_spec.rb +16 -3
  52. data/spec/bullet/notification/unused_eager_loading_spec.rb +5 -1
  53. data/spec/bullet/rack_spec.rb +140 -5
  54. data/spec/bullet/registry/association_spec.rb +2 -2
  55. data/spec/bullet/registry/base_spec.rb +1 -1
  56. data/spec/bullet_spec.rb +10 -29
  57. data/spec/integration/active_record/association_spec.rb +122 -118
  58. data/spec/integration/counter_cache_spec.rb +11 -31
  59. data/spec/integration/mongoid/association_spec.rb +18 -32
  60. data/spec/models/attachment.rb +5 -0
  61. data/spec/models/client.rb +2 -0
  62. data/spec/models/firm.rb +1 -0
  63. data/spec/models/folder.rb +1 -2
  64. data/spec/models/group.rb +3 -0
  65. data/spec/models/page.rb +1 -2
  66. data/spec/models/post.rb +15 -0
  67. data/spec/models/submission.rb +1 -0
  68. data/spec/models/user.rb +1 -0
  69. data/spec/models/writer.rb +1 -2
  70. data/spec/spec_helper.rb +6 -10
  71. data/spec/support/bullet_ext.rb +8 -9
  72. data/spec/support/mongo_seed.rb +2 -16
  73. data/spec/support/sqlite_seed.rb +17 -2
  74. data/test.sh +2 -0
  75. data/update.sh +1 -0
  76. metadata +24 -11
@@ -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)
@@ -73,11 +69,11 @@ module Bullet
73
69
  protected
74
70
 
75
71
  def klazz_associations_str
76
- " #{@base_class} => [#{@associations.map(&:inspect).join(', '.freeze)}]"
72
+ " #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]"
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
data/lib/bullet/rack.rb CHANGED
@@ -10,17 +10,24 @@ module Bullet
10
10
 
11
11
  def call(env)
12
12
  return @app.call(env) unless Bullet.enable?
13
+
13
14
  Bullet.start_request
14
15
  status, headers, response = @app.call(env)
15
16
 
16
17
  response_body = nil
18
+
17
19
  if Bullet.notification?
18
- if !file?(headers) && !sse?(headers) && !empty?(response) &&
19
- status == 200 && html_request?(headers, response)
20
- response_body = response_body(response)
21
- response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
22
- response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
23
- headers['Content-Length'] = response_body.bytesize.to_s
20
+ if Bullet.inject_into_page? && !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) if Bullet.add_footer && !Bullet.skip_http_headers
26
+ headers['Content-Length'] = response_body.bytesize.to_s
27
+ elsif !Bullet.skip_http_headers
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
24
31
  end
25
32
  Bullet.perform_out_of_channel_notifications(env)
26
33
  end
@@ -31,20 +38,15 @@ module Bullet
31
38
 
32
39
  # fix issue if response's body is a Proc
33
40
  def empty?(response)
34
- # response may be ["Not Found"], ["Move Permanently"], etc.
35
- if rails?
36
- (response.is_a?(Array) && response.size <= 1) ||
37
- !response.respond_to?(:body) ||
38
- !response_body(response).respond_to?(:empty?) ||
39
- response_body(response).empty?
40
- else
41
- body = response_body(response)
42
- body.nil? || body.empty?
43
- 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?
44
45
  end
45
46
 
46
47
  def append_to_html_body(response_body, content)
47
48
  body = response_body.dup
49
+ content = content.html_safe if content.respond_to?(:html_safe)
48
50
  if body.include?('</body>')
49
51
  position = body.rindex('</body>')
50
52
  body.insert(position, content)
@@ -54,7 +56,15 @@ module Bullet
54
56
  end
55
57
 
56
58
  def footer_note
57
- "<div #{footer_div_attributes}>" + footer_close_button + Bullet.footer_info.uniq.join('<br>') + '</div>'
59
+ "<details #{details_attributes}><summary #{summary_attributes}>Bullet Warnings</summary><div #{footer_content_attributes}>#{Bullet.footer_info.uniq.join('<br>')}#{footer_console_message}</div></details>"
60
+ end
61
+
62
+ def set_header(headers, header_name, header_array)
63
+ # Many proxy applications such as Nginx and AWS ELB limit
64
+ # the size a header to 8KB, so truncate the list of reports to
65
+ # be under that limit
66
+ header_array.pop while header_array.to_json.length > 8 * 1024
67
+ headers[header_name] = header_array.to_json
58
68
  end
59
69
 
60
70
  def file?(headers)
@@ -66,7 +76,7 @@ module Bullet
66
76
  end
67
77
 
68
78
  def html_request?(headers, response)
69
- headers['Content-Type'] && headers['Content-Type'].include?('text/html') && response_body(response).include?('<html')
79
+ headers['Content-Type']&.include?('text/html') && response_body(response).include?('<html')
70
80
  end
71
81
 
72
82
  def response_body(response)
@@ -79,18 +89,34 @@ module Bullet
79
89
 
80
90
  private
81
91
 
82
- def footer_div_attributes
83
- <<EOF
84
- 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);
85
- -moz-border-top-colors: none; -moz-border-right-colors: none; -moz-border-bottom-colors: none;
86
- -moz-border-left-colors: none; -moz-border-image: none; border-width: 2pt 2pt 0px 0px;
87
- padding: 3px 5px; border-radius: 0pt 10pt 0pt 0px; background: none repeat scroll 0% 0% rgba(200, 200, 200, 0.8);
88
- color: rgb(119, 119, 119); font-size: 16px; font-family: 'Arial', sans-serif; z-index:9999;"
89
- EOF
92
+ def details_attributes
93
+ <<~EOF
94
+ id="bullet-footer" data-is-bullet-footer
95
+ style="cursor: pointer; position: fixed; left: 0px; bottom: 0px; z-index: 9999; background: #fdf2f2; color: #9b1c1c; font-size: 12px; border-radius: 0px 8px 0px 0px; border: 1px solid #9b1c1c;"
96
+ EOF
97
+ end
98
+
99
+ def summary_attributes
100
+ <<~EOF
101
+ style="font-weight: 600; padding: 2px 8px"
102
+ EOF
103
+ end
104
+
105
+ def footer_content_attributes
106
+ <<~EOF
107
+ style="padding: 8px; border-top: 1px solid #9b1c1c;"
108
+ EOF
109
+ end
110
+
111
+ def footer_console_message
112
+ if Bullet.console_enabled?
113
+ "<br/><span style='font-style: italic;'>See 'Uniform Notifier' in JS Console for Stacktrace</span>"
114
+ end
90
115
  end
91
116
 
92
- def footer_close_button
93
- "<span onclick='this.parentNode.remove()' style='position:absolute; right: 10px; top: 0px; font-weight: bold; color: #333;'>&times;</span>"
117
+ # Make footer work for XHR requests by appending data to the footer
118
+ def xhr_script
119
+ "<script type='text/javascript'>#{File.read("#{__dir__}/bullet_xhr.js")}</script>"
94
120
  end
95
121
  end
96
122
  end
@@ -2,51 +2,60 @@
2
2
 
3
3
  module Bullet
4
4
  module StackTraceFilter
5
- VENDOR_PATH = '/vendor'.freeze
5
+ VENDOR_PATH = '/vendor'
6
+ IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
6
7
 
7
8
  def caller_in_project
8
- app_root = rails? ? Rails.root.to_s : Dir.pwd
9
- vendor_root = app_root + VENDOR_PATH
9
+ vendor_root = Bullet.app_root + VENDOR_PATH
10
10
  bundler_path = Bundler.bundle_path.to_s
11
- select_caller_locations do |caller_path|
12
- caller_path.include?(app_root) && !caller_path.include?(vendor_root) && !caller_path.include?(bundler_path) ||
13
- Bullet.stacktrace_includes.any? do |include_pattern|
14
- case include_pattern
15
- when String
16
- caller_path.include?(include_pattern)
17
- when Regexp
18
- caller_path =~ include_pattern
19
- end
20
- end
11
+ select_caller_locations do |location|
12
+ caller_path = location_as_path(location)
13
+ caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
14
+ !caller_path.include?(bundler_path) ||
15
+ Bullet.stacktrace_includes.any? { |include_pattern| pattern_matches?(location, include_pattern) }
21
16
  end
22
17
  end
23
18
 
24
19
  def excluded_stacktrace_path?
25
20
  Bullet.stacktrace_excludes.any? do |exclude_pattern|
26
- caller_in_project.any? do |location|
27
- caller_path = location.absolute_path.to_s
28
- case exclude_pattern
29
- when String
30
- caller_path.include?(exclude_pattern)
31
- when Regexp
32
- caller_path =~ exclude_pattern
33
- end
34
- end
21
+ caller_in_project.any? { |location| pattern_matches?(location, exclude_pattern) }
35
22
  end
36
23
  end
37
24
 
38
25
  private
39
26
 
40
- def select_caller_locations
41
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
42
- caller.select do |caller_path|
43
- yield caller_path
27
+ def pattern_matches?(location, pattern)
28
+ path = location_as_path(location)
29
+ case pattern
30
+ when Array
31
+ pattern_path = pattern.first
32
+ filter = pattern.last
33
+ return false unless pattern_matches?(location, pattern_path)
34
+
35
+ case filter
36
+ when Range
37
+ filter.include?(location.lineno)
38
+ when Integer
39
+ filter == location.lineno
40
+ when String
41
+ filter == location.base_label
44
42
  end
43
+ when String
44
+ path.include?(pattern)
45
+ when Regexp
46
+ path =~ pattern
47
+ end
48
+ end
49
+
50
+ def location_as_path(location)
51
+ IS_RUBY_19 ? location : location.absolute_path.to_s
52
+ end
53
+
54
+ def select_caller_locations
55
+ if IS_RUBY_19
56
+ caller.select { |caller_path| yield caller_path }
45
57
  else
46
- caller_locations.select do |location|
47
- caller_path = location.absolute_path.to_s
48
- yield caller_path
49
- end
58
+ caller_locations.select { |location| yield location }
50
59
  end
51
60
  end
52
61
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '5.7.5'.freeze
4
+ VERSION = '6.1.4'
5
5
  end
@@ -3,24 +3,24 @@
3
3
  module Bullet
4
4
  module Generators
5
5
  class InstallGenerator < ::Rails::Generators::Base
6
- desc <<-DESC
7
- Description:
8
- Enable bullet in development/test for your application.
6
+ desc <<~DESC
7
+ Description:
8
+ Enable bullet in development/test for your application.
9
9
  DESC
10
10
 
11
11
  def enable_in_development
12
12
  environment(nil, env: 'development') do
13
- <<-"FILE".strip
14
-
15
- config.after_initialize do
16
- Bullet.enable = true
17
- Bullet.alert = true
18
- Bullet.bullet_logger = true
19
- Bullet.console = true
20
- # Bullet.growl = true
21
- Bullet.rails_logger = true
22
- Bullet.add_footer = true
23
- end
13
+ <<~FILE
14
+ config.after_initialize do
15
+ Bullet.enable = true
16
+ Bullet.alert = true
17
+ Bullet.bullet_logger = true
18
+ Bullet.console = true
19
+ # Bullet.growl = true
20
+ Bullet.rails_logger = true
21
+ Bullet.add_footer = true
22
+ end
23
+
24
24
  FILE
25
25
  end
26
26
 
@@ -28,20 +28,20 @@ Description:
28
28
  end
29
29
 
30
30
  def enable_in_test
31
- if yes?('Would you like to enable bullet in test environment? (y/n)')
32
- environment(nil, env: 'test') do
33
- <<-"FILE".strip
34
-
35
- config.after_initialize do
36
- Bullet.enable = true
37
- Bullet.bullet_logger = true
38
- Bullet.raise = true # raise an error if n+1 query occurs
39
- end
40
- FILE
41
- end
31
+ return unless yes?('Would you like to enable bullet in test environment? (y/n)')
42
32
 
43
- say 'Enabled bullet in config/environments/test.rb'
33
+ environment(nil, env: 'test') do
34
+ <<~FILE
35
+ config.after_initialize do
36
+ Bullet.enable = true
37
+ Bullet.bullet_logger = true
38
+ Bullet.raise = true # raise an error if n+1 query occurs
39
+ end
40
+
41
+ FILE
44
42
  end
43
+
44
+ say 'Enabled bullet in config/environments/test.rb'
45
45
  end
46
46
  end
47
47
  end
data/perf/benchmark.rb CHANGED
@@ -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...'