prosopite 1.4.2 → 2.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0509715095d0797998d5bd88ad33bf6390bbc68d5da7f34a10ada120bbcbe96d'
4
- data.tar.gz: 3e05d4ae63f68955c002853d5820bac19ab239b7d4987576bf1d7b9dd85f7d06
3
+ metadata.gz: 3ba705386994a5f73eb64b024eadd45fd4c29db669ccc408283746d03d58bdf8
4
+ data.tar.gz: b3d52ea151dd88b9da262286df321fcdafd2245ce3b09c9539fd54e9ae54ee8d
5
5
  SHA512:
6
- metadata.gz: 63408215134ec404aba9cd4728ae2bdbb57b0924c08449a1cccc1f72d46cfdedec33685643b452eac0a23ff05abbb0fc74f96954b0ec874b94aef974f46d2ed0
7
- data.tar.gz: d1b9a6d908bf2b17bbe9710b446a0c7b3f66a1a5b0468aaabb37b085d152d609a973faec5ae529d859ecea8c297d93ff3128917344ae6ff0159fb4fc5041a10b
6
+ metadata.gz: e6f74f6cd59e7537429f32300072394c428837e3ec0dd8aed971d601f447c0f3b28e2bdd68f240f98906be205d23958c3cdd8cbf181821cc26f4f435de5f17c8
7
+ data.tar.gz: 570b9ed4965908b45b5f1f5d6dc1b41d84e0b00759c93e1ff875aa181f143c445ec910ad96dfb4c52ba2020e4948660fc203d3ff8cd8c7496bc870acf0df8180
@@ -4,7 +4,7 @@ jobs:
4
4
  test:
5
5
  strategy:
6
6
  matrix:
7
- ruby: [2.7, '3.0', 3.1, 3.2]
7
+ ruby: [3.2, 3.3, 3.4]
8
8
  runs-on: ubuntu-latest
9
9
  steps:
10
10
  - uses: actions/checkout@v3
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ *.gem
data/Gemfile.lock CHANGED
@@ -1,87 +1,136 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- prosopite (1.4.2)
4
+ prosopite (2.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- actionpack (6.1.3)
10
- actionview (= 6.1.3)
11
- activesupport (= 6.1.3)
12
- rack (~> 2.0, >= 2.0.9)
9
+ actionpack (8.0.1)
10
+ actionview (= 8.0.1)
11
+ activesupport (= 8.0.1)
12
+ nokogiri (>= 1.8.5)
13
+ rack (>= 2.2.4)
14
+ rack-session (>= 1.0.1)
13
15
  rack-test (>= 0.6.3)
14
- rails-dom-testing (~> 2.0)
15
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
16
- actionview (6.1.3)
17
- activesupport (= 6.1.3)
16
+ rails-dom-testing (~> 2.2)
17
+ rails-html-sanitizer (~> 1.6)
18
+ useragent (~> 0.16)
19
+ actionview (8.0.1)
20
+ activesupport (= 8.0.1)
18
21
  builder (~> 3.1)
19
- erubi (~> 1.4)
20
- rails-dom-testing (~> 2.0)
21
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
22
- activemodel (6.1.3)
23
- activesupport (= 6.1.3)
24
- activerecord (6.1.3)
25
- activemodel (= 6.1.3)
26
- activesupport (= 6.1.3)
27
- activesupport (6.1.3)
28
- concurrent-ruby (~> 1.0, >= 1.0.2)
22
+ erubi (~> 1.11)
23
+ rails-dom-testing (~> 2.2)
24
+ rails-html-sanitizer (~> 1.6)
25
+ activemodel (8.0.1)
26
+ activesupport (= 8.0.1)
27
+ activerecord (8.0.1)
28
+ activemodel (= 8.0.1)
29
+ activesupport (= 8.0.1)
30
+ timeout (>= 0.4.0)
31
+ activesupport (8.0.1)
32
+ base64
33
+ benchmark (>= 0.3)
34
+ bigdecimal
35
+ concurrent-ruby (~> 1.0, >= 1.3.1)
36
+ connection_pool (>= 2.2.5)
37
+ drb
29
38
  i18n (>= 1.6, < 2)
39
+ logger (>= 1.4.2)
30
40
  minitest (>= 5.1)
31
- tzinfo (~> 2.0)
32
- zeitwerk (~> 2.3)
41
+ securerandom (>= 0.3)
42
+ tzinfo (~> 2.0, >= 2.0.5)
43
+ uri (>= 0.13.1)
33
44
  ansi (1.5.0)
45
+ base64 (0.2.0)
46
+ benchmark (0.4.0)
47
+ bigdecimal (3.1.9)
34
48
  builder (3.2.4)
35
49
  coderay (1.1.3)
36
- concurrent-ruby (1.1.8)
50
+ concurrent-ruby (1.3.5)
51
+ connection_pool (2.5.0)
37
52
  crass (1.0.6)
53
+ date (3.4.1)
54
+ drb (2.2.1)
38
55
  erubi (1.12.0)
39
- factory_bot (6.1.0)
40
- activesupport (>= 5.0.0)
56
+ factory_bot (6.5.1)
57
+ activesupport (>= 6.1.0)
41
58
  i18n (1.8.9)
42
59
  concurrent-ruby (~> 1.0)
43
- loofah (2.19.1)
60
+ io-console (0.8.0)
61
+ irb (1.15.1)
62
+ pp (>= 0.6.0)
63
+ rdoc (>= 4.0.0)
64
+ reline (>= 0.4.2)
65
+ logger (1.6.6)
66
+ loofah (2.24.0)
44
67
  crass (~> 1.0.2)
45
- nokogiri (>= 1.5.9)
68
+ nokogiri (>= 1.12.0)
46
69
  method_source (1.0.0)
47
- mini_portile2 (2.8.1)
48
- minitest (5.14.3)
70
+ mini_portile2 (2.8.8)
71
+ minitest (5.25.4)
49
72
  minitest-reporters (1.5.0)
50
73
  ansi
51
74
  builder
52
75
  minitest (>= 5.0)
53
76
  ruby-progressbar
54
- nokogiri (1.14.1)
55
- mini_portile2 (~> 2.8.0)
77
+ nokogiri (1.18.3)
78
+ mini_portile2 (~> 2.8.2)
56
79
  racc (~> 1.4)
57
- nokogiri (1.14.1-x86_64-linux)
80
+ nokogiri (1.18.3-x86_64-linux-gnu)
58
81
  racc (~> 1.4)
59
82
  pg_query (1.3.0)
83
+ pp (0.6.2)
84
+ prettyprint
85
+ prettyprint (0.2.0)
60
86
  pry (0.14.0)
61
87
  coderay (~> 1.1)
62
88
  method_source (~> 1.0)
63
- racc (1.6.2)
64
- rack (2.2.6.2)
89
+ psych (5.2.3)
90
+ date
91
+ stringio
92
+ racc (1.8.1)
93
+ rack (3.1.10)
94
+ rack-session (2.1.0)
95
+ base64 (>= 0.1.0)
96
+ rack (>= 3.0.0)
65
97
  rack-test (2.0.2)
66
98
  rack (>= 1.3)
67
- rails-dom-testing (2.0.3)
68
- activesupport (>= 4.2.0)
99
+ rackup (2.2.1)
100
+ rack (>= 3)
101
+ rails-dom-testing (2.2.0)
102
+ activesupport (>= 5.0.0)
103
+ minitest
69
104
  nokogiri (>= 1.6)
70
- rails-html-sanitizer (1.5.0)
71
- loofah (~> 2.19, >= 2.19.1)
72
- railties (6.1.3)
73
- actionpack (= 6.1.3)
74
- activesupport (= 6.1.3)
75
- method_source
76
- rake (>= 0.8.7)
77
- thor (~> 1.0)
105
+ rails-html-sanitizer (1.6.2)
106
+ loofah (~> 2.21)
107
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
108
+ railties (8.0.1)
109
+ actionpack (= 8.0.1)
110
+ activesupport (= 8.0.1)
111
+ irb (~> 1.13)
112
+ rackup (>= 1.0.0)
113
+ rake (>= 12.2)
114
+ thor (~> 1.0, >= 1.2.2)
115
+ zeitwerk (~> 2.6)
78
116
  rake (13.0.6)
117
+ rdoc (6.12.0)
118
+ psych (>= 4.0.0)
119
+ reline (0.6.0)
120
+ io-console (~> 0.5)
79
121
  ruby-progressbar (1.11.0)
80
- sqlite3 (1.4.2)
81
- thor (1.2.1)
82
- tzinfo (2.0.4)
122
+ securerandom (0.4.1)
123
+ sqlite3 (2.5.0)
124
+ mini_portile2 (~> 2.8.0)
125
+ sqlite3 (2.5.0-x86_64-linux-gnu)
126
+ stringio (3.1.4)
127
+ thor (1.3.2)
128
+ timeout (0.4.3)
129
+ tzinfo (2.0.6)
83
130
  concurrent-ruby (~> 1.0)
84
- zeitwerk (2.4.2)
131
+ uri (1.0.2)
132
+ useragent (0.16.11)
133
+ zeitwerk (2.7.2)
85
134
 
86
135
  PLATFORMS
87
136
  ruby
@@ -100,4 +149,4 @@ DEPENDENCIES
100
149
  sqlite3
101
150
 
102
151
  BUNDLED WITH
103
- 2.2.10
152
+ 2.6.5
data/README.md CHANGED
@@ -116,37 +116,17 @@ Or install it yourself as:
116
116
  The preferred type of notifications can be configured with:
117
117
 
118
118
  * `Prosopite.min_n_queries`: Minimum number of N queries to report per N+1 case. Defaults to 2.
119
- * `Prosopite.raise = true`: Raise warnings as exceptions
120
- * `Prosopite.rails_logger = true`: Send warnings to the Rails log
121
- * `Prosopite.prosopite_logger = true`: Send warnings to `log/prosopite.log`
122
- * `Prosopite.stderr_logger = true`: Send warnings to STDERR
119
+ * `Prosopite.raise = true`: Raise warnings as exceptions. Defaults to `false`.
120
+ * `Prosopite.start_raise`: Raises warnings as exceptions from when this is called. Overrides `Proposite.raise`.
121
+ * `Propsoite.stop_raise`: Disables raising warnings as exceptions if previously enabled with `Proposite.start_raise`.
122
+ * `Prosopite.local_raise?`: Returns `true` if `Prosopite.start_raise` has been called previously.
123
+ * `Prosopite.rails_logger = true`: Send warnings to the Rails log. Defaults to `false`.
124
+ * `Prosopite.prosopite_logger = true`: Send warnings to `log/prosopite.log`. Defaults to `false`.
125
+ * `Prosopite.stderr_logger = true`: Send warnings to STDERR. Defaults to `false`.
123
126
  * `Prosopite.backtrace_cleaner = my_custom_backtrace_cleaner`: use a different [ActiveSupport::BacktraceCleaner](https://api.rubyonrails.org/classes/ActiveSupport/BacktraceCleaner.html). Defaults to `Rails.backtrace_cleaner`.
124
- * `Prosopite.custom_logger = my_custom_logger`:
127
+ * `Prosopite.custom_logger = my_custom_logger`: Set a custom logger. See the following section for the details. Defaults to `false`.
125
128
  * `Prosopite.enabled = true`: Enables or disables the gem. Defaults to `true`.
126
129
 
127
- ### Custom Logging Configuration
128
-
129
- You can supply a custom logger with the `Prosopite.custom_logger` setting.
130
-
131
- This is useful for circumstances where you don't want your logs to be
132
- highlighted with red, or you want logs sent to a custom location.
133
-
134
- One common scenario is that you may be generating json logs and sending them to
135
- Datadog, ELK stack, or similar, and don't want to have to remove the default red
136
- escaping data from messages sent to the Rails logger, or want to tag them
137
- differently with your own custom logger.
138
-
139
- ```ruby
140
- # Turns off logging with red highlights, but still sends them to the Rails logger
141
- Prosopite.custom_logger = Rails.logger
142
- ```
143
-
144
- ```ruby
145
- # Use a completely custom logging instance
146
- Prosopite.custom_logger = MyLoggerClass.new
147
-
148
- ```
149
-
150
130
  ## Development Environment Usage
151
131
 
152
132
  Prosopite auto-detection can be enabled on all controllers:
@@ -174,7 +154,7 @@ config.after_initialize do
174
154
  Prosopite.rails_logger = true
175
155
  end
176
156
  ```
177
-
157
+ ```
178
158
  ## Test Environment Usage
179
159
 
180
160
  Tests with N+1 queries can be configured to fail with:
@@ -315,6 +295,79 @@ Pauses can be ignored with `Prosopite.ignore_pauses = true` in case you want to
315
295
  An example of when you might use this is if you are [testing Active Jobs inline](https://guides.rubyonrails.org/testing.html#testing-jobs),
316
296
  and don't want to run Prosopite on background job code, just foreground app code. In that case you could write an [Active Job callback](https://edgeguides.rubyonrails.org/active_job_basics.html#callbacks) that pauses the scan while the job is running.
317
297
 
298
+ ## Local Raise
299
+
300
+ In some cases you may want to configure prosopite to not raise by default and only raise in certain scenarios.
301
+ In this example we scan on all controllers but also provide an API to only raise on specific actions.
302
+
303
+ ```ruby
304
+ Proposite.raise = false
305
+ ```
306
+
307
+ ```ruby
308
+ # app/controllers/application_controller.rb
309
+ class ApplicationController < ActionController::Base
310
+ def raise_on_n_plus_ones!(**options)
311
+ return if Rails.env.production?
312
+
313
+ prepend_around_action(:_raise_on_n_plus_ones, **options)
314
+ end
315
+
316
+ unless Rails.env.production?
317
+ around_action :n_plus_one_detection
318
+
319
+ def n_plus_one_detection
320
+ ...
321
+ end
322
+
323
+ def _raise_on_n_plus_ones
324
+ Proposite.start_raise
325
+ yield
326
+ ensure
327
+ Prosopite.stop_raise
328
+ end
329
+ end
330
+ end
331
+ ```
332
+
333
+ ```ruby
334
+ # app/controllers/books_controller.rb
335
+ class BooksController < ApplicationController
336
+ raise_on_n_plus_ones!(only: [:index])
337
+
338
+ def index
339
+ @books = Book.all.map(&:author) # This will raise N+1 errors
340
+ end
341
+
342
+ def show
343
+ @book = Book.find(params[:id])
344
+ @book.reviews.map(&:author) # This will not raise N+1 errors
345
+ end
346
+ end
347
+ ```
348
+
349
+ ## Custom Logging Configuration
350
+
351
+ You can supply a custom logger with the `Prosopite.custom_logger` setting.
352
+
353
+ This is useful for circumstances where you don't want your logs to be
354
+ highlighted with red, or you want logs sent to a custom location.
355
+
356
+ One common scenario is that you may be generating json logs and sending them to
357
+ Datadog, ELK stack, or similar, and don't want to have to remove the default red
358
+ escaping data from messages sent to the Rails logger, or want to tag them
359
+ differently with your own custom logger.
360
+
361
+ ```ruby
362
+ # Turns off logging with red highlights, but still sends them to the Rails logger
363
+ Prosopite.custom_logger = Rails.logger
364
+ ```
365
+
366
+ ```ruby
367
+ # Use a completely custom logging instance
368
+ Prosopite.custom_logger = MyLoggerClass.new
369
+ ```
370
+
318
371
  ## Contributing
319
372
 
320
373
  Bug reports and pull requests are welcome on GitHub at https://github.com/charkost/prosopite.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prosopite
4
- VERSION = "1.4.2"
4
+ VERSION = "2.2.0"
5
5
  end
data/lib/prosopite.rb CHANGED
@@ -51,6 +51,7 @@ module Prosopite
51
51
  tc[:prosopite_query_counter] = Hash.new(0)
52
52
  tc[:prosopite_query_holder] = Hash.new { |h, k| h[k] = [] }
53
53
  tc[:prosopite_query_caller] = {}
54
+ tc[:prosopite_query_duration] = Hash.new(0.0)
54
55
 
55
56
  @allow_stack_paths ||= []
56
57
  @ignore_pauses ||= false
@@ -111,6 +112,23 @@ module Prosopite
111
112
  tc[:prosopite_query_counter] = nil
112
113
  tc[:prosopite_query_holder] = nil
113
114
  tc[:prosopite_query_caller] = nil
115
+ tc[:prosopite_query_duration] = nil
116
+ end
117
+
118
+ def start_raise
119
+ tc[:prosopite_local_raise] = true
120
+ end
121
+
122
+ def stop_raise
123
+ tc[:prosopite_local_raise] = false
124
+ end
125
+
126
+ def local_raise?
127
+ tc[:prosopite_local_raise] == true
128
+ end
129
+
130
+ def raise?
131
+ local_raise? || !!@raise
114
132
  end
115
133
 
116
134
  def create_notifications
@@ -130,13 +148,14 @@ module Prosopite
130
148
 
131
149
  next unless queries.any?
132
150
 
133
- kaller = tc[:prosopite_query_caller][location_key]
151
+ kaller = tc[:prosopite_query_caller][location_key].map(&:to_s)
134
152
  allow_list = (@allow_stack_paths + DEFAULT_ALLOW_LIST)
135
153
  is_allowed = kaller.any? { |f| allow_list.any? { |s| f.match?(s) } }
136
154
 
137
155
  unless is_allowed
156
+ duration_ms = tc[:prosopite_query_duration][location_key]
138
157
  queries.each do |q|
139
- tc[:prosopite_notifications][q] = kaller
158
+ tc[:prosopite_notifications][q] = { kaller: kaller, duration_ms: duration_ms }
140
159
  end
141
160
  end
142
161
  end
@@ -144,7 +163,7 @@ module Prosopite
144
163
  end
145
164
 
146
165
  def fingerprint(query)
147
- db_adapter = ActiveRecord::Base.connection.adapter_name.downcase
166
+ db_adapter = ActiveRecord::Base.connection_db_config.adapter
148
167
  if db_adapter.include?('mysql') || db_adapter.include?('trilogy')
149
168
  mysql_fingerprint(query)
150
169
  else
@@ -212,12 +231,15 @@ module Prosopite
212
231
  @rails_logger ||= false
213
232
  @stderr_logger ||= false
214
233
  @prosopite_logger ||= false
215
- @raise ||= false
216
234
 
217
- notifications_str = ''
235
+ notifications_str = String.new('')
218
236
 
219
- tc[:prosopite_notifications].each do |queries, kaller|
220
- notifications_str << "N+1 queries detected:\n"
237
+ tc[:prosopite_notifications].each do |queries, info|
238
+ kaller = info[:kaller]
239
+ duration_ms = info[:duration_ms]
240
+ time_str = duration_ms ? " (#{duration_ms.round(1)}ms)" : ''
241
+
242
+ notifications_str << "N+1 queries detected#{time_str}:\n"
221
243
 
222
244
  queries.each { |q| notifications_str << " #{q}\n" }
223
245
 
@@ -241,7 +263,7 @@ module Prosopite
241
263
  end
242
264
  end
243
265
 
244
- raise NPlusOneQueriesError.new(notifications_str) if @raise
266
+ raise NPlusOneQueriesError.new(notifications_str) if raise?
245
267
  end
246
268
 
247
269
  def red(str)
@@ -257,18 +279,25 @@ module Prosopite
257
279
  @subscribed ||= false
258
280
  return if @subscribed
259
281
 
260
- ActiveSupport::Notifications.subscribe 'sql.active_record' do |_, _, _, _, data|
282
+ ActiveSupport::Notifications.subscribe 'sql.active_record' do |_, start, finish, _, data|
261
283
  sql, name = data[:sql], data[:name]
262
284
 
263
285
  if scan? && name != "SCHEMA" && sql.include?('SELECT') && data[:cached].nil? && !ignore_query?(sql)
264
- query_caller = caller
265
- location_key = Digest::SHA256.hexdigest(query_caller.join)
286
+ query_caller = caller_locations
287
+ # Calculate the location key with as few allocations as possible
288
+ location_key = [].tap do |array|
289
+ query_caller.each do |loc|
290
+ array << loc.path
291
+ array << loc.lineno
292
+ end
293
+ end.hash
266
294
 
267
295
  tc[:prosopite_query_counter][location_key] += 1
268
296
  tc[:prosopite_query_holder][location_key] << sql
297
+ tc[:prosopite_query_duration][location_key] += (finish - start) * 1000 if tc[:prosopite_query_duration]
269
298
 
270
299
  if tc[:prosopite_query_counter][location_key] > 1
271
- tc[:prosopite_query_caller][location_key] = query_caller.dup
300
+ tc[:prosopite_query_caller][location_key] = query_caller
272
301
  end
273
302
  end
274
303
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prosopite
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.2
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mpampis Kostas
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-14 00:00:00.000000000 Z
11
+ date: 2026-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -133,7 +133,7 @@ licenses:
133
133
  metadata:
134
134
  homepage_uri: https://github.com/charkost/prosopite
135
135
  source_code_uri: https://github.com/charkost/prosopite
136
- post_install_message:
136
+ post_install_message:
137
137
  rdoc_options: []
138
138
  require_paths:
139
139
  - lib
@@ -148,8 +148,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
148
  - !ruby/object:Gem::Version
149
149
  version: '0'
150
150
  requirements: []
151
- rubygems_version: 3.4.6
152
- signing_key:
151
+ rubygems_version: 3.5.3
152
+ signing_key:
153
153
  specification_version: 4
154
154
  summary: N+1 auto-detection for Rails with zero false positives / false negatives
155
155
  test_files: []