rails_error_dashboard 0.8.4 → 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 +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 +10 -3
  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: ba282b251af897146a14dce9cbd14aae0032428b96bb8d274d7400db3b1386cd
4
- data.tar.gz: 4d4de4a7d143fe4233ea283766f452018f0a057652620077579686c6a10b1cbe
3
+ metadata.gz: f85952e5800c354ae158411afbec605cddbc5e19595e8e94620d1b13fc17894b
4
+ data.tar.gz: 1af4bb9352a7fcffc10fc6b913b896ca2fd75198d31e044ff156b76cb52f6afc
5
5
  SHA512:
6
- metadata.gz: e794d0737d5c8381a8a127ff30ebea8023fbf3e9b73f089d2cde9832617f953a6a589481a954dad41e1a18c85400e36e3a04a8a3bac35eaedc35aa53cf3a3d87
7
- data.tar.gz: b9cff40ccba8d4459e0fe829dbf43fd054c66ff3f2a28a15d0be334e84fe757fc02c1d34e326ab1956a5d6074474113951e42bfdd9f9ef43705a0cfae46c0165
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,6 +20,10 @@ 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
@@ -70,9 +86,10 @@ module RailsErrorDashboard
70
86
 
71
87
  private
72
88
 
73
- # Set Pagy's locale for this request and restore whatever was there before.
89
+ # Set the dashboard's locale for this request and restore whatever was
90
+ # there before.
74
91
  #
75
- # Two details are load-bearing:
92
+ # Three details are load-bearing:
76
93
  #
77
94
  # 1. The previous value is read from Thread.current directly, NOT from
78
95
  # Pagy::I18n.locale. The getter coerces nil to "en", so restoring through
@@ -82,12 +99,58 @@ module RailsErrorDashboard
82
99
  # the dashboard's locale on the thread for the host app's next request.
83
100
  # The ensure also covers the rescue_from handlers above, which still
84
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.
85
110
  def with_dashboard_locale
86
- previous = Thread.current[:pagy_locale]
111
+ previous_pagy = Thread.current[:pagy_locale]
87
112
  Pagy::I18n.locale = dashboard_pagy_locale
113
+ Current.locale = dashboard_locale
88
114
  yield
89
115
  ensure
90
- Thread.current[:pagy_locale] = previous
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
91
154
  end
92
155
 
93
156
  def dashboard_pagy_locale
@@ -136,5 +199,45 @@ module RailsErrorDashboard
136
199
  # Tracked by a flag rather than the value, since the common case is nil.
137
200
  @storm_banner_event = Queries::StormHistory.banner_event unless @storm_banner_loaded
138
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
139
242
  end
140
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,7 +517,7 @@ 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
@@ -510,7 +542,7 @@ module RailsErrorDashboard
510
542
  def actioncable_health_summary
511
543
  unless RailsErrorDashboard.configuration.enable_actioncable_tracking &&
512
544
  RailsErrorDashboard.configuration.enable_breadcrumbs
513
- 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")
514
546
  redirect_to errors_path(**app_context_params)
515
547
  return
516
548
  end
@@ -551,7 +583,7 @@ module RailsErrorDashboard
551
583
  def activestorage_health_summary
552
584
  unless RailsErrorDashboard.configuration.enable_activestorage_tracking &&
553
585
  RailsErrorDashboard.configuration.enable_breadcrumbs
554
- 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")
555
587
  redirect_to errors_path(**app_context_params)
556
588
  return
557
589
  end
@@ -571,7 +603,7 @@ module RailsErrorDashboard
571
603
 
572
604
  def diagnostic_dumps
573
605
  unless RailsErrorDashboard.configuration.enable_diagnostic_dump
574
- 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)
575
607
  redirect_to errors_path(**app_context_params)
576
608
  return
577
609
  end
@@ -585,7 +617,7 @@ module RailsErrorDashboard
585
617
 
586
618
  def create_diagnostic_dump
587
619
  unless RailsErrorDashboard.configuration.enable_diagnostic_dump
588
- flash[:alert] = "Diagnostic dumps are not enabled."
620
+ flash[:alert] = red_t("red.flash.diagnostic_dump.disabled")
589
621
  redirect_to errors_path(**app_context_params)
590
622
  return
591
623
  end
@@ -605,31 +637,31 @@ module RailsErrorDashboard
605
637
  note: params[:note].presence
606
638
  )
607
639
 
608
- flash[:notice] = "Diagnostic dump captured successfully."
640
+ flash[:notice] = red_t("red.flash.diagnostic_dump.captured")
609
641
  redirect_to diagnostic_dumps_errors_path
610
642
  rescue => e
611
- flash[:alert] = "Failed to capture diagnostic dump: #{e.message}"
643
+ flash[:alert] = red_t("red.flash.diagnostic_dump.failed", message: e.message)
612
644
  redirect_to diagnostic_dumps_errors_path
613
645
  end
614
646
 
615
647
  def enable_coverage
616
648
  unless RailsErrorDashboard.configuration.enable_coverage_tracking
617
- flash[:alert] = "Coverage tracking is not enabled in configuration."
649
+ flash[:alert] = red_t("red.flash.coverage.not_enabled")
618
650
  redirect_to errors_path(**app_context_params)
619
651
  return
620
652
  end
621
653
 
622
654
  if Services::CoverageTracker.enable!
623
- 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")
624
656
  else
625
- flash[:alert] = "Could not enable coverage. Requires Ruby 3.2+."
657
+ flash[:alert] = red_t("red.flash.coverage.unavailable")
626
658
  end
627
659
  redirect_back fallback_location: errors_path
628
660
  end
629
661
 
630
662
  def disable_coverage
631
663
  Services::CoverageTracker.disable!
632
- flash[:notice] = "Code path coverage disabled."
664
+ flash[:notice] = red_t("red.flash.coverage.disabled")
633
665
  redirect_back fallback_location: errors_path
634
666
  end
635
667
 
@@ -643,10 +675,10 @@ module RailsErrorDashboard
643
675
 
644
676
  Commands::LogError.call(exception, { request: request, source: "dashboard.test_error" })
645
677
 
646
- 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")
647
679
  redirect_to errors_path(**app_context_params)
648
680
  rescue => e
649
- flash[:alert] = "Failed to log test error: #{e.message}"
681
+ flash[:alert] = red_t("red.flash.test_error.failed", message: e.message)
650
682
  redirect_to settings_path(**app_context_params)
651
683
  end
652
684
 
@@ -656,6 +688,32 @@ module RailsErrorDashboard
656
688
 
657
689
  private
658
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
+
659
717
  def calculate_release_comparison
660
718
  return {} if @errors_by_version.empty? || @errors_by_version.count < 2
661
719
 
@@ -743,45 +801,5 @@ module RailsErrorDashboard
743
801
  @storm_banner_loaded = true
744
802
  @storm_banner_event = Queries::StormHistory.banner_event
745
803
  end
746
-
747
- def authenticate_dashboard_user!
748
- auth_lambda = RailsErrorDashboard.configuration.authenticate_with
749
-
750
- if auth_lambda
751
- authenticate_with_lambda(auth_lambda)
752
- else
753
- authenticate_with_basic_auth
754
- end
755
- end
756
-
757
- def authenticate_with_lambda(auth_lambda)
758
- authorized = begin
759
- instance_exec(&auth_lambda)
760
- rescue => e
761
- Rails.logger.error(
762
- "[RailsErrorDashboard] authenticate_with lambda raised #{e.class}: #{e.message}"
763
- )
764
- false
765
- end
766
-
767
- return if performed?
768
-
769
- unless authorized
770
- render plain: "Access Denied", status: :forbidden
771
- end
772
- end
773
-
774
- def authenticate_with_basic_auth
775
- authenticate_or_request_with_http_basic do |username, password|
776
- ActiveSupport::SecurityUtils.secure_compare(
777
- username,
778
- RailsErrorDashboard.configuration.dashboard_username
779
- ) &
780
- ActiveSupport::SecurityUtils.secure_compare(
781
- password,
782
- RailsErrorDashboard.configuration.dashboard_password
783
- )
784
- end
785
- end
786
804
  end
787
805
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ # Per-user language selection for the dashboard, persisted in the session.
5
+ #
6
+ # Its own controller rather than another action on ErrorsController: this
7
+ # touches no error data, and keeping it separate means the picker cannot be
8
+ # reached by anything that filters or loads errors.
9
+ class LocalesController < ApplicationController
10
+ # POST /locale
11
+ #
12
+ # Sets session[:red_locale] and returns the user to where they were.
13
+ #
14
+ # The new locale takes effect on the NEXT request, not this one. That is
15
+ # deliberate and it is what keeps the around_action ordering intact
16
+ # (P5-T1 REQ-9): with_dashboard_locale has already resolved and set
17
+ # Current.locale by the time this action body runs, so writing the session
18
+ # here cannot disturb a locale that is mid-request. The redirect then
19
+ # renders the target page under the new value.
20
+ def create
21
+ requested = params[:locale].to_s
22
+
23
+ if I18nStore.available?(requested)
24
+ # Store the canonical shipped spelling, not what the user sent.
25
+ # I18nStore.resolve matches case-insensitively, so "PT-br" would
26
+ # otherwise be written to the session and re-resolved on every
27
+ # subsequent request.
28
+ session[:red_locale] = I18nStore.resolve(requested)
29
+ else
30
+ # REQ-6: an unrecognized value is cleared rather than left to fail
31
+ # silently on every later request. Falling through to config is the
32
+ # correct behaviour, and clearing is what makes it happen.
33
+ session.delete(:red_locale)
34
+ flash[:alert] = red_t("red.flash.locale.unavailable")
35
+ end
36
+
37
+ redirect_to safe_return_path
38
+ rescue StandardError => e
39
+ # Session storage can be unavailable (REQ-7: an API-only host where the
40
+ # engine's session middleware did not take effect). Changing the language
41
+ # is never worth breaking the dashboard over — the user keeps the
42
+ # configured locale and the page still renders.
43
+ Rails.logger.warn("[RailsErrorDashboard] Locale selection failed: #{e.class} - #{e.message}")
44
+ redirect_to safe_return_path
45
+ end
46
+
47
+ private
48
+
49
+ # Where to send the user back to (REQ-8: same page, query params intact).
50
+ #
51
+ # Never trusts the raw Referer. An absolute URL from another origin would
52
+ # turn the picker into an open redirect, so only the path and query of a
53
+ # referer that belongs to this engine are reused; anything else falls back
54
+ # to the dashboard root.
55
+ def safe_return_path
56
+ referer = request.referer.to_s
57
+ return default_return_path if referer.empty?
58
+
59
+ uri = URI.parse(referer)
60
+ return default_return_path unless same_origin?(uri)
61
+
62
+ candidate = uri.path.to_s
63
+ return default_return_path unless candidate.start_with?(engine_mount_path)
64
+
65
+ uri.query.present? ? "#{candidate}?#{uri.query}" : candidate
66
+ rescue StandardError
67
+ default_return_path
68
+ end
69
+
70
+ def same_origin?(uri)
71
+ return true if uri.host.nil? # a relative referer
72
+
73
+ uri.host == request.host && uri.port == request.port
74
+ end
75
+
76
+ def engine_mount_path
77
+ path = RailsErrorDashboard::Engine.routes.find_script_name({})
78
+ path.presence || "/"
79
+ rescue StandardError
80
+ "/"
81
+ end
82
+
83
+ def default_return_path
84
+ overview_path
85
+ end
86
+ end
87
+ end