and_one 0.1.0 → 0.3.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.
@@ -5,12 +5,10 @@ module AndOne
5
5
  # session. Mount at `/__and_one` in development to get a mini dashboard
6
6
  # for N+1 queries with fix suggestions.
7
7
  #
8
- # Requires `aggregate_mode = true` to collect detections across requests.
9
- #
10
8
  # Usage (manual):
11
9
  # app.middleware.use AndOne::DevUI
12
10
  #
13
- # Or it's auto-mounted by the Railtie in development when aggregate_mode is on.
11
+ # Or it's auto-mounted by the Railtie in development.
14
12
  class DevUI
15
13
  MOUNT_PATH = "/__and_one"
16
14
 
@@ -28,8 +26,8 @@ module AndOne
28
26
 
29
27
  private
30
28
 
31
- def serve_dashboard(env)
32
- entries = AndOne.aggregate_mode ? AndOne.aggregate.detections : {}
29
+ def serve_dashboard(_env)
30
+ entries = AndOne.aggregate.detections
33
31
 
34
32
  html = render_html(entries)
35
33
  [200, { "content-type" => "text/html; charset=utf-8" }, [html]]
@@ -37,49 +35,48 @@ module AndOne
37
35
 
38
36
  def render_html(entries)
39
37
  rows = if entries.empty?
40
- <<~HTML
41
- <tr>
42
- <td colspan="6" class="empty">
43
- No N+1 queries detected yet.
44
- #{AndOne.aggregate_mode ? "" : "<br><strong>Tip:</strong> Set <code>AndOne.aggregate_mode = true</code> to collect detections across requests."}
45
- </td>
46
- </tr>
47
- HTML
48
- else
49
- entries.map.with_index do |(fp, entry), i|
50
- det = entry.detection
51
- suggestion = begin
52
- AndOne::AssociationResolver.resolve(det, det.raw_caller_strings)
53
- rescue
54
- nil
55
- end
56
- fix = suggestion&.actionable? ? h(suggestion.fix_hint) : ""
57
- strict_hint = suggestion&.strict_loading_hint ? h(suggestion.strict_loading_hint) : ""
58
- loading_hint = suggestion&.loading_strategy_hint ? h(suggestion.loading_strategy_hint) : ""
38
+ <<~HTML
39
+ <tr>
40
+ <td colspan="6" class="empty">
41
+ No N+1 queries detected yet.
42
+ </td>
43
+ </tr>
44
+ HTML
45
+ else
46
+ entries.map.with_index do |(fp, entry), i|
47
+ det = entry.detection
48
+ suggestion = begin
49
+ AndOne::AssociationResolver.resolve(det, det.raw_caller_strings)
50
+ rescue StandardError
51
+ nil
52
+ end
53
+ fix = suggestion&.actionable? ? h(suggestion.fix_hint) : "—"
54
+ strict_hint = suggestion&.strict_loading_hint ? h(suggestion.strict_loading_hint) : ""
55
+ loading_hint = suggestion&.loading_strategy_hint ? h(suggestion.loading_strategy_hint) : ""
59
56
 
60
- origin = det.origin_frame ? format_frame(det.origin_frame) : "—"
61
- fix_loc = det.fix_location ? format_frame(det.fix_location) : "—"
57
+ origin = det.origin_frame ? format_frame(det.origin_frame) : "—"
58
+ fix_loc = det.fix_location ? format_frame(det.fix_location) : "—"
62
59
 
63
- <<~HTML
64
- <tr>
65
- <td>#{i + 1}</td>
66
- <td><code>#{h(det.table_name || 'unknown')}</code></td>
67
- <td>#{entry.occurrences}</td>
68
- <td><code class="sql">#{h(truncate(det.sample_query, 200))}</code></td>
69
- <td>
70
- <div class="origin">#{h(origin)}</div>
71
- <div class="fix-loc">⇒ #{h(fix_loc)}</div>
72
- </td>
73
- <td>
74
- <div class="suggestion">#{fix}</div>
75
- #{"<div class=\"strategy\">#{loading_hint}</div>" unless loading_hint.empty?}
76
- #{"<div class=\"strict\">#{strict_hint}</div>" unless strict_hint.empty?}
77
- <div class="fingerprint">#{h(fp)}</div>
78
- </td>
79
- </tr>
80
- HTML
81
- end.join
82
- end
60
+ <<~HTML
61
+ <tr>
62
+ <td>#{i + 1}</td>
63
+ <td><code>#{h(det.table_name || "unknown")}</code></td>
64
+ <td>#{entry.occurrences}</td>
65
+ <td><code class="sql">#{h(truncate(det.sample_query, 200))}</code></td>
66
+ <td>
67
+ <div class="origin">#{h(origin)}</div>
68
+ <div class="fix-loc">⇒ #{h(fix_loc)}</div>
69
+ </td>
70
+ <td>
71
+ <div class="suggestion">#{fix}</div>
72
+ #{"<div class=\"strategy\">#{loading_hint}</div>" unless loading_hint.empty?}
73
+ #{"<div class=\"strict\">#{strict_hint}</div>" unless strict_hint.empty?}
74
+ <div class="fingerprint">#{h(fp)}</div>
75
+ </td>
76
+ </tr>
77
+ HTML
78
+ end.join
79
+ end
83
80
 
84
81
  <<~HTML
85
82
  <!DOCTYPE html>
@@ -114,7 +111,7 @@ module AndOne
114
111
  </head>
115
112
  <body>
116
113
  <h1>🏀 AndOne — N+1 Dashboard</h1>
117
- <p class="subtitle">#{entries.size} unique N+1 pattern#{'s' if entries.size != 1} detected this session</p>
114
+ <p class="subtitle">#{entries.size} unique N+1 pattern#{"s" if entries.size != 1} detected this session</p>
118
115
  <div class="actions">
119
116
  <a href="#{MOUNT_PATH}">↻ Refresh</a>
120
117
  </div>
@@ -140,14 +137,15 @@ module AndOne
140
137
 
141
138
  def h(text)
142
139
  text.to_s
143
- .gsub("&", "&amp;")
144
- .gsub("<", "&lt;")
145
- .gsub(">", "&gt;")
146
- .gsub('"', "&quot;")
140
+ .gsub("&", "&amp;")
141
+ .gsub("<", "&lt;")
142
+ .gsub(">", "&gt;")
143
+ .gsub('"', "&quot;")
147
144
  end
148
145
 
149
146
  def truncate(text, max)
150
147
  return text if text.length <= max
148
+
151
149
  "#{text[0...max]}..."
152
150
  end
153
151
 
@@ -22,7 +22,7 @@ module AndOne
22
22
  normalized.gsub!(/`(\w+)`/, '\1')
23
23
 
24
24
  # Normalize single-quoted string literals
25
- normalized.gsub!(/\\[']/, "")
25
+ normalized.gsub!("\\'", "")
26
26
  normalized.gsub!(/'(?:[^'\\]|\\.)*'/m, "?")
27
27
 
28
28
  # Normalize numbers (standalone, not part of identifiers)
@@ -23,7 +23,8 @@ module AndOne
23
23
  parts = []
24
24
  parts << ""
25
25
  parts << colorize(SEPARATOR, :red)
26
- parts << colorize(" 🏀 And One! #{detections.size} N+1 quer#{detections.size == 1 ? 'y' : 'ies'} detected", :red, :bold)
26
+ parts << colorize(" 🏀 And One! #{detections.size} N+1 quer#{detections.size == 1 ? "y" : "ies"} detected", :red,
27
+ :bold)
27
28
  parts << colorize(SEPARATOR, :red)
28
29
 
29
30
  detections.each_with_index do |detection, i|
@@ -38,12 +39,13 @@ module AndOne
38
39
 
39
40
  private
40
41
 
41
- def format_detection(detection, index)
42
+ def format_detection(detection, index) # rubocop:disable Metrics
42
43
  lines = []
43
44
  cleaned_bt = clean_backtrace(detection.raw_caller_strings)
44
45
 
45
46
  # Header with count and fingerprint
46
- lines << colorize(" #{index}) #{detection.count}x repeated query on `#{detection.table_name || 'unknown'}`", :yellow, :bold)
47
+ lines << colorize(" #{index}) #{detection.count}x repeated query on `#{detection.table_name || "unknown"}`",
48
+ :yellow, :bold)
47
49
  lines << colorize(" fingerprint: #{detection.fingerprint}", :dim)
48
50
  lines << ""
49
51
 
@@ -68,9 +70,8 @@ module AndOne
68
70
 
69
71
  # Abbreviated call stack
70
72
  lines << colorize(" Call stack:", :cyan)
71
- cleaned_bt.first(6).each_with_index do |frame, fi|
72
- prefix = fi == 0 ? " " : " "
73
- lines << colorize("#{prefix}#{frame}", :dim)
73
+ cleaned_bt.first(6).each_with_index do |frame, _fi|
74
+ lines << colorize(" #{frame}", :dim)
74
75
  end
75
76
  lines << colorize(" ... (#{cleaned_bt.size - 6} more frames)", :dim) if cleaned_bt.size > 6
76
77
  lines << ""
@@ -80,12 +81,8 @@ module AndOne
80
81
  if suggestion&.actionable?
81
82
  lines << colorize(" 💡 Suggestion:", :cyan, :bold)
82
83
  lines << colorize(" #{suggestion.fix_hint}", :green)
83
- if suggestion.loading_strategy_hint
84
- lines << colorize(" #{suggestion.loading_strategy_hint}", :green)
85
- end
86
- if suggestion.strict_loading_hint
87
- lines << colorize(" #{suggestion.strict_loading_hint}", :dim)
88
- end
84
+ lines << colorize(" #{suggestion.loading_strategy_hint}", :green) if suggestion.loading_strategy_hint
85
+ lines << colorize(" #{suggestion.strict_loading_hint}", :dim) if suggestion.strict_loading_hint
89
86
  end
90
87
 
91
88
  # Ignore hint
@@ -98,7 +95,7 @@ module AndOne
98
95
 
99
96
  def resolve_suggestion(detection, cleaned_backtrace)
100
97
  AssociationResolver.resolve(detection, cleaned_backtrace)
101
- rescue
98
+ rescue StandardError
102
99
  nil
103
100
  end
104
101
 
@@ -121,6 +118,7 @@ module AndOne
121
118
 
122
119
  def truncate_query(sql, max_length: 200)
123
120
  return sql if sql.length <= max_length
121
+
124
122
  "#{sql[0...max_length]}..."
125
123
  end
126
124
 
@@ -23,7 +23,7 @@ module AndOne
23
23
  # fingerprint:abc123def456
24
24
  #
25
25
  class IgnoreFile
26
- Rule = Struct.new(:type, :pattern, keyword_init: true)
26
+ Rule = Struct.new(:type, :pattern)
27
27
 
28
28
  attr_reader :rules
29
29
 
@@ -90,8 +90,8 @@ module AndOne
90
90
  def matches_path?(glob, raw_caller_strings)
91
91
  # Convert glob to regex: * -> [^/]*, ** -> .*
92
92
  regex_str = Regexp.escape(glob)
93
- .gsub('\*\*', '.*')
94
- .gsub('\*', '[^/]*')
93
+ .gsub('\*\*', ".*")
94
+ .gsub('\*', "[^/]*")
95
95
  regex = Regexp.new(regex_str)
96
96
  raw_caller_strings.any? { |frame| frame.match?(regex) }
97
97
  end
@@ -69,7 +69,7 @@ module AndOne
69
69
 
70
70
  def resolve_suggestion(detection)
71
71
  AssociationResolver.resolve(detection, detection.raw_caller_strings)
72
- rescue
72
+ rescue StandardError
73
73
  nil
74
74
  end
75
75
 
@@ -15,25 +15,25 @@ module AndOne
15
15
  #
16
16
  module MinitestHelper
17
17
  # Assert that the block does NOT trigger any N+1 queries.
18
- def assert_no_n_plus_one(message = nil, &block)
19
- detections = scan_for_n_plus_ones(&block)
20
-
21
- if detections.any?
22
- formatter = Formatter.new(
23
- backtrace_cleaner: AndOne.backtrace_cleaner || AndOne.send(:default_backtrace_cleaner)
24
- )
25
- detail = formatter.format(detections)
26
- summary = detections.map { |d|
27
- "#{d.count} queries to `#{d.table_name || 'unknown'}` (expected 1)"
28
- }.join("; ")
29
- msg = message || "Expected no N+1 queries, but #{detections.size} detected: #{summary}\n#{detail}"
30
- flunk(msg)
31
- end
18
+ def assert_no_n_plus_one(message = nil, &)
19
+ detections = scan_for_n_plus_ones(&)
20
+
21
+ return unless detections.any?
22
+
23
+ formatter = Formatter.new(
24
+ backtrace_cleaner: AndOne.backtrace_cleaner || AndOne.send(:default_backtrace_cleaner)
25
+ )
26
+ detail = formatter.format(detections)
27
+ summary = detections.map do |d|
28
+ "#{d.count} queries to `#{d.table_name || "unknown"}` (expected 1)"
29
+ end.join("; ")
30
+ msg = message || "Expected no N+1 queries, but #{detections.size} detected: #{summary}\n#{detail}"
31
+ flunk(msg)
32
32
  end
33
33
 
34
34
  # Assert that the block DOES trigger N+1 queries (useful for documenting known issues).
35
- def assert_n_plus_one(message = nil, &block)
36
- detections = scan_for_n_plus_ones(&block)
35
+ def assert_n_plus_one(message = nil, &)
36
+ detections = scan_for_n_plus_ones(&)
37
37
 
38
38
  if detections.empty?
39
39
  msg = message || "Expected N+1 queries, but none were detected"
@@ -45,7 +45,7 @@ module AndOne
45
45
 
46
46
  private
47
47
 
48
- def scan_for_n_plus_ones(&block)
48
+ def scan_for_n_plus_ones(&)
49
49
  # Temporarily disable raise_on_detect so scan returns detections
50
50
  # instead of raising
51
51
  previous_raise = AndOne.raise_on_detect
@@ -54,7 +54,7 @@ module AndOne
54
54
  AndOne.notifications_callback = nil
55
55
 
56
56
  begin
57
- AndOne.scan(&block) || []
57
+ AndOne.scan(&) || []
58
58
  ensure
59
59
  AndOne.raise_on_detect = previous_raise
60
60
  AndOne.notifications_callback = previous_callback
@@ -100,9 +100,9 @@ module AndOne
100
100
  backtrace_cleaner: AndOne.backtrace_cleaner || AndOne.send(:default_backtrace_cleaner)
101
101
  )
102
102
  detail = formatter.format(@detections)
103
- summary = @detections.map { |d|
104
- "#{d.count} queries to `#{d.table_name || 'unknown'}` (expected 1)"
105
- }.join("; ")
103
+ summary = @detections.map do |d|
104
+ "#{d.count} queries to `#{d.table_name || "unknown"}` (expected 1)"
105
+ end.join("; ")
106
106
  "expected no N+1 queries, but #{@detections.size} detected: #{summary}\n#{detail}"
107
107
  end
108
108
 
@@ -5,6 +5,10 @@ module AndOne
5
5
  # Designed to NOT interfere with error propagation —
6
6
  # if the app raises, we cleanly stop scanning without adding
7
7
  # to or corrupting the original backtrace.
8
+ #
9
+ # When `AndOne.dev_toast` is enabled (default in development),
10
+ # detected N+1s are injected as a toast notification into HTML responses
11
+ # with a link to the DevUI dashboard.
8
12
  class Middleware
9
13
  include ScanHelper
10
14
 
@@ -13,7 +17,41 @@ module AndOne
13
17
  end
14
18
 
15
19
  def call(env)
16
- and_one_wrap { @app.call(env) }
20
+ return @app.call(env) if !AndOne.enabled? || AndOne.scanning?
21
+
22
+ begin
23
+ AndOne.scan
24
+ status, headers, body = @app.call(env)
25
+ detections = AndOne.finish
26
+
27
+ if AndOne.dev_toast && detections&.any? && html_response?(headers) && status == 200
28
+ body = inject_toast(body, detections)
29
+ # Recalculate Content-Length since we modified the body
30
+ headers.delete("content-length")
31
+ headers.delete("Content-Length")
32
+ end
33
+
34
+ [status, headers, body]
35
+ rescue Exception # rubocop:disable Lint/RescueException
36
+ and_one_quietly_stop
37
+ raise
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def html_response?(headers)
44
+ content_type = headers["content-type"] || headers["Content-Type"]
45
+ content_type&.include?("text/html")
46
+ end
47
+
48
+ def inject_toast(body, detections)
49
+ full_body = +""
50
+ body.each { |chunk| full_body << chunk }
51
+ body.close if body.respond_to?(:close)
52
+
53
+ injected = DevToast.inject(full_body, detections)
54
+ [injected]
17
55
  end
18
56
  end
19
57
  end
@@ -13,9 +13,12 @@ module AndOne
13
13
  # Rack middleware for web requests
14
14
  app.middleware.insert_before(0, AndOne::Middleware)
15
15
 
16
- # Dev UI dashboard for N+1 overview (requires aggregate_mode)
17
16
  if Rails.env.development?
17
+ # Dev UI dashboard for N+1 overview
18
18
  app.middleware.use(AndOne::DevUI)
19
+
20
+ # Dev toast: show in-page N+1 notifications (default on in development)
21
+ AndOne.dev_toast = true if AndOne.dev_toast.nil?
19
22
  end
20
23
 
21
24
  # ActiveJob hook — covers all job backends (Sidekiq, GoodJob, SolidQueue, etc.)
data/lib/and_one/rspec.rb CHANGED
@@ -13,6 +13,8 @@
13
13
 
14
14
  require "and_one"
15
15
 
16
- RSpec.configure do |config|
17
- config.include AndOne::RSpecHelper
18
- end if defined?(RSpec)
16
+ if defined?(RSpec)
17
+ RSpec.configure do |config|
18
+ config.include AndOne::RSpecHelper
19
+ end
20
+ end
@@ -15,7 +15,7 @@ module AndOne
15
15
  result = yield
16
16
  AndOne.finish
17
17
  result
18
- rescue Exception
18
+ rescue Exception # rubocop:disable Lint/RescueException
19
19
  and_one_quietly_stop
20
20
  raise
21
21
  end
@@ -25,7 +25,7 @@ module AndOne
25
25
  Thread.current[:and_one_detector]&.send(:unsubscribe)
26
26
  Thread.current[:and_one_detector] = nil
27
27
  Thread.current[:and_one_paused] = false
28
- rescue
28
+ rescue StandardError
29
29
  nil
30
30
  end
31
31
  end
@@ -19,8 +19,8 @@ module AndOne
19
19
  class SidekiqMiddleware
20
20
  include ScanHelper
21
21
 
22
- def call(_worker, _msg, _queue)
23
- and_one_wrap { yield }
22
+ def call(_worker, _msg, _queue, &)
23
+ and_one_wrap(&)
24
24
  end
25
25
  end
26
26
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AndOne
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/and_one.rb CHANGED
@@ -14,8 +14,9 @@ module AndOne
14
14
  class << self
15
15
  attr_accessor :enabled, :raise_on_detect, :backtrace_cleaner,
16
16
  :allow_stack_paths, :ignore_queries, :ignore_callers,
17
- :min_n_queries, :notifications_callback, :aggregate_mode,
18
- :ignore_file_path, :json_logging, :env_thresholds
17
+ :min_n_queries, :notifications_callback,
18
+ :ignore_file_path, :json_logging, :env_thresholds,
19
+ :dev_toast
19
20
 
20
21
  def configure
21
22
  yield self
@@ -33,19 +34,19 @@ module AndOne
33
34
 
34
35
  start_scan
35
36
 
36
- if block_given?
37
- begin
38
- yield
39
- detections = detector.finish
40
- stop_scan
41
- report(detections) if detections.any?
42
- detections
43
- rescue Exception => e
44
- # On error, clean up without reporting — don't add noise to real errors
45
- detector&.send(:unsubscribe)
46
- stop_scan
47
- raise
48
- end
37
+ return unless block_given?
38
+
39
+ begin
40
+ yield
41
+ detections = detector.finish
42
+ stop_scan
43
+ report(detections) if detections.any?
44
+ detections
45
+ rescue Exception # rubocop:disable Lint/RescueException
46
+ # On error, clean up without reporting — don't add noise to real errors
47
+ detector&.send(:unsubscribe)
48
+ stop_scan
49
+ raise
49
50
  end
50
51
  end
51
52
 
@@ -133,7 +134,7 @@ module AndOne
133
134
  if defined?(Rails) && Rails.respond_to?(:env)
134
135
  Rails.env.to_s
135
136
  else
136
- ENV["RAILS_ENV"] || ENV["RACK_ENV"]
137
+ ENV["RAILS_ENV"] || ENV.fetch("RACK_ENV", nil)
137
138
  end
138
139
  end
139
140
 
@@ -150,7 +151,7 @@ module AndOne
150
151
  Thread.current
151
152
  end
152
153
 
153
- def report(detections)
154
+ def report(detections) # rubocop:disable Metrics
154
155
  # Filter out ignored detections
155
156
  detections = detections.reject do |d|
156
157
  ignore_list.ignored?(d, d.raw_caller_strings) ||
@@ -159,11 +160,9 @@ module AndOne
159
160
 
160
161
  return if detections.empty?
161
162
 
162
- # In aggregate mode, only report NEW unique detections
163
- if aggregate_mode
164
- detections = detections.select { |d| aggregate.record(d) }
165
- return if detections.empty?
166
- end
163
+ # Record to aggregate and only report NEW unique detections
164
+ detections = detections.select { |d| aggregate.record(d) }
165
+ return if detections.empty?
167
166
 
168
167
  cleaner = backtrace_cleaner || default_backtrace_cleaner
169
168
 
@@ -181,7 +180,7 @@ module AndOne
181
180
  if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
182
181
  Rails.logger.warn(json_output)
183
182
  else
184
- $stderr.puts(json_output)
183
+ warn(json_output)
185
184
  end
186
185
  end
187
186
 
@@ -191,7 +190,7 @@ module AndOne
191
190
  if ENV["GITHUB_ACTIONS"]
192
191
  detections.each do |d|
193
192
  file, line = parse_frame_location(d.fix_location || d.origin_frame)
194
- query_count = "#{d.count} queries to `#{d.table_name || 'unknown'}`"
193
+ query_count = "#{d.count} queries to `#{d.table_name || "unknown"}`"
195
194
  if file
196
195
  $stdout.puts "::warning file=#{file},line=#{line || 1}::N+1 detected: #{query_count}. Add `.includes(:#{suggest_association_name(d)})` to fix."
197
196
  else
@@ -200,15 +199,11 @@ module AndOne
200
199
  end
201
200
  end
202
201
 
203
- if raise_on_detect
204
- raise NPlus1Error, "\n#{message}"
205
- else
206
- unless json_logging
207
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
208
- Rails.logger.warn("\n#{message}")
209
- end
210
- $stderr.puts("\n#{message}") if $stderr.tty?
211
- end
202
+ raise NPlus1Error, "\n#{message}" if raise_on_detect
203
+
204
+ unless json_logging
205
+ Rails.logger.warn("\n#{message}") if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
206
+ warn("\n#{message}") if $stderr.tty?
212
207
  end
213
208
  end
214
209
  end
@@ -222,7 +217,7 @@ module AndOne
222
217
  return false unless patterns&.any?
223
218
 
224
219
  raw_caller_strings.any? do |frame|
225
- patterns.any? { |pattern| pattern === frame }
220
+ patterns.any? { |pattern| pattern.match?(frame) }
226
221
  end
227
222
  end
228
223
 
@@ -231,20 +226,24 @@ module AndOne
231
226
 
232
227
  # Extract file:line from a backtrace frame like "app/controllers/posts_controller.rb:15:in `index'"
233
228
  clean = frame
234
- .sub(%r{.*/app/}, "app/")
235
- .sub(%r{.*/lib/}, "lib/")
236
- .sub(%r{.*/test/}, "test/")
237
- .sub(%r{.*/spec/}, "spec/")
229
+ .sub(%r{.*/app/}, "app/")
230
+ .sub(%r{.*/lib/}, "lib/")
231
+ .sub(%r{.*/test/}, "test/")
232
+ .sub(%r{.*/spec/}, "spec/")
238
233
 
239
234
  if clean =~ /\A(.+?):(\d+)/
240
- [$1, $2.to_i]
235
+ [::Regexp.last_match(1), ::Regexp.last_match(2).to_i]
241
236
  else
242
237
  [clean, nil]
243
238
  end
244
239
  end
245
240
 
246
241
  def suggest_association_name(detection)
247
- suggestion = AssociationResolver.resolve(detection, detection.raw_caller_strings) rescue nil
242
+ suggestion = begin
243
+ AssociationResolver.resolve(detection, detection.raw_caller_strings)
244
+ rescue StandardError
245
+ nil
246
+ end
248
247
  suggestion&.association_name || detection.table_name || "association"
249
248
  end
250
249
 
@@ -271,6 +270,7 @@ require_relative "and_one/aggregate"
271
270
  require_relative "and_one/matchers"
272
271
  require_relative "and_one/scan_helper"
273
272
  require_relative "and_one/dev_ui"
273
+ require_relative "and_one/dev_toast"
274
274
  require_relative "and_one/console"
275
275
  require_relative "and_one/middleware"
276
276
  require_relative "and_one/active_job_hook"