rails_error_dashboard 0.11.9 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +14 -4
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +79 -6
- data/app/jobs/rails_error_dashboard/add_issue_recurrence_comment_job.rb +4 -2
- data/app/jobs/rails_error_dashboard/application_job.rb +56 -14
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +22 -4
- data/app/jobs/rails_error_dashboard/close_linked_issue_job.rb +4 -2
- data/app/jobs/rails_error_dashboard/create_issue_job.rb +1 -1
- data/app/jobs/rails_error_dashboard/reopen_linked_issue_job.rb +4 -2
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +25 -0
- data/app/jobs/rails_error_dashboard/storm_flush_job.rb +22 -4
- data/app/models/rails_error_dashboard/error_log.rb +47 -0
- data/app/models/rails_error_dashboard/storm_flush_batch.rb +50 -0
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +27 -1
- data/app/views/rails_error_dashboard/errors/_stats.html.erb +6 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +6 -0
- data/app/views/rails_error_dashboard/errors/overview.html.erb +13 -1
- data/config/locales/de.yml +9 -0
- data/config/locales/en.yml +35 -0
- data/config/locales/es.yml +9 -0
- data/config/locales/fr.yml +10 -1
- data/config/locales/it.yml +9 -0
- data/config/locales/ja.yml +9 -0
- data/config/locales/pl.yml +9 -0
- data/config/locales/pt-BR.yml +9 -0
- data/config/locales/ru.yml +9 -0
- data/config/locales/uk.yml +9 -0
- data/config/locales/zh-CN.yml +9 -0
- data/db/migrate/20260915000001_add_group_identity_unique_index_to_error_logs.rb +207 -0
- data/db/migrate/20260915000002_add_issue_repo_identity_to_error_logs.rb +89 -0
- data/db/migrate/20260915000003_add_context_provenance_to_error_logs.rb +42 -0
- data/db/migrate/20260915000004_create_storm_flush_batches.rb +50 -0
- data/lib/rails_error_dashboard/commands/create_issue.rb +10 -2
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +135 -4
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +139 -34
- data/lib/rails_error_dashboard/commands/link_existing_issue.rb +20 -2
- data/lib/rails_error_dashboard/commands/log_error.rb +206 -19
- data/lib/rails_error_dashboard/configuration.rb +4 -3
- data/lib/rails_error_dashboard/engine.rb +28 -0
- data/lib/rails_error_dashboard/integrations/tracer.rb +26 -7
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +103 -23
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +147 -61
- data/lib/rails_error_dashboard/queries/user_impact_summary.rb +57 -7
- data/lib/rails_error_dashboard/services/error_hash_generator.rb +61 -19
- data/lib/rails_error_dashboard/services/issue_tracker_client.rb +38 -0
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +22 -3
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +101 -14
- data/lib/rails_error_dashboard/version.rb +1 -1
- metadata +9 -3
|
@@ -94,6 +94,65 @@ module RailsErrorDashboard
|
|
|
94
94
|
@fingerprint_buckets ||= FingerprintBuckets.new
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# Drain the count buffer if the flush interval has elapsed.
|
|
98
|
+
#
|
|
99
|
+
# Wired to Rails.application.executor.to_complete, so it runs at the
|
|
100
|
+
# end of every request and every job. Without it the buffer is only
|
|
101
|
+
# ever drained by a LATER admit! on this process (see maybe_flush!),
|
|
102
|
+
# and the tail of a burst sits in memory indefinitely: when the
|
|
103
|
+
# errors stop, so does the only thing that would write them out.
|
|
104
|
+
#
|
|
105
|
+
# Interval-gated, so a flood does not turn into one enqueue per
|
|
106
|
+
# request -- the cost per call is a monotonic clock read and a
|
|
107
|
+
# comparison.
|
|
108
|
+
def flush_if_due!
|
|
109
|
+
return unless enabled?
|
|
110
|
+
|
|
111
|
+
maybe_flush!
|
|
112
|
+
nil
|
|
113
|
+
rescue => e
|
|
114
|
+
RailsErrorDashboard::Logger.debug(
|
|
115
|
+
"[RailsErrorDashboard] Gate.flush_if_due! failed: #{e.class} - #{e.message}"
|
|
116
|
+
)
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Drain everything still buffered, NOW, whatever the interval says.
|
|
121
|
+
#
|
|
122
|
+
# Wired to at_exit. Everything counted since the last flush lives in
|
|
123
|
+
# this process's memory, so without this every deploy (SIGTERM) drops
|
|
124
|
+
# the tail of whatever was in flight.
|
|
125
|
+
#
|
|
126
|
+
# Writes SYNCHRONOUSLY rather than enqueueing: at process exit a job
|
|
127
|
+
# handed to the queue may never be picked up, and on an in-process
|
|
128
|
+
# adapter it certainly will not. This is the same choice
|
|
129
|
+
# RackAttackTracker#flush_all_threads! makes for the same reason.
|
|
130
|
+
#
|
|
131
|
+
# at_exit, not Signal.trap -- trapping would clobber Puma's USR1/USR2
|
|
132
|
+
# handlers (safety rule 9).
|
|
133
|
+
def drain!
|
|
134
|
+
return unless enabled?
|
|
135
|
+
return unless count_buffer.any? || breaker.episode_snapshot
|
|
136
|
+
|
|
137
|
+
snapshot = count_buffer.snapshot!
|
|
138
|
+
episode = breaker.episode_snapshot
|
|
139
|
+
|
|
140
|
+
Commands::FlushStormCounts.call(
|
|
141
|
+
entries: snapshot[:entries],
|
|
142
|
+
overflow: snapshot[:overflow],
|
|
143
|
+
episode: serialize_episode(episode),
|
|
144
|
+
batch_id: snapshot[:batch_id]
|
|
145
|
+
)
|
|
146
|
+
breaker.clear_closed_episode!
|
|
147
|
+
nil
|
|
148
|
+
rescue => e
|
|
149
|
+
# A drain at shutdown must never raise into whatever is exiting.
|
|
150
|
+
RailsErrorDashboard::Logger.error(
|
|
151
|
+
"[RailsErrorDashboard] Gate.drain! failed: #{e.class} - #{e.message}"
|
|
152
|
+
)
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
97
156
|
# Test hook + fork hygiene: fresh state, no leftover episodes.
|
|
98
157
|
def reset!
|
|
99
158
|
@breaker = nil
|
|
@@ -167,14 +226,37 @@ module RailsErrorDashboard
|
|
|
167
226
|
end
|
|
168
227
|
|
|
169
228
|
def identity_key(parts)
|
|
170
|
-
|
|
171
|
-
|
|
229
|
+
parts[:opaque_identity] || ErrorHashGenerator.opaque_identity(
|
|
230
|
+
error_class: parts[:error_class],
|
|
231
|
+
normalized_message: ErrorHashGenerator.normalize_message(parts[:message]),
|
|
232
|
+
frames: parts[:first_app_frame],
|
|
233
|
+
controller_name: parts[:controller_name],
|
|
234
|
+
action_name: parts[:action_name]
|
|
235
|
+
)
|
|
172
236
|
end
|
|
173
237
|
|
|
174
238
|
def gate_parts(exception, context)
|
|
239
|
+
raw_message = exception.message.to_s[0, ErrorHashGenerator::HASH_MESSAGE_LIMIT]
|
|
240
|
+
|
|
175
241
|
{
|
|
176
242
|
error_class: exception.class.name,
|
|
177
|
-
message
|
|
243
|
+
# The identity is hashed from the RAW message here, on the hot
|
|
244
|
+
# path, and only the digest is buffered. The message itself is
|
|
245
|
+
# redacted before it goes in the buffer, because the buffer is
|
|
246
|
+
# shipped to StormFlushJob -- a durable queue on Sidekiq or Solid
|
|
247
|
+
# Queue. It survives as an exemplar for the minimal ErrorLog row
|
|
248
|
+
# a first-seen fingerprint gets, which FlushStormCounts would
|
|
249
|
+
# redact at INSERT anyway; doing it here means the secret never
|
|
250
|
+
# reaches the queue's backing store at all.
|
|
251
|
+
opaque_identity: ErrorHashGenerator.opaque_identity(
|
|
252
|
+
error_class: exception.class.name,
|
|
253
|
+
normalized_message: ErrorHashGenerator.normalize_message(raw_message),
|
|
254
|
+
frames: ErrorHashGenerator.extract_app_frame_from_locations(exception) ||
|
|
255
|
+
ErrorHashGenerator.extract_app_frame(exception.backtrace),
|
|
256
|
+
controller_name: context[:controller_name]&.to_s,
|
|
257
|
+
action_name: context[:action_name]&.to_s
|
|
258
|
+
),
|
|
259
|
+
message: redact(raw_message),
|
|
178
260
|
first_app_frame: ErrorHashGenerator.extract_app_frame_from_locations(exception) ||
|
|
179
261
|
ErrorHashGenerator.extract_app_frame(exception.backtrace),
|
|
180
262
|
controller_name: context[:controller_name]&.to_s,
|
|
@@ -201,6 +283,18 @@ module RailsErrorDashboard
|
|
|
201
283
|
nil
|
|
202
284
|
end
|
|
203
285
|
|
|
286
|
+
# Memoized ActiveSupport::ParameterFilter, no I/O -- safe on the hot
|
|
287
|
+
# path. Fails open to the raw message only if filtering itself
|
|
288
|
+
# breaks, which FlushStormCounts would then still redact at INSERT.
|
|
289
|
+
def redact(message)
|
|
290
|
+
return message if message.blank?
|
|
291
|
+
return message unless RailsErrorDashboard.configuration.filter_sensitive_data
|
|
292
|
+
|
|
293
|
+
SensitiveDataFilter.filter_attributes({ message: message })[:message] || message
|
|
294
|
+
rescue StandardError
|
|
295
|
+
message
|
|
296
|
+
end
|
|
297
|
+
|
|
204
298
|
def probe_counter
|
|
205
299
|
@probe_counter ||= Concurrent::AtomicFixnum.new(0)
|
|
206
300
|
end
|
|
@@ -221,14 +315,14 @@ module RailsErrorDashboard
|
|
|
221
315
|
job = StormFlushJob.perform_later(
|
|
222
316
|
entries: snapshot[:entries],
|
|
223
317
|
overflow: snapshot[:overflow],
|
|
224
|
-
episode: serialize_episode(episode)
|
|
318
|
+
episode: serialize_episode(episode),
|
|
319
|
+
batch_id: snapshot[:batch_id]
|
|
225
320
|
)
|
|
226
321
|
# Active Job swallows ActiveJob::EnqueueError and returns false
|
|
227
322
|
# (and a job that was not enqueued says so) — a failed handoff
|
|
228
323
|
# that never raises.
|
|
229
|
-
unless enqueued?(job)
|
|
230
|
-
|
|
231
|
-
raise "StormFlushJob was not enqueued: #{reason}"
|
|
324
|
+
unless ApplicationJob.enqueued?(job)
|
|
325
|
+
raise "StormFlushJob was not enqueued: #{ApplicationJob.enqueue_failure_reason(job)}"
|
|
232
326
|
end
|
|
233
327
|
rescue => e
|
|
234
328
|
# The queue is often the very thing that is down during a storm
|
|
@@ -249,13 +343,6 @@ module RailsErrorDashboard
|
|
|
249
343
|
)
|
|
250
344
|
end
|
|
251
345
|
|
|
252
|
-
def enqueued?(job)
|
|
253
|
-
return false unless job
|
|
254
|
-
return job.successfully_enqueued? if job.respond_to?(:successfully_enqueued?)
|
|
255
|
-
|
|
256
|
-
true
|
|
257
|
-
end
|
|
258
|
-
|
|
259
346
|
def serialize_episode(episode)
|
|
260
347
|
return nil unless episode
|
|
261
348
|
|
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.
|
|
4
|
+
version: 0.12.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -247,7 +247,8 @@ description: |
|
|
|
247
247
|
|
|
248
248
|
Run RED with your application's database or an isolated error database, on
|
|
249
249
|
PostgreSQL, MySQL/Trilogy or SQLite. The dashboard is translated into 11
|
|
250
|
-
languages (machine-translated
|
|
250
|
+
languages (French native-reviewed; the rest machine-translated and awaiting
|
|
251
|
+
native review). A
|
|
251
252
|
self-hosted Sentry alternative that keeps error data in your own database. The
|
|
252
253
|
gem is MIT and free forever.
|
|
253
254
|
|
|
@@ -308,6 +309,7 @@ files:
|
|
|
308
309
|
- app/models/rails_error_dashboard/error_occurrence.rb
|
|
309
310
|
- app/models/rails_error_dashboard/rack_attack_event.rb
|
|
310
311
|
- app/models/rails_error_dashboard/storm_event.rb
|
|
312
|
+
- app/models/rails_error_dashboard/storm_flush_batch.rb
|
|
311
313
|
- app/models/rails_error_dashboard/swallowed_exception.rb
|
|
312
314
|
- app/views/layouts/rails_error_dashboard.html.erb
|
|
313
315
|
- app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb
|
|
@@ -411,6 +413,10 @@ files:
|
|
|
411
413
|
- db/migrate/20260908000001_add_release_to_error_occurrences.rb
|
|
412
414
|
- db/migrate/20260908220001_add_missing_postgresql_indexes_to_error_logs.rb
|
|
413
415
|
- db/migrate/20260909000001_replace_message_gin_with_searchable_text_index.rb
|
|
416
|
+
- db/migrate/20260915000001_add_group_identity_unique_index_to_error_logs.rb
|
|
417
|
+
- db/migrate/20260915000002_add_issue_repo_identity_to_error_logs.rb
|
|
418
|
+
- db/migrate/20260915000003_add_context_provenance_to_error_logs.rb
|
|
419
|
+
- db/migrate/20260915000004_create_storm_flush_batches.rb
|
|
414
420
|
- lib/generators/rails_error_dashboard/install/install_generator.rb
|
|
415
421
|
- lib/generators/rails_error_dashboard/install/templates/README
|
|
416
422
|
- lib/generators/rails_error_dashboard/install/templates/initializer.rb
|
|
@@ -579,7 +585,7 @@ metadata:
|
|
|
579
585
|
funding_uri: https://github.com/sponsors/AnjanJ
|
|
580
586
|
post_install_message: |
|
|
581
587
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
582
|
-
RED (Rails Error Dashboard) v0.
|
|
588
|
+
RED (Rails Error Dashboard) v0.12.1
|
|
583
589
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
584
590
|
|
|
585
591
|
First install:
|