rails_error_dashboard 0.8.3 → 0.9.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.
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 +156 -0
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +95 -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 +39 -28
  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 -0
  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 +24 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 270ac48ebee51c2f68cab29abdbf1169b7db7d7b860bf1fc5a43883960cdc0c6
4
- data.tar.gz: 6def5b5a2bbcabffd1116f13607771f6923823dc65eec803e028ba911af85ec9
3
+ metadata.gz: f85952e5800c354ae158411afbec605cddbc5e19595e8e94620d1b13fc17894b
4
+ data.tar.gz: 1af4bb9352a7fcffc10fc6b913b896ca2fd75198d31e044ff156b76cb52f6afc
5
5
  SHA512:
6
- metadata.gz: fd6984fc8ccb2a68f9543786ffd6157d1a975ed5d446a13e38b3a38b42cc93047117918186b7f3936c2ddc3a53c3609db63073abad5f25d02edba4961e7b1f30
7
- data.tar.gz: 55ba8d78b0d06176ac80e63919cd29a6ca94423d8880712c8fb82979a17741b5881878ef2351efd89c468fbb3d03c6411c82c5c4668a687f4f441b512c169814
6
+ metadata.gz: a6bc19522b5797095df0545fd6eca1b2552bfcd693ebb4363a17e8076ad784654340a70d1e9591f2ab3fc17a5fc9be10e65efa1b0211b329203c639b085b37a7
7
+ data.tar.gz: c0885c4d27dde62b2439a40dac82acdee468f34ce9dd3d188b2f589bbceedab680c746ec7793e894cb3d5dacb7aff68908139cf90d655dc9790c0aea503ef784
data/README.md CHANGED
@@ -602,6 +602,40 @@ end
602
602
 
603
603
  ---
604
604
 
605
+ ## Languages
606
+
607
+ The dashboard, its emails and its notification payloads are translated. Eleven locales ship:
608
+
609
+ | Locale | Language | Status |
610
+ |---|---|---|
611
+ | `en` | English | Source language |
612
+ | `de` | Deutsch | Machine-translated, unreviewed |
613
+ | `es` | Español | Machine-translated, unreviewed |
614
+ | `fr` | Français | Machine-translated, unreviewed |
615
+ | `pt-BR` | Português (Brasil) | Machine-translated, unreviewed |
616
+ | `ja` | 日本語 | Machine-translated, unreviewed |
617
+ | `ru` | Русский | Machine-translated, unreviewed |
618
+ | `uk` | Українська | Machine-translated, unreviewed |
619
+ | `pl` | Polski | Machine-translated, unreviewed |
620
+ | `it` | Italiano | Machine-translated, unreviewed |
621
+ | `zh-CN` | 简体中文 | Machine-translated, unreviewed |
622
+
623
+ ```ruby
624
+ config.dashboard_locale = "de" # en, de, es, fr, pt-BR, ja, ru, uk, pl, it, zh-CN — default "en"
625
+ ```
626
+
627
+ Users can also switch language per-session from the picker in the dashboard navbar, which overrides the configured default for them alone.
628
+
629
+ **Everything but English is machine-translated and has not been reviewed by a native speaker.** That is stated plainly rather than as "beta", which would imply a review process that has not happened — RED's maintainer reads only English. Key structure, interpolation variables and plural categories *are* verified mechanically in every locale; wording, register and idiom are not verified by anyone. A wrong or missing translation falls back to **English**, never to a broken page.
630
+
631
+ **Corrections are very welcome, and a one-key PR is a perfectly good PR.** If you read one of these languages, [every locale has an open issue](https://github.com/AnjanJ/rails_error_dashboard/issues?q=is%3Aissue+is%3Aopen+label%3Atranslation%3Aneeds-review) tracking its review — comment there, or [report a bad translation](https://github.com/AnjanJ/rails_error_dashboard/issues/new?template=translation_report.yml) without touching any code. You do not need to know Ruby, and you are not expected to review a whole file.
632
+
633
+ RED translates through its own private I18n backend, so it never reads, writes or mutates your application's `I18n` configuration — your locale and its `available_locales` are untouched.
634
+
635
+ [Translations guide →](docs/guides/TRANSLATIONS.md) — how the system works, how to correct a string, and how to add a locale.
636
+
637
+ ---
638
+
605
639
  ## FAQ
606
640
 
607
641
  **Does Rails Error Dashboard support a separate database for errors?**
@@ -647,6 +681,7 @@ Rails 7.0–8.1 and Ruby 3.2–4.0.
647
681
  - **[Batch Operations](docs/guides/BATCH_OPERATIONS.md)** — Bulk resolve/delete
648
682
  - **[Real-Time Updates](docs/guides/REAL_TIME_UPDATES.md)** — Live dashboard
649
683
  - **[Error Trends](docs/guides/ERROR_TREND_VISUALIZATIONS.md)** — Charts and analytics
684
+ - **[Translations](docs/guides/TRANSLATIONS.md)** — Seven shipped locales, correcting a string, adding a language
650
685
 
651
686
  ### Advanced
652
687
  - **[Multi-App Support](docs/MULTI_APP_PERFORMANCE.md)** — Track multiple applications
@@ -1,5 +1,17 @@
1
1
  module RailsErrorDashboard
2
2
  class ApplicationController < ActionController::Base
3
+ # Authenticate EVERY dashboard controller, not just the ones that remember
4
+ # to ask. This filter used to live on ErrorsController, which meant a new
5
+ # controller inherited no protection at all — LocalesController (the P5-T1
6
+ # language picker) shipped unauthenticated for exactly that reason, and an
7
+ # unauthenticated POST reached controller code and 500'd instead of being
8
+ # refused with a 401.
9
+ #
10
+ # Declaring it here inverts the default: a controller is protected unless
11
+ # it explicitly opts out with skip_before_action, which is a visible,
12
+ # reviewable act rather than an omission nobody notices.
13
+ before_action :authenticate_dashboard_user!
14
+
3
15
  include Pagy::Method
4
16
 
5
17
  # Enable features that are disabled in API-only mode
@@ -8,10 +20,24 @@ module RailsErrorDashboard
8
20
  include ActionController::Flash
9
21
  include ActionController::RequestForgeryProtection
10
22
 
23
+ # red_t for flash messages. Not the view helper — that html-escapes, and a
24
+ # flash string is escaped again when the layout renders it.
25
+ include Translation
26
+
11
27
  layout "rails_error_dashboard"
12
28
 
13
29
  protect_from_forgery with: :exception
14
30
 
31
+ # Render pagination in the dashboard's own locale, not the host app's.
32
+ #
33
+ # Pagy stores its locale in Thread.current[:pagy_locale] and never resets it
34
+ # (pagy 43.6.1, modules/i18n/i18n.rb:24-31 — the "for the duration of a single
35
+ # request" comment is aspirational; nothing in the gem enforces it). A host app
36
+ # that sets a per-request locale leaves that value on the Puma thread, so a
37
+ # dashboard request landing on a recycled thread inherits whatever language the
38
+ # host last used — Russian on one refresh, Portuguese on the next (issue #148).
39
+ around_action :with_dashboard_locale
40
+
15
41
  # CRITICAL: Ensure dashboard errors never break the app
16
42
  # Catch all exceptions and render user-friendly error page
17
43
  # NOTE: rescue_from is checked in reverse declaration order (last = highest priority).
@@ -60,6 +86,96 @@ module RailsErrorDashboard
60
86
 
61
87
  private
62
88
 
89
+ # Set the dashboard's locale for this request and restore whatever was
90
+ # there before.
91
+ #
92
+ # Three details are load-bearing:
93
+ #
94
+ # 1. The previous value is read from Thread.current directly, NOT from
95
+ # Pagy::I18n.locale. The getter coerces nil to "en", so restoring through
96
+ # it would stamp "en" onto a thread that started clean — making the
97
+ # dashboard a source of the very leak it is fixing.
98
+ # 2. around_action + ensure, not before_action. A before_action would strand
99
+ # the dashboard's locale on the thread for the host app's next request.
100
+ # The ensure also covers the rescue_from handlers above, which still
101
+ # render through the view layer.
102
+ # 3. RED's own locale is set on RailsErrorDashboard::Current, NOT via
103
+ # I18n.with_locale. RED translates through a private backend
104
+ # (I18nStore); touching the host's I18n.locale would mutate host global
105
+ # state for no benefit and re-introduce exactly the coupling #148 removed.
106
+ #
107
+ # Pagy's locale and RED's are resolved independently: they ship different
108
+ # dictionaries, so a locale RED can serve but Pagy cannot must still render
109
+ # the UI translated, with English pagination, rather than raising.
110
+ def with_dashboard_locale
111
+ previous_pagy = Thread.current[:pagy_locale]
112
+ Pagy::I18n.locale = dashboard_pagy_locale
113
+ Current.locale = dashboard_locale
114
+ yield
115
+ ensure
116
+ Thread.current[:pagy_locale] = previous_pagy
117
+ Current.locale = nil
118
+ end
119
+
120
+ # The locale RED renders its own strings in. Resolved against the locales
121
+ # RED ships (not Pagy's), and never raises.
122
+ #
123
+ # Precedence (P5-T1 REQ-3): session -> config.dashboard_locale -> "en".
124
+ # The session half is applied here and the rest by locale_or_default, which
125
+ # validates whatever it is given against the locales RED actually ships.
126
+ # Assigning Current.locale only when the session holds a usable value keeps
127
+ # an empty session from overriding the configured default.
128
+ def dashboard_locale
129
+ Current.locale = session_locale
130
+ Current.locale_or_default
131
+ rescue StandardError
132
+ I18nStore::DEFAULT_LOCALE
133
+ end
134
+
135
+ # The user's picked locale, or nil.
136
+ #
137
+ # Returns nil rather than raising for every way this can go wrong: no
138
+ # session at all (an API-only host where the engine's session middleware
139
+ # did not take effect — REQ-7), or a tampered value (REQ-6). A garbage
140
+ # value is not merely ignored but CLEARED, so a session poisoned once does
141
+ # not cost a lookup on every subsequent request.
142
+ #
143
+ # #available? is total for any input, including an Array or a Hash, so no
144
+ # type check is needed before it.
145
+ def session_locale
146
+ stored = session[:red_locale]
147
+ return nil if stored.nil?
148
+ return I18nStore.resolve(stored) if I18nStore.available?(stored)
149
+
150
+ session.delete(:red_locale)
151
+ nil
152
+ rescue StandardError
153
+ nil
154
+ end
155
+
156
+ def dashboard_pagy_locale
157
+ configured = RailsErrorDashboard.configuration.dashboard_locale.to_s.strip
158
+ return "en" if configured.empty?
159
+
160
+ self.class.resolved_pagy_locales[configured] ||= resolve_pagy_locale(configured)
161
+ rescue StandardError
162
+ "en"
163
+ end
164
+
165
+ # Pagy looks its dictionary up by exact filename AND by an exact top-level
166
+ # key inside that YAML, so "EN" finds en.yml but then reads a nil dictionary
167
+ # and raises mid-render. Match the shipped filenames case-insensitively and
168
+ # fall back to English for anything Pagy cannot serve.
169
+ def resolve_pagy_locale(configured)
170
+ available = Pagy::I18n.pathnames.flat_map { |dir| Dir.glob(dir.join("*.yml")) }
171
+ .map { |path| File.basename(path, ".yml") }
172
+ available.find { |locale| locale.casecmp?(configured) } || "en"
173
+ end
174
+
175
+ def self.resolved_pagy_locales
176
+ @resolved_pagy_locales ||= {}
177
+ end
178
+
63
179
  def render_dashboard_error(icon:, title:, message:, detail: nil, icon_style: nil, status: :internal_server_error)
64
180
  set_common_view_variables
65
181
  error_html = <<~ERB
@@ -83,5 +199,45 @@ module RailsErrorDashboard
83
199
  # Tracked by a flag rather than the value, since the common case is nil.
84
200
  @storm_banner_event = Queries::StormHistory.banner_event unless @storm_banner_loaded
85
201
  end
202
+
203
+ def authenticate_dashboard_user!
204
+ auth_lambda = RailsErrorDashboard.configuration.authenticate_with
205
+
206
+ if auth_lambda
207
+ authenticate_with_lambda(auth_lambda)
208
+ else
209
+ authenticate_with_basic_auth
210
+ end
211
+ end
212
+
213
+ def authenticate_with_lambda(auth_lambda)
214
+ authorized = begin
215
+ instance_exec(&auth_lambda)
216
+ rescue => e
217
+ Rails.logger.error(
218
+ "[RailsErrorDashboard] authenticate_with lambda raised #{e.class}: #{e.message}"
219
+ )
220
+ false
221
+ end
222
+
223
+ return if performed?
224
+
225
+ unless authorized
226
+ render plain: "Access Denied", status: :forbidden
227
+ end
228
+ end
229
+
230
+ def authenticate_with_basic_auth
231
+ authenticate_or_request_with_http_basic do |username, password|
232
+ ActiveSupport::SecurityUtils.secure_compare(
233
+ username,
234
+ RailsErrorDashboard.configuration.dashboard_username
235
+ ) &
236
+ ActiveSupport::SecurityUtils.secure_compare(
237
+ password,
238
+ RailsErrorDashboard.configuration.dashboard_password
239
+ )
240
+ end
241
+ end
86
242
  end
87
243
  end
@@ -30,6 +30,23 @@ module RailsErrorDashboard
30
30
  sort_direction
31
31
  ].freeze
32
32
 
33
+ # Batch action param => the key naming its outcome. English happens to form
34
+ # all four by adding "d"/"ed"; most languages do not, so each outcome is
35
+ # named rather than derived from the param.
36
+ BATCH_OUTCOMES = {
37
+ "resolve" => "resolved",
38
+ "mute" => "muted",
39
+ "unmute" => "unmuted",
40
+ "delete" => "deleted"
41
+ }.freeze
42
+
43
+ # Named once: the check and the message that reports it were both writing
44
+ # 4000, and the copy that drifts is the one the user reads.
45
+ AI_HELP_QUESTION_LIMIT = 4000
46
+
47
+ # The initializer every "not enabled" notice points at. A path, not prose.
48
+ INITIALIZER_PATH = "config/initializers/rails_error_dashboard.rb"
49
+
33
50
  def overview
34
51
  # Get dashboard stats using Query (pass application filter)
35
52
  @stats = Queries::DashboardStats.call(application_id: @current_application_id)
@@ -155,10 +172,10 @@ module RailsErrorDashboard
155
172
  result = Commands::CreateIssue.call(params[:id], dashboard_url: dashboard_url)
156
173
 
157
174
  if result[:success]
158
- flash[:notice] = "Issue created successfully"
175
+ flash[:notice] = red_t("red.flash.issue.created")
159
176
  flash[:new_issue_url] = result[:issue_url]
160
177
  else
161
- flash[:alert] = "Failed to create issue: #{result[:error]}"
178
+ flash[:alert] = red_t("red.flash.issue.create_failed", reason: result[:error])
162
179
  end
163
180
  redirect_to error_path(params[:id], anchor: "issue-tracking", **app_context_params)
164
181
  end
@@ -167,27 +184,30 @@ module RailsErrorDashboard
167
184
  result = Commands::LinkExistingIssue.call(params[:id], issue_url: params[:issue_url])
168
185
 
169
186
  if result[:success]
170
- flash[:notice] = "Issue linked successfully"
187
+ flash[:notice] = red_t("red.flash.issue.linked")
171
188
  else
172
- flash[:alert] = "Failed to link issue: #{result[:error]}"
189
+ flash[:alert] = red_t("red.flash.issue.link_failed", reason: result[:error])
173
190
  end
174
191
  redirect_to error_path(params[:id], anchor: "issue-tracking", **app_context_params)
175
192
  end
176
193
 
177
194
  def ai_help
178
195
  unless RailsErrorDashboard.configuration.llm_configured?
179
- render json: { error: "AI Help is not configured" }, status: :not_found
196
+ render json: { error: red_t("red.flash.ai_help.not_configured") }, status: :not_found
180
197
  return
181
198
  end
182
199
 
183
200
  question = params[:question].to_s.strip
184
201
  if question.blank?
185
- render json: { error: "Question cannot be blank" }, status: :unprocessable_entity
202
+ render json: { error: red_t("red.flash.ai_help.blank_question") }, status: :unprocessable_entity
186
203
  return
187
204
  end
188
205
 
189
- if question.length > 4000
190
- render json: { error: "Question is too long. Keep it under 4,000 characters." }, status: :unprocessable_entity
206
+ if question.length > AI_HELP_QUESTION_LIMIT
207
+ render json: {
208
+ error: red_t("red.flash.ai_help.question_too_long",
209
+ limit: helpers.number_with_delimiter(AI_HELP_QUESTION_LIMIT))
210
+ }, status: :unprocessable_entity
191
211
  return
192
212
  end
193
213
 
@@ -248,7 +268,7 @@ module RailsErrorDashboard
248
268
  def platform_comparison
249
269
  # Check if feature is enabled
250
270
  unless RailsErrorDashboard.configuration.enable_platform_comparison
251
- flash[:alert] = "Platform Comparison is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
271
+ flash[:alert] = feature_disabled_message("platform_comparison")
252
272
  redirect_to errors_path(**app_context_params)
253
273
  return
254
274
  end
@@ -287,13 +307,25 @@ module RailsErrorDashboard
287
307
  when "delete"
288
308
  Commands::BatchDeleteErrors.call(error_ids)
289
309
  else
290
- { success: false, count: 0, errors: [ "Invalid action type" ] }
310
+ { success: false, count: 0, errors: [ red_t("red.commands.invalid_action") ] }
291
311
  end
292
312
 
293
313
  if result[:success]
294
- flash[:notice] = "Successfully #{action_type}d #{result[:count]} error(s)"
314
+ # One key per outcome rather than "Successfully #{action_type}d": the
315
+ # original conjugated English past tense by appending a "d" to the raw
316
+ # param, which no other language can be asked to do.
317
+ #
318
+ # An unrecognized action cannot reach here (the case above returns
319
+ # success: false), but the lookup still degrades rather than raising —
320
+ # a flash message is never worth a 500 on the error dashboard.
321
+ outcome = BATCH_OUTCOMES[action_type]
322
+ flash[:notice] = if outcome
323
+ red_tp("red.flash.batch.#{outcome}", count: result[:count])
324
+ else
325
+ red_t("red.commands.invalid_action")
326
+ end
295
327
  else
296
- flash[:alert] = "Batch operation failed: #{result[:errors].join(', ')}"
328
+ flash[:alert] = red_t("red.flash.batch.failed", reason: result[:errors].join(", "))
297
329
  end
298
330
 
299
331
  redirect_to errors_path(**app_context_params)
@@ -302,7 +334,7 @@ module RailsErrorDashboard
302
334
  def correlation
303
335
  # Check if feature is enabled
304
336
  unless RailsErrorDashboard.configuration.enable_error_correlation
305
- flash[:alert] = "Error Correlation is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
337
+ flash[:alert] = feature_disabled_message("error_correlation")
306
338
  redirect_to errors_path(**app_context_params)
307
339
  return
308
340
  end
@@ -348,7 +380,7 @@ module RailsErrorDashboard
348
380
 
349
381
  def deprecations
350
382
  unless RailsErrorDashboard.configuration.enable_breadcrumbs
351
- flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
383
+ flash[:alert] = feature_disabled_message("breadcrumbs", plural: true)
352
384
  redirect_to errors_path(**app_context_params)
353
385
  return
354
386
  end
@@ -368,7 +400,7 @@ module RailsErrorDashboard
368
400
 
369
401
  def n_plus_one_summary
370
402
  unless RailsErrorDashboard.configuration.enable_breadcrumbs
371
- flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
403
+ flash[:alert] = feature_disabled_message("breadcrumbs", plural: true)
372
404
  redirect_to errors_path(**app_context_params)
373
405
  return
374
406
  end
@@ -388,7 +420,7 @@ module RailsErrorDashboard
388
420
 
389
421
  def cache_health_summary
390
422
  unless RailsErrorDashboard.configuration.enable_breadcrumbs
391
- flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
423
+ flash[:alert] = feature_disabled_message("breadcrumbs", plural: true)
392
424
  redirect_to errors_path(**app_context_params)
393
425
  return
394
426
  end
@@ -409,7 +441,7 @@ module RailsErrorDashboard
409
441
 
410
442
  def job_health_summary
411
443
  unless RailsErrorDashboard.configuration.enable_system_health
412
- flash[:alert] = "System health is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
444
+ flash[:alert] = feature_disabled_message("system_health")
413
445
  redirect_to errors_path(**app_context_params)
414
446
  return
415
447
  end
@@ -429,7 +461,7 @@ module RailsErrorDashboard
429
461
 
430
462
  def database_health_summary
431
463
  unless RailsErrorDashboard.configuration.enable_system_health
432
- flash[:alert] = "System health is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
464
+ flash[:alert] = feature_disabled_message("system_health")
433
465
  redirect_to errors_path(**app_context_params)
434
466
  return
435
467
  end
@@ -462,9 +494,9 @@ module RailsErrorDashboard
462
494
  unless RailsErrorDashboard.configuration.detect_swallowed_exceptions
463
495
  # On Ruby < 3.3, validate! auto-disables this feature — tell the user why
464
496
  if RUBY_VERSION < "3.3"
465
- flash[:alert] = "Swallowed exception detection requires Ruby 3.3+ (you have #{RUBY_VERSION}). Upgrade Ruby to use this feature."
497
+ flash[:alert] = red_t("red.flash.swallowed_requires_ruby", version: RUBY_VERSION)
466
498
  else
467
- flash[:alert] = "Swallowed exception detection is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
499
+ flash[:alert] = feature_disabled_message("swallowed_exceptions")
468
500
  end
469
501
  redirect_to errors_path(**app_context_params)
470
502
  return
@@ -485,11 +517,15 @@ module RailsErrorDashboard
485
517
 
486
518
  def rack_attack_summary
487
519
  unless RailsErrorDashboard.configuration.enable_rack_attack_tracking
488
- flash[:alert] = "Rack Attack tracking is not enabled. Set enable_rack_attack_tracking = true in config/initializers/rails_error_dashboard.rb"
520
+ flash[:alert] = feature_disabled_message("rack_attack", options: "enable_rack_attack_tracking = true", set: true)
489
521
  redirect_to errors_path(**app_context_params)
490
522
  return
491
523
  end
492
524
 
525
+ # Distinguishes "gem not installed" from "installed but no rules matched"
526
+ # in the empty state — both otherwise render an identical blank page.
527
+ @rack_attack_missing = !defined?(::Rack::Attack)
528
+
493
529
  days = days_param(default: 30)
494
530
  @days = days
495
531
  result = Queries::RackAttackSummary.call(days, application_id: @current_application_id)
@@ -506,7 +542,7 @@ module RailsErrorDashboard
506
542
  def actioncable_health_summary
507
543
  unless RailsErrorDashboard.configuration.enable_actioncable_tracking &&
508
544
  RailsErrorDashboard.configuration.enable_breadcrumbs
509
- flash[:alert] = "ActionCable tracking is not enabled. Enable enable_actioncable_tracking and enable_breadcrumbs in config/initializers/rails_error_dashboard.rb"
545
+ flash[:alert] = feature_disabled_message("actioncable", options: "enable_actioncable_tracking and enable_breadcrumbs")
510
546
  redirect_to errors_path(**app_context_params)
511
547
  return
512
548
  end
@@ -547,7 +583,7 @@ module RailsErrorDashboard
547
583
  def activestorage_health_summary
548
584
  unless RailsErrorDashboard.configuration.enable_activestorage_tracking &&
549
585
  RailsErrorDashboard.configuration.enable_breadcrumbs
550
- flash[:alert] = "ActiveStorage tracking is not enabled. Enable enable_activestorage_tracking and enable_breadcrumbs in config/initializers/rails_error_dashboard.rb"
586
+ flash[:alert] = feature_disabled_message("activestorage", options: "enable_activestorage_tracking and enable_breadcrumbs")
551
587
  redirect_to errors_path(**app_context_params)
552
588
  return
553
589
  end
@@ -567,7 +603,7 @@ module RailsErrorDashboard
567
603
 
568
604
  def diagnostic_dumps
569
605
  unless RailsErrorDashboard.configuration.enable_diagnostic_dump
570
- flash[:alert] = "Diagnostic dumps are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
606
+ flash[:alert] = feature_disabled_message("diagnostic_dumps", plural: true)
571
607
  redirect_to errors_path(**app_context_params)
572
608
  return
573
609
  end
@@ -581,7 +617,7 @@ module RailsErrorDashboard
581
617
 
582
618
  def create_diagnostic_dump
583
619
  unless RailsErrorDashboard.configuration.enable_diagnostic_dump
584
- flash[:alert] = "Diagnostic dumps are not enabled."
620
+ flash[:alert] = red_t("red.flash.diagnostic_dump.disabled")
585
621
  redirect_to errors_path(**app_context_params)
586
622
  return
587
623
  end
@@ -601,31 +637,31 @@ module RailsErrorDashboard
601
637
  note: params[:note].presence
602
638
  )
603
639
 
604
- flash[:notice] = "Diagnostic dump captured successfully."
640
+ flash[:notice] = red_t("red.flash.diagnostic_dump.captured")
605
641
  redirect_to diagnostic_dumps_errors_path
606
642
  rescue => e
607
- flash[:alert] = "Failed to capture diagnostic dump: #{e.message}"
643
+ flash[:alert] = red_t("red.flash.diagnostic_dump.failed", message: e.message)
608
644
  redirect_to diagnostic_dumps_errors_path
609
645
  end
610
646
 
611
647
  def enable_coverage
612
648
  unless RailsErrorDashboard.configuration.enable_coverage_tracking
613
- flash[:alert] = "Coverage tracking is not enabled in configuration."
649
+ flash[:alert] = red_t("red.flash.coverage.not_enabled")
614
650
  redirect_to errors_path(**app_context_params)
615
651
  return
616
652
  end
617
653
 
618
654
  if Services::CoverageTracker.enable!
619
- flash[:notice] = "Code path coverage enabled. Reproduce the error, then view source code to see executed lines."
655
+ flash[:notice] = red_t("red.flash.coverage.enabled")
620
656
  else
621
- flash[:alert] = "Could not enable coverage. Requires Ruby 3.2+."
657
+ flash[:alert] = red_t("red.flash.coverage.unavailable")
622
658
  end
623
659
  redirect_back fallback_location: errors_path
624
660
  end
625
661
 
626
662
  def disable_coverage
627
663
  Services::CoverageTracker.disable!
628
- flash[:notice] = "Code path coverage disabled."
664
+ flash[:notice] = red_t("red.flash.coverage.disabled")
629
665
  redirect_back fallback_location: errors_path
630
666
  end
631
667
 
@@ -639,10 +675,10 @@ module RailsErrorDashboard
639
675
 
640
676
  Commands::LogError.call(exception, { request: request, source: "dashboard.test_error" })
641
677
 
642
- flash[:notice] = "Test error logged successfully. Check your notification channels (Slack, Discord, email, etc.) to confirm delivery."
678
+ flash[:notice] = red_t("red.flash.test_error.logged")
643
679
  redirect_to errors_path(**app_context_params)
644
680
  rescue => e
645
- flash[:alert] = "Failed to log test error: #{e.message}"
681
+ flash[:alert] = red_t("red.flash.test_error.failed", message: e.message)
646
682
  redirect_to settings_path(**app_context_params)
647
683
  end
648
684
 
@@ -652,6 +688,32 @@ module RailsErrorDashboard
652
688
 
653
689
  private
654
690
 
691
+ # "X is not enabled. Enable it in <initializer>" was written out eleven
692
+ # times with only the feature name changing. One helper, so the wording
693
+ # cannot drift page to page — and the initializer path is interpolated
694
+ # rather than translated, because it is a path.
695
+ #
696
+ # English agrees the verb and pronoun with the feature name, so a plural
697
+ # feature ("Breadcrumbs are... Enable them") needs a different key from a
698
+ # singular one ("System health is... Enable it"). The caller says which;
699
+ # inferring it from the English string would be guessing at grammar.
700
+ #
701
+ # @param feature [String] a key under red.flash.features
702
+ # @param plural [Boolean] whether the feature name takes a plural verb
703
+ # @param options [String, nil] config option names to cite instead
704
+ # @param set [Boolean] use "Set" rather than "Enable" for those options
705
+ def feature_disabled_message(feature, plural: false, options: nil, set: false)
706
+ name = red_t("red.flash.features.#{feature}")
707
+
708
+ if options
709
+ key = set ? "red.flash.not_enabled_set_options" : "red.flash.not_enabled_options"
710
+ red_t(key, feature: name, options: options, file: INITIALIZER_PATH)
711
+ else
712
+ key = plural ? "red.flash.not_enabled_plural" : "red.flash.not_enabled"
713
+ red_t(key, feature: name, file: INITIALIZER_PATH)
714
+ end
715
+ end
716
+
655
717
  def calculate_release_comparison
656
718
  return {} if @errors_by_version.empty? || @errors_by_version.count < 2
657
719
 
@@ -739,45 +801,5 @@ module RailsErrorDashboard
739
801
  @storm_banner_loaded = true
740
802
  @storm_banner_event = Queries::StormHistory.banner_event
741
803
  end
742
-
743
- def authenticate_dashboard_user!
744
- auth_lambda = RailsErrorDashboard.configuration.authenticate_with
745
-
746
- if auth_lambda
747
- authenticate_with_lambda(auth_lambda)
748
- else
749
- authenticate_with_basic_auth
750
- end
751
- end
752
-
753
- def authenticate_with_lambda(auth_lambda)
754
- authorized = begin
755
- instance_exec(&auth_lambda)
756
- rescue => e
757
- Rails.logger.error(
758
- "[RailsErrorDashboard] authenticate_with lambda raised #{e.class}: #{e.message}"
759
- )
760
- false
761
- end
762
-
763
- return if performed?
764
-
765
- unless authorized
766
- render plain: "Access Denied", status: :forbidden
767
- end
768
- end
769
-
770
- def authenticate_with_basic_auth
771
- authenticate_or_request_with_http_basic do |username, password|
772
- ActiveSupport::SecurityUtils.secure_compare(
773
- username,
774
- RailsErrorDashboard.configuration.dashboard_username
775
- ) &
776
- ActiveSupport::SecurityUtils.secure_compare(
777
- password,
778
- RailsErrorDashboard.configuration.dashboard_password
779
- )
780
- end
781
- end
782
804
  end
783
805
  end