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.
- checksums.yaml +4 -4
- data/README.md +35 -0
- data/app/controllers/rails_error_dashboard/application_controller.rb +107 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +91 -73
- data/app/controllers/rails_error_dashboard/locales_controller.rb +87 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +20 -14
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +15 -6
- data/app/helpers/rails_error_dashboard/i18n_helper.rb +154 -0
- data/app/helpers/rails_error_dashboard/mailer_i18n_helper.rb +69 -0
- data/app/helpers/rails_error_dashboard/overview_helper.rb +18 -6
- data/app/helpers/rails_error_dashboard/user_agent_helper.rb +25 -11
- data/app/jobs/rails_error_dashboard/application_job.rb +5 -0
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +18 -16
- data/app/jobs/rails_error_dashboard/concerns/localized_job.rb +83 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -2
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +4 -2
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +6 -2
- data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +8 -3
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +6 -4
- data/app/jobs/rails_error_dashboard/storm_notification_job.rb +26 -8
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +4 -2
- data/app/mailers/rails_error_dashboard/application_mailer.rb +14 -0
- data/app/mailers/rails_error_dashboard/digest_mailer.rb +18 -2
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +19 -2
- data/app/views/layouts/rails_error_dashboard.html.erb +255 -92
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +23 -22
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +30 -19
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +18 -16
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +21 -14
- data/app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb +11 -7
- data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +45 -38
- data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +17 -14
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +27 -9
- data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +25 -21
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +25 -22
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +24 -12
- data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +12 -11
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +19 -13
- data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +14 -14
- data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +8 -8
- data/app/views/rails_error_dashboard/errors/_modals.html.erb +60 -52
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +59 -49
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +23 -20
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +14 -14
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +118 -93
- data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +18 -12
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -5
- data/app/views/rails_error_dashboard/errors/_stats.html.erb +5 -5
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +15 -12
- data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +25 -25
- data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -28
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +82 -82
- data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +27 -27
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +61 -57
- data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +64 -65
- data/app/views/rails_error_dashboard/errors/deprecations.html.erb +23 -23
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +48 -36
- data/app/views/rails_error_dashboard/errors/index.html.erb +121 -70
- data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +30 -30
- data/app/views/rails_error_dashboard/errors/llm_health_summary.html.erb +38 -39
- data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +28 -25
- data/app/views/rails_error_dashboard/errors/overview.html.erb +45 -39
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +45 -44
- data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +30 -29
- data/app/views/rails_error_dashboard/errors/releases.html.erb +46 -47
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +48 -44
- data/app/views/rails_error_dashboard/errors/settings.html.erb +122 -112
- data/app/views/rails_error_dashboard/errors/show.html.erb +44 -37
- data/app/views/rails_error_dashboard/errors/storms.html.erb +30 -24
- data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +26 -26
- data/app/views/rails_error_dashboard/errors/user_impact.html.erb +31 -30
- data/config/locales/de.yml +2007 -0
- data/config/locales/en.yml +2719 -0
- data/config/locales/es.yml +2079 -0
- data/config/locales/fr.yml +2082 -0
- data/config/locales/it.yml +2060 -0
- data/config/locales/ja.yml +1802 -0
- data/config/locales/pl.yml +2108 -0
- data/config/locales/pt-BR.yml +2064 -0
- data/config/locales/ru.yml +2109 -0
- data/config/locales/uk.yml +2106 -0
- data/config/locales/zh-CN.yml +1791 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +9 -3
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +3 -1
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +4 -2
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +4 -2
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +4 -2
- data/lib/rails_error_dashboard/commands/create_issue.rb +5 -3
- data/lib/rails_error_dashboard/commands/link_existing_issue.rb +4 -2
- data/lib/rails_error_dashboard/commands/log_error.rb +1 -1
- data/lib/rails_error_dashboard/configuration.rb +10 -3
- data/lib/rails_error_dashboard/current.rb +56 -0
- data/lib/rails_error_dashboard/i18n_store.rb +293 -0
- data/lib/rails_error_dashboard/private_backend.rb +213 -0
- data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +89 -22
- data/lib/rails_error_dashboard/services/digest_builder.rb +21 -7
- data/lib/rails_error_dashboard/services/discord_payload_builder.rb +27 -15
- data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +10 -5
- data/lib/rails_error_dashboard/services/localized_time_formatter.rb +117 -0
- data/lib/rails_error_dashboard/services/notification_helpers.rb +87 -4
- data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +22 -5
- data/lib/rails_error_dashboard/services/slack_payload_builder.rb +48 -28
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -1
- data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +15 -1
- data/lib/rails_error_dashboard/translation.rb +39 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +4 -0
- data/lib/tasks/error_dashboard.rake +5 -1
- metadata +24 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f85952e5800c354ae158411afbec605cddbc5e19595e8e94620d1b13fc17894b
|
|
4
|
+
data.tar.gz: 1af4bb9352a7fcffc10fc6b913b896ca2fd75198d31e044ff156b76cb52f6afc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
89
|
+
# Set the dashboard's locale for this request and restore whatever was
|
|
90
|
+
# there before.
|
|
74
91
|
#
|
|
75
|
-
#
|
|
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
|
-
|
|
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] =
|
|
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] = "
|
|
175
|
+
flash[:notice] = red_t("red.flash.issue.created")
|
|
159
176
|
flash[:new_issue_url] = result[:issue_url]
|
|
160
177
|
else
|
|
161
|
-
flash[:alert] = "
|
|
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] = "
|
|
187
|
+
flash[:notice] = red_t("red.flash.issue.linked")
|
|
171
188
|
else
|
|
172
|
-
flash[:alert] = "
|
|
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: "
|
|
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: "
|
|
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 >
|
|
190
|
-
render json: {
|
|
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] = "
|
|
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: [ "
|
|
310
|
+
{ success: false, count: 0, errors: [ red_t("red.commands.invalid_action") ] }
|
|
291
311
|
end
|
|
292
312
|
|
|
293
313
|
if result[:success]
|
|
294
|
-
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
497
|
+
flash[:alert] = red_t("red.flash.swallowed_requires_ruby", version: RUBY_VERSION)
|
|
466
498
|
else
|
|
467
|
-
flash[:alert] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
655
|
+
flash[:notice] = red_t("red.flash.coverage.enabled")
|
|
624
656
|
else
|
|
625
|
-
flash[:alert] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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
|