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.
- checksums.yaml +4 -4
- data/README.md +22 -0
- data/app/controllers/rails_error_dashboard/application_controller.rb +5 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +12 -0
- data/app/jobs/rails_error_dashboard/storm_flush_job.rb +19 -0
- data/app/jobs/rails_error_dashboard/storm_notification_job.rb +74 -0
- data/app/models/rails_error_dashboard/storm_event.rb +34 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +21 -0
- data/app/views/rails_error_dashboard/errors/storms.html.erb +91 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20260306000002_add_instance_variables_to_error_logs.rb +7 -1
- data/db/migrate/20260306000003_create_rails_error_dashboard_swallowed_exceptions.rb +4 -0
- data/db/migrate/20260307000001_create_rails_error_dashboard_diagnostic_dumps.rb +4 -0
- data/db/migrate/20260613000001_create_storm_events.rb +28 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +36 -0
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +188 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +70 -12
- data/lib/rails_error_dashboard/configuration.rb +60 -0
- data/lib/rails_error_dashboard/queries/storm_history.rb +39 -0
- data/lib/rails_error_dashboard/services/storm_protection/circuit_breaker.rb +195 -0
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +100 -0
- data/lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb +123 -0
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +258 -0
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +12 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +6 -0
- 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
|
|
@@ -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.
|
|
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.
|
|
526
|
+
RED (Rails Error Dashboard) v0.8.2
|
|
516
527
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
517
528
|
|
|
518
529
|
First install:
|