bullet 8.0.2 → 8.0.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69ace15062b915463985d5257ca7b054b482ca842f05f832dc45c1134bc7c8cd
4
- data.tar.gz: a43533c8d894791edffcb4cbd9ad4ceba1dd238a8b736d23e3444ca7f766c57c
3
+ metadata.gz: 7ed7921469ed264816680bf580bd37cc8e9033287d90a86c65bb4d486fb789f3
4
+ data.tar.gz: a0c0ca44bc16b3673b3c7ea14f03a12dee3c73fc72084ee959ef19839bbd4f68
5
5
  SHA512:
6
- metadata.gz: 262e12b9be4eeae23494a06eb71d6c6823674ecfa8ec9a0c5a5eaff7d7bf268a52cb35ee8198cf0712312745735bc4ad2a6474e3a6a5a6fa8820ed896754795a
7
- data.tar.gz: b16503b483682cb94121442f8ee1d74145cb835031e62a864765629b0dbffd6b360136ee3b09f1b14d1e35a25794c06ace1579415829ad0dfdcff1ae2be0b3bf
6
+ metadata.gz: 354d9bd9b4d126ef45edf063184e7c9d612aa243caa7c2044a67f665f625a40e7c23ca61efcfe70193823689499bb6bb868022647770c8ed9dba60f9dff3e26f
7
+ data.tar.gz: f19a948e75f612a65143fb069559c4691d12a73bb8cd0b45699b4b80e372565d39ed0c193eabcdc4d8e289cefaa3f0845f380d2509b4d6f69a32d57392339840
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  ## Next Release
2
2
 
3
+ ## 8.0.7 (05/15/2025)
4
+
5
+ * Try to insert `Bullet::Rack` properly
6
+
7
+ ## 8.0.6 (05/07/2025)
8
+
9
+ * Add CSP nonce for footer styles as well
10
+ * Add support for OpenTelemetry reporting
11
+
12
+ ## 8.0.5 (04/21/2025)
13
+
14
+ * Properly insert ContentSecurityPolicy middleware
15
+ * Properly parse query string
16
+
17
+ ## 8.0.4 (04/18/2025)
18
+
19
+ * Insert bullet middleware before `ContentSecurityPolicy`
20
+ * Support url query `skip_html_injection=true`
21
+ * Mark object as impossible after updating inversed
22
+
23
+ ## 8.0.3 (04/04/2025)
24
+
25
+ * Update non persisted `inversed_objects`
26
+
3
27
  ## 8.0.2 (04/02/2025)
4
28
 
5
29
  * Do not cache `bullet_key` if object is not persisted
data/README.md CHANGED
@@ -74,6 +74,7 @@ config.after_initialize do
74
74
  Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ]
75
75
  Bullet.stacktrace_excludes = [ 'their_gem', 'their_middleware', ['my_file.rb', 'my_method'], ['my_file.rb', 16..20] ]
76
76
  Bullet.slack = { webhook_url: 'http://some.slack.url', channel: '#default', username: 'notifier' }
77
+ Bullet.opentelemetry = true
77
78
  end
78
79
  ```
79
80
 
@@ -100,6 +101,7 @@ The code above will enable all of the Bullet notification systems:
100
101
  Each item can be a string (match substring), a regex, or an array where the first item is a path to match, and the second
101
102
  item is a line number, a Range of line numbers, or a (bare) method name, to exclude only particular lines in a file.
102
103
  * `Bullet.slack`: add notifications to slack
104
+ * `Bullet.opentelemetry`: add notifications to OpenTelemetry
103
105
  * `Bullet.raise`: raise errors, useful for making your specs fail unless they have optimized queries
104
106
  * `Bullet.always_append_html_body`: always append the html body even if no notifications are present. Note: `console` or `add_footer` must also be true. Useful for Single Page Applications where the initial page load might not have any notifications present.
105
107
  * `Bullet.skip_user_in_notification`: exclude the OS user (`whoami`) from notifications.
@@ -192,6 +194,11 @@ see [https://github.com/flyerhzm/uniform_notifier](https://github.com/flyerhzm/u
192
194
 
193
195
  Growl support is dropped from uniform_notifier 1.16.0, if you still want it, please use uniform_notifier 1.15.0.
194
196
 
197
+ ## URL query control
198
+
199
+ You can add the URL query parameter `skip_html_injection` to make the current HTML request behave as if `Bullet.skip_html_injection` is enabled,
200
+ e.g. `http://localhost:3000/posts?skip_html_injection=true`
201
+
195
202
  ## Important
196
203
 
197
204
  If you find Bullet does not work for you, *please disable your browser's cache*.
@@ -49,7 +49,10 @@ module Bullet
49
49
 
50
50
  ::ActiveRecord::Persistence.class_eval do
51
51
  def _create_record_with_bullet(*args)
52
- _create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.add_impossible_object(self) }
52
+ _create_record_without_bullet(*args).tap do
53
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
54
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
55
+ end
53
56
  end
54
57
  alias_method_chain :_create_record, :bullet
55
58
  end
@@ -52,7 +52,10 @@ module Bullet
52
52
 
53
53
  ::ActiveRecord::Persistence.class_eval do
54
54
  def _create_record_with_bullet(*args)
55
- _create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.add_impossible_object(self) }
55
+ _create_record_without_bullet(*args).tap do
56
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
57
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
58
+ end
56
59
  end
57
60
  alias_method_chain :_create_record, :bullet
58
61
  end
@@ -45,7 +45,10 @@ module Bullet
45
45
 
46
46
  ::ActiveRecord::Persistence.class_eval do
47
47
  def _create_record_with_bullet(*args)
48
- _create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.add_impossible_object(self) }
48
+ _create_record_without_bullet(*args).tap do
49
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
50
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
51
+ end
49
52
  end
50
53
  alias_method_chain :_create_record, :bullet
51
54
  end
@@ -4,6 +4,7 @@ module Bullet
4
4
  module SaveWithBulletSupport
5
5
  def _create_record(*)
6
6
  super do
7
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
7
8
  Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
@@ -4,6 +4,7 @@ module Bullet
4
4
  module SaveWithBulletSupport
5
5
  def _create_record(*)
6
6
  super do
7
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
7
8
  Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
@@ -4,6 +4,7 @@ module Bullet
4
4
  module SaveWithBulletSupport
5
5
  def _create_record(*)
6
6
  super do
7
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
7
8
  Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
@@ -4,6 +4,7 @@ module Bullet
4
4
  module SaveWithBulletSupport
5
5
  def _create_record(*)
6
6
  super do
7
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
7
8
  Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
@@ -4,6 +4,7 @@ module Bullet
4
4
  module SaveWithBulletSupport
5
5
  def _create_record(*)
6
6
  super do
7
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
7
8
  Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
@@ -4,6 +4,7 @@ module Bullet
4
4
  module SaveWithBulletSupport
5
5
  def _create_record(*)
6
6
  super do
7
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
7
8
  Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
@@ -4,6 +4,7 @@ module Bullet
4
4
  module SaveWithBulletSupport
5
5
  def _create_record(*)
6
6
  super do
7
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
7
8
  Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
@@ -4,6 +4,7 @@ module Bullet
4
4
  module SaveWithBulletSupport
5
5
  def _create_record(*)
6
6
  super do
7
+ Bullet::Detector::NPlusOneQuery.update_inversed_object(self)
7
8
  Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
9
  yield(self) if block_given?
9
10
  end
@@ -67,13 +67,23 @@ module Bullet
67
67
  def add_inversed_object(object, association)
68
68
  return unless Bullet.start?
69
69
  return unless Bullet.n_plus_one_query_enable?
70
- return unless object.bullet_primary_key_value
71
70
 
71
+ object_key = object.bullet_primary_key_value ? object.bullet_key : object.object_id
72
72
  Bullet.debug(
73
73
  'Detector::NPlusOneQuery#add_inversed_object',
74
- "object: #{object.bullet_key}, association: #{association}"
74
+ "object: #{object_key}, association: #{association}"
75
75
  )
76
- inversed_objects.add object.bullet_key, association
76
+ inversed_objects.add object_key, association
77
+ end
78
+
79
+ def update_inversed_object(object)
80
+ if inversed_objects&.key?(object.object_id)
81
+ Bullet.debug(
82
+ 'Detector::NPlusOneQuery#update_inversed_object',
83
+ "object from #{object.object_id} to #{object.bullet_key}"
84
+ )
85
+ inversed_objects.add(object.bullet_key, inversed_objects[object.object_id].to_a)
86
+ end
77
87
  end
78
88
 
79
89
  # decide whether the object.associations is unpreloaded or not.
data/lib/bullet/rack.rb CHANGED
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rack/request'
4
+ require 'json'
5
+ require 'cgi'
6
+
3
7
  module Bullet
4
8
  class Rack
5
9
  include Dependency
6
10
 
7
- NONCE_MATCHER = /script-src .*'nonce-(?<nonce>[A-Za-z0-9+\/]+={0,2})'/
11
+ NONCE_MATCHER = /(script|style)-src .*'nonce-(?<nonce>[A-Za-z0-9+\/]+={0,2})'/
8
12
 
9
13
  def initialize(app)
10
14
  @app = app
@@ -19,12 +23,13 @@ module Bullet
19
23
  response_body = nil
20
24
 
21
25
  if Bullet.notification? || Bullet.always_append_html_body
22
- if Bullet.inject_into_page? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
26
+ request = ::Rack::Request.new(env)
27
+ if Bullet.inject_into_page? && !skip_html_injection?(request) && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
23
28
  if html_request?(headers, response)
24
29
  response_body = response_body(response)
25
30
 
26
31
  with_security_policy_nonce(headers) do |nonce|
27
- response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
32
+ response_body = append_to_html_body(response_body, footer_note(nonce)) if Bullet.add_footer
28
33
  response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
29
34
  if Bullet.add_footer && !Bullet.skip_http_headers
30
35
  response_body = append_to_html_body(response_body, xhr_script(nonce))
@@ -65,16 +70,48 @@ module Bullet
65
70
  end
66
71
  end
67
72
 
68
- def footer_note
69
- "<details #{details_attributes}><summary #{summary_attributes}>Bullet Warnings</summary><div #{footer_content_attributes}>#{Bullet.footer_info.uniq.join('<br>')}#{footer_console_message}</div></details>"
73
+ def footer_note(nonce = nil)
74
+ %(<details id="bullet-footer" data-is-bullet-footer><summary>Bullet Warnings</summary><div>#{Bullet.footer_info.uniq.join('<br>')}#{footer_console_message(nonce)}</div>#{footer_style(nonce)}</details>)
75
+ end
76
+
77
+ # Make footer styles work with ContentSecurityPolicy style-src as self
78
+ def footer_style(nonce = nil)
79
+ css = <<~CSS
80
+ details#bullet-footer {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;}
81
+ details#bullet-footer summary {font-weight: 600; padding: 2px 8px;}
82
+ details#bullet-footer div {padding: 8px; border-top: 1px solid #9b1c1c;}
83
+ CSS
84
+ if nonce
85
+ %(<style type="text/css" nonce="#{nonce}">#{css}</style>)
86
+ else
87
+ %(<style type="text/css">#{css}</style>)
88
+ end
70
89
  end
71
90
 
72
91
  def set_header(headers, header_name, header_array)
73
92
  # Many proxy applications such as Nginx and AWS ELB limit
74
93
  # the size a header to 8KB, so truncate the list of reports to
75
94
  # be under that limit
76
- header_array.pop while header_array.to_json.length > 8 * 1024
77
- headers[header_name] = header_array.to_json
95
+ header_array.pop while JSON.generate(header_array).length > 8 * 1024
96
+ headers[header_name] = JSON.generate(header_array)
97
+ end
98
+
99
+ def skip_html_injection?(request)
100
+ query_string = request.env['QUERY_STRING']
101
+ return false if query_string.nil? || query_string.empty?
102
+
103
+ params = simple_parse_query_string(query_string)
104
+ params['skip_html_injection'] == 'true'
105
+ end
106
+
107
+ # Simple query string parser
108
+ def simple_parse_query_string(query_string)
109
+ params = {}
110
+ query_string.split('&').each do |pair|
111
+ key, value = pair.split('=', 2).map { |s| CGI.unescape(s) }
112
+ params[key] = value if key && !key.empty?
113
+ end
114
+ params
78
115
  end
79
116
 
80
117
  def file?(headers)
@@ -99,28 +136,18 @@ module Bullet
99
136
 
100
137
  private
101
138
 
102
- def details_attributes
103
- <<~EOF
104
- id="bullet-footer" data-is-bullet-footer
105
- 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;"
106
- EOF
107
- end
108
-
109
- def summary_attributes
110
- <<~EOF
111
- style="font-weight: 600; padding: 2px 8px"
112
- EOF
113
- end
114
-
115
- def footer_content_attributes
116
- <<~EOF
117
- style="padding: 8px; border-top: 1px solid #9b1c1c;"
118
- EOF
119
- end
120
-
121
- def footer_console_message
139
+ def footer_console_message(nonce = nil)
122
140
  if Bullet.console_enabled?
123
- "<br/><span style='font-style: italic;'>See 'Uniform Notifier' in JS Console for Stacktrace</span>"
141
+ footer = %(<br/><span id="console-message">See 'Uniform Notifier' in JS Console for Stacktrace</span>)
142
+ css = "details#bullet-footer #console-message {font-style: italic;}"
143
+ style =
144
+ if nonce
145
+ %(<style type="text/css" nonce="#{nonce}">#{css}</style>)
146
+ else
147
+ %(<style type="text/css">#{css}</style>)
148
+ end
149
+
150
+ footer + style
124
151
  end
125
152
  end
126
153
 
@@ -35,7 +35,11 @@ module Bullet
35
35
  end
36
36
 
37
37
  def include?(key, value)
38
- !@registry[key].nil? && @registry[key].include?(value)
38
+ key?(key) && @registry[key].include?(value)
39
+ end
40
+
41
+ def key?(key)
42
+ @registry.key?(key)
39
43
  end
40
44
  end
41
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '8.0.2'
4
+ VERSION = '8.0.7'
5
5
  end
data/lib/bullet.rb CHANGED
@@ -23,8 +23,8 @@ module Bullet
23
23
 
24
24
  if defined?(Rails::Railtie)
25
25
  class BulletRailtie < Rails::Railtie
26
- initializer 'bullet.configure_rails_initialization' do |app|
27
- if defined?(ActionDispatch::ContentSecurityPolicy::Middleware) && Rails.application.config.content_security_policy
26
+ initializer 'bullet.add_middleware' do |app|
27
+ if defined?(ActionDispatch::ContentSecurityPolicy::Middleware) && Rails.application.config.content_security_policy && !app.config.api_only
28
28
  app.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, Bullet::Rack
29
29
  else
30
30
  app.middleware.use Bullet::Rack
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.2
4
+ version: 8.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -112,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
112
  - !ruby/object:Gem::Version
113
113
  version: 1.3.6
114
114
  requirements: []
115
- rubygems_version: 3.6.2
115
+ rubygems_version: 3.6.7
116
116
  specification_version: 4
117
117
  summary: help to kill N+1 queries and unused eager loading.
118
118
  test_files: []