rails_error_dashboard 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +10 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +54 -0
- data/app/jobs/rails_error_dashboard/storm_flush_job.rb +7 -4
- data/app/models/rails_error_dashboard/error_log.rb +10 -0
- data/app/models/rails_error_dashboard/event_count.rb +132 -0
- data/app/models/rails_error_dashboard/event_timing_gap.rb +55 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +47 -2
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +2 -0
- data/app/views/rails_error_dashboard/errors/overview.html.erb +12 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +3 -3
- data/config/locales/de.yml +2 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/es.yml +2 -0
- data/config/locales/fr.yml +2 -0
- data/config/locales/it.yml +2 -0
- data/config/locales/ja.yml +2 -0
- data/config/locales/pl.yml +2 -0
- data/config/locales/pt-BR.yml +2 -0
- data/config/locales/ru.yml +2 -0
- data/config/locales/uk.yml +2 -0
- data/config/locales/zh-CN.yml +2 -0
- data/db/migrate/20260919000001_create_event_counts.rb +71 -0
- data/db/migrate/20260920000001_add_buckets_incomplete_to_storm_events.rb +25 -0
- data/db/migrate/20260920000002_create_event_timing_gaps.rb +55 -0
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +70 -4
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +225 -8
- data/lib/rails_error_dashboard/commands/log_error.rb +213 -21
- data/lib/rails_error_dashboard/configuration.rb +20 -0
- data/lib/rails_error_dashboard/engine.rb +13 -0
- data/lib/rails_error_dashboard/manual_error_reporter.rb +16 -5
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +85 -27
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +167 -30
- data/lib/rails_error_dashboard/queries/event_volume.rb +503 -0
- data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +23 -0
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +64 -6
- data/lib/rails_error_dashboard/services/variable_serializer.rb +125 -10
- data/lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb +111 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +37 -2
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +3 -0
- metadata +8 -2
|
@@ -5,6 +5,10 @@ module RailsErrorDashboard
|
|
|
5
5
|
# Query: Fetch dashboard statistics
|
|
6
6
|
# This is a read operation that aggregates error data for the dashboard
|
|
7
7
|
class DashboardStats
|
|
8
|
+
# The widest window any figure on the Overview covers (total_month).
|
|
9
|
+
# The completeness predicate is matched to this, not to "today".
|
|
10
|
+
WIDEST_DISPLAYED_WINDOW = 30.days
|
|
11
|
+
|
|
8
12
|
# One instance answers ONE call: several aggregates (today's event count,
|
|
9
13
|
# the 7-day trend, spike detection) are memoised on it so that they are
|
|
10
14
|
# computed once per call rather than once per card that shows them.
|
|
@@ -28,8 +32,8 @@ module RailsErrorDashboard
|
|
|
28
32
|
# Summing is also exact during a storm: counted-only events never
|
|
29
33
|
# create occurrence rows, but they DO raise occurrence_count.
|
|
30
34
|
total_today: today_event_count,
|
|
31
|
-
total_week:
|
|
32
|
-
total_month:
|
|
35
|
+
total_week: week_event_counts.values.sum,
|
|
36
|
+
total_month: month_event_count,
|
|
33
37
|
unresolved: base_scope.unresolved.count,
|
|
34
38
|
resolved: base_scope.resolved.count,
|
|
35
39
|
reopened: reopened_count,
|
|
@@ -54,6 +58,13 @@ module RailsErrorDashboard
|
|
|
54
58
|
# window, the dimension is incomplete and the page says so
|
|
55
59
|
# rather than presenting an undercount as fact.
|
|
56
60
|
affected_users_incomplete: affected_users_incomplete?,
|
|
61
|
+
# A storm episode in this window lost its per-bucket TIMING
|
|
62
|
+
# evidence, so every time-window figure for it rests on the
|
|
63
|
+
# group's own occurred_at rather than on per-event records.
|
|
64
|
+
# Reported for the same reason as affected_users_incomplete:
|
|
65
|
+
# an incomplete dimension should say so rather than present a
|
|
66
|
+
# figure of unknown completeness as fact.
|
|
67
|
+
event_timing_incomplete: event_timing_incomplete?,
|
|
57
68
|
data_unavailable: false
|
|
58
69
|
}
|
|
59
70
|
end
|
|
@@ -71,6 +82,7 @@ module RailsErrorDashboard
|
|
|
71
82
|
{
|
|
72
83
|
data_unavailable: true,
|
|
73
84
|
affected_users_incomplete: false,
|
|
85
|
+
event_timing_incomplete: false,
|
|
74
86
|
total_today: 0,
|
|
75
87
|
total_week: 0,
|
|
76
88
|
total_month: 0,
|
|
@@ -116,14 +128,29 @@ module RailsErrorDashboard
|
|
|
116
128
|
scope
|
|
117
129
|
end
|
|
118
130
|
|
|
119
|
-
# Total EVENTS in a window
|
|
120
|
-
#
|
|
131
|
+
# Total EVENTS in a window -- the events that HAPPENED in it, not the
|
|
132
|
+
# lifetime volume of the groups first seen in it.
|
|
133
|
+
#
|
|
134
|
+
# This used to filter groups by occurred_at and sum occurrence_count.
|
|
135
|
+
# occurred_at is first-seen and is never rewritten on recurrence, so an
|
|
136
|
+
# error first seen at 23:59 that recurred at 00:01 reported zero errors
|
|
137
|
+
# today and two yesterday. Queries::EventVolume counts per-event records
|
|
138
|
+
# instead: occurrence rows, plus storm-shed time buckets, plus the
|
|
139
|
+
# remainder of any group that has neither.
|
|
121
140
|
def event_count_since(since)
|
|
122
|
-
|
|
141
|
+
Queries::EventVolume.in_window(base_scope, since)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The widest-window total, memoised: the stats hash shows it and the
|
|
145
|
+
# completeness predicate needs the same number. One instance answers one
|
|
146
|
+
# call, and this runs on the capture path via the stats broadcast, so a
|
|
147
|
+
# second identical aggregate here is pure waste.
|
|
148
|
+
def month_event_count
|
|
149
|
+
@month_event_count ||= event_count_since(WIDEST_DISPLAYED_WINDOW.ago)
|
|
123
150
|
end
|
|
124
151
|
|
|
125
152
|
def event_count_between(from, to)
|
|
126
|
-
|
|
153
|
+
Queries::EventVolume.in_window(base_scope, from, to)
|
|
127
154
|
end
|
|
128
155
|
|
|
129
156
|
# Occurrence rows carry the user of EACH event. The group's user_id is
|
|
@@ -156,44 +183,143 @@ module RailsErrorDashboard
|
|
|
156
183
|
false
|
|
157
184
|
end
|
|
158
185
|
|
|
186
|
+
# True when a storm episode overlapping this window degraded without
|
|
187
|
+
# writing its time buckets.
|
|
188
|
+
#
|
|
189
|
+
# Read from the persisted episode rather than recomputed: the flush that
|
|
190
|
+
# knew is long over by the time the dashboard renders, and the flag has
|
|
191
|
+
# to survive a replay. Every guard here is deliberate -- the storm table
|
|
192
|
+
# may not exist, and the column may not be migrated yet on an older host.
|
|
193
|
+
# True when any recorded timing gap overlaps a window this page shows.
|
|
194
|
+
#
|
|
195
|
+
# The page displays today, 7-day AND 30-day figures, so the predicate has
|
|
196
|
+
# to match the WIDEST of them. An earlier version asked only about today
|
|
197
|
+
# and dropped the warning while the weekly and monthly numbers on the
|
|
198
|
+
# same screen still contained the affected events.
|
|
199
|
+
#
|
|
200
|
+
# Read from EventTimingGap rather than the storm episode: the episode is
|
|
201
|
+
# optional (the gate can shed with its breaker closed), and it is written
|
|
202
|
+
# after the counts transaction commits. See the migration for the full
|
|
203
|
+
# history of why this moved.
|
|
204
|
+
def event_timing_incomplete?
|
|
205
|
+
# Nothing left to qualify, so no warning.
|
|
206
|
+
#
|
|
207
|
+
# Gaps are deliberately retained past their own retention while the
|
|
208
|
+
# figures they describe are still shown (see the cleanup job), so a gap
|
|
209
|
+
# can outlive every event it covered -- a group expiring is what
|
|
210
|
+
# removes those events. Warning about a window whose subject matter is
|
|
211
|
+
# gone is noise, and noise trains people to ignore the banner.
|
|
212
|
+
#
|
|
213
|
+
# Asked of surviving GROUPS, not of the event aggregate. The aggregate
|
|
214
|
+
# is the wrong witness here: losing the time buckets is exactly what
|
|
215
|
+
# makes events invisible to it, so an old group that recurs today
|
|
216
|
+
# reports zero for every window (EventVolume can only place an
|
|
217
|
+
# untracked remainder at the group's own occurred_at, which precedes
|
|
218
|
+
# the window -- see untracked_groups) while the events are real, the
|
|
219
|
+
# group is alive and a current gap records them. Reading that zero as
|
|
220
|
+
# "nothing happened" suppressed the banner in precisely the state it
|
|
221
|
+
# exists to announce.
|
|
222
|
+
#
|
|
223
|
+
# last_seen_at is the right evidence because it is what retention
|
|
224
|
+
# deletes on: a group is expired only once it has not been seen for
|
|
225
|
+
# retention_days, so "no group seen in this window" is the same fact as
|
|
226
|
+
# "the covered events are gone" -- and it is indexed, and immune to the
|
|
227
|
+
# timing loss itself.
|
|
228
|
+
#
|
|
229
|
+
# Suppressed only when BOTH witnesses are silent, because neither alone
|
|
230
|
+
# is sufficient and they fail in opposite directions. The aggregate
|
|
231
|
+
# misses an old group's untracked recurrence (the defect above); the
|
|
232
|
+
# liveness check misses the converse, since EventVolume windows
|
|
233
|
+
# occurrence rows and buckets on THEIR OWN timestamps against an
|
|
234
|
+
# unwindowed group set (see group_ids), so a group whose last_seen_at
|
|
235
|
+
# has fallen behind its own event rows can still put events on the page.
|
|
236
|
+
# Requiring both to be empty means the banner can never be dropped
|
|
237
|
+
# while any displayed figure is non-zero, which is the F22 invariant.
|
|
238
|
+
return false unless groups_seen_in_widest_window? || month_event_count.positive?
|
|
239
|
+
|
|
240
|
+
EventTimingGap.affecting?(WIDEST_DISPLAYED_WINDOW.ago, application_id: @application_id)
|
|
241
|
+
rescue StandardError
|
|
242
|
+
false
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Whether any error group was last seen inside the widest displayed
|
|
246
|
+
# window. Existence, not a count: one indexed row settles it, and this
|
|
247
|
+
# runs on the capture path via the stats broadcast.
|
|
248
|
+
#
|
|
249
|
+
# The NULL arm covers rows written before last_seen_at existed, whose
|
|
250
|
+
# occurred_at is the only timestamp they have -- the same COALESCE
|
|
251
|
+
# equivalence the retention job documents, written so each arm keeps its
|
|
252
|
+
# own index.
|
|
253
|
+
def groups_seen_in_widest_window?
|
|
254
|
+
cutoff = WIDEST_DISPLAYED_WINDOW.ago
|
|
255
|
+
base_scope.where(
|
|
256
|
+
"last_seen_at >= :cutoff OR (last_seen_at IS NULL AND occurred_at >= :cutoff)", cutoff: cutoff
|
|
257
|
+
).exists?
|
|
258
|
+
end
|
|
259
|
+
|
|
159
260
|
def reopened_count
|
|
160
261
|
return 0 unless ErrorLog.column_names.include?("reopened_at")
|
|
161
262
|
|
|
162
263
|
base_scope.where.not(reopened_at: nil).count
|
|
163
264
|
end
|
|
164
265
|
|
|
266
|
+
# Top error types in the last 7 days, by EVENTS.
|
|
267
|
+
#
|
|
268
|
+
# Filtering groups by first-seen and summing their LIFETIME
|
|
269
|
+
# occurrence_count answered a different question: an August group that
|
|
270
|
+
# recurred today was excluded entirely, so reopening it produced an empty
|
|
271
|
+
# list while the headline total (already on EventVolume) counted the
|
|
272
|
+
# event. Routed through the same primitive as the total so the page's
|
|
273
|
+
# parts and its whole cannot drift apart.
|
|
165
274
|
def top_errors
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
275
|
+
weekly_volume.by_group_attribute(:error_type)
|
|
276
|
+
.sort_by { |_, count| -count }
|
|
277
|
+
.first(10)
|
|
278
|
+
.to_h
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# One EventVolume for the 7-day window, shared by every weekly breakdown.
|
|
282
|
+
def weekly_volume
|
|
283
|
+
@weekly_volume ||= Queries::EventVolume.new(base_scope, 7.days.ago)
|
|
172
284
|
end
|
|
173
285
|
|
|
174
286
|
# Get 7-day error trend (daily counts)
|
|
287
|
+
# Events per day, placed on the day they HAPPENED. Grouping the ErrorLog
|
|
288
|
+
# table by its own occurred_at put a group's whole lifetime volume on the
|
|
289
|
+
# day it was first seen, so a recurrence never moved the trend.
|
|
290
|
+
#
|
|
291
|
+
# The zero-filled date range is preserved: the chart needs a point for
|
|
292
|
+
# every day in the window, not only the days that had errors.
|
|
175
293
|
def errors_trend_7d
|
|
176
|
-
|
|
177
|
-
base_scope.where("occurred_at >= ?", 7.days.ago)
|
|
178
|
-
.group_by_day(:occurred_at, range: 7.days.ago.to_date..Date.current, default_value: 0)
|
|
179
|
-
.sum(:occurrence_count)
|
|
294
|
+
week_event_counts
|
|
180
295
|
end
|
|
181
296
|
|
|
182
297
|
# Get error counts by severity for last 7 days
|
|
183
298
|
# OPTIMIZED: Use database filtering instead of loading all records into Ruby
|
|
299
|
+
# Weekly events by severity.
|
|
300
|
+
#
|
|
301
|
+
# Folded from the SAME per-type breakdown top_errors uses, rather than
|
|
302
|
+
# four independently scoped sums. Two reasons: it was summing LIFETIME
|
|
303
|
+
# counts of groups born in the window (so a reopened August group scored
|
|
304
|
+
# zero across every severity), and deriving both figures from one
|
|
305
|
+
# breakdown makes them agree by construction instead of by luck --
|
|
306
|
+
# asserted by spec/queries/dashboard_breakdowns_event_volume_spec.rb.
|
|
184
307
|
def errors_by_severity_7d
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
error_type
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
308
|
+
critical = Services::SeverityClassifier::CRITICAL_ERROR_TYPES
|
|
309
|
+
high = Services::SeverityClassifier::HIGH_SEVERITY_ERROR_TYPES
|
|
310
|
+
medium = Services::SeverityClassifier::MEDIUM_SEVERITY_ERROR_TYPES
|
|
311
|
+
|
|
312
|
+
totals = { critical: 0, high: 0, medium: 0, low: 0 }
|
|
313
|
+
weekly_volume.by_group_attribute(:error_type).each do |error_type, count|
|
|
314
|
+
bucket =
|
|
315
|
+
if critical.include?(error_type) then :critical
|
|
316
|
+
elsif high.include?(error_type) then :high
|
|
317
|
+
elsif medium.include?(error_type) then :medium
|
|
318
|
+
else :low
|
|
319
|
+
end
|
|
320
|
+
totals[bucket] += count
|
|
321
|
+
end
|
|
322
|
+
totals
|
|
197
323
|
end
|
|
198
324
|
|
|
199
325
|
# Detect if there's an error spike
|
|
@@ -241,8 +367,19 @@ module RailsErrorDashboard
|
|
|
241
367
|
info
|
|
242
368
|
end
|
|
243
369
|
|
|
370
|
+
# Today, yesterday and the 7-day total all come from ONE daily breakdown.
|
|
371
|
+
# Each window is three queries (occurrence rows, shed buckets, untracked
|
|
372
|
+
# remainder), and this method runs on the capture path via the stats
|
|
373
|
+
# broadcast, so asking per window multiplied the query count.
|
|
374
|
+
def week_event_counts
|
|
375
|
+
@week_event_counts ||= begin
|
|
376
|
+
counts = Queries::EventVolume.by_day(base_scope, 7.days.ago)
|
|
377
|
+
(7.days.ago.to_date..Date.current).to_h { |day| [ day, counts[day] || 0 ] }
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
244
381
|
def today_event_count
|
|
245
|
-
@today_event_count ||=
|
|
382
|
+
@today_event_count ||= week_event_counts[Date.current].to_i
|
|
246
383
|
end
|
|
247
384
|
|
|
248
385
|
# Every anomalous (error_type, platform) pair, from a fixed number of
|
|
@@ -340,7 +477,7 @@ module RailsErrorDashboard
|
|
|
340
477
|
|
|
341
478
|
def compute_trend_percentage
|
|
342
479
|
today = today_event_count
|
|
343
|
-
yesterday =
|
|
480
|
+
yesterday = week_event_counts[Date.current - 1].to_i
|
|
344
481
|
|
|
345
482
|
return 0.0 if today.zero? && yesterday.zero?
|
|
346
483
|
return 100.0 if yesterday.zero? && today.positive?
|