rails_error_dashboard 0.8.1 → 0.8.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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -0
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +5 -0
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +12 -0
  5. data/app/jobs/rails_error_dashboard/storm_flush_job.rb +19 -0
  6. data/app/jobs/rails_error_dashboard/storm_notification_job.rb +74 -0
  7. data/app/models/rails_error_dashboard/storm_event.rb +34 -0
  8. data/app/views/layouts/rails_error_dashboard.html.erb +21 -0
  9. data/app/views/rails_error_dashboard/errors/storms.html.erb +91 -0
  10. data/config/routes.rb +1 -0
  11. data/db/migrate/20260306000002_add_instance_variables_to_error_logs.rb +7 -1
  12. data/db/migrate/20260306000003_create_rails_error_dashboard_swallowed_exceptions.rb +4 -0
  13. data/db/migrate/20260307000001_create_rails_error_dashboard_diagnostic_dumps.rb +4 -0
  14. data/db/migrate/20260613000001_create_storm_events.rb +28 -0
  15. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +36 -0
  16. data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +188 -0
  17. data/lib/rails_error_dashboard/commands/log_error.rb +70 -12
  18. data/lib/rails_error_dashboard/configuration.rb +60 -0
  19. data/lib/rails_error_dashboard/queries/storm_history.rb +39 -0
  20. data/lib/rails_error_dashboard/services/storm_protection/circuit_breaker.rb +195 -0
  21. data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +100 -0
  22. data/lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb +123 -0
  23. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +258 -0
  24. data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +12 -0
  25. data/lib/rails_error_dashboard/version.rb +1 -1
  26. data/lib/rails_error_dashboard.rb +6 -0
  27. metadata +13 -2
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Services
5
+ module StormProtection
6
+ # Layer 1: per-fingerprint rate limiting with graceful degradation.
7
+ #
8
+ # Each fingerprint gets a 60-second window. Within the window:
9
+ # - first N events → :full (everything captured)
10
+ # - past N, every Mth event → :lite (row captured, context shed)
11
+ # - everything else → :count_only (in-memory count, flushed later)
12
+ #
13
+ # The first event of every window is ALWAYS at least :lite, so a
14
+ # melting-down fingerprint still has a fresh exemplar each minute —
15
+ # deterministic, unlike rand-based sampling.
16
+ #
17
+ # Calm-mode adaptive context sampling rides the same entries: after K
18
+ # full-context captures per fingerprint per day, context is captured
19
+ # only every Mth time (an error firing 1000×/day doesn't need 1000
20
+ # breadcrumb trails). Occurrence rows are unaffected in calm mode.
21
+ #
22
+ # Concurrency: entries are mutable structs in a Concurrent::Map with
23
+ # plain (unlocked) field increments — races can miscount by a handful
24
+ # of events, which is acceptable for rate limiting. No mutex anywhere.
25
+ #
26
+ # Memory: the map is bounded. Once full, unseen fingerprints are NOT
27
+ # tracked and decide as :full — in calm weather that's harmless; in a
28
+ # storm of unique fingerprints the global breaker (Layer 2) takes over.
29
+ class FingerprintBuckets
30
+ WINDOW_SECONDS = 60
31
+ DAY_SECONDS = 86_400
32
+
33
+ Entry = Struct.new(:window_start, :window_count, :day_start, :day_full_context_count)
34
+
35
+ def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
36
+ @clock = clock
37
+ reset!
38
+ end
39
+
40
+ def reset!
41
+ @entries = Concurrent::Map.new
42
+ end
43
+
44
+ # Decide capture fidelity for one event of this fingerprint.
45
+ # @return [Symbol] :full, :lite, or :count_only
46
+ def decide(gate_key)
47
+ now = @clock.call
48
+ entry = fetch_entry(gate_key, now)
49
+ return :full unless entry # map full — Layer 2 owns the storm case
50
+
51
+ roll_windows(entry, now)
52
+ entry.window_count += 1
53
+ n = entry.window_count
54
+
55
+ if n <= full_per_minute
56
+ decide_calm_context(entry)
57
+ elsif n == full_per_minute + 1 || ((n - full_per_minute) % keep_every).zero?
58
+ :lite
59
+ else
60
+ :count_only
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def fetch_entry(gate_key, now)
67
+ existing = @entries[gate_key]
68
+ return existing if existing
69
+
70
+ # Bounded: never insert past the cap (size check is approximate
71
+ # under concurrency — a few entries over the cap is fine)
72
+ return nil if @entries.size >= max_tracked
73
+
74
+ @entries.compute_if_absent(gate_key) { Entry.new(now, 0, now, 0) }
75
+ end
76
+
77
+ def roll_windows(entry, now)
78
+ if now - entry.window_start >= WINDOW_SECONDS
79
+ entry.window_start = now
80
+ entry.window_count = 0
81
+ end
82
+ if now - entry.day_start >= DAY_SECONDS
83
+ entry.day_start = now
84
+ entry.day_full_context_count = 0
85
+ end
86
+ end
87
+
88
+ # Under the per-minute cap: full context unless this fingerprint has
89
+ # already produced plenty of full-context captures today.
90
+ def decide_calm_context(entry)
91
+ entry.day_full_context_count += 1
92
+ k = entry.day_full_context_count
93
+
94
+ if k <= context_threshold_per_day || (k % context_keep_every).zero?
95
+ :full
96
+ else
97
+ :lite
98
+ end
99
+ end
100
+
101
+ def full_per_minute
102
+ RailsErrorDashboard.configuration.storm_fingerprint_full_per_minute.to_i
103
+ end
104
+
105
+ def keep_every
106
+ [ RailsErrorDashboard.configuration.storm_occurrence_sample_keep_every.to_i, 1 ].max
107
+ end
108
+
109
+ def context_threshold_per_day
110
+ RailsErrorDashboard.configuration.context_sampling_threshold_per_day.to_i
111
+ end
112
+
113
+ def context_keep_every
114
+ [ RailsErrorDashboard.configuration.context_sampling_keep_every.to_i, 1 ].max
115
+ end
116
+
117
+ def max_tracked
118
+ RailsErrorDashboard.configuration.storm_max_tracked_fingerprints.to_i
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Services
5
+ module StormProtection
6
+ # Facade for the storm-protection hot path. One call per capture attempt:
7
+ #
8
+ # Gate.admit!(exception, context) # => :full | :lite | :count_only
9
+ #
10
+ # :full — capture everything (the normal path)
11
+ # :lite — capture the error + occurrence row, shed context
12
+ # (breadcrumbs / system health / locals / ivars)
13
+ # :count_only — nothing stored now; counted in memory, reconciled
14
+ # onto ErrorLog.occurrence_count by the flush job
15
+ #
16
+ # Safety contract (mirrors SwallowedExceptionTracker):
17
+ # - FAILS OPEN: any internal error → :full. Protection must never be
18
+ # the thing that loses an error.
19
+ # - Zero I/O on the hot path. The only DB-adjacent work is enqueueing
20
+ # the flush job at most once per flush interval.
21
+ # - Budget: digest + atomic increment + comparisons (~µs). Benchmarked.
22
+ # - Per-process state; Puma workers each run their own breaker. No
23
+ # thread-locals — shared atomics, so no Thread.current cleanup needed.
24
+ #
25
+ # IMPORTANT ordering: callers must run ExceptionFilter (ignore list +
26
+ # static sampling) BEFORE this gate — ignored exceptions must never
27
+ # count toward storm state or be reconciled into ErrorLogs.
28
+ class Gate
29
+ class << self
30
+ def admit!(exception, context = {})
31
+ return :full unless enabled?
32
+
33
+ state = breaker.record!
34
+ maybe_storm_notification(state)
35
+
36
+ decision = decide(state, exception, context)
37
+ maybe_flush!
38
+ decision
39
+ rescue => e
40
+ RailsErrorDashboard::Logger.error(
41
+ "[RailsErrorDashboard] StormProtection failed open: #{e.class} - #{e.message}"
42
+ )
43
+ :full
44
+ end
45
+
46
+ # While the breaker is not closed, per-error notifications are
47
+ # suppressed (a single storm notification replaces them).
48
+ def notifications_suppressed?
49
+ enabled? && breaker.state != :closed
50
+ rescue
51
+ false
52
+ end
53
+
54
+ # Always-on cap for auto-created issues (a storm of NEW critical
55
+ # fingerprints must not open 500 GitHub/Linear issues). Token
56
+ # bucket: N per rolling window, per process. Each call consumes a
57
+ # token — call only when actually about to create an issue.
58
+ def issue_creation_allowed?
59
+ return true unless enabled?
60
+
61
+ now = monotonic_now
62
+ window = RailsErrorDashboard.configuration.auto_issue_rate_limit_window_minutes.to_i * 60
63
+ limit = RailsErrorDashboard.configuration.auto_issue_rate_limit_count.to_i
64
+
65
+ @issue_window_start ||= now
66
+ @issue_window_count ||= Concurrent::AtomicFixnum.new(0)
67
+
68
+ if now - @issue_window_start >= window
69
+ @issue_window_start = now
70
+ @issue_window_count = Concurrent::AtomicFixnum.new(0)
71
+ end
72
+
73
+ @issue_window_count.increment <= limit
74
+ rescue
75
+ true
76
+ end
77
+
78
+ def state
79
+ enabled? ? breaker.state : :closed
80
+ rescue
81
+ :closed
82
+ end
83
+
84
+ # Exposed for the flush job (episode metadata for storm_events).
85
+ def breaker
86
+ @breaker ||= CircuitBreaker.new
87
+ end
88
+
89
+ def count_buffer
90
+ @count_buffer ||= CountBuffer.new
91
+ end
92
+
93
+ def fingerprint_buckets
94
+ @fingerprint_buckets ||= FingerprintBuckets.new
95
+ end
96
+
97
+ # Test hook + fork hygiene: fresh state, no leftover episodes.
98
+ def reset!
99
+ @breaker = nil
100
+ @count_buffer = nil
101
+ @fingerprint_buckets = nil
102
+ @probe_counter = nil
103
+ @issue_window_start = nil
104
+ @issue_window_count = nil
105
+ @last_flush = nil
106
+ @storm_notified_episode = nil
107
+ end
108
+
109
+ private
110
+
111
+ def enabled?
112
+ RailsErrorDashboard.configuration.enable_storm_protection
113
+ end
114
+
115
+ # All identity computation happens here, once, in locals — no
116
+ # shared mutable caches (thread safety by construction).
117
+ def decide(state, exception, context)
118
+ case state
119
+ when :open
120
+ count!(exception, context)
121
+ :count_only
122
+ when :half_open
123
+ # Probe: a trickle of :lite captures tells us whether the storm
124
+ # has actually subsided; everything else stays counted.
125
+ if (probe_counter.increment % 10).zero?
126
+ :lite
127
+ else
128
+ count!(exception, context)
129
+ :count_only
130
+ end
131
+ when :shedding
132
+ # Never :full while shedding — context capture is request-thread
133
+ # CPU we can't afford. Buckets still apply their per-fingerprint
134
+ # row sampling underneath.
135
+ parts = gate_parts(exception, context)
136
+ if fingerprint_buckets.decide(gate_key(parts)) == :count_only
137
+ count_buffer.record(gate_key(parts), parts)
138
+ :count_only
139
+ else
140
+ :lite
141
+ end
142
+ else # :closed
143
+ parts = gate_parts(exception, context)
144
+ decision = fingerprint_buckets.decide(gate_key(parts))
145
+ count_buffer.record(gate_key(parts), parts) if decision == :count_only
146
+ decision
147
+ end
148
+ end
149
+
150
+ def count!(exception, context)
151
+ parts = gate_parts(exception, context)
152
+ count_buffer.record(gate_key(parts), parts)
153
+ end
154
+
155
+ # Cheap in-process bucketing key. Deliberately NOT the canonical
156
+ # error_hash (that needs application_id = DB); the flush job
157
+ # recomputes the canonical hash from the stored parts.
158
+ def gate_key(parts)
159
+ parts[:gate_key] ||= parts[:custom_hash] || Digest::SHA256.hexdigest(
160
+ "#{parts[:error_class]}|#{ErrorHashGenerator.normalize_message(parts[:message])}|" \
161
+ "#{parts[:first_app_frame]}|#{parts[:controller_name]}|#{parts[:action_name]}"
162
+ )[0..15]
163
+ end
164
+
165
+ def gate_parts(exception, context)
166
+ {
167
+ error_class: exception.class.name,
168
+ message: exception.message.to_s[0, 500],
169
+ first_app_frame: ErrorHashGenerator.extract_app_frame_from_locations(exception) ||
170
+ ErrorHashGenerator.extract_app_frame(exception.backtrace),
171
+ controller_name: context[:controller_name]&.to_s,
172
+ action_name: context[:action_name]&.to_s,
173
+ custom_hash: custom_hash_for(exception, context)
174
+ }
175
+ end
176
+
177
+ # When a custom fingerprint lambda is configured the canonical hash
178
+ # doesn't include application_id, so the gate can compute it exactly
179
+ # — the flush job then reconciles by hash directly.
180
+ def custom_hash_for(exception, context)
181
+ return nil unless RailsErrorDashboard.configuration.custom_fingerprint
182
+
183
+ result = RailsErrorDashboard.configuration.custom_fingerprint.call(exception, context)
184
+ return nil unless result.is_a?(String) && !result.empty?
185
+
186
+ Digest::SHA256.hexdigest(result)[0..15]
187
+ rescue
188
+ nil
189
+ end
190
+
191
+ def probe_counter
192
+ @probe_counter ||= Concurrent::AtomicFixnum.new(0)
193
+ end
194
+
195
+ # Piggyback flush (SwallowedExceptionTracker pattern): cheap
196
+ # timestamp check per admit; enqueue at most once per interval.
197
+ def maybe_flush!
198
+ now = monotonic_now
199
+ interval = RailsErrorDashboard.configuration.storm_flush_interval_seconds.to_i
200
+ return if now - (@last_flush ||= now) < interval
201
+ return unless count_buffer.any? || breaker.episode_snapshot
202
+
203
+ @last_flush = now
204
+ snapshot = count_buffer.snapshot!
205
+ episode = breaker.episode_snapshot
206
+ breaker.clear_closed_episode!
207
+
208
+ StormFlushJob.perform_later(
209
+ entries: snapshot[:entries],
210
+ overflow: snapshot[:overflow],
211
+ episode: serialize_episode(episode)
212
+ )
213
+ rescue => e
214
+ RailsErrorDashboard::Logger.error(
215
+ "[RailsErrorDashboard] Storm flush enqueue failed: #{e.class} - #{e.message}"
216
+ )
217
+ end
218
+
219
+ def serialize_episode(episode)
220
+ return nil unless episode
221
+
222
+ {
223
+ "started_at" => episode[:started_at]&.iso8601,
224
+ "ended_at" => episode[:ended_at]&.iso8601,
225
+ "peak_rate_per_minute" => episode[:peak_rate_per_minute],
226
+ "reached_open" => episode[:reached_open]
227
+ }
228
+ end
229
+
230
+ # One notification per storm episode, on the first transition out
231
+ # of :closed.
232
+ def maybe_storm_notification(state)
233
+ return if state == :closed
234
+ return unless RailsErrorDashboard.configuration.storm_notification
235
+
236
+ episode = breaker.episode_snapshot
237
+ return unless episode
238
+ return if @storm_notified_episode == episode[:started_at]
239
+
240
+ @storm_notified_episode = episode[:started_at]
241
+ StormNotificationJob.perform_later(
242
+ started_at: episode[:started_at].iso8601,
243
+ state: state.to_s
244
+ )
245
+ rescue => e
246
+ RailsErrorDashboard::Logger.error(
247
+ "[RailsErrorDashboard] Storm notification enqueue failed: #{e.class} - #{e.message}"
248
+ )
249
+ end
250
+
251
+ def monotonic_now
252
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
@@ -13,6 +13,18 @@ module RailsErrorDashboard
13
13
  # Called when a new error is first logged
14
14
  def on_error_logged(error_log)
15
15
  return unless should_auto_create?(error_log)
16
+
17
+ # Always-on rate cap (default 5 per 10 min): a storm of NEW distinct
18
+ # fingerprints must not open hundreds of issues. Checked LAST so a
19
+ # token is only consumed when we were actually about to create.
20
+ unless Services::StormProtection::Gate.issue_creation_allowed?
21
+ RailsErrorDashboard::Logger.warn(
22
+ "[RailsErrorDashboard] Auto-issue creation rate limit reached — " \
23
+ "skipping issue for ErrorLog ##{error_log.id} (#{error_log.error_type})"
24
+ )
25
+ return
26
+ end
27
+
16
28
  dashboard_url = Services::NotificationHelpers.dashboard_url(error_log)
17
29
  CreateIssueJob.perform_later(error_log.id, dashboard_url: dashboard_url)
18
30
  rescue => e
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.8.1"
2
+ VERSION = "0.8.2"
3
3
  end
@@ -69,6 +69,10 @@ require "rails_error_dashboard/services/github_issue_client"
69
69
  require "rails_error_dashboard/services/gitlab_issue_client"
70
70
  require "rails_error_dashboard/services/codeberg_issue_client"
71
71
  require "rails_error_dashboard/services/linear_issue_client"
72
+ require "rails_error_dashboard/services/storm_protection/circuit_breaker"
73
+ require "rails_error_dashboard/services/storm_protection/fingerprint_buckets"
74
+ require "rails_error_dashboard/services/storm_protection/count_buffer"
75
+ require "rails_error_dashboard/services/storm_protection/gate"
72
76
  require "rails_error_dashboard/services/database_health_inspector"
73
77
  require "rails_error_dashboard/services/cache_analyzer"
74
78
  require "rails_error_dashboard/services/llm_summary"
@@ -80,6 +84,7 @@ require "rails_error_dashboard/services/diagnostic_dump_generator"
80
84
  require "rails_error_dashboard/services/coverage_tracker"
81
85
  require "rails_error_dashboard/services/digest_builder"
82
86
  require "rails_error_dashboard/queries/user_impact_summary"
87
+ require "rails_error_dashboard/queries/storm_history"
83
88
  require "rails_error_dashboard/subscribers/breadcrumb_subscriber"
84
89
  require "rails_error_dashboard/subscribers/rack_attack_subscriber"
85
90
  require "rails_error_dashboard/subscribers/action_cable_subscriber"
@@ -97,6 +102,7 @@ require "rails_error_dashboard/commands/log_error"
97
102
  require "rails_error_dashboard/commands/resolve_error"
98
103
  require "rails_error_dashboard/commands/create_issue"
99
104
  require "rails_error_dashboard/commands/link_existing_issue"
105
+ require "rails_error_dashboard/commands/flush_storm_counts"
100
106
  require "rails_error_dashboard/services/issue_body_formatter"
101
107
  require "rails_error_dashboard/commands/batch_resolve_errors"
102
108
  require "rails_error_dashboard/commands/batch_delete_errors"
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.8.1
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -262,6 +262,8 @@ files:
262
262
  - app/jobs/rails_error_dashboard/retention_cleanup_job.rb
263
263
  - app/jobs/rails_error_dashboard/scheduled_digest_job.rb
264
264
  - app/jobs/rails_error_dashboard/slack_error_notification_job.rb
265
+ - app/jobs/rails_error_dashboard/storm_flush_job.rb
266
+ - app/jobs/rails_error_dashboard/storm_notification_job.rb
265
267
  - app/jobs/rails_error_dashboard/swallowed_exception_flush_job.rb
266
268
  - app/jobs/rails_error_dashboard/webhook_error_notification_job.rb
267
269
  - app/mailers/rails_error_dashboard/application_mailer.rb
@@ -275,6 +277,7 @@ files:
275
277
  - app/models/rails_error_dashboard/error_log.rb
276
278
  - app/models/rails_error_dashboard/error_logs_record.rb
277
279
  - app/models/rails_error_dashboard/error_occurrence.rb
280
+ - app/models/rails_error_dashboard/storm_event.rb
278
281
  - app/models/rails_error_dashboard/swallowed_exception.rb
279
282
  - app/views/layouts/rails_error_dashboard.html.erb
280
283
  - app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb
@@ -321,6 +324,7 @@ files:
321
324
  - app/views/rails_error_dashboard/errors/settings.html.erb
322
325
  - app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb
323
326
  - app/views/rails_error_dashboard/errors/show.html.erb
327
+ - app/views/rails_error_dashboard/errors/storms.html.erb
324
328
  - app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb
325
329
  - app/views/rails_error_dashboard/errors/user_impact.html.erb
326
330
  - config/routes.rb
@@ -359,6 +363,7 @@ files:
359
363
  - db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb
360
364
  - db/migrate/20260326000001_add_issue_tracking_to_error_logs.rb
361
365
  - db/migrate/20260503000001_backfill_resolved_status.rb
366
+ - db/migrate/20260613000001_create_storm_events.rb
362
367
  - lib/generators/rails_error_dashboard/install/install_generator.rb
363
368
  - lib/generators/rails_error_dashboard/install/templates/README
364
369
  - lib/generators/rails_error_dashboard/install/templates/initializer.rb
@@ -376,6 +381,7 @@ files:
376
381
  - lib/rails_error_dashboard/commands/create_issue.rb
377
382
  - lib/rails_error_dashboard/commands/find_or_create_application.rb
378
383
  - lib/rails_error_dashboard/commands/find_or_increment_error.rb
384
+ - lib/rails_error_dashboard/commands/flush_storm_counts.rb
379
385
  - lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb
380
386
  - lib/rails_error_dashboard/commands/increment_cascade_detection.rb
381
387
  - lib/rails_error_dashboard/commands/link_existing_issue.rb
@@ -431,6 +437,7 @@ files:
431
437
  - lib/rails_error_dashboard/queries/recurring_issues.rb
432
438
  - lib/rails_error_dashboard/queries/release_timeline.rb
433
439
  - lib/rails_error_dashboard/queries/similar_errors.rb
440
+ - lib/rails_error_dashboard/queries/storm_history.rb
434
441
  - lib/rails_error_dashboard/queries/swallowed_exception_summary.rb
435
442
  - lib/rails_error_dashboard/queries/user_impact_summary.rb
436
443
  - lib/rails_error_dashboard/services/analytics_cache_manager.rb
@@ -484,6 +491,10 @@ files:
484
491
  - lib/rails_error_dashboard/services/slack_payload_builder.rb
485
492
  - lib/rails_error_dashboard/services/source_code_reader.rb
486
493
  - lib/rails_error_dashboard/services/statistical_classifier.rb
494
+ - lib/rails_error_dashboard/services/storm_protection/circuit_breaker.rb
495
+ - lib/rails_error_dashboard/services/storm_protection/count_buffer.rb
496
+ - lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb
497
+ - lib/rails_error_dashboard/services/storm_protection/gate.rb
487
498
  - lib/rails_error_dashboard/services/swallowed_exception_tracker.rb
488
499
  - lib/rails_error_dashboard/services/system_health_snapshot.rb
489
500
  - lib/rails_error_dashboard/services/variable_serializer.rb
@@ -512,7 +523,7 @@ metadata:
512
523
  funding_uri: https://github.com/sponsors/AnjanJ
513
524
  post_install_message: |
514
525
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
515
- RED (Rails Error Dashboard) v0.8.1
526
+ RED (Rails Error Dashboard) v0.8.2
516
527
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
517
528
 
518
529
  First install: