and_one 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f74cbffeb954bdf743f6028d005244aa5d4f76c229f7543145d3e77f2b468398
4
- data.tar.gz: 418273820bcdb2b8a86a3d8b76922ba97b0540bd48b6a6ac5c3e3317fe3ac703
3
+ metadata.gz: 51b6c654539d7d462eee3fa1f498e12c67d083032532f70e9068357aaf720813
4
+ data.tar.gz: 9fbd318564ff10362c9e7f24bed9f05654a4db1cb19b2b6f80e76fe7748ec380
5
5
  SHA512:
6
- metadata.gz: 159e5753a847aa025f9168c9962cdb47d53489ccd0b81272b81039f6784be1cd2b3ecc9ec7537a19245b030602f539e7578d52d05dcdd28abcc45101a47d2a44
7
- data.tar.gz: 9885b04b8ed1b4aded500bb673ac5f4526ea834b96c3beee407a7e3e165fa2a3eea9100edd5d67c4d0c6a78cde3940557b77a2609f283249afd1d6ce7f73b936
6
+ metadata.gz: e616668cf8527dd81bdf5d31ab89974ba651a8cdb587f8ff33e5a71fbdae9077474d28da036fc4689ab01fe10a4b7814006c54f25026356606245733c75fab95
7
+ data.tar.gz: e4efa724ed029637706a90007a8e4f16005e15c4278c79fb7d6e1dec4a2c2d8bdab03c6b21ae3da9378b53795e7e4aaa39704bdab878880506e021ecfc075edb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-03-05
4
+
5
+ ### Removed
6
+
7
+ - **`aggregate_mode` configuration option** — Deduplication and aggregate tracking are now always-on when AndOne is enabled. The dev toast and dashboard both depend on the aggregate, so having it off was a bug in disguise (detections would show in the toast but never appear on the dashboard). If you had `AndOne.aggregate_mode = true` in an initializer, simply remove the line.
8
+
9
+ ### Fixed
10
+
11
+ - N+1 detections now always appear on the `/__and_one` dashboard. Previously, detections would show in the toast notification but not on the dashboard unless `aggregate_mode` was explicitly enabled.
12
+
3
13
  ## [0.2.0] - 2026-03-02
4
14
 
5
15
  ### Added
data/README.md CHANGED
@@ -14,7 +14,7 @@ AndOne stays completely invisible until it detects an N+1 query — then it tell
14
14
  - **Auto-raises in test** — N+1s fail your test suite by default
15
15
  - **Background job support** — ActiveJob (`around_perform`) and Sidekiq server middleware, with double-scan protection
16
16
  - **Ignore file** — `.and_one_ignore` with `gem:`, `path:`, `query:`, and `fingerprint:` rules
17
- - **Aggregate mode** — report each unique N+1 once per server session with occurrence counts
17
+ - **Automatic deduplication** — each unique N+1 is reported once per server session with occurrence counts
18
18
  - **Test matchers** — Minitest (`assert_no_n_plus_one`) and RSpec (`expect { }.not_to cause_n_plus_one`)
19
19
  - **Dev toast notifications** — in-page toast on every page that triggers an N+1, with a link to the dashboard
20
20
  - **Dev UI dashboard** — browse `/__and_one` in development for a live N+1 overview
@@ -132,14 +132,9 @@ This is especially useful for **N+1s coming from gems** where you can't add `.in
132
132
  | `query:some_table` | A specific query pattern should always be ignored |
133
133
  | `fingerprint:abc123` | You want to silence one specific detection (shown in output) |
134
134
 
135
- ## Aggregate Mode
135
+ ## Deduplication
136
136
 
137
- In development, the same N+1 can fire on every request, flooding your logs. Aggregate mode reports each unique pattern only once per server session:
138
-
139
- ```ruby
140
- # config/initializers/and_one.rb
141
- AndOne.aggregate_mode = true
142
- ```
137
+ In development, the same N+1 can fire on every request, flooding your logs. AndOne automatically deduplicates each unique pattern is reported only once per server session. Subsequent occurrences are silently counted.
143
138
 
144
139
  You can check the session summary at any time:
145
140
 
@@ -170,13 +165,6 @@ The toast only appears on HTML responses with a 200 status, so it won't interfer
170
165
 
171
166
  Browse `/__and_one` in development for a full overview of every unique N+1 detected in the current server session. The dashboard shows the query, origin, fix location, and suggested `.includes()` call for each detection.
172
167
 
173
- The dashboard requires aggregate mode to track detections across requests:
174
-
175
- ```ruby
176
- # config/initializers/and_one.rb
177
- AndOne.aggregate_mode = true
178
- ```
179
-
180
168
  Both features work together: the toast gives you immediate feedback on the page you're looking at, and the dashboard link takes you to the full picture.
181
169
 
182
170
  ## Test Matchers
@@ -245,9 +233,6 @@ AndOne.configure do |config|
245
233
  # Minimum repeated queries to trigger (default: 2)
246
234
  config.min_n_queries = 3
247
235
 
248
- # Aggregate mode — only report each unique N+1 once per session
249
- config.aggregate_mode = true
250
-
251
236
  # In-page toast notifications (default: true in development)
252
237
  config.dev_toast = true
253
238
 
@@ -2,12 +2,9 @@
2
2
 
3
3
  module AndOne
4
4
  # Tracks unique N+1 detections across requests/jobs in a server session.
5
- # In aggregate mode, each unique N+1 (by fingerprint) is only reported once.
5
+ # Each unique N+1 (by fingerprint) is only reported once.
6
6
  # Subsequent occurrences are silently counted.
7
7
  #
8
- # Usage:
9
- # AndOne.aggregate_mode = true
10
- #
11
8
  # The aggregate can be queried at any time:
12
9
  # AndOne.aggregate.summary # => formatted string
13
10
  # AndOne.aggregate.detections # => { fingerprint => { detection:, count:, first_seen_at: } }
@@ -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
 
@@ -29,7 +27,7 @@ module AndOne
29
27
  private
30
28
 
31
29
  def serve_dashboard(_env)
32
- entries = AndOne.aggregate_mode ? AndOne.aggregate.detections : {}
30
+ entries = AndOne.aggregate.detections
33
31
 
34
32
  html = render_html(entries)
35
33
  [200, { "content-type" => "text/html; charset=utf-8" }, [html]]
@@ -41,7 +39,6 @@ module AndOne
41
39
  <tr>
42
40
  <td colspan="6" class="empty">
43
41
  No N+1 queries detected yet.
44
- #{"<br><strong>Tip:</strong> Set <code>AndOne.aggregate_mode = true</code> to collect detections across requests." unless AndOne.aggregate_mode}
45
42
  </td>
46
43
  </tr>
47
44
  HTML
@@ -14,7 +14,7 @@ module AndOne
14
14
  app.middleware.insert_before(0, AndOne::Middleware)
15
15
 
16
16
  if Rails.env.development?
17
- # Dev UI dashboard for N+1 overview (requires aggregate_mode)
17
+ # Dev UI dashboard for N+1 overview
18
18
  app.middleware.use(AndOne::DevUI)
19
19
 
20
20
  # Dev toast: show in-page N+1 notifications (default on in development)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AndOne
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/and_one.rb CHANGED
@@ -14,7 +14,7 @@ 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,
17
+ :min_n_queries, :notifications_callback,
18
18
  :ignore_file_path, :json_logging, :env_thresholds,
19
19
  :dev_toast
20
20
 
@@ -160,11 +160,9 @@ module AndOne
160
160
 
161
161
  return if detections.empty?
162
162
 
163
- # In aggregate mode, only report NEW unique detections
164
- if aggregate_mode
165
- detections = detections.select { |d| aggregate.record(d) }
166
- return if detections.empty?
167
- end
163
+ # Record to aggregate and only report NEW unique detections
164
+ detections = detections.select { |d| aggregate.record(d) }
165
+ return if detections.empty?
168
166
 
169
167
  cleaner = backtrace_cleaner || default_backtrace_cleaner
170
168
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: and_one
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Thompson
@@ -66,7 +66,6 @@ files:
66
66
  - LICENSE.txt
67
67
  - README.md
68
68
  - Rakefile
69
- - TODO.md
70
69
  - lib/and_one.rb
71
70
  - lib/and_one/active_job_hook.rb
72
71
  - lib/and_one/aggregate.rb
data/TODO.md DELETED
@@ -1,52 +0,0 @@
1
- # AndOne — Feature Roadmap
2
-
3
- ## ✅ Completed
4
-
5
- - [x] Core detection engine using `sql.active_record` notifications
6
- - [x] SQL fingerprinting without external dependencies
7
- - [x] Association resolver that suggests exact `.includes()` fixes
8
- - [x] Rich formatted output with query, call stack, and fix suggestions
9
- - [x] Rack middleware that never corrupts error backtraces
10
- - [x] Railtie for zero-config auto-setup in dev/test
11
- - [x] Raises in test env, warns in dev, disabled in production
12
- - [x] Pause/resume support for known N+1s
13
- - [x] ActiveJob `around_perform` hook (works with any backend)
14
- - [x] Sidekiq server middleware (for jobs bypassing ActiveJob)
15
- - [x] `ScanHelper` shared module to DRY up scan lifecycle across entry points
16
- - [x] Double-scan protection (ActiveJob + Sidekiq don't conflict)
17
-
18
- ## 🎯 High Value — Completed
19
-
20
- - [x] **Auto-detect the "fix location"** — Walks the backtrace to identify two key frames: the "origin" (where the N+1 is triggered inside a loop) and the "fix location" (the outer frame where `.includes()` should be added). Both are highlighted in the output.
21
-
22
- - [x] **Ignore file (`.and_one_ignore`)** — Supports four rule types: `gem:` (for N+1s from gems like devise/administrate you can't fix), `path:` (glob patterns for app areas), `query:` (SQL patterns), and `fingerprint:` (specific detections). Checked into source control.
23
-
24
- - [x] **Aggregate mode for development** — `AndOne.aggregate_mode = true` reports each unique N+1 only once per server session. Tracks occurrence counts. `AndOne.aggregate.summary` shows a session overview. Thread-safe.
25
-
26
- - [x] **RSpec / Minitest matchers** — `assert_no_n_plus_one { ... }` / `assert_n_plus_one { ... }` for Minitest. `expect { ... }.not_to cause_n_plus_one` for RSpec. Matchers temporarily disable `raise_on_detect` internally so they work regardless of config.
27
-
28
- ## ✅ Medium Value — Polish & Power User Features (Completed)
29
-
30
- - [x] **`strict_loading` suggestion** — When an N+1 is detected, also suggest the `strict_loading` approach as an alternative: "You could also add `has_many :comments, strict_loading: true` to prevent this at the model level."
31
-
32
- - [x] **Query count in test failure messages** — "N+1 detected: 47 queries to `comments` (expected 1). Add `.includes(:comments)` to reduce to 1 query." Makes severity immediately obvious.
33
-
34
- - [x] **Dev UI endpoint** — A tiny Rack endpoint (e.g., `/__and_one`) in development that shows all N+1s detected in the current server session with fix suggestions. Like a mini BetterErrors for N+1s.
35
-
36
- - [x] **GitHub Actions / CI annotations** — When `GITHUB_ACTIONS` env var is set, output detections in `::warning file=...` format so they appear as annotations on the PR diff.
37
-
38
- - [x] **Ignore by caller pattern** — In addition to `ignore_queries` (SQL patterns), support `ignore_callers` to suppress detections originating from specific paths: "ignore any N+1 from `app/views/admin/*`".
39
-
40
- - [x] **`has_many :through` and polymorphic support** — Extend the association resolver to handle `has_many :through` join chains and polymorphic associations, which are common sources of confusing N+1s.
41
-
42
- - [x] **`preload` vs `includes` vs `eager_load` recommendation** — Suggest the optimal loading strategy based on the query pattern (e.g., `eager_load` when there's a WHERE on the association).
43
-
44
- ## ✅ Lower Priority — Nice to Have (Completed)
45
-
46
- - [x] **Structured JSON logging** — A JSON output mode for log aggregation services (Datadog, Splunk, etc.). Set `AndOne.json_logging = true`. Uses `JsonFormatter` which outputs structured JSON with event, table, fingerprint, query count, suggestion, and backtrace. Also provides `format_hashes` for integrations that accept Ruby hashes directly.
47
-
48
- - [x] **Thread-safety audit for Puma** — Formal audit and stress test suite complete. Found and fixed a **critical cross-thread contamination bug** in `Detector#subscribe`: the `ActiveSupport::Notifications` callback closure captured `self`, causing SQL from one thread to be recorded in another thread's Detector. Fixed by checking `Thread.current[:and_one_detector].object_id` in the callback. Also added Mutex protection for lazy singletons (`aggregate`, `ignore_list`), `AssociationResolver.@table_model_cache`, and report output serialization. 14 concurrent stress tests verify isolation, atomicity, and correctness under Puma-like load.
49
-
50
- - [x] **Rails console integration** — Auto-scan in `rails console` sessions and print warnings inline. Activated automatically by the Railtie in development, or manually via `AndOne::Console.activate!`. Hooks into IRB (via `Context#evaluate` prepend) and Pry (via `:after_eval` hook) to cycle scans between commands.
51
-
52
- - [x] **Configurable per-environment thresholds** — Different `min_n_queries` for dev vs test. Configure via `AndOne.env_thresholds = { "development" => 3, "test" => 2 }`. Falls back to global `min_n_queries` when no env-specific threshold is set. Supports both string and symbol keys.