and_one 0.2.0 → 0.3.1
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 +16 -0
- data/README.md +10 -20
- data/lib/and_one/aggregate.rb +1 -4
- data/lib/and_one/dev_toast.rb +17 -6
- 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 +5 -7
- 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: f97b3753e3181eb460649cb7624c9744d03df8dfb6205bae46f8069cc47d3448
|
|
4
|
+
data.tar.gz: 815b5b9c7f5617faae5f77ac11e87ee4aa6653933a5c475a4fa5c5283ec1765d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c3a672c29fffc69baadee284fc973e335fa1a64086369a7e3852f71e7c49fa3402ab86c0d3b8d79e1ca69be380a14a8d7eafda0de17686daa23ff8e67be62ec
|
|
7
|
+
data.tar.gz: eeaa76d8407541c4668bc211a1aacba0eb528917ae36ae093948cd12514974a6df7c611a5de64880aa59ee9f4115cda22ecac9525e36e2081662c640360afbb9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-03-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Configurable toast position** — `AndOne.dev_toast_position` accepts `:top_right` (default), `:top_left`, `:bottom_right`, or `:bottom_left`. The slide-in animation direction adjusts automatically to match.
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-03-05
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- **`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.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
3
19
|
## [0.2.0] - 2026-03-02
|
|
4
20
|
|
|
5
21
|
### 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
|
|
|
@@ -157,11 +152,12 @@ When an N+1 is detected during a request, AndOne injects a small toast notificat
|
|
|
157
152
|
|
|
158
153
|
This is enabled by default in development — no configuration needed. The toast auto-dismisses after 8 seconds, but hovering over it keeps it open.
|
|
159
154
|
|
|
160
|
-
To disable it:
|
|
155
|
+
To change the position or disable it:
|
|
161
156
|
|
|
162
157
|
```ruby
|
|
163
158
|
# config/initializers/and_one.rb
|
|
164
|
-
AndOne.
|
|
159
|
+
AndOne.dev_toast_position = :bottom_right # :top_right (default), :top_left, :bottom_right, :bottom_left
|
|
160
|
+
AndOne.dev_toast = false # disable entirely
|
|
165
161
|
```
|
|
166
162
|
|
|
167
163
|
The toast only appears on HTML responses with a 200 status, so it won't interfere with API endpoints, redirects, or error pages.
|
|
@@ -170,13 +166,6 @@ The toast only appears on HTML responses with a 200 status, so it won't interfer
|
|
|
170
166
|
|
|
171
167
|
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
168
|
|
|
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
169
|
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
170
|
|
|
182
171
|
## Test Matchers
|
|
@@ -245,12 +234,13 @@ AndOne.configure do |config|
|
|
|
245
234
|
# Minimum repeated queries to trigger (default: 2)
|
|
246
235
|
config.min_n_queries = 3
|
|
247
236
|
|
|
248
|
-
# Aggregate mode — only report each unique N+1 once per session
|
|
249
|
-
config.aggregate_mode = true
|
|
250
|
-
|
|
251
237
|
# In-page toast notifications (default: true in development)
|
|
252
238
|
config.dev_toast = true
|
|
253
239
|
|
|
240
|
+
# Toast position (default: :top_right)
|
|
241
|
+
# Options: :top_right, :top_left, :bottom_right, :bottom_left
|
|
242
|
+
config.dev_toast_position = :top_right
|
|
243
|
+
|
|
254
244
|
# Path to ignore file (default: Rails.root/.and_one_ignore)
|
|
255
245
|
config.ignore_file_path = Rails.root.join(".and_one_ignore").to_s
|
|
256
246
|
|
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_toast.rb
CHANGED
|
@@ -8,9 +8,19 @@ module AndOne
|
|
|
8
8
|
# or can be enabled manually:
|
|
9
9
|
# AndOne.dev_toast = true
|
|
10
10
|
#
|
|
11
|
-
# The toast appears as a fixed-position badge
|
|
12
|
-
#
|
|
11
|
+
# The toast appears as a fixed-position badge and auto-dismisses after
|
|
12
|
+
# 8 seconds (click to keep open).
|
|
13
|
+
#
|
|
14
|
+
# Position is configurable via `AndOne.dev_toast_position`:
|
|
15
|
+
# :top_right (default), :top_left, :bottom_right, :bottom_left
|
|
13
16
|
module DevToast
|
|
17
|
+
POSITIONS = {
|
|
18
|
+
top_right: { vertical: "top", horizontal: "right", slide_from: "-1rem" },
|
|
19
|
+
top_left: { vertical: "top", horizontal: "left", slide_from: "-1rem" },
|
|
20
|
+
bottom_right: { vertical: "bottom", horizontal: "right", slide_from: "1rem" },
|
|
21
|
+
bottom_left: { vertical: "bottom", horizontal: "left", slide_from: "1rem" }
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
14
24
|
module_function
|
|
15
25
|
|
|
16
26
|
# Injects toast HTML/JS/CSS before </body> in an HTML response.
|
|
@@ -33,6 +43,7 @@ module AndOne
|
|
|
33
43
|
end.first(5)
|
|
34
44
|
|
|
35
45
|
extra = count > 5 ? "<div class=\"and-one-toast-extra\">...and #{count - 5} more</div>" : ""
|
|
46
|
+
pos = POSITIONS[AndOne.dev_toast_position || :top_right] || POSITIONS[:top_right]
|
|
36
47
|
|
|
37
48
|
<<~HTML
|
|
38
49
|
<div id="and-one-toast" class="and-one-toast" role="status" aria-live="polite">
|
|
@@ -50,8 +61,8 @@ module AndOne
|
|
|
50
61
|
<style>
|
|
51
62
|
.and-one-toast {
|
|
52
63
|
position: fixed;
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
#{pos[:vertical]}: 1rem;
|
|
65
|
+
#{pos[:horizontal]}: 1rem;
|
|
55
66
|
z-index: 999999;
|
|
56
67
|
background: #1a1a2e;
|
|
57
68
|
color: #e0e0e0;
|
|
@@ -120,8 +131,8 @@ module AndOne
|
|
|
120
131
|
text-decoration: underline;
|
|
121
132
|
}
|
|
122
133
|
@keyframes and-one-slide-in {
|
|
123
|
-
from { transform: translateY(
|
|
124
|
-
to { transform: translateY(0);
|
|
134
|
+
from { transform: translateY(#{pos[:slide_from]}); opacity: 0; }
|
|
135
|
+
to { transform: translateY(0); opacity: 1; }
|
|
125
136
|
}
|
|
126
137
|
</style>
|
|
127
138
|
<script>
|
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,9 +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,
|
|
17
|
+
:min_n_queries, :notifications_callback,
|
|
18
18
|
:ignore_file_path, :json_logging, :env_thresholds,
|
|
19
|
-
:dev_toast
|
|
19
|
+
:dev_toast, :dev_toast_position
|
|
20
20
|
|
|
21
21
|
def configure
|
|
22
22
|
yield self
|
|
@@ -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.1
|
|
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.
|