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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +3 -18
- data/lib/and_one/aggregate.rb +1 -4
- data/lib/and_one/dev_ui.rb +2 -5
- data/lib/and_one/railtie.rb +1 -1
- data/lib/and_one/version.rb +1 -1
- data/lib/and_one.rb +4 -6
- metadata +1 -2
- data/TODO.md +0 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51b6c654539d7d462eee3fa1f498e12c67d083032532f70e9068357aaf720813
|
|
4
|
+
data.tar.gz: 9fbd318564ff10362c9e7f24bed9f05654a4db1cb19b2b6f80e76fe7748ec380
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
- **
|
|
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
|
-
##
|
|
135
|
+
## Deduplication
|
|
136
136
|
|
|
137
|
-
In development, the same N+1 can fire on every request, flooding your logs.
|
|
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
|
|
data/lib/and_one/aggregate.rb
CHANGED
|
@@ -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
|
-
#
|
|
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: } }
|
data/lib/and_one/dev_ui.rb
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
data/lib/and_one/railtie.rb
CHANGED
|
@@ -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
|
|
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)
|
data/lib/and_one/version.rb
CHANGED
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,
|
|
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
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
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.
|