rails_error_dashboard 0.8.4 → 0.9.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +107 -4
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +91 -73
  5. data/app/controllers/rails_error_dashboard/locales_controller.rb +87 -0
  6. data/app/helpers/rails_error_dashboard/application_helper.rb +20 -14
  7. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +15 -6
  8. data/app/helpers/rails_error_dashboard/i18n_helper.rb +154 -0
  9. data/app/helpers/rails_error_dashboard/mailer_i18n_helper.rb +69 -0
  10. data/app/helpers/rails_error_dashboard/overview_helper.rb +18 -6
  11. data/app/helpers/rails_error_dashboard/user_agent_helper.rb +25 -11
  12. data/app/jobs/rails_error_dashboard/application_job.rb +5 -0
  13. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +18 -16
  14. data/app/jobs/rails_error_dashboard/concerns/localized_job.rb +83 -0
  15. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -2
  16. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +4 -2
  17. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +6 -2
  18. data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +8 -3
  19. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +6 -4
  20. data/app/jobs/rails_error_dashboard/storm_notification_job.rb +26 -8
  21. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +4 -2
  22. data/app/mailers/rails_error_dashboard/application_mailer.rb +14 -0
  23. data/app/mailers/rails_error_dashboard/digest_mailer.rb +18 -2
  24. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +19 -2
  25. data/app/views/layouts/rails_error_dashboard.html.erb +255 -92
  26. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +23 -22
  27. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +30 -19
  28. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +18 -16
  29. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +21 -14
  30. data/app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb +11 -7
  31. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +45 -38
  32. data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +17 -14
  33. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +27 -9
  34. data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +25 -21
  35. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +25 -22
  36. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +24 -12
  37. data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +12 -11
  38. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +19 -13
  39. data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +14 -14
  40. data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +8 -8
  41. data/app/views/rails_error_dashboard/errors/_modals.html.erb +60 -52
  42. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +59 -49
  43. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +23 -20
  44. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +14 -14
  45. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +118 -93
  46. data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +18 -12
  47. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -5
  48. data/app/views/rails_error_dashboard/errors/_stats.html.erb +5 -5
  49. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +7 -7
  50. data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +15 -12
  51. data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +25 -25
  52. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -28
  53. data/app/views/rails_error_dashboard/errors/analytics.html.erb +82 -82
  54. data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +27 -27
  55. data/app/views/rails_error_dashboard/errors/correlation.html.erb +61 -57
  56. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +64 -65
  57. data/app/views/rails_error_dashboard/errors/deprecations.html.erb +23 -23
  58. data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +48 -36
  59. data/app/views/rails_error_dashboard/errors/index.html.erb +121 -70
  60. data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +30 -30
  61. data/app/views/rails_error_dashboard/errors/llm_health_summary.html.erb +38 -39
  62. data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +28 -25
  63. data/app/views/rails_error_dashboard/errors/overview.html.erb +45 -39
  64. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +45 -44
  65. data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +30 -29
  66. data/app/views/rails_error_dashboard/errors/releases.html.erb +46 -47
  67. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +48 -44
  68. data/app/views/rails_error_dashboard/errors/settings.html.erb +122 -112
  69. data/app/views/rails_error_dashboard/errors/show.html.erb +44 -37
  70. data/app/views/rails_error_dashboard/errors/storms.html.erb +30 -24
  71. data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +26 -26
  72. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +31 -30
  73. data/config/locales/de.yml +2007 -0
  74. data/config/locales/en.yml +2719 -0
  75. data/config/locales/es.yml +2079 -0
  76. data/config/locales/fr.yml +2082 -0
  77. data/config/locales/it.yml +2060 -0
  78. data/config/locales/ja.yml +1802 -0
  79. data/config/locales/pl.yml +2108 -0
  80. data/config/locales/pt-BR.yml +2064 -0
  81. data/config/locales/ru.yml +2109 -0
  82. data/config/locales/uk.yml +2106 -0
  83. data/config/locales/zh-CN.yml +1791 -0
  84. data/config/routes.rb +5 -0
  85. data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +9 -3
  86. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +3 -1
  87. data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +4 -2
  88. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +4 -2
  89. data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +4 -2
  90. data/lib/rails_error_dashboard/commands/create_issue.rb +5 -3
  91. data/lib/rails_error_dashboard/commands/link_existing_issue.rb +4 -2
  92. data/lib/rails_error_dashboard/commands/log_error.rb +1 -1
  93. data/lib/rails_error_dashboard/configuration.rb +23 -6
  94. data/lib/rails_error_dashboard/current.rb +56 -0
  95. data/lib/rails_error_dashboard/i18n_store.rb +293 -0
  96. data/lib/rails_error_dashboard/private_backend.rb +213 -0
  97. data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +89 -22
  98. data/lib/rails_error_dashboard/services/digest_builder.rb +21 -7
  99. data/lib/rails_error_dashboard/services/discord_payload_builder.rb +27 -15
  100. data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +10 -5
  101. data/lib/rails_error_dashboard/services/localized_time_formatter.rb +117 -0
  102. data/lib/rails_error_dashboard/services/notification_helpers.rb +87 -4
  103. data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +22 -5
  104. data/lib/rails_error_dashboard/services/slack_payload_builder.rb +48 -28
  105. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -1
  106. data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +15 -1
  107. data/lib/rails_error_dashboard/translation.rb +39 -0
  108. data/lib/rails_error_dashboard/version.rb +1 -1
  109. data/lib/rails_error_dashboard.rb +4 -0
  110. data/lib/tasks/error_dashboard.rake +5 -1
  111. metadata +35 -8
@@ -5,13 +5,15 @@ module RailsErrorDashboard
5
5
  class DiscordErrorNotificationJob < ApplicationJob
6
6
  queue_as :default
7
7
 
8
- def perform(error_log_id)
8
+ # @param locale [String, nil] resolved at enqueue time. nil for jobs
9
+ # enqueued by a pre-Phase-4 version still draining from the queue.
10
+ def perform(error_log_id, locale = nil)
9
11
  error_log = ErrorLog.find(error_log_id)
10
12
  webhook_url = RailsErrorDashboard.configuration.discord_webhook_url
11
13
 
12
14
  return unless webhook_url.present?
13
15
 
14
- payload = Services::DiscordPayloadBuilder.call(error_log)
16
+ payload = Services::DiscordPayloadBuilder.call(error_log, locale: job_locale(locale))
15
17
  post_json(webhook_url, payload)
16
18
  rescue StandardError => e
17
19
  Rails.logger.error("[RailsErrorDashboard] Failed to send Discord notification: #{e.message}")
@@ -4,14 +4,16 @@ module RailsErrorDashboard
4
4
  class EmailErrorNotificationJob < ApplicationJob
5
5
  queue_as :error_notifications
6
6
 
7
- def perform(error_log_id)
7
+ # @param locale [String, nil] resolved at enqueue time. nil for jobs
8
+ # enqueued by a pre-Phase-4 version still draining from the queue.
9
+ def perform(error_log_id, locale = nil)
8
10
  error_log = ErrorLog.find_by(id: error_log_id)
9
11
  return unless error_log
10
12
 
11
13
  recipients = RailsErrorDashboard.configuration.notification_email_recipients
12
14
  return unless recipients.present?
13
15
 
14
- ErrorNotificationMailer.error_alert(error_log, recipients).deliver_now
16
+ ErrorNotificationMailer.error_alert(error_log, recipients, locale: job_locale(locale)).deliver_now
15
17
  rescue => e
16
18
  Rails.logger.error("[RailsErrorDashboard] Failed to send email notification: #{e.message}")
17
19
  Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
@@ -8,7 +8,9 @@ module RailsErrorDashboard
8
8
 
9
9
  PAGERDUTY_EVENTS_API = "https://events.pagerduty.com/v2/enqueue"
10
10
 
11
- def perform(error_log_id)
11
+ # @param locale [String, nil] resolved at enqueue time. nil for jobs
12
+ # enqueued by a pre-Phase-4 version still draining from the queue.
13
+ def perform(error_log_id, locale = nil)
12
14
  error_log = ErrorLog.find(error_log_id)
13
15
 
14
16
  # Only trigger PagerDuty for critical errors
@@ -17,7 +19,9 @@ module RailsErrorDashboard
17
19
  routing_key = RailsErrorDashboard.configuration.pagerduty_integration_key
18
20
  return unless routing_key.present?
19
21
 
20
- payload = Services::PagerdutyPayloadBuilder.call(error_log, routing_key: routing_key)
22
+ payload = Services::PagerdutyPayloadBuilder.call(
23
+ error_log, routing_key: routing_key, locale: job_locale(locale)
24
+ )
21
25
  response = post_json(PAGERDUTY_EVENTS_API, payload)
22
26
 
23
27
  unless response_success?(response)
@@ -12,18 +12,23 @@ module RailsErrorDashboard
12
12
  class ScheduledDigestJob < ApplicationJob
13
13
  queue_as :default
14
14
 
15
- def perform(period: "daily", application_id: nil)
15
+ # @param locale [String, nil] resolved at enqueue time. nil for jobs
16
+ # enqueued by a pre-Phase-4 version still draining from the queue.
17
+ def perform(period: "daily", application_id: nil, locale: nil)
16
18
  return unless RailsErrorDashboard.configuration.enable_scheduled_digests
17
19
 
18
20
  recipients = effective_recipients
19
21
  return if recipients.blank?
20
22
 
23
+ resolved_locale = job_locale(locale)
24
+
21
25
  digest = Services::DigestBuilder.call(
22
26
  period: period.to_sym,
23
- application_id: application_id
27
+ application_id: application_id,
28
+ locale: resolved_locale
24
29
  )
25
30
 
26
- DigestMailer.digest_summary(digest, recipients).deliver_now
31
+ DigestMailer.digest_summary(digest, recipients, locale: resolved_locale).deliver_now
27
32
  rescue => e
28
33
  Rails.logger.error("[RailsErrorDashboard] ScheduledDigestJob failed: #{e.class}: #{e.message}")
29
34
  end
@@ -4,21 +4,23 @@ module RailsErrorDashboard
4
4
  class SlackErrorNotificationJob < ApplicationJob
5
5
  queue_as :error_notifications
6
6
 
7
- def perform(error_log_id)
7
+ # @param locale [String, nil] resolved at enqueue time. nil for jobs
8
+ # enqueued by a pre-Phase-4 version still draining from the queue.
9
+ def perform(error_log_id, locale = nil)
8
10
  error_log = ErrorLog.find_by(id: error_log_id)
9
11
  return unless error_log
10
12
 
11
13
  webhook_url = RailsErrorDashboard.configuration.slack_webhook_url
12
14
  return unless webhook_url.present?
13
15
 
14
- send_slack_notification(error_log, webhook_url)
16
+ send_slack_notification(error_log, webhook_url, job_locale(locale))
15
17
  rescue => e
16
18
  Rails.logger.error("Failed to send Slack notification: #{e.message}")
17
19
  end
18
20
 
19
21
  private
20
22
 
21
- def send_slack_notification(error_log, webhook_url)
23
+ def send_slack_notification(error_log, webhook_url, locale)
22
24
  require "net/http"
23
25
  require "json"
24
26
 
@@ -31,7 +33,7 @@ module RailsErrorDashboard
31
33
  http.read_timeout = 10 # 10 seconds to read response
32
34
 
33
35
  request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
34
- request.body = Services::SlackPayloadBuilder.call(error_log).to_json
36
+ request.body = Services::SlackPayloadBuilder.call(error_log, locale: locale).to_json
35
37
 
36
38
  response = http.request(request)
37
39
 
@@ -11,9 +11,11 @@ module RailsErrorDashboard
11
11
 
12
12
  # @param started_at [String] ISO8601 episode start
13
13
  # @param state [String] breaker state at notification time ("shedding"/"open")
14
- def perform(started_at:, state: "shedding")
14
+ # @param locale [String, nil] resolved at enqueue time. nil for jobs
15
+ # enqueued by a pre-Phase-4 version still draining from the queue.
16
+ def perform(started_at:, state: "shedding", locale: nil)
15
17
  config = RailsErrorDashboard.configuration
16
- message = build_message(started_at, state, config)
18
+ message = build_message(started_at, state, config, job_locale(locale))
17
19
 
18
20
  if config.enable_slack_notifications && config.slack_webhook_url.present?
19
21
  post_json(config.slack_webhook_url, { text: message })
@@ -38,14 +40,30 @@ module RailsErrorDashboard
38
40
 
39
41
  private
40
42
 
41
- def build_message(started_at, state, config)
42
- mode = state == "open" ? "count-only mode (occurrences tallied, detail paused)" : "shedding mode (context sampling active)"
43
+ # Assembled from four keys rather than one, because the dashboard link is
44
+ # conditional. Each key is a whole sentence never a fragment joined to
45
+ # another, which would bake English word order into every translation.
46
+ #
47
+ # ":warning:" is Slack/Discord emoji shortcode, not text.
48
+ def build_message(started_at, state, config, locale)
49
+ mode_key = state == "open" ? "open" : "shedding"
50
+ mode = t("red.notifications.storm.mode.#{mode_key}", locale)
51
+
43
52
  dashboard = (config.dashboard_base_url || "").chomp("/")
44
- link = dashboard.present? ? " Dashboard: #{dashboard}/errors/storms" : ""
53
+ link = if dashboard.present?
54
+ " " + t("red.notifications.storm.dashboard", locale, url: "#{dashboard}/errors/storms")
55
+ else
56
+ ""
57
+ end
58
+
59
+ ":warning: " \
60
+ "#{t("red.notifications.storm.detected", locale, application: app_name(config), started_at: started_at)} " \
61
+ "#{t("red.notifications.storm.engaged", locale, mode: mode)} " \
62
+ "#{t("red.notifications.storm.suppressed", locale)}#{link}"
63
+ end
45
64
 
46
- ":warning: Error storm detected in #{app_name(config)} at #{started_at}. " \
47
- "Storm protection engaged — #{mode}. Per-error notifications are " \
48
- "suppressed until the storm subsides; exact counts are preserved.#{link}"
65
+ def t(key, locale, **options)
66
+ RailsErrorDashboard::I18nStore.translate(key, locale: locale, **options)
49
67
  end
50
68
 
51
69
  def app_name(config)
@@ -6,7 +6,9 @@ module RailsErrorDashboard
6
6
  class WebhookErrorNotificationJob < ApplicationJob
7
7
  queue_as :default
8
8
 
9
- def perform(error_log_id)
9
+ # @param locale [String, nil] resolved at enqueue time. nil for jobs
10
+ # enqueued by a pre-Phase-4 version still draining from the queue.
11
+ def perform(error_log_id, locale = nil)
10
12
  error_log = ErrorLog.find(error_log_id)
11
13
  webhook_urls = RailsErrorDashboard.configuration.webhook_urls
12
14
 
@@ -15,7 +17,7 @@ module RailsErrorDashboard
15
17
  # Ensure webhook_urls is an array
16
18
  urls = Array(webhook_urls)
17
19
 
18
- payload = Services::WebhookPayloadBuilder.call(error_log)
20
+ payload = Services::WebhookPayloadBuilder.call(error_log, locale: job_locale(locale))
19
21
 
20
22
  urls.each do |url|
21
23
  send_webhook(url, payload, error_log)
@@ -4,5 +4,19 @@ module RailsErrorDashboard
4
4
  class ApplicationMailer < ActionMailer::Base
5
5
  default from: -> { RailsErrorDashboard.configuration.notification_email_from }
6
6
  layout false
7
+
8
+ # Mailers do not pick up engine helpers the way controllers do, so these
9
+ # are included explicitly.
10
+ #
11
+ # I18nHelper gives the .html.erb parts red_t; MailerI18nHelper adds
12
+ # red_mail_t (unescaped, for the .text.erb parts) and red_mail_time (a
13
+ # localized timestamp that needs no JavaScript).
14
+ #
15
+ # The locale arrives as an explicit argument and is assigned to
16
+ # @red_locale, which I18nHelper#red_locale prefers over Current — a mailer
17
+ # renders outside the request that set Current, and on a reused thread
18
+ # Current holds an unrelated request's value (P4-T1).
19
+ helper RailsErrorDashboard::I18nHelper
20
+ helper RailsErrorDashboard::MailerI18nHelper
7
21
  end
8
22
  end
@@ -2,18 +2,34 @@
2
2
 
3
3
  module RailsErrorDashboard
4
4
  class DigestMailer < ApplicationMailer
5
- def digest_summary(digest, recipients)
5
+ # @param locale [String] resolved at enqueue time, never read from
6
+ # Current — a mailer renders outside the request that set it.
7
+ def digest_summary(digest, recipients, locale: I18nStore::DEFAULT_LOCALE)
6
8
  @digest = digest
7
9
  @dashboard_url = dashboard_base_url
10
+ @red_locale = locale
8
11
 
9
12
  mail(
10
13
  to: recipients,
11
- subject: "RED Digest — #{digest[:stats][:new_errors]} new errors (#{digest[:period_label]})"
14
+ subject: subject_for(digest, locale)
12
15
  )
13
16
  end
14
17
 
15
18
  private
16
19
 
20
+ # "N new errors" was an English binary plural built by interpolation. It is
21
+ # now a real plural key, selected by the locale's CLDR rules (REQ-1).
22
+ def subject_for(digest, locale)
23
+ count = digest[:stats][:new_errors].to_i
24
+
25
+ I18nStore.translate(
26
+ "red.mailers.digest.subject",
27
+ locale: locale,
28
+ count: count,
29
+ period: digest[:period_label]
30
+ )
31
+ end
32
+
17
33
  def dashboard_base_url
18
34
  base = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
19
35
  mount_path = RailsErrorDashboard.configuration.engine_mount_path
@@ -2,18 +2,35 @@
2
2
 
3
3
  module RailsErrorDashboard
4
4
  class ErrorNotificationMailer < ApplicationMailer
5
- def error_alert(error_log, recipients)
5
+ # @param locale [String] resolved at enqueue time, never read from
6
+ # Current — a mailer renders outside the request that set it.
7
+ def error_alert(error_log, recipients, locale: I18nStore::DEFAULT_LOCALE)
6
8
  @error_log = error_log
7
9
  @dashboard_url = dashboard_url(error_log)
10
+ @red_locale = locale
8
11
 
9
12
  mail(
10
13
  to: recipients,
11
- subject: "🚨 [#{error_log.application&.name || 'Unknown'}] #{error_log.error_type}: #{truncate_subject(error_log.message)}"
14
+ subject: subject_for(error_log, locale)
12
15
  )
13
16
  end
14
17
 
15
18
  private
16
19
 
20
+ # The subject is assembled from a key rather than interpolated inline, so a
21
+ # locale can reorder or drop the emoji. error_type and message are verbatim
22
+ # error content and are never translated.
23
+ def subject_for(error_log, locale)
24
+ I18nStore.translate(
25
+ "red.mailers.error_alert.subject",
26
+ locale: locale,
27
+ application: error_log.application&.name ||
28
+ I18nStore.translate("red.mailers.shared.unknown_application", locale: locale),
29
+ error_type: error_log.error_type,
30
+ message: truncate_subject(error_log.message)
31
+ )
32
+ end
33
+
17
34
  def dashboard_url(error_log)
18
35
  base_url = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
19
36
  mount_path = RailsErrorDashboard.configuration.engine_mount_path