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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +14 -4
  4. data/app/controllers/rails_error_dashboard/webhooks_controller.rb +79 -6
  5. data/app/jobs/rails_error_dashboard/add_issue_recurrence_comment_job.rb +4 -2
  6. data/app/jobs/rails_error_dashboard/application_job.rb +56 -14
  7. data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +22 -4
  8. data/app/jobs/rails_error_dashboard/close_linked_issue_job.rb +4 -2
  9. data/app/jobs/rails_error_dashboard/create_issue_job.rb +1 -1
  10. data/app/jobs/rails_error_dashboard/reopen_linked_issue_job.rb +4 -2
  11. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +25 -0
  12. data/app/jobs/rails_error_dashboard/storm_flush_job.rb +22 -4
  13. data/app/models/rails_error_dashboard/error_log.rb +47 -0
  14. data/app/models/rails_error_dashboard/storm_flush_batch.rb +50 -0
  15. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +27 -1
  16. data/app/views/rails_error_dashboard/errors/_stats.html.erb +6 -0
  17. data/app/views/rails_error_dashboard/errors/analytics.html.erb +6 -0
  18. data/app/views/rails_error_dashboard/errors/overview.html.erb +13 -1
  19. data/config/locales/de.yml +9 -0
  20. data/config/locales/en.yml +35 -0
  21. data/config/locales/es.yml +9 -0
  22. data/config/locales/fr.yml +10 -1
  23. data/config/locales/it.yml +9 -0
  24. data/config/locales/ja.yml +9 -0
  25. data/config/locales/pl.yml +9 -0
  26. data/config/locales/pt-BR.yml +9 -0
  27. data/config/locales/ru.yml +9 -0
  28. data/config/locales/uk.yml +9 -0
  29. data/config/locales/zh-CN.yml +9 -0
  30. data/db/migrate/20260915000001_add_group_identity_unique_index_to_error_logs.rb +207 -0
  31. data/db/migrate/20260915000002_add_issue_repo_identity_to_error_logs.rb +89 -0
  32. data/db/migrate/20260915000003_add_context_provenance_to_error_logs.rb +42 -0
  33. data/db/migrate/20260915000004_create_storm_flush_batches.rb +50 -0
  34. data/lib/rails_error_dashboard/commands/create_issue.rb +10 -2
  35. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +135 -4
  36. data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +139 -34
  37. data/lib/rails_error_dashboard/commands/link_existing_issue.rb +20 -2
  38. data/lib/rails_error_dashboard/commands/log_error.rb +206 -19
  39. data/lib/rails_error_dashboard/configuration.rb +4 -3
  40. data/lib/rails_error_dashboard/engine.rb +28 -0
  41. data/lib/rails_error_dashboard/integrations/tracer.rb +26 -7
  42. data/lib/rails_error_dashboard/queries/analytics_stats.rb +103 -23
  43. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +147 -61
  44. data/lib/rails_error_dashboard/queries/user_impact_summary.rb +57 -7
  45. data/lib/rails_error_dashboard/services/error_hash_generator.rb +61 -19
  46. data/lib/rails_error_dashboard/services/issue_tracker_client.rb +38 -0
  47. data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +22 -3
  48. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +101 -14
  49. data/lib/rails_error_dashboard/version.rb +1 -1
  50. 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
- "#{parts[:error_class]}|#{ErrorHashGenerator.normalize_message(parts[:message])}|" \
171
- "#{parts[:first_app_frame]}|#{parts[:controller_name]}|#{parts[:action_name]}"
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: exception.message.to_s[0, ErrorHashGenerator::HASH_MESSAGE_LIMIT],
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
- reason = (job.respond_to?(:enqueue_error) && job.enqueue_error&.message) || "enqueue returned #{job.inspect}"
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
 
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.11.9"
2
+ VERSION = "0.12.1"
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.9
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 outside English, awaiting native review). A
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.11.9
588
+ RED (Rails Error Dashboard) v0.12.1
583
589
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
584
590
 
585
591
  First install: