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.
- checksums.yaml +4 -4
- data/README.md +35 -0
- data/app/controllers/rails_error_dashboard/application_controller.rb +156 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +95 -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 +39 -28
- 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 +23 -0
- 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,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] = "
|
|
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,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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
655
|
+
flash[:notice] = red_t("red.flash.coverage.enabled")
|
|
620
656
|
else
|
|
621
|
-
flash[:alert] = "
|
|
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] = "
|
|
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] = "
|
|
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] = "
|
|
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
|