bullet 5.7.5 → 6.1.4

Sign up to get free protection for your applications and to get access to all the features.
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...'