bullet 4.0.0 → 4.13.1

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 (112) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -1
  4. data/.travis.yml +17 -6
  5. data/CHANGELOG.md +86 -0
  6. data/Gemfile +13 -12
  7. data/Gemfile.mongoid +14 -0
  8. data/Gemfile.mongoid-2.4 +19 -0
  9. data/Gemfile.mongoid-2.5 +19 -0
  10. data/Gemfile.mongoid-2.6 +19 -0
  11. data/Gemfile.mongoid-2.7 +19 -0
  12. data/Gemfile.mongoid-2.8 +19 -0
  13. data/Gemfile.mongoid-3.0 +19 -0
  14. data/Gemfile.mongoid-3.1 +19 -0
  15. data/Gemfile.mongoid-4.0 +19 -0
  16. data/Gemfile.rails-3.0 +19 -0
  17. data/Gemfile.rails-3.1 +19 -0
  18. data/Gemfile.rails-3.2 +19 -0
  19. data/Gemfile.rails-4.0 +19 -0
  20. data/Gemfile.rails-4.1 +19 -0
  21. data/{Hacking.textile → Hacking.md} +10 -5
  22. data/README.md +432 -0
  23. data/Rakefile +13 -6
  24. data/bullet.gemspec +6 -3
  25. data/lib/bullet/active_record3.rb +49 -11
  26. data/lib/bullet/active_record3x.rb +134 -0
  27. data/lib/bullet/{active_record31.rb → active_record4.rb} +45 -15
  28. data/lib/bullet/active_record41.rb +128 -0
  29. data/lib/bullet/dependency.rb +81 -0
  30. data/lib/bullet/detector/association.rb +24 -68
  31. data/lib/bullet/detector/base.rb +0 -6
  32. data/lib/bullet/detector/counter_cache.rb +59 -0
  33. data/lib/bullet/detector/n_plus_one_query.rb +56 -15
  34. data/lib/bullet/detector/unused_eager_loading.rb +84 -0
  35. data/lib/bullet/detector.rb +2 -2
  36. data/lib/bullet/ext/object.rb +10 -2
  37. data/lib/bullet/{mongoid.rb → mongoid2x.rb} +18 -18
  38. data/lib/bullet/mongoid3x.rb +56 -0
  39. data/lib/bullet/mongoid4x.rb +56 -0
  40. data/lib/bullet/notification/base.rb +22 -4
  41. data/lib/bullet/notification/n_plus_one_query.rb +1 -1
  42. data/lib/bullet/notification.rb +2 -0
  43. data/lib/bullet/rack.rb +51 -17
  44. data/lib/bullet/registry/base.rb +1 -1
  45. data/lib/bullet/registry/object.rb +4 -4
  46. data/lib/bullet/version.rb +1 -2
  47. data/lib/bullet.rb +119 -39
  48. data/perf/benchmark.rb +1 -4
  49. data/spec/bullet/detector/association_spec.rb +2 -70
  50. data/spec/bullet/detector/base_spec.rb +0 -6
  51. data/spec/bullet/detector/counter_cache_spec.rb +56 -0
  52. data/spec/bullet/detector/n_plus_one_query_spec.rb +78 -34
  53. data/spec/bullet/detector/unused_eager_loading_spec.rb +88 -0
  54. data/spec/bullet/ext/object_spec.rb +18 -7
  55. data/spec/bullet/ext/string_spec.rb +2 -2
  56. data/spec/bullet/notification/base_spec.rb +68 -15
  57. data/spec/bullet/notification/counter_cache_spec.rb +2 -2
  58. data/spec/bullet/notification/n_plus_one_query_spec.rb +4 -3
  59. data/spec/bullet/notification/unused_eager_loading_spec.rb +2 -2
  60. data/spec/bullet/notification_collector_spec.rb +4 -4
  61. data/spec/bullet/rack_spec.rb +40 -39
  62. data/spec/bullet/registry/association_spec.rb +3 -3
  63. data/spec/bullet/registry/base_spec.rb +6 -6
  64. data/spec/bullet/registry/object_spec.rb +4 -5
  65. data/spec/bullet_spec.rb +41 -0
  66. data/spec/integration/active_record3/association_spec.rb +662 -0
  67. data/spec/integration/active_record4/association_spec.rb +660 -0
  68. data/spec/integration/counter_cache_spec.rb +63 -0
  69. data/spec/integration/mongoid/association_spec.rb +190 -208
  70. data/spec/models/category.rb +1 -1
  71. data/spec/models/client.rb +1 -1
  72. data/spec/models/comment.rb +2 -2
  73. data/spec/models/document.rb +2 -2
  74. data/spec/models/firm.rb +1 -1
  75. data/spec/models/mongoid/address.rb +2 -0
  76. data/spec/models/mongoid/category.rb +2 -0
  77. data/spec/models/mongoid/comment.rb +2 -0
  78. data/spec/models/mongoid/company.rb +2 -0
  79. data/spec/models/mongoid/entry.rb +2 -0
  80. data/spec/models/mongoid/post.rb +4 -0
  81. data/spec/models/mongoid/user.rb +5 -0
  82. data/spec/models/newspaper.rb +1 -1
  83. data/spec/models/pet.rb +1 -1
  84. data/spec/models/post.rb +6 -13
  85. data/spec/spec_helper.rb +60 -25
  86. data/spec/support/bullet_ext.rb +1 -1
  87. data/spec/support/mongo_seed.rb +26 -2
  88. data/spec/support/sqlite_seed.rb +0 -6
  89. data/test.sh +15 -0
  90. metadata +77 -54
  91. data/.rvmrc +0 -2
  92. data/.rvmrc.example +0 -2
  93. data/Gemfile.lock +0 -132
  94. data/Gemfile.rails-2.3.14 +0 -16
  95. data/Gemfile.rails-2.3.14.lock +0 -65
  96. data/Gemfile.rails-3.0.12 +0 -17
  97. data/Gemfile.rails-3.0.12.lock +0 -116
  98. data/Gemfile.rails-3.1.4 +0 -18
  99. data/Gemfile.rails-3.1.4.lock +0 -134
  100. data/README.textile +0 -395
  101. data/README_for_rails2.textile +0 -400
  102. data/lib/bullet/action_controller2.rb +0 -49
  103. data/lib/bullet/active_record2.rb +0 -121
  104. data/lib/bullet/detector/counter.rb +0 -48
  105. data/lib/bullet/detector/unused_eager_association.rb +0 -42
  106. data/spec/bullet/detector/counter_spec.rb +0 -64
  107. data/spec/bullet/detector/unused_eager_association_spec.rb +0 -62
  108. data/spec/integration/association_spec.rb +0 -593
  109. data/spec/integration/counter_spec.rb +0 -39
  110. data/spec/integration/rails2/association_spec.rb +0 -593
  111. data/spec/integration/rails2/counter_spec.rb +0 -39
  112. data/test.result +0 -2293
@@ -1,48 +1,89 @@
1
1
  module Bullet
2
2
  module Detector
3
3
  class NPlusOneQuery < Association
4
+ extend Dependency
5
+
4
6
  class <<self
5
7
  # executed when object.assocations is called.
6
8
  # first, it keeps this method call for object.association.
7
9
  # then, it checks if this associations call is unpreload.
8
10
  # if it is, keeps this unpreload associations and caller.
9
11
  def call_association(object, associations)
10
- @@checked = true
12
+ return unless Bullet.start?
13
+ return unless object.primary_key_value
14
+ return if inversed_objects.include?(object.bullet_key, associations)
11
15
  add_call_object_associations(object, associations)
12
16
 
13
- if conditions_met?(object.bullet_ar_key, associations)
17
+ Bullet.debug("Detector::NPlusOneQuery#call_association", "object: #{object.bullet_key}, associations: #{associations}")
18
+ if conditions_met?(object.bullet_key, associations)
19
+ Bullet.debug("detect n + 1 query", "object: #{object.bullet_key}, associations: #{associations}")
14
20
  create_notification caller_in_project, object.class.to_s, associations
15
21
  end
16
22
  end
17
23
 
24
+ def add_possible_objects(object_or_objects)
25
+ return unless Bullet.start?
26
+ return unless Bullet.n_plus_one_query_enable?
27
+ objects = Array(object_or_objects)
28
+ return if objects.map(&:primary_key_value).compact.empty?
29
+
30
+ Bullet.debug("Detector::NPlusOneQuery#add_possible_objects", "objects: #{objects.map(&:bullet_key).join(', ')}")
31
+ objects.each { |object| possible_objects.add object.bullet_key }
32
+ end
33
+
34
+ def add_impossible_object(object)
35
+ return unless Bullet.start?
36
+ return unless Bullet.n_plus_one_query_enable?
37
+ return unless object.primary_key_value
38
+
39
+ Bullet.debug("Detector::NPlusOneQuery#add_impossible_object", "object: #{object.bullet_key}")
40
+ impossible_objects.add object.bullet_key
41
+ end
42
+
43
+ def add_inversed_object(object, association)
44
+ return unless Bullet.start?
45
+ return unless Bullet.n_plus_one_query_enable?
46
+ return unless object.primary_key_value
47
+
48
+ Bullet.debug("Detector::NPlusOneQuery#add_inversed_object", "object: #{object.bullet_key}, association: #{association}")
49
+ inversed_objects.add object.bullet_key, association
50
+ end
51
+
18
52
  private
19
53
  def create_notification(callers, klazz, associations)
20
- notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, associations)
21
- Bullet.notification_collector.add(notice)
54
+ notify_associations = Array(associations) - Bullet.get_whitelist_associations(:n_plus_one_query, klazz)
55
+
56
+ if notify_associations.present?
57
+ notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, notify_associations)
58
+ Bullet.notification_collector.add(notice)
59
+ end
22
60
  end
23
61
 
24
62
  # decide whether the object.associations is unpreloaded or not.
25
- def conditions_met?(bullet_ar_key, associations)
26
- possible?(bullet_ar_key) && !impossible?(bullet_ar_key) && !association?(bullet_ar_key, associations)
63
+ def conditions_met?(bullet_key, associations)
64
+ possible?(bullet_key) && !impossible?(bullet_key) && !association?(bullet_key, associations)
27
65
  end
28
66
 
29
67
  def caller_in_project
30
- rails_root = Rails.root.to_s
31
- vendor_root = rails_root + "/vendor"
32
- caller.select { |c| c.include?(rails_root) && !c.include?(vendor_root) }
68
+ app_root = rails? ? Rails.root.to_s : Dir.pwd
69
+ vendor_root = app_root + "/vendor"
70
+ caller.select do |c|
71
+ c.include?(app_root) && !c.include?(vendor_root) ||
72
+ Bullet.stacktrace_includes.any? { |include| c.include?(include) }
73
+ end
33
74
  end
34
75
 
35
- def possible?(bullet_ar_key)
36
- possible_objects.include? bullet_ar_key
76
+ def possible?(bullet_key)
77
+ possible_objects.include? bullet_key
37
78
  end
38
79
 
39
- def impossible?(bullet_ar_key)
40
- impossible_objects.include? bullet_ar_key
80
+ def impossible?(bullet_key)
81
+ impossible_objects.include? bullet_key
41
82
  end
42
83
 
43
84
  # check if object => associations already exists in object_associations.
44
- def association?(bullet_ar_key, associations)
45
- value = object_associations[bullet_ar_key]
85
+ def association?(bullet_key, associations)
86
+ value = object_associations[bullet_key]
46
87
  if value
47
88
  value.each do |v|
48
89
  result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
@@ -0,0 +1,84 @@
1
+ module Bullet
2
+ module Detector
3
+ class UnusedEagerLoading < Association
4
+ class <<self
5
+ # check if there are unused preload associations.
6
+ # get related_objects from eager_loadings associated with object and associations
7
+ # get call_object_association from associations of call_object_associations whose object is in related_objects
8
+ # if association not in call_object_association, then the object => association - call_object_association is ununsed preload assocations
9
+ def check_unused_preload_associations
10
+ return unless Bullet.start?
11
+ return unless Bullet.unused_eager_loading_enable?
12
+
13
+ object_associations.each do |bullet_key, associations|
14
+ object_association_diff = diff_object_associations bullet_key, associations
15
+ next if object_association_diff.empty?
16
+
17
+ Bullet.debug("detect unused preload", "object: #{bullet_key}, associations: #{object_association_diff}")
18
+ create_notification bullet_key.bullet_class_name, object_association_diff
19
+ end
20
+ end
21
+
22
+ def add_eager_loadings(objects, associations)
23
+ return unless Bullet.start?
24
+ return unless Bullet.unused_eager_loading_enable?
25
+ return if objects.map(&:primary_key_value).compact.empty?
26
+
27
+ Bullet.debug("Detector::UnusedEagerLoading#add_eager_loadings", "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}")
28
+ bullet_keys = objects.map(&:bullet_key)
29
+
30
+ to_add = nil
31
+ to_merge, to_delete = [], []
32
+ eager_loadings.each do |k, v|
33
+ key_objects_overlap = k & bullet_keys
34
+
35
+ next if key_objects_overlap.empty?
36
+
37
+ if key_objects_overlap == k
38
+ to_add = [k, associations]
39
+ break
40
+ else
41
+ to_merge << [key_objects_overlap, ( eager_loadings[k].dup << associations )]
42
+
43
+ keys_without_objects = k - bullet_keys
44
+ to_merge << [keys_without_objects, eager_loadings[k]]
45
+ to_delete << k
46
+ bullet_keys = bullet_keys - k
47
+ end
48
+ end
49
+
50
+ eager_loadings.add *to_add if to_add
51
+ to_merge.each { |k,val| eager_loadings.merge k, val }
52
+ to_delete.each { |k| eager_loadings.delete k }
53
+
54
+ eager_loadings.add bullet_keys, associations unless bullet_keys.empty?
55
+ end
56
+
57
+ private
58
+ def create_notification(klazz, associations)
59
+ notify_associations = Array(associations) - Bullet.get_whitelist_associations(:unused_eager_loading, klazz)
60
+
61
+ if notify_associations.present?
62
+ notice = Bullet::Notification::UnusedEagerLoading.new(klazz, notify_associations)
63
+ Bullet.notification_collector.add(notice)
64
+ end
65
+ end
66
+
67
+ def call_associations(bullet_key, associations)
68
+ all = Set.new
69
+ eager_loadings.similarly_associated(bullet_key, associations).each do |related_bullet_key|
70
+ coa = call_object_associations[related_bullet_key]
71
+ next if coa.nil?
72
+ all.merge coa
73
+ end
74
+ all.to_a
75
+ end
76
+
77
+ def diff_object_associations(bullet_key, associations)
78
+ potential_associations = associations - call_associations(bullet_key, associations)
79
+ potential_associations.reject { |a| a.is_a?(Hash) }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -3,7 +3,7 @@ module Bullet
3
3
  autoload :Base, 'bullet/detector/base'
4
4
  autoload :Association, 'bullet/detector/association'
5
5
  autoload :NPlusOneQuery, 'bullet/detector/n_plus_one_query'
6
- autoload :UnusedEagerAssociation, 'bullet/detector/unused_eager_association'
7
- autoload :Counter, 'bullet/detector/counter'
6
+ autoload :UnusedEagerLoading, 'bullet/detector/unused_eager_loading'
7
+ autoload :CounterCache, 'bullet/detector/counter_cache'
8
8
  end
9
9
  end
@@ -1,5 +1,13 @@
1
1
  class Object
2
- def bullet_ar_key
3
- "#{self.class}:#{self.id}"
2
+ def bullet_key
3
+ [self.class, self.primary_key_value].join(':')
4
+ end
5
+
6
+ def primary_key_value
7
+ if self.class.respond_to?(:primary_key) && self.class.primary_key
8
+ self.send self.class.primary_key
9
+ else
10
+ self.id
11
+ end
4
12
  end
5
13
  end
@@ -2,55 +2,55 @@ module Bullet
2
2
  module Mongoid
3
3
  def self.enable
4
4
  require 'mongoid'
5
+
5
6
  ::Mongoid::Contexts::Mongo.class_eval do
6
7
  alias_method :origin_first, :first
7
8
  alias_method :origin_last, :last
8
9
  alias_method :origin_iterate, :iterate
10
+ alias_method :origin_eager_load, :eager_load
9
11
 
10
12
  def first
11
13
  result = origin_first
12
- Bullet::Detector::Association.add_impossible_object(result)
14
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
13
15
  result
14
16
  end
15
17
 
16
18
  def last
17
19
  result = origin_last
18
- Bullet::Detector::Association.add_impossible_object(result)
20
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
19
21
  result
20
22
  end
21
23
 
22
24
  def iterate(&block)
23
25
  records = execute.to_a
24
26
  if records.size > 1
25
- Bullet::Detector::Association.add_possible_objects(records)
27
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
26
28
  elsif records.size == 1
27
- Bullet::Detector::Association.add_impossible_object(records.first)
29
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
28
30
  end
29
31
  origin_iterate(&block)
30
32
  end
31
- end
32
-
33
- ::Mongoid::Relations::Accessors.class_eval do
34
- alias_method :origin_set_relation, :set_relation
35
-
36
- def set_relation(name, relation)
37
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
38
- origin_set_relation(name, relation)
39
- end
40
- end
41
-
42
- ::Mongoid::Contexts::Mongo.class_eval do
43
- alias_method :origin_eager_load, :eager_load
44
33
 
45
34
  def eager_load(docs)
46
35
  associations = criteria.inclusions.map(&:name)
47
36
  docs.each do |doc|
48
37
  Bullet::Detector::Association.add_object_associations(doc, associations)
49
38
  end
50
- Bullet::Detector::Association.add_eager_loadings(docs, associations)
39
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
51
40
  origin_eager_load(docs)
52
41
  end
53
42
  end
43
+
44
+ ::Mongoid::Relations::Accessors.class_eval do
45
+ alias_method :origin_set_relation, :set_relation
46
+
47
+ def set_relation(name, relation)
48
+ if relation && relation.metadata.macro !~ /embed/
49
+ Bullet::Detector::NPlusOneQuery.call_association(self, name)
50
+ end
51
+ origin_set_relation(name, relation)
52
+ end
53
+ end
54
54
  end
55
55
  end
56
56
  end
@@ -0,0 +1,56 @@
1
+ module Bullet
2
+ module Mongoid
3
+ def self.enable
4
+ require 'mongoid'
5
+ ::Mongoid::Contextual::Mongo.class_eval do
6
+ alias_method :origin_first, :first
7
+ alias_method :origin_last, :last
8
+ alias_method :origin_each, :each
9
+ alias_method :origin_eager_load, :eager_load
10
+
11
+ def first
12
+ result = origin_first
13
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
14
+ result
15
+ end
16
+
17
+ def last
18
+ result = origin_last
19
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
20
+ result
21
+ end
22
+
23
+ def each(&block)
24
+ records = query.map{ |doc| ::Mongoid::Factory.from_db(klass, doc) }
25
+ if records.length > 1
26
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
27
+ elsif records.size == 1
28
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
29
+ end
30
+ origin_each(&block)
31
+ end
32
+
33
+ def eager_load(docs)
34
+ associations = criteria.inclusions.map(&:name)
35
+ docs.each do |doc|
36
+ Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
37
+ end
38
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
39
+ origin_eager_load(docs)
40
+ end
41
+ end
42
+
43
+ ::Mongoid::Relations::Accessors.class_eval do
44
+ alias_method :origin_get_relation, :get_relation
45
+
46
+ def get_relation(name, metadata, reload = false)
47
+ result = origin_get_relation(name, metadata, reload)
48
+ if metadata.macro !~ /embed/
49
+ Bullet::Detector::NPlusOneQuery.call_association(self, name)
50
+ end
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ module Bullet
2
+ module Mongoid
3
+ def self.enable
4
+ require 'mongoid'
5
+ ::Mongoid::Contextual::Mongo.class_eval do
6
+ alias_method :origin_first, :first
7
+ alias_method :origin_last, :last
8
+ alias_method :origin_each, :each
9
+ alias_method :origin_eager_load, :eager_load
10
+
11
+ def first
12
+ result = origin_first
13
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
14
+ result
15
+ end
16
+
17
+ def last
18
+ result = origin_last
19
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
20
+ result
21
+ end
22
+
23
+ def each(&block)
24
+ records = query.map{ |doc| ::Mongoid::Factory.from_db(klass, doc) }
25
+ if records.length > 1
26
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
27
+ elsif records.size == 1
28
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
29
+ end
30
+ origin_each(&block)
31
+ end
32
+
33
+ def eager_load(docs)
34
+ associations = criteria.inclusions.map(&:name)
35
+ docs.each do |doc|
36
+ Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
37
+ end
38
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
39
+ origin_eager_load(docs)
40
+ end
41
+ end
42
+
43
+ ::Mongoid::Relations::Accessors.class_eval do
44
+ alias_method :origin_get_relation, :get_relation
45
+
46
+ def get_relation(name, metadata, object, reload = false)
47
+ result = origin_get_relation(name, metadata, object, reload)
48
+ if metadata.macro !~ /embed/
49
+ Bullet::Detector::NPlusOneQuery.call_association(self, name)
50
+ end
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -19,7 +19,12 @@ module Bullet
19
19
  end
20
20
 
21
21
  def whoami
22
- "user: " << `whoami`.chomp
22
+ @user ||= ENV['USER'].presence || (`whoami`.chomp rescue "")
23
+ if @user.present?
24
+ "user: #{@user}"
25
+ else
26
+ ""
27
+ end
23
28
  end
24
29
 
25
30
  def body_with_caller
@@ -31,15 +36,28 @@ module Bullet
31
36
  end
32
37
 
33
38
  def full_notice
34
- [whoami, url, title, body_with_caller].compact.join("\n")
39
+ [whoami.presence, url, title, body_with_caller].compact.join("\n")
35
40
  end
36
41
 
37
42
  def notify_inline
38
- self.notifier.inline_notify(self.full_notice)
43
+ self.notifier.inline_notify(notification_data)
39
44
  end
40
45
 
41
46
  def notify_out_of_channel
42
- self.notifier.out_of_channel_notify(self.full_notice)
47
+ self.notifier.out_of_channel_notify(notification_data)
48
+ end
49
+
50
+ def short_notice
51
+ [whoami.presence, url, title, body].compact.join("\n")
52
+ end
53
+
54
+ def notification_data
55
+ {
56
+ :user => whoami,
57
+ :url => url,
58
+ :title => title,
59
+ :body => body_with_caller,
60
+ }
43
61
  end
44
62
 
45
63
  def eql?(other)
@@ -21,7 +21,7 @@ module Bullet
21
21
 
22
22
  protected
23
23
  def call_stack_messages
24
- @callers.unshift('N+1 Query method call stack').join( "\n " )
24
+ (['N+1 Query method call stack'] + @callers).join( "\n " )
25
25
  end
26
26
  end
27
27
  end
@@ -4,5 +4,7 @@ module Bullet
4
4
  autoload :UnusedEagerLoading, 'bullet/notification/unused_eager_loading'
5
5
  autoload :NPlusOneQuery, 'bullet/notification/n_plus_one_query'
6
6
  autoload :CounterCache, 'bullet/notification/counter_cache'
7
+
8
+ class UnoptimizedQueryError < StandardError; end
7
9
  end
8
10
  end
data/lib/bullet/rack.rb CHANGED
@@ -1,47 +1,81 @@
1
1
  module Bullet
2
2
  class Rack
3
+ include Dependency
4
+
3
5
  def initialize(app)
4
6
  @app = app
5
7
  end
6
8
 
7
9
  def call(env)
8
10
  return @app.call(env) unless Bullet.enable?
9
-
10
11
  Bullet.start_request
11
12
  status, headers, response = @app.call(env)
12
- return [status, headers, response] if empty?(response)
13
+ return [status, headers, response] if file?(headers) || sse?(response) || empty?(response)
13
14
 
14
15
  response_body = nil
15
16
  if Bullet.notification?
16
- if status == 200 && !response.body.frozen? && html_request?(headers, response)
17
- response_body = response.body << Bullet.gather_inline_notifications
18
- headers['Content-Length'] = response_body.length.to_s
17
+ if status == 200 && !response_body(response).frozen? && html_request?(headers, response)
18
+ response_body = response_body(response) << Bullet.gather_inline_notifications
19
+ add_footer_note(response_body) if Bullet.add_footer
20
+ headers['Content-Length'] = response_body.bytesize.to_s
19
21
  end
22
+ end
23
+ if Bullet.enable? && Bullet.notification?
20
24
  Bullet.perform_out_of_channel_notifications(env)
21
25
  end
22
- Bullet.end_request
23
- no_browser_cache(headers) if Bullet.disable_browser_cache
24
26
  [status, headers, response_body ? [response_body] : response]
27
+ ensure
28
+ Bullet.end_request
25
29
  end
26
30
 
27
31
  # fix issue if response's body is a Proc
28
32
  def empty?(response)
29
33
  # response may be ["Not Found"], ["Move Permanently"], etc.
30
- (response.is_a?(Array) && response.size <= 1) ||
31
- !response.body.is_a?(String) || response.body.empty?
34
+ if rails?
35
+ (response.is_a?(Array) && response.size <= 1) ||
36
+ !response.respond_to?(:body) ||
37
+ !response_body(response).respond_to?(:empty?) ||
38
+ response_body(response).empty?
39
+ else
40
+ body = response_body(response)
41
+ body.nil? || body.empty?
42
+ end
43
+ end
44
+
45
+ def add_footer_note(response_body)
46
+ response_body << "<div #{footer_div_style}>" + Bullet.footer_info.uniq.join("<br>") + "</div>"
47
+ end
48
+
49
+ def file?(headers)
50
+ headers["Content-Transfer-Encoding"] == "binary"
51
+ end
52
+
53
+ def sse?(response)
54
+ response.respond_to?(:stream) && response.stream.is_a?(ActionController::Live::Buffer)
32
55
  end
33
56
 
34
57
  def html_request?(headers, response)
35
- headers['Content-Type'] &&
36
- headers['Content-Type'].include?('text/html') &&
37
- response.body.include?("<html>") &&
38
- response.body.include?("</html>")
58
+ headers['Content-Type'] && headers['Content-Type'].include?('text/html') && response_body(response).include?("<html")
39
59
  end
40
60
 
41
- def no_browser_cache(headers)
42
- headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
43
- headers["Pragma"] = "no-cache"
44
- headers["Expires"] = "Wed, 09 Sep 2009 09:09:09 GMT"
61
+ def response_body(response)
62
+ if rails?
63
+ Array === response.body ? response.body.first : response.body
64
+ else
65
+ response.first
66
+ end
67
+ end
68
+
69
+ private
70
+ def footer_div_style
71
+ <<EOF
72
+ style="position: fixed; bottom: 0pt; left: 0pt; cursor: pointer; border-style: solid; border-color: rgb(153, 153, 153);
73
+ -moz-border-top-colors: none; -moz-border-right-colors: none; -moz-border-bottom-colors: none;
74
+ -moz-border-left-colors: none; -moz-border-image: none; border-width: 2pt 2pt 0px 0px;
75
+ padding: 5px; border-radius: 0pt 10pt 0pt 0px; background: none repeat scroll 0% 0% rgba(200, 200, 200, 0.8);
76
+ color: rgb(119, 119, 119); font-size: 18px; font-family: 'Arial', sans-serif; z-index:9999;"
77
+ EOF
45
78
  end
46
79
  end
47
80
  end
81
+
@@ -33,7 +33,7 @@ module Bullet
33
33
  end
34
34
 
35
35
  def include?(key, value)
36
- @registry[key] && @registry[key].include?(value)
36
+ !@registry[key].nil? && @registry[key].include?(value)
37
37
  end
38
38
  end
39
39
  end
@@ -1,12 +1,12 @@
1
1
  module Bullet
2
2
  module Registry
3
3
  class Object < Base
4
- def add(bullet_ar_key)
5
- super(bullet_ar_key.bullet_class_name, bullet_ar_key)
4
+ def add(bullet_key)
5
+ super(bullet_key.bullet_class_name, bullet_key)
6
6
  end
7
7
 
8
- def include?(bullet_ar_key)
9
- super(bullet_ar_key.bullet_class_name, bullet_ar_key)
8
+ def include?(bullet_key)
9
+ super(bullet_key.bullet_class_name, bullet_key)
10
10
  end
11
11
  end
12
12
  end
@@ -1,5 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Bullet
3
- VERSION = "4.0.0"
3
+ VERSION = "4.13.1"
4
4
  end
5
-