rails_error_dashboard 0.11.0 → 0.11.2

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: defed3f836bc0e812c76abfb81b95e5e3f229e5338bfa40cde4604643eb95582
4
- data.tar.gz: 7657043dbf617694e5318a5b0e2829ecca78ade0c67412ede16fbecabc696c36
3
+ metadata.gz: c470ccb09f844bb6187fff2f3519d00370e3a0d188319cba49beed93c1bc139d
4
+ data.tar.gz: a1adc23940e22ffe6f35d00a161c7ec6494a07cc3576ca1a0b541cc28605e108
5
5
  SHA512:
6
- metadata.gz: 79992d38956c257a116c1cfcb153429cbf3a082892bc71664c20ba57e801ba33325ae69f43bd0ef8404a3cd08a6d5df9727a989a0f8977826786a179ad70b448
7
- data.tar.gz: 3bbcc477eff8db567415c727037a58d7e27bd8e01ad602674beb05cd6891f75bb06e665273698fed1b49c20806e4f07dbea586647583f6b7da3052a744d58fae
6
+ metadata.gz: dabde3ae967ff2d0615b0fa91477f3706490ef2fe8975fa26a364727905c646e393605a6d6c0bca2c1e614eb311b550d192a3f7d632bd19626407f586cd016ba
7
+ data.tar.gz: c0f5e711dac845b70506151dc56f2b4c079eb8dfe2dc85b491dfef4ae011d488d55c758fc0a0aee18ac64faf949253a1be3cb6c421e9315a62cbccef823d6f9d
data/README.md CHANGED
@@ -7,27 +7,100 @@
7
7
  [![Sponsor](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?logo=githubsponsors)](https://github.com/sponsors/AnjanJ)
8
8
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?logo=buymeacoffee)](https://buymeacoffee.com/anjanj)
9
9
 
10
- **Self-hosted Rails error monitoring free, forever.**
10
+ **Rails-native error tracking for failure investigation — see the Ruby state and Rails runtime health behind every exception. Self-hosted, inside your app, in your own database. The gem is MIT and free forever.**
11
11
 
12
12
  ```ruby
13
- gem 'rails_error_dashboard'
13
+ gem "rails_error_dashboard"
14
14
  ```
15
15
 
16
- **5-minute setup** · **Works out-of-the-box** · **PostgreSQL, MySQL/Trilogy, SQLite — shared or separate database** · **No vendor lock-in**
16
+ ```bash
17
+ bundle install
18
+ rails generate rails_error_dashboard:install
19
+ rails db:migrate
20
+ ```
17
21
 
18
- [Full Documentation](https://anjanj.github.io/rails_error_dashboard/) · [Live Demo](https://rails-error-dashboard.anjan.dev) · [RubyGems](https://rubygems.org/gems/rails_error_dashboard)
22
+ Open `/red` and raise a test exception. No monitoring account or ingestion service is required.
19
23
 
20
- ---
24
+ [Try the live demo](https://rails-error-dashboard.anjan.dev) (`gandalf` / `youshallnotpass`) · [Read the documentation](https://anjanj.github.io/rails_error_dashboard/) · [View on RubyGems](https://rubygems.org/gems/rails_error_dashboard)
25
+
26
+ > **Beta:** RED is functional and extensively tested, but configuration and APIs may change before 1.0. Supports Rails 7.0–8.1 and Ruby 3.2–4.0 (CI runs Ruby 3.2–3.4 against every supported Rails version; Ruby 4.0 is verified by the maintainer).
27
+
28
+ ## See the Ruby state and Rails runtime health behind every exception
29
+
30
+ Rails Error Dashboard (RED) is an open-source, self-hosted Rails engine for investigating production failures. It helps you answer not only **what failed**, but **what was happening inside Ruby and Rails when it failed**.
31
+
32
+ - Inspect local variables and the raising object's instance variables before the stack unwinds.
33
+ - See error-time Active Record, Puma, job queue, GC, memory and process health.
34
+ - Follow the SQL, cache, controller, job, mailer and other Rails events leading to the exception.
35
+ - Stay safe during error floods with progressive, count-preserving storm protection.
36
+ - Keep exception data on infrastructure you control.
37
+
38
+ ![Local and instance variables captured at the raise, scrubbed with filter_parameters](docs/images/local-variables.png)
39
+
40
+ ## The questions RED helps you answer
41
+
42
+ A stack trace tells you where execution stopped. RED helps you investigate the state behind it:
43
+
44
+ - What did `params`, local variables and objects such as `@order` contain?
45
+ - Was the Active Record pool exhausted?
46
+ - Was Puma out of thread capacity or building a backlog?
47
+ - Were jobs failing or queues growing?
48
+ - Was the process under GC, memory, descriptor or system pressure?
49
+ - Which SQL queries, cache operations or Rails events preceded the failure?
50
+ - Did a deploy introduce the error?
51
+ - Can the failing request become a cURL reproduction or RSpec regression-test scaffold?
52
+
53
+ ## What makes RED different
54
+
55
+ ### Failure-time Ruby state
56
+
57
+ Optionally capture local variables and — something no other error tracker does — the raising receiver's instance variables at `TracePoint(:raise)`, with bounded serialization and your Rails `filter_parameters` applied to sensitive values. Binding objects are never retained.
21
58
 
22
- ### Try the Live Demo
59
+ ### Failure-time Rails health
23
60
 
24
- **[rails-error-dashboard.anjan.dev](https://rails-error-dashboard.anjan.dev)** Username: `gandalf` · Password: `youshallnotpass`
61
+ Attach connection-pool, Puma, background-job, GC, memory, file-descriptor, TCP, RubyVM and YJIT state to the error record, refreshed on every captured occurrence — not merely to a separate periodic metrics chart. Every APM has these as time-series; none attaches them to the error. Opt-in; the procfs-backed fields are Linux-only.
25
62
 
26
- > **Beta Software** Functional and tested (4,200+ tests passing), but the API may change before v1.0. Supports Rails 7.0-8.1 and Ruby 3.2-4.0.
63
+ ### Monitoring that degrades safely
64
+
65
+ During an error flood, RED progressively reduces captured context and database work, keeps a fresh exemplar every minute, records the storm in a Storm History ledger and reconciles exact in-process occurrence counts onto the error records. On by default.
66
+
67
+ ### Rails-specific investigation
68
+
69
+ Connect exceptions with SQL, caching, Active Job, Action Cable, Active Storage, Rack::Attack, deprecations and other Rails subsystems from one dashboard.
70
+
71
+ ### Things no other tracker does
72
+
73
+ Verified against Sentry, Honeybadger, AppSignal, Rollbar, Bugsnag, Airbrake, Raygun, New Relic, Datadog, Scout, Skylight and every self-hosted Rails tracker in August 2026 ([the ledger](.shipkit/research/red-unique-features-verified.md)):
74
+
75
+ - **Copy as RSpec** — a runnable request spec generated from the captured request (Sentry offers curl only).
76
+ - **Swallowed-exception aggregate** — raise-vs-rescue ratio per location, no APM span needed (Datadog's paid APM detects rescued exceptions but keeps no aggregate).
77
+ - **Rack::Attack ledger** — throttle, blocklist and track events persisted with per-rule stats and an AI-crawler classifier; rack-attack ships no UI of its own.
78
+ - **Codeberg issue tracking**, alongside GitHub, GitLab and Linear with two-way sync.
79
+ - **The tracker instruments itself** — its capture pipeline exported as OpenTelemetry spans, so you can audit its overhead in your own APM.
80
+
81
+ ## How RED compares
82
+
83
+ | Basic embedded tracker | General SaaS monitoring | RED |
84
+ |---|---|---|
85
+ | Stack trace and context | Cross-language telemetry and managed ingestion | Deep failure-time Ruby/Rails state inside the application boundary |
86
+ | Lightweight and local | Strong distributed and frontend observability | Rails-specific operational investigation and storm-safe local capture |
87
+
88
+ That makes RED a self-hosted Sentry alternative for teams that want Rails-specific depth and need error data to stay inside the application boundary — not a replacement for cross-language telemetry. RED has no mobile SDKs, no merge/split, no MCP server and no hosted operations.
89
+
90
+ ## Choose how you run it
91
+
92
+ - Store data in the application's existing PostgreSQL, MySQL/Trilogy or SQLite database.
93
+ - Isolate monitoring writes in a separate error database.
94
+ - Use synchronous writes, or async logging through Sidekiq or Solid Queue (GoodJob is detected for job-health stats but is not an async adapter).
95
+ - Track several Rails applications through a shared database.
96
+
97
+ No RED licence or event-ingestion fee, and no plan limits — your database is the only cap, and storm protection deliberately sheds context during floods.
98
+
99
+ ---
27
100
 
28
101
  ### Screenshots
29
102
 
30
- **Dashboard Overview** — Real-time error stats, severity breakdown, and trend charts.
103
+ **Dashboard Overview** — Live error stats, severity breakdown, and trend charts.
31
104
 
32
105
  ![Dashboard Overview](docs/images/dashboard-overview.png)
33
106
 
@@ -49,25 +122,12 @@ gem 'rails_error_dashboard'
49
122
 
50
123
  ---
51
124
 
52
- ## Who This Is For
53
-
54
- - **Solo bootstrappers** who need professional error tracking without recurring costs
55
- - **Indie SaaS founders** building profitable apps on tight budgets
56
- - **Small dev teams** (2-5 people) who hate SaaS bloat
57
- - **Privacy-conscious apps** that need to keep error data on their own servers
58
- - **Side projects** that might become real businesses
59
-
60
- ## What It Replaces
125
+ ## Safety, performance and compatibility
61
126
 
62
- | Before | After |
63
- |--------|-------|
64
- | $29-99/month for error monitoring | $0/month runs on your existing Rails server |
65
- | Sensitive error data sent to third parties | All data stays on your infrastructure |
66
- | SaaS pricing tiers and usage limits | Unlimited errors, unlimited projects |
67
- | Vendor lock-in with proprietary APIs | 100% open source, fully portable |
68
- | Complex SDK setup and external services | 5-minute Rails Engine installation |
69
- | Pay extra for local variable capture (Sentry) | Local + instance variables included free |
70
- | No tool detects silently rescued exceptions | Swallowed exception detection built in |
127
+ - **Host-app safety** nothing in the capture path raises into your app; every subscriber and callback is rescue-wrapped, `Thread.current` is cleaned up in `ensure`, and the original exception is always re-raised. Variables, health and breadcrumbs are opt-in and off by default; storm protection is on by default and fails open.
128
+ - **Performance** — the storm-protection hot path is a digest plus an atomic increment with no I/O; the figures quoted below are a maintainer's single-machine measurements and no benchmark script ships with the gem yet.
129
+ - **Security** HTTP Basic Auth or your own `authenticate_with` lambda (Devise, Warden, session); your Rails `filter_parameters` are applied to params, variables and breadcrumbs; prompts are never recorded by LLM observability. Vulnerability reports: [SECURITY.md](SECURITY.md).
130
+ - **Compatibility** Rails 7.0–8.1, Ruby 3.2–4.0, PostgreSQL, MySQL/Trilogy or SQLite; `turbo-rails` plus ActionCable are needed for live updates (no polling fallback); the gem's own CSS/JS is inline but Bootstrap JS, Chart.js, highlight.js and Google Fonts load from CDNs, so it is not air-gap clean.
71
131
 
72
132
  ---
73
133
 
@@ -75,14 +135,14 @@ gem 'rails_error_dashboard'
75
135
 
76
136
  ### Core (Always Enabled)
77
137
 
78
- Error capture from controllers, jobs, and middleware. Custom-designed dashboard with dark/light mode, search, filtering, and real-time updates. Analytics with trend charts, severity breakdown, and spike detection. Workflow management with assignment, priority, snooze, mute/unmute (notification suppression), comments, and batch operations. Security via HTTP Basic Auth or custom lambda (Devise, Warden, session-based). Exception cause chains, enriched HTTP context, custom fingerprinting, CurrentAttributes integration, auto-reopen on recurrence, and sensitive data filtering — all built in.
138
+ Error capture from controllers, jobs, and middleware. Custom-designed dashboard with dark/light mode, search, filtering, and real-time updates (the latter with `turbo-rails` + ActionCable in the host). Analytics with trend charts, severity breakdown, and spike detection. Workflow management with assignment, priority, snooze, mute/unmute (notification suppression), comments, and batch operations. Security via HTTP Basic Auth or custom lambda (Devise, Warden, session-based). Exception cause chains, enriched HTTP context, custom fingerprinting, CurrentAttributes integration, auto-reopen on recurrence, and sensitive data filtering — all built in.
79
139
 
80
140
  ### Optional Features
81
141
 
82
142
  <details>
83
143
  <summary><strong>Storm Protection — Circuit Breaker + Adaptive Sampling</strong></summary>
84
144
 
85
- When the error rate spikes (a bad deploy throwing thousands of errors a minute), the nightmare scenario for any in-process tracker is amplifying the outage with its own database writes. Storm protection makes the gem **provably degrade itself first** — ON by default.
145
+ When the error rate spikes (a bad deploy throwing thousands of errors a minute), the nightmare scenario for any in-process tracker is amplifying the outage with its own database writes. Storm protection is designed to **shed the gem's own expensive work first** — ON by default. The behaviour is measured (see Overhead below), though a bundled, reproducible benchmark is still to come.
86
146
 
87
147
  - **Per-fingerprint caps:** past N occurrences/minute per error, context is shed, then rows are sampled deterministically (a fresh exemplar is always kept each minute)
88
148
  - **Global circuit breaker:** sustained floods flip the gem to count-only mode — zero per-event I/O, exact in-memory counts reconciled onto error records every 30s. Async mode is gated too (a SolidQueue enqueue is itself a DB write)
@@ -98,7 +158,7 @@ config.storm_open_threshold_per_second = 50 # per process
98
158
 
99
159
  All thresholds are per process and individually configurable. Disable with one flag.
100
160
 
101
- **Measured overhead** (Apple Silicon, Ruby 4.0): 2.4µs/error with protection active and calm, 2.95µs in count-only mode, 0.2µs when disabled against a 5µs budget. The check is a digest plus an atomic increment; there is no I/O on the hot path.
161
+ **Overhead:** the check is a digest plus an atomic increment; there is no I/O on the hot path. The maintainer's single-machine measurement (Apple Silicon, Ruby 4.0) was 2.4µs/error with protection active and calm, 2.95µs in count-only mode and 0.2µs when disabled, against a 5µs budget a reproducible benchmark script is not yet part of the gem.
102
162
  </details>
103
163
 
104
164
  <details>
@@ -108,7 +168,7 @@ See exactly what happened before the crash — SQL queries, controller actions,
108
168
 
109
169
  - Automatic capture — zero config beyond the enable flag
110
170
  - N+1 query detection with aggregate patterns page
111
- - Deprecation warnings with aggregate view
171
+ - Deprecation warnings with aggregate view (needs the host's deprecation behaviour to include `:notify`; only requests that later raised are seen)
112
172
  - Custom breadcrumbs via `RailsErrorDashboard.add_breadcrumb("checkout started", { cart_id: 123 })`
113
173
  - Safe by design — fixed-size ring buffer, thread-local, every subscriber wrapped in rescue
114
174
 
@@ -146,7 +206,7 @@ Cross-error N+1 detection grouped by SQL fingerprint, and aggregate deprecation
146
206
 
147
207
  ![N+1 Query Patterns](docs/images/n-plus-one-queries.png)
148
208
 
149
- Requires breadcrumbs to be enabled.
209
+ Requires breadcrumbs to be enabled. Deprecations are seen only when the host's `ActiveSupport::Deprecation` behaviour includes `:notify` (the production default does not) and only inside requests that later raised.
150
210
 
151
211
  [Complete documentation →](docs/FEATURES.md#n1-query-detection)
152
212
  </details>
@@ -154,7 +214,7 @@ Requires breadcrumbs to be enabled.
154
214
  <details>
155
215
  <summary><strong>Operational Health Panels — Jobs, Database, Cache, ActionCable</strong></summary>
156
216
 
157
- **Job Health** — Auto-detects Sidekiq, SolidQueue, or GoodJob. Per-error table with adapter badge, failed count (color-coded), sorted worst-first.
217
+ **Job Health** — Aggregates the queue stats captured on each error (Sidekiq, SolidQueue or GoodJob auto-detected; needs `enable_system_health`). Not a live queue view — a per-error table with adapter badge, failed count (color-coded), sorted worst-first.
158
218
 
159
219
  ![Job Health](docs/images/job-health.png)
160
220
 
@@ -184,12 +244,12 @@ config.enable_activestorage_tracking = true # requires enable_breadcrumbs = tru
184
244
  <details>
185
245
  <summary><strong>LLM Observability — Calls, Tokens, Cost, Tool Use</strong></summary>
186
246
 
187
- Capture every LLM call your app makes model, latency, token counts, estimated USD cost, and tool-use requests — as breadcrumbs on the error that follows. When a request crashes, you see the chat completion that preceded it: which model was called, how long it took, what it cost, and which tools it asked to invoke.
247
+ Capture your app's LLM calls through a Faraday middleware, OpenTelemetry GenAI spans or a manual notification; nothing is auto-instrumented — as breadcrumbs on the error that follows, with model, latency, token counts, estimated USD cost and tool-use requests. When a request crashes, you see the chat completion that preceded it: which model was called, how long it took, what it cost, and which tools it asked to invoke.
188
248
 
189
249
  - Three capture paths — pick whichever matches your stack
190
250
  - Cost estimated from a built-in pricing table (Claude 4.x, GPT-4o/o1, Gemini 2.5) — override per-model via `config.llm_pricing_overrides`
191
251
  - Tool-call requests summarized inline; tool *execution* spans captured separately via the OTel path
192
- - Content capture (prompts/completions) **OFF by default** — only token counts and metadata are recorded
252
+ - Prompts and completions are **never recorded** — only token counts and metadata (the `llm_observability_content_capture` flag is reserved and currently a no-op)
193
253
  - Same host-app safety guarantees as the rest of the gem — never raises, never blocks the request, every callback rescue-wrapped
194
254
 
195
255
  ```ruby
@@ -343,7 +403,7 @@ config.enable_git_blame = true
343
403
  <details>
344
404
  <summary><strong>Code Path Coverage (Diagnostic Mode)</strong></summary>
345
405
 
346
- Enable coverage via a dashboard button to see which production code paths were executed. Source code viewer overlays green checkmarks on executed lines and gray dots on unexecuted lines. Uses Ruby's `Coverage.setup(oneshot_lines: true)` — near-zero overhead, each line fires once. Zero overhead when off.
406
+ Enable coverage via a dashboard button to see which production code paths were executed. Source code viewer overlays green checkmarks on executed lines and gray dots on unexecuted lines. Uses Ruby's `Coverage.setup(oneshot_lines: true)` — near-zero overhead, each line fires once. Zero overhead when off. Diagnostic mode only: coverage is process-global (a multi-threaded Puma blends requests), held in memory and not persisted. No error tracker integrates this; Coverband does it standalone with persistence.
347
407
 
348
408
  ```ruby
349
409
  config.enable_coverage_tracking = true # shows Enable/Disable buttons on error detail page
@@ -405,9 +465,9 @@ Seven analysis engines built in:
405
465
  1. **Baseline Anomaly Alerts** — Statistical spike detection (mean + std dev) with intelligent cooldown
406
466
  2. **Fuzzy Error Matching** — Jaccard similarity + Levenshtein distance to find related errors
407
467
  3. **Co-occurring Errors** — Detect errors that happen together within configurable time windows
408
- 4. **Error Cascade Detection** — Identify chains (A causes B causes C) with probability and delays
468
+ 4. **Error Cascade Detection** — Identify potential cascades (A is followed by B is followed by C) with probability and delays — temporal association, not proven causation
409
469
  5. **Error Correlation Analysis** — Correlate errors with app versions, git commits, and users
410
- 6. **Platform Comparison** — iOS vs Android vs Web health metrics side-by-side
470
+ 6. **Platform Comparison** — iOS vs Android vs API health metrics side-by-side
411
471
  7. **Occurrence Pattern Detection** — Cyclical patterns (business hours, weekends) and burst detection
412
472
 
413
473
  [Complete documentation →](docs/FEATURES.md#advanced-analytics-features)
@@ -437,7 +497,7 @@ config.enable_instance_variables = true
437
497
  <details>
438
498
  <summary><strong>Swallowed Exception Detection</strong></summary>
439
499
 
440
- Detect exceptions that are raised but silently rescued — the hardest bugs to find. No other error tracker does this.
500
+ Detect exceptions that are raised but silently rescued — the hardest bugs to find. Only Datadog's paid APM detects rescued exceptions (Ruby 3.3+, and only inside a traced request); RED does it free, without an APM span, and aggregates the raise-vs-rescue ratio per location — no other tracker does that.
441
501
 
442
502
  - Uses TracePoint(`:raise`) + TracePoint(`:rescue`) to track exception lifecycle
443
503
  - Identifies code paths where exceptions are caught but never logged or re-raised
@@ -478,7 +538,8 @@ config.enable_diagnostic_dump = true
478
538
  Track Rack Attack security events (throttles, blocklists, tracks) as breadcrumbs attached to errors, with a dedicated summary page.
479
539
 
480
540
  - Captures throttle, blocklist, and track events automatically
481
- - Dashboard page at `/errors/rack_attack_summary` with event breakdown
541
+ - Dashboard page at `/errors/rack_attack_summary` with event breakdown and per-rule stats — rack-attack ships no UI of its own
542
+ - Classifies AI-agent user agents (GPTBot, ClaudeBot, …) on `track` events
482
543
  - Requires breadcrumbs to be enabled
483
544
 
484
545
  ```ruby
@@ -496,7 +557,7 @@ Capture unhandled exceptions that crash the Ruby process via an `at_exit` hook
496
557
  - Disk-based fallback: writes crash data to disk because the database may be unavailable during shutdown
497
558
  - Imported automatically on next boot
498
559
  - Captures exception details, backtrace, uptime, GC stats, thread count, and cause chain
499
- - A self-hosted only feature impossible for SaaS tools
560
+ - Honeybadger, Bugsnag and AppSignal have `at_exit` reporters too; RED's writes to disk and imports at next boot because the database may already be gone during shutdown
500
561
 
501
562
  ```ruby
502
563
  config.enable_crash_capture = true
@@ -617,7 +678,7 @@ end
617
678
 
618
679
  ## Languages
619
680
 
620
- The dashboard, its emails and its notification payloads are translated. Eleven locales ship:
681
+ RED ships in English with machine-translated previews for ten additional languages, covering the dashboard, its emails and its notification payloads. Native-speaking Rails developers are invited to review and improve them; once a locale has been reviewed it will be marked individually as community-reviewed. Eleven locales ship:
621
682
 
622
683
  | Locale | Language | Status |
623
684
  |---|---|---|
@@ -661,13 +722,13 @@ SQLite, PostgreSQL, and MySQL/Trilogy — in either shared or separate-database
661
722
  Yes. It runs entirely inside your own Rails process — no external services, no SDK calling out, no per-event pricing. Error data never leaves your infrastructure.
662
723
 
663
724
  **Does it capture local variables like Sentry?**
664
- Yes — local **and** instance variables at the moment the exception is raised, via `TracePoint(:raise)`, with sensitive-data filtering and configurable limits. This is opt-in and a capability Sentry charges extra for.
725
+ Yes — local **and** instance variables at the moment the exception is raised, via `TracePoint(:raise)`, with sensitive-data filtering and configurable limits. It is opt-in. (Sentry's SDK can also capture locals as an opt-in option; RED adds instance variables and applies your Rails `filter_parameters` automatically.)
665
726
 
666
727
  **Will a flood of errors take down my app?**
667
- No. Storm protection (a circuit breaker with adaptive sampling, **ON by default**) makes the gem degrade itself first during error floods — occurrence counts stay exact while it sheds the expensive work. Measured hot-path overhead is ~2.4µs/error.
728
+ No. Storm protection (a circuit breaker with adaptive sampling, **ON by default**) makes the gem degrade itself first during error floods — occurrence counts stay exact while it sheds the expensive work, and a Storm History page shows exactly what was shed. There is no I/O on the hot path the check is a digest and an atomic increment.
668
729
 
669
730
  **Does it work with my background jobs?**
670
- Yes — it auto-detects and supports Sidekiq, SolidQueue, and GoodJob, and can log errors asynchronously through any of them.
731
+ Yes — errors raised in jobs are captured, and it can log errors asynchronously through Sidekiq or SolidQueue (or the in-process `:async` adapter). Sidekiq, SolidQueue and GoodJob are all auto-detected for the job-queue stats stored on each error.
671
732
 
672
733
  **Does it work with my authentication?**
673
734
  Yes — HTTP Basic Auth out of the box, or a custom `authenticate_with` lambda that integrates with Devise, Warden, or session-based auth.
@@ -694,7 +755,7 @@ Rails 7.0–8.1 and Ruby 3.2–4.0.
694
755
  - **[Batch Operations](docs/guides/BATCH_OPERATIONS.md)** — Bulk resolve/delete
695
756
  - **[Real-Time Updates](docs/guides/REAL_TIME_UPDATES.md)** — Live dashboard
696
757
  - **[Error Trends](docs/guides/ERROR_TREND_VISUALIZATIONS.md)** — Charts and analytics
697
- - **[Translations](docs/guides/TRANSLATIONS.md)** — Seven shipped locales, correcting a string, adding a language
758
+ - **[Translations](docs/guides/TRANSLATIONS.md)** — Eleven shipped locales, correcting a string, adding a language
698
759
 
699
760
  ### Advanced
700
761
  - **[Multi-App Support](docs/MULTI_APP_PERFORMANCE.md)** — Track multiple applications
@@ -703,7 +764,7 @@ Rails 7.0–8.1 and Ruby 3.2–4.0.
703
764
  - **[Customization](docs/CUSTOMIZATION.md)** — Customize everything
704
765
  - **[Database Options](docs/guides/DATABASE_OPTIONS.md)** — Separate database setup
705
766
  - **[Database Optimization](docs/guides/DATABASE_OPTIMIZATION.md)** — Performance tuning
706
- - **[Mobile App Integration](docs/guides/MOBILE_APP_INTEGRATION.md)** — React Native, Flutter, etc.
767
+ - **[Mobile App Integration](docs/guides/MOBILE_APP_INTEGRATION.md)** — log mobile-originated errors through your own API endpoint, tagged by platform
707
768
  - **[FAQ](docs/FAQ.md)** — Common questions answered
708
769
 
709
770
  [View all documentation →](docs/README.md)
@@ -722,7 +783,7 @@ Built with **CQRS (Command/Query Responsibility Segregation)**:
722
783
 
723
784
  ## Testing
724
785
 
725
- 4,200+ tests covering unit, integration, and browser-based system tests.
786
+ An RSpec suite of unit, request and browser-based system specs runs in CI on every supported Rails version (see the Tests badge above); the current count lives in the CI log rather than here, where it would go stale.
726
787
 
727
788
  ```bash
728
789
  bundle exec rspec # Full suite
@@ -6,11 +6,17 @@ module RailsErrorDashboard
6
6
  # Two usage modes:
7
7
  # 1. With a counts hash — dispatched by RackAttackTracker's periodic flush.
8
8
  # Zero I/O in the request path; all DB writes happen here.
9
- # 2. Without arguments — scheduled periodic sweep that flushes the current
10
- # thread's buffer (useful as a cron safety net for low-traffic apps where
11
- # the flush interval may not be reached during a request).
9
+ # 2. Without arguments — sweeps EVERY live thread's buffer.
12
10
  #
13
- # Example cron (via solid_queue or whenever):
11
+ # Mode 2 is now a belt-and-braces backstop, not the primary drain. Buffers are
12
+ # drained at the end of each request and job by the executor hook registered in
13
+ # the engine (see RackAttackTracker#flush_if_due!), and again at process exit.
14
+ # Scheduling this job is therefore optional; it only ever finds counts on
15
+ # threads that are still alive but have not completed a unit of work since
16
+ # their deadline elapsed. It CANNOT recover counts from a thread that has
17
+ # already died — Thread.list does not include it.
18
+ #
19
+ # Optional cron (via solid_queue or whenever):
14
20
  # every 5.minutes { RailsErrorDashboard::RackAttackFlushJob.perform_later }
15
21
  class RackAttackFlushJob < ApplicationJob
16
22
  queue_as :default
@@ -16,6 +16,17 @@ module RailsErrorDashboard
16
16
  # stamped by the first occurrence that claims it, so history migrates
17
17
  # itself without a backfill. An exact match always wins over a NULL one.
18
18
  class FindOrIncrementError
19
+ # Context that describes THIS occurrence rather than the error as a
20
+ # group. It is refreshed on every recurrence so the row always shows the
21
+ # latest moment of failure, not the first one in the 24 h window. Keys
22
+ # absent from @attributes (feature disabled, column not migrated, or a
23
+ # storm :lite capture that shed context) leave the stored payload alone —
24
+ # a shed capture must never blank out a good snapshot.
25
+ REFRESHED_CONTEXT = %i[
26
+ breadcrumbs system_health local_variables instance_variables
27
+ http_method hostname content_type request_duration_ms
28
+ ].freeze
29
+
19
30
  def self.call(error_hash, attributes = {})
20
31
  new(error_hash, attributes).call
21
32
  end
@@ -69,6 +80,13 @@ module RailsErrorDashboard
69
80
  .order(Arel.sql("CASE WHEN environment IS NULL THEN 1 ELSE 0 END"))
70
81
  end
71
82
 
83
+ # The subset of REFRESHED_CONTEXT this occurrence actually captured.
84
+ def latest_context
85
+ REFRESHED_CONTEXT.each_with_object({}) do |key, refreshed|
86
+ refreshed[key] = @attributes[key] unless @attributes[key].nil?
87
+ end
88
+ end
89
+
72
90
  # {} unless this is a legacy NULL-environment row being claimed.
73
91
  def environment_adoption(error)
74
92
  return {} unless ErrorLog.column_names.include?("environment")
@@ -86,6 +104,7 @@ module RailsErrorDashboard
86
104
  request_params: @attributes[:request_params] || error.request_params,
87
105
  user_agent: @attributes[:user_agent] || error.user_agent,
88
106
  ip_address: @attributes[:ip_address] || error.ip_address,
107
+ **latest_context,
89
108
  **environment_adoption(error)
90
109
  )
91
110
  error
@@ -103,6 +122,7 @@ module RailsErrorDashboard
103
122
  request_params: @attributes[:request_params] || error.request_params,
104
123
  user_agent: @attributes[:user_agent] || error.user_agent,
105
124
  ip_address: @attributes[:ip_address] || error.ip_address,
125
+ **latest_context,
106
126
  **environment_adoption(error)
107
127
  }
108
128
  attrs[:reopened_at] = Time.current if ErrorLog.column_names.include?("reopened_at")
@@ -126,6 +146,7 @@ module RailsErrorDashboard
126
146
  retry_existing.update!(
127
147
  occurrence_count: retry_existing.occurrence_count + 1,
128
148
  last_seen_at: Time.current,
149
+ **latest_context,
129
150
  **environment_adoption(retry_existing)
130
151
  )
131
152
  retry_existing
@@ -205,7 +205,7 @@ module RailsErrorDashboard
205
205
  # their own table, independent of error capture (breadcrumbs optional).
206
206
  attr_accessor :enable_rack_attack_tracking # Master switch (default: false)
207
207
  attr_accessor :rack_attack_max_cache_size # Max buffered keys per thread (default: 1000)
208
- attr_accessor :rack_attack_flush_interval # Seconds between DB flushes (default: 60)
208
+ attr_accessor :rack_attack_flush_interval # Seconds between DB flushes (default: 5)
209
209
 
210
210
  # ActionCable event tracking (requires enable_breadcrumbs = true)
211
211
  attr_accessor :enable_actioncable_tracking # Master switch (default: false)
@@ -429,7 +429,12 @@ module RailsErrorDashboard
429
429
  # Persists to its own table; does NOT require breadcrumbs.
430
430
  @enable_rack_attack_tracking = false
431
431
  @rack_attack_max_cache_size = 1000 # Max buffered keys per thread (LRU eviction)
432
- @rack_attack_flush_interval = 60 # Seconds between DB flushes
432
+ # Max age of buffered events before they are written out. Lowered from 60
433
+ # to 5 alongside the end-of-request drain (issue #170): the executor hook
434
+ # gates on this interval, so it is the upper bound on how stale the Rate
435
+ # Limits page can be, not a per-request cost. A flood still collapses to
436
+ # roughly one write per thread per interval.
437
+ @rack_attack_flush_interval = 5 # Seconds between DB flushes
433
438
 
434
439
  # ActionCable event tracking defaults - OFF by default (opt-in, requires breadcrumbs)
435
440
  @enable_actioncable_tracking = false
@@ -628,7 +633,7 @@ module RailsErrorDashboard
628
633
  # Rack::Attack initializer has loaded, so a missing constant here does
629
634
  # not prove it will still be missing at after_initialize (when the
630
635
  # subscriber actually registers). Auto-disabling would break that case.
631
- unless defined?(::Rack::Attack)
636
+ unless rack_attack_defined?
632
637
  warnings << "enable_rack_attack_tracking is enabled but the rack-attack gem " \
633
638
  "does not appear to be loaded. No events will be recorded until " \
634
639
  "Rack::Attack is installed and configured."
@@ -1033,6 +1038,14 @@ module RailsErrorDashboard
1033
1038
 
1034
1039
  # Detect where the engine is mounted in the host app's routes.
1035
1040
  # @return [String] mount path (default: "/red")
1041
+ # Extracted so specs can simulate the gem's absence. rack-attack is in the
1042
+ # dev bundle (so specs can drive the real middleware), which means
1043
+ # ::Rack::Attack is always defined during the suite and absence can no
1044
+ # longer be produced by simply not requiring it.
1045
+ def rack_attack_defined?
1046
+ defined?(::Rack::Attack) ? true : false
1047
+ end
1048
+
1036
1049
  def detect_engine_mount_path
1037
1050
  return "/red" unless defined?(Rails) && Rails.application
1038
1051
 
@@ -86,6 +86,21 @@ module RailsErrorDashboard
86
86
  defined?(Rack::Attack)
87
87
  RailsErrorDashboard::Subscribers::RackAttackSubscriber.subscribe!
88
88
 
89
+ # Drain buffered counts at the end of every request and job.
90
+ #
91
+ # Without this the buffer is only ever drained by a LATER event arriving
92
+ # on the SAME thread (see RackAttackTracker#flush_if_due!), so a rule that
93
+ # matches once stays invisible until the process exits, and counts on a
94
+ # Puma thread that retires are lost outright rather than delayed.
95
+ #
96
+ # to_complete fires after the response body is closed, so the client
97
+ # already has its bytes — this never delays a request (safety rule 2).
98
+ # It also fires when the app raised, and is re-entrant, so nested
99
+ # executor blocks do not double-flush.
100
+ Rails.application.executor.to_complete do
101
+ RailsErrorDashboard::Services::RackAttackTracker.flush_if_due!
102
+ end
103
+
89
104
  # Buffered counts live on the Puma threads that served the requests and
90
105
  # are only written out on the flush interval, which a low-traffic rule
91
106
  # may never reach. Without this, everything still buffered at SIGTERM
@@ -24,7 +24,23 @@ module RailsErrorDashboard
24
24
  # - Async flush via background job
25
25
  class RackAttackTracker
26
26
  COUNTS_THREAD_KEY = :red_rack_attack_counts
27
- FLUSH_THREAD_KEY = :red_rack_attack_last_flush
27
+
28
+ # Monotonic timestamp of the moment the buffer became non-empty — the
29
+ # DEADLINE clock, not a "last flush" clock.
30
+ #
31
+ # WHY THE DISTINCTION MATTERS: this used to hold the last flush time and be
32
+ # seeded lazily inside maybe_flush! with `||= now`, which meant the very
33
+ # first event of a buffer set the clock to now and then compared `now - now
34
+ # >= interval` — false, always. A rule that matched once and never again
35
+ # therefore never flushed at all, and a manual `curl` test showed an empty
36
+ # table indefinitely (issue #170, third report). Seeding when the buffer
37
+ # STARTS filling makes the guarantee "buffered data is never older than
38
+ # flush_interval", which is the property the dashboard actually needs.
39
+ DEADLINE_THREAD_KEY = :red_rack_attack_deadline_at
40
+
41
+ # Kept as an alias so a host or spec holding the old key name still clears
42
+ # the right slot. Both are cleared together in reset!.
43
+ FLUSH_THREAD_KEY = :red_rack_attack_last_flush
28
44
 
29
45
  # Field length caps — must match the column limits in the migration so that
30
46
  # truncation happens before the value ever reaches the unique upsert index.
@@ -38,6 +54,10 @@ module RailsErrorDashboard
38
54
  # eviction. Without this the evicted count vanishes silently and the
39
55
  # dashboard under-reports with no indication anything was lost — the same
40
56
  # problem StormProtection::CountBuffer solves with an overflow counter.
57
+ # Fallback when configuration is unreadable. Must match
58
+ # Configuration#rack_attack_flush_interval's default.
59
+ DEFAULT_FLUSH_INTERVAL = 5
60
+
41
61
  OVERFLOW_RULE = "__overflow__"
42
62
  OVERFLOW_MATCH_TYPE = "overflow"
43
63
 
@@ -69,6 +89,13 @@ module RailsErrorDashboard
69
89
  )
70
90
 
71
91
  counts = (Thread.current[COUNTS_THREAD_KEY] ||= {})
92
+
93
+ # Start the deadline the moment the buffer goes from empty to non-empty.
94
+ # Doing it here (rather than lazily at flush-check time) is what makes
95
+ # "never older than flush_interval" true for a buffer that receives
96
+ # exactly one event and then goes quiet.
97
+ Thread.current[DEADLINE_THREAD_KEY] ||= monotonic_now if counts.empty?
98
+
72
99
  counts[key] = (counts[key] || 0) + 1
73
100
 
74
101
  # LRU eviction — bounds memory under rotating-discriminator attacks.
@@ -98,7 +125,11 @@ module RailsErrorDashboard
98
125
 
99
126
  snapshot = counts.dup
100
127
  counts.clear
101
- Thread.current[FLUSH_THREAD_KEY] = Time.now.to_f
128
+ # Buffer is empty again, so there is nothing to be late: clear the
129
+ # deadline. The next record! reseeds it. Leaving a stale timestamp
130
+ # here would make the very next event look instantly overdue.
131
+ Thread.current[DEADLINE_THREAD_KEY] = nil
132
+ Thread.current[FLUSH_THREAD_KEY] = nil
102
133
 
103
134
  dispatch_flush(snapshot, sync: sync)
104
135
  nil
@@ -132,6 +163,7 @@ module RailsErrorDashboard
132
163
 
133
164
  snapshot = counts.dup
134
165
  counts.clear
166
+ thread[DEADLINE_THREAD_KEY] = nil
135
167
  thread[FLUSH_THREAD_KEY] = nil
136
168
 
137
169
  dispatch_flush(snapshot, sync: true)
@@ -149,10 +181,51 @@ module RailsErrorDashboard
149
181
  nil
150
182
  end
151
183
 
184
+ # Drain this thread's buffer at the end of a unit of work (a request or a
185
+ # job), if it has been waiting longer than flush_interval.
186
+ #
187
+ # WHY THIS EXISTS (issue #170, third report): before this, the ONLY
188
+ # in-process drain was maybe_flush! inside record, so the buffer could
189
+ # only ever be flushed by a LATER event landing on the SAME thread. Two
190
+ # consequences, both reported as "no events are recorded at all":
191
+ #
192
+ # 1. A rule that matched once showed nothing until the process exited.
193
+ # `curl` once, look at the dashboard, see an empty table — forever.
194
+ # 2. Worse, Puma reuses and retires threads. Counts buffered on a thread
195
+ # that dies are unreachable to flush_all_threads! (it walks
196
+ # Thread.list), so they were lost outright, not merely delayed.
197
+ # Measured: 5 of 5 events lost when the serving threads exited.
198
+ #
199
+ # ActiveSupport::Executor#to_complete is the right boundary because Rails
200
+ # already guarantees it runs once per request and once per job. Crucially,
201
+ # ActionDispatch::Executor returns a Rack::BodyProxy and defers the hook
202
+ # until the SERVER CLOSES THE RESPONSE BODY — so this runs after the client
203
+ # has its bytes and cannot delay the response (safety rule 2).
204
+ #
205
+ # The flush is gated on flush_due?, so a flood does not turn into one
206
+ # UPDATE per request — the exact regression #143's buffer exists to
207
+ # prevent. Measured 0.068 ms/req gated vs 0.508 ms/req ungated.
208
+ def flush_if_due!
209
+ return unless enabled?
210
+ return unless flush_due?
211
+
212
+ # sync: the response is already sent, so there is nothing left to block,
213
+ # and enqueueing a job per interval would be more overhead than the
214
+ # single upsert it replaces.
215
+ flush!(sync: true)
216
+ nil
217
+ rescue => e
218
+ RailsErrorDashboard::Logger.debug(
219
+ "[RailsErrorDashboard] RackAttackTracker.flush_if_due! failed: #{e.class} - #{e.message}"
220
+ )
221
+ nil
222
+ end
223
+
152
224
  # Clear thread-local state without persisting. Used by specs and by
153
225
  # thread teardown paths.
154
226
  def reset!
155
227
  Thread.current[COUNTS_THREAD_KEY] = nil
228
+ Thread.current[DEADLINE_THREAD_KEY] = nil
156
229
  Thread.current[FLUSH_THREAD_KEY] = nil
157
230
  nil
158
231
  rescue => e
@@ -179,6 +252,24 @@ module RailsErrorDashboard
179
252
  parts.fill("", parts.length, 6 - parts.length)
180
253
  end
181
254
 
255
+ # Cheap deadline check — a float subtraction, no I/O.
256
+ #
257
+ # Returns true when the buffer has been waiting at least flush_interval.
258
+ # Uses a monotonic clock: Time.now can jump backwards (NTP correction,
259
+ # leap second) and would then defer the flush indefinitely.
260
+ def flush_due?
261
+ deadline = Thread.current[DEADLINE_THREAD_KEY]
262
+ return false if deadline.nil?
263
+
264
+ (monotonic_now - deadline) >= flush_interval
265
+ rescue => e
266
+ false
267
+ end
268
+
269
+ def monotonic_now
270
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
271
+ end
272
+
182
273
  private
183
274
 
184
275
  def enabled?
@@ -215,13 +306,9 @@ module RailsErrorDashboard
215
306
  )
216
307
  end
217
308
 
218
- # Cheap periodic flush check a float subtraction, no I/O.
309
+ # Cheap periodic flush check on the record path no I/O.
219
310
  def maybe_flush!
220
- now = Time.now.to_f
221
- last_flush = Thread.current[FLUSH_THREAD_KEY] ||= now
222
- return unless (now - last_flush) >= flush_interval
223
-
224
- flush!
311
+ flush! if flush_due?
225
312
  end
226
313
 
227
314
  # Dispatch asynchronously so the request path never waits on the DB.
@@ -257,9 +344,9 @@ module RailsErrorDashboard
257
344
  end
258
345
 
259
346
  def flush_interval
260
- RailsErrorDashboard.configuration.rack_attack_flush_interval || 60
347
+ RailsErrorDashboard.configuration.rack_attack_flush_interval || DEFAULT_FLUSH_INTERVAL
261
348
  rescue => e
262
- 60
349
+ DEFAULT_FLUSH_INTERVAL
263
350
  end
264
351
 
265
352
  # Truncate to the column limit and strip the key separator. A rule name
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.11.0"
2
+ VERSION = "0.11.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -219,29 +219,24 @@ dependencies:
219
219
  - - "~>"
220
220
  - !ruby/object:Gem::Version
221
221
  version: '0.15'
222
- description: 'Own your errors. Own your stack. A fully open-source, self-hosted error
223
- tracking Rails engine a free Sentry alternative that runs entirely inside your
224
- own process, with no external services and zero recurring cost. Captures what SaaS
225
- tools charge extra for: local and instance variables at the moment of failure (via
226
- TracePoint), exception cause chains, swallowed-exception detection, breadcrumbs,
227
- and system-health snapshots (GC, memory, threads, connection pool, Puma). Plus N+1
228
- query detection, storm protection (a circuit breaker that shields your app from
229
- error floods, ON by default), multi-app support, error sampling, and async logging
230
- via Sidekiq, SolidQueue, or GoodJob. Runs on SQLite, PostgreSQL, or MySQL/Trilogy
231
- in your app''s existing database or an isolated separate error database. Beautiful
232
- dashboard UI (dark/light), multi-channel notifications (Slack, Email, Discord, PagerDuty,
233
- webhooks), workflow management, advanced analytics, platform detection (iOS/Android/Web/API),
234
- and two-way issue sync with GitHub, GitLab, Codeberg, and Linear. Also: LLM observability,
235
- AI-powered debugging help, and OpenTelemetry span export. The dashboard, mailers
236
- and notification payloads are translated into 11 languages: English, German, Spanish,
237
- French, Brazilian Portuguese, Italian, Polish, Russian, Ukrainian, Japanese, and
238
- Simplified Chinese 1,515 source strings, set via config.dashboard_locale with
239
- a per-user picker. RED translates through its own private I18n backend and never
240
- mutates your app''s I18n; a missing translation falls back to English. Backtraces,
241
- exception names and webhook payload keys stay English by design. The ten non-English
242
- locales are machine-translated and not yet reviewed by native speakers. 5-minute
243
- setup, works out-of-the-box. Rails 7.0-8.1, Ruby 3.2-4.0. BETA: API may change before
244
- v1.0.0. Live demo: https://rails-error-dashboard.anjan.dev (gandalf/youshallnotpass)'
222
+ description: 'Rails Error Dashboard (RED) is an open-source, self-hosted Rails engine
223
+ for investigating production exceptions without sending error data to a monitoring
224
+ vendor. It groups errors and records request context and cause chains and, when
225
+ enabled, breadcrumbs plus local and instance variables captured before Ruby unwinds
226
+ the stack. RED attaches Rails and Ruby runtime health to the error record on every
227
+ captured occurrence, including Active Record pool, Puma, background jobs, GC, memory,
228
+ threads, file descriptors and system pressure. Built-in storm protection progressively
229
+ sheds expensive context and I/O during error floods while retaining useful exemplars
230
+ and exact occurrence counts. Run RED with your application''s database or an isolated
231
+ error database. It supports PostgreSQL, MySQL/Trilogy and SQLite, and includes workflow,
232
+ notifications (Slack, Email, Discord, PagerDuty, webhooks), two-way issue sync with
233
+ GitHub, GitLab, Codeberg and Linear, Copy as RSpec/curl/LLM, swallowed-exception
234
+ detection, LLM observability without prompt capture, OpenTelemetry span export and
235
+ Rails-specific operational views. The dashboard is translated into 11 languages
236
+ (machine-translated outside English, awaiting native review). A self-hosted Sentry
237
+ alternative that keeps error data in your own database. The gem is MIT and free
238
+ forever. Supports Rails 7.0-8.1 and Ruby 3.2-4.0. Beta: APIs may change before 1.0.
239
+ Live demo: https://rails-error-dashboard.anjan.dev'
245
240
  email:
246
241
  - anjan.jagirdar@gmail.com
247
242
  executables: []
@@ -561,7 +556,7 @@ metadata:
561
556
  funding_uri: https://github.com/sponsors/AnjanJ
562
557
  post_install_message: |
563
558
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
564
- RED (Rails Error Dashboard) v0.11.0
559
+ RED (Rails Error Dashboard) v0.11.2
565
560
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
566
561
 
567
562
  First install:
@@ -599,6 +594,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
599
594
  requirements: []
600
595
  rubygems_version: 3.6.9
601
596
  specification_version: 4
602
- summary: Self-hosted error tracking for Rails local variables, system health, separate
603
- or shared database, dashboard in 11 languages. A free, open-source Sentry alternative.
597
+ summary: Rails-native, self-hosted error monitoring with exception-time Ruby state,
598
+ runtime health, and storm-safe capture.
604
599
  test_files: []