rails_error_dashboard 0.14.3 → 0.15.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 +45 -17
- data/app/controllers/rails_error_dashboard/application_controller.rb +28 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +68 -2
- data/app/controllers/rails_error_dashboard/health_controller.rb +38 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +10 -2
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +56 -2
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +20 -2
- data/app/jobs/rails_error_dashboard/cascade_detection_job.rb +30 -0
- data/app/jobs/rails_error_dashboard/concerns/plain_channel_message.rb +33 -6
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +1 -1
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +1 -1
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +26 -0
- data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +6 -1
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +1 -1
- data/app/jobs/rails_error_dashboard/telegram_error_notification_job.rb +25 -0
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +2 -18
- data/app/models/rails_error_dashboard/error_log.rb +43 -12
- data/app/models/rails_error_dashboard/missing_translation.rb +43 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +24 -1
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +3 -1
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +3 -1
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +3 -9
- data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -0
- data/app/views/rails_error_dashboard/errors/boot_errors.html.erb +130 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/missing_translations.html.erb +143 -0
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +7 -4
- data/app/views/rails_error_dashboard/errors/settings.html.erb +19 -5
- data/config/locales/de.yml +72 -5
- data/config/locales/en.yml +83 -4
- data/config/locales/es.yml +72 -5
- data/config/locales/fr.yml +72 -5
- data/config/locales/it.yml +72 -5
- data/config/locales/ja.yml +72 -4
- data/config/locales/pl.yml +72 -5
- data/config/locales/pt-BR.yml +72 -5
- data/config/locales/ru.yml +72 -5
- data/config/locales/uk.yml +72 -5
- data/config/locales/zh-CN.yml +72 -4
- data/config/routes.rb +6 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +10 -2
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +9 -0
- data/db/migrate/20261010000001_create_missing_translations.rb +50 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +40 -38
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +31 -3
- data/lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +11 -2
- data/lib/rails_error_dashboard/commands/assign_error.rb +24 -1
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +9 -1
- data/lib/rails_error_dashboard/commands/flush_missing_translations.rb +103 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +6 -3
- data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -15
- data/lib/rails_error_dashboard/commands/update_error_status.rb +7 -0
- data/lib/rails_error_dashboard/configuration.rb +75 -2
- data/lib/rails_error_dashboard/engine.rb +52 -5
- data/lib/rails_error_dashboard/middleware/rate_limiter.rb +21 -43
- data/lib/rails_error_dashboard/plugin.rb +49 -3
- data/lib/rails_error_dashboard/plugin_registry.rb +61 -12
- data/lib/rails_error_dashboard/queries/boot_errors.rb +76 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +3 -11
- data/lib/rails_error_dashboard/queries/errors_list.rb +11 -17
- data/lib/rails_error_dashboard/queries/health_status.rb +191 -0
- data/lib/rails_error_dashboard/queries/missing_translation_summary.rb +82 -0
- data/lib/rails_error_dashboard/services/active_storage_probe.rb +91 -0
- data/lib/rails_error_dashboard/services/backtrace_parser.rb +13 -7
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +15 -0
- data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +30 -0
- data/lib/rails_error_dashboard/services/crash_capture.rb +85 -2
- data/lib/rails_error_dashboard/services/digest_builder.rb +9 -6
- data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +6 -0
- data/lib/rails_error_dashboard/services/exception_filter.rb +4 -1
- data/lib/rails_error_dashboard/services/github_link_generator.rb +25 -9
- data/lib/rails_error_dashboard/services/missing_translation_handler.rb +152 -0
- data/lib/rails_error_dashboard/services/missing_translation_tracker.rb +316 -0
- data/lib/rails_error_dashboard/services/resolution_callbacks.rb +44 -0
- data/lib/rails_error_dashboard/services/severity_classifier.rb +52 -0
- data/lib/rails_error_dashboard/services/solid_queue_config_check.rb +47 -0
- data/lib/rails_error_dashboard/services/source_code_reader.rb +23 -3
- data/lib/rails_error_dashboard/services/telegram_delivery.rb +134 -0
- data/lib/rails_error_dashboard/services/telegram_payload_builder.rb +105 -0
- data/lib/rails_error_dashboard/services/webhook_delivery.rb +63 -0
- data/lib/rails_error_dashboard/services/webhook_signer.rb +80 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +12 -0
- data/lib/tasks/error_dashboard.rake +43 -2
- metadata +21 -2
|
@@ -44,7 +44,7 @@ module RailsErrorDashboard
|
|
|
44
44
|
http.use_ssl = uri.scheme == "https"
|
|
45
45
|
http.open_timeout = 5
|
|
46
46
|
http.read_timeout = 10
|
|
47
|
-
request = Net::HTTP::Post.new(uri.
|
|
47
|
+
request = Net::HTTP::Post.new(uri.request_uri, { "Content-Type" => "application/json" })
|
|
48
48
|
request.body = payload.to_json
|
|
49
49
|
http.request(request)
|
|
50
50
|
end
|
|
@@ -70,6 +70,7 @@ module RailsErrorDashboard
|
|
|
70
70
|
# errors — clean them up BEFORE the early return below, which fires
|
|
71
71
|
# whenever no error logs happen to be expired.
|
|
72
72
|
cleanup_rack_attack_events(cutoff)
|
|
73
|
+
cleanup_missing_translations(cutoff)
|
|
73
74
|
cleanup_storm_flush_batches(cutoff)
|
|
74
75
|
cleanup_diagnostic_dumps(cutoff)
|
|
75
76
|
cleanup_swallowed_exceptions(cutoff)
|
|
@@ -211,6 +212,31 @@ module RailsErrorDashboard
|
|
|
211
212
|
)
|
|
212
213
|
end
|
|
213
214
|
|
|
215
|
+
# Expire missing-translation rows not seen since the cutoff. A key that
|
|
216
|
+
# is still missing is still being seen, so it keeps its row (and its
|
|
217
|
+
# first_seen_at); only keys that stopped missing age out.
|
|
218
|
+
#
|
|
219
|
+
# Not gated on the feature flag: rows collected while tracking was on
|
|
220
|
+
# must still age out after it is turned off.
|
|
221
|
+
def cleanup_missing_translations(cutoff)
|
|
222
|
+
return unless MissingTranslation.table_exists?
|
|
223
|
+
|
|
224
|
+
deleted = 0
|
|
225
|
+
MissingTranslation.where("last_seen_at < ?", cutoff).in_batches(of: 1000) do |batch|
|
|
226
|
+
deleted += batch.delete_all
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if deleted > 0
|
|
230
|
+
RailsErrorDashboard::Logger.info(
|
|
231
|
+
"[RailsErrorDashboard] Retention cleanup: deleted #{deleted} missing translations"
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
rescue => e
|
|
235
|
+
RailsErrorDashboard::Logger.debug(
|
|
236
|
+
"[RailsErrorDashboard] Missing translation retention cleanup failed: #{e.class} - #{e.message}"
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
214
240
|
# Expire aggregated Rack Attack event rows. Isolated in its own rescue so a
|
|
215
241
|
# failure here (e.g. table not yet migrated) never blocks error cleanup.
|
|
216
242
|
def cleanup_rack_attack_events(cutoff)
|
|
@@ -12,11 +12,16 @@ module RailsErrorDashboard
|
|
|
12
12
|
class ScheduledDigestJob < ApplicationJob
|
|
13
13
|
queue_as :default
|
|
14
14
|
|
|
15
|
+
# @param period [String, Symbol, nil] "daily" or "weekly"; nil takes
|
|
16
|
+
# config.digest_frequency, which was shown on the Settings page but
|
|
17
|
+
# read by nothing until now.
|
|
15
18
|
# @param locale [String, nil] resolved at enqueue time. nil for jobs
|
|
16
19
|
# enqueued by a pre-Phase-4 version still draining from the queue.
|
|
17
|
-
def perform(period:
|
|
20
|
+
def perform(period: nil, application_id: nil, locale: nil)
|
|
18
21
|
return unless RailsErrorDashboard.configuration.enable_scheduled_digests
|
|
19
22
|
|
|
23
|
+
period ||= RailsErrorDashboard.configuration.digest_frequency || :daily
|
|
24
|
+
|
|
20
25
|
recipients = effective_recipients
|
|
21
26
|
return if recipients.blank?
|
|
22
27
|
|
|
@@ -32,7 +32,7 @@ module RailsErrorDashboard
|
|
|
32
32
|
http.open_timeout = 5 # 5 seconds to establish connection
|
|
33
33
|
http.read_timeout = 10 # 10 seconds to read response
|
|
34
34
|
|
|
35
|
-
request = Net::HTTP::Post.new(uri.
|
|
35
|
+
request = Net::HTTP::Post.new(uri.request_uri, { "Content-Type" => "application/json" })
|
|
36
36
|
request.body = Services::SlackPayloadBuilder.call(error_log, locale: locale).to_json
|
|
37
37
|
|
|
38
38
|
response = http.request(request)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Job to send error notifications to a Telegram chat via the Bot API
|
|
5
|
+
class TelegramErrorNotificationJob < ApplicationJob
|
|
6
|
+
queue_as :default
|
|
7
|
+
|
|
8
|
+
# @param locale [String, nil] resolved at enqueue time.
|
|
9
|
+
def perform(error_log_id, locale = nil)
|
|
10
|
+
return unless Services::TelegramDelivery.credentials?
|
|
11
|
+
|
|
12
|
+
error_log = ErrorLog.find(error_log_id)
|
|
13
|
+
payload = Services::TelegramPayloadBuilder.call(error_log, locale: job_locale(locale))
|
|
14
|
+
|
|
15
|
+
Services::TelegramDelivery.post(payload)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
# The bot token is in the request URL; an exception that quotes it must
|
|
18
|
+
# not reach the log as-is.
|
|
19
|
+
Rails.logger.error(
|
|
20
|
+
"[RailsErrorDashboard] Failed to send Telegram notification: #{Services::TelegramDelivery.redact(e.message)}"
|
|
21
|
+
)
|
|
22
|
+
Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -31,13 +31,12 @@ module RailsErrorDashboard
|
|
|
31
31
|
|
|
32
32
|
def send_webhook(url, payload, error_log)
|
|
33
33
|
headers = {
|
|
34
|
-
"Content-Type" => "application/json",
|
|
35
|
-
"User-Agent" => "RailsErrorDashboard/1.0",
|
|
36
34
|
"X-Error-Dashboard-Event" => "error.created",
|
|
37
35
|
"X-Error-Dashboard-ID" => error_log.id.to_s
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
# Serialises, signs (when webhook_signing_secret is set) and posts.
|
|
39
|
+
response = Services::WebhookDelivery.post(url, payload, headers: headers)
|
|
41
40
|
|
|
42
41
|
unless response_success?(response)
|
|
43
42
|
Rails.logger.warn("[RailsErrorDashboard] Webhook failed for #{url}: #{response_code(response)}")
|
|
@@ -46,21 +45,6 @@ module RailsErrorDashboard
|
|
|
46
45
|
Rails.logger.error("[RailsErrorDashboard] Webhook error for #{url}: #{e.message}")
|
|
47
46
|
end
|
|
48
47
|
|
|
49
|
-
def post_json(url, payload, headers)
|
|
50
|
-
if defined?(HTTParty)
|
|
51
|
-
HTTParty.post(url, body: payload.to_json, headers: headers, timeout: 10)
|
|
52
|
-
else
|
|
53
|
-
uri = URI(url)
|
|
54
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
55
|
-
http.use_ssl = uri.scheme == "https"
|
|
56
|
-
http.open_timeout = 5
|
|
57
|
-
http.read_timeout = 10
|
|
58
|
-
request = Net::HTTP::Post.new(uri.path, headers)
|
|
59
|
-
request.body = payload.to_json
|
|
60
|
-
http.request(request)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
48
|
def response_success?(response)
|
|
65
49
|
if response.respond_to?(:success?)
|
|
66
50
|
response.success?
|
|
@@ -204,6 +204,13 @@ module RailsErrorDashboard
|
|
|
204
204
|
Commands::LogError.call(exception, context)
|
|
205
205
|
end
|
|
206
206
|
|
|
207
|
+
# nil rather than a wrong user: an integer column casts "3f2a9c1e-..." to 3
|
|
208
|
+
def self.storable_user_id(user_id)
|
|
209
|
+
return user_id unless type_for_attribute("user_id").type == :integer
|
|
210
|
+
|
|
211
|
+
Integer(user_id.to_s, 10, exception: false)
|
|
212
|
+
end
|
|
213
|
+
|
|
207
214
|
# Mark error as resolved (delegates to Command)
|
|
208
215
|
def resolve!(resolution_data = {})
|
|
209
216
|
Commands::ResolveError.call(id, resolution_data)
|
|
@@ -415,13 +422,8 @@ module RailsErrorDashboard
|
|
|
415
422
|
return {} unless RailsErrorDashboard.configuration.enable_occurrence_patterns
|
|
416
423
|
return {} unless defined?(Services::PatternDetector)
|
|
417
424
|
|
|
418
|
-
timestamps = self.class
|
|
419
|
-
.where(error_type: error_type, platform: platform)
|
|
420
|
-
.where("occurred_at >= ?", days.days.ago)
|
|
421
|
-
.pluck(:occurred_at)
|
|
422
|
-
|
|
423
425
|
Services::PatternDetector.analyze_cyclical_pattern(
|
|
424
|
-
timestamps:
|
|
426
|
+
timestamps: pattern_timestamps(days: days),
|
|
425
427
|
days: days
|
|
426
428
|
)
|
|
427
429
|
end
|
|
@@ -433,13 +435,42 @@ module RailsErrorDashboard
|
|
|
433
435
|
return [] unless RailsErrorDashboard.configuration.enable_occurrence_patterns
|
|
434
436
|
return [] unless defined?(Services::PatternDetector)
|
|
435
437
|
|
|
436
|
-
timestamps
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
Services::PatternDetector.detect_bursts(timestamps: pattern_timestamps(days: days))
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# The most recent occurrence timestamps a Pattern Insights sample reads.
|
|
442
|
+
# A grouped row holds one occurred_at for hundreds of events, so the
|
|
443
|
+
# pattern used to be computed from one timestamp per error: the per-event
|
|
444
|
+
# rows are what a daily or weekly rhythm is visible in. Capped so an error
|
|
445
|
+
# with a million occurrences does not load a million Time objects to
|
|
446
|
+
# bucket them by hour; the newest PATTERN_SAMPLE_LIMIT are enough for a
|
|
447
|
+
# distribution and for the bursts of the last week.
|
|
448
|
+
PATTERN_SAMPLE_LIMIT = 10_000
|
|
449
|
+
|
|
450
|
+
def pattern_timestamps(days:)
|
|
451
|
+
since = days.days.ago
|
|
452
|
+
grouped = self.class.where(error_type: error_type, platform: platform)
|
|
453
|
+
|
|
454
|
+
if occurrence_rows_available?
|
|
455
|
+
timestamps = ErrorOccurrence.where(error_log_id: grouped.select(:id))
|
|
456
|
+
.where("occurred_at >= ?", since)
|
|
457
|
+
.order(occurred_at: :desc)
|
|
458
|
+
.limit(PATTERN_SAMPLE_LIMIT)
|
|
459
|
+
.pluck(:occurred_at)
|
|
460
|
+
.reverse
|
|
461
|
+
return timestamps if timestamps.any?
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# No per-event rows: an install from before the occurrences table, or
|
|
465
|
+
# rows captured while a storm was being shed. The grouped rows are the
|
|
466
|
+
# only timestamps there are.
|
|
467
|
+
grouped.where("occurred_at >= ?", since).order(:occurred_at).pluck(:occurred_at)
|
|
468
|
+
end
|
|
441
469
|
|
|
442
|
-
|
|
470
|
+
def occurrence_rows_available?
|
|
471
|
+
defined?(ErrorOccurrence) && ErrorOccurrence.table_exists?
|
|
472
|
+
rescue StandardError
|
|
473
|
+
false
|
|
443
474
|
end
|
|
444
475
|
|
|
445
476
|
private
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# A translation key the host app looked up and did not have, counted per
|
|
5
|
+
# (locale, key, application).
|
|
6
|
+
#
|
|
7
|
+
# Rows are written by Commands::FlushMissingTranslations from the tracker's
|
|
8
|
+
# in-memory buffer, never one per lookup. Inherits ErrorLogsRecord so
|
|
9
|
+
# separate-database routing applies.
|
|
10
|
+
class MissingTranslation < ErrorLogsRecord
|
|
11
|
+
self.table_name = "rails_error_dashboard_missing_translations"
|
|
12
|
+
|
|
13
|
+
# Not a translation key: a synthetic row holding the misses the tracker
|
|
14
|
+
# dropped because its per-thread buffer was full, kept so the totals the
|
|
15
|
+
# dashboard shows are never silently understated. Stored under a locale
|
|
16
|
+
# no real lookup uses. The tracker owns the values; aliased here so the
|
|
17
|
+
# writer and the scopes below cannot drift apart.
|
|
18
|
+
OVERFLOW_KEY = Services::MissingTranslationTracker::OVERFLOW_KEY
|
|
19
|
+
OVERFLOW_LOCALE = Services::MissingTranslationTracker::OVERFLOW_LOCALE
|
|
20
|
+
|
|
21
|
+
belongs_to :application, optional: true
|
|
22
|
+
|
|
23
|
+
validates :locale, presence: true
|
|
24
|
+
validates :translation_key, presence: true
|
|
25
|
+
validates :miss_count, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
26
|
+
|
|
27
|
+
scope :for_application, ->(app_id) { where(application_id: app_id) }
|
|
28
|
+
scope :seen_since, ->(time) { where("last_seen_at >= ?", time) }
|
|
29
|
+
scope :overflow, -> { where(translation_key: OVERFLOW_KEY) }
|
|
30
|
+
scope :real, -> { where.not(translation_key: OVERFLOW_KEY) }
|
|
31
|
+
|
|
32
|
+
# The table may not be migrated yet on a host that upgraded the gem before
|
|
33
|
+
# running the migration. Everything that touches this model on a request
|
|
34
|
+
# path checks first and must never raise.
|
|
35
|
+
def self.table_exists?
|
|
36
|
+
connection.table_exists?(table_name)
|
|
37
|
+
rescue *Commands::LogError::RETRYABLE_STORE_ERRORS
|
|
38
|
+
raise
|
|
39
|
+
rescue StandardError
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -1156,6 +1156,12 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1156
1156
|
<% if RailsErrorDashboard.configuration.enable_storm_protection %>
|
|
1157
1157
|
<% diag_items << { path: storms_errors_path(nav_params), icon: 'bi-cloud-lightning-rain', label: red_t("red.nav.storms") } %>
|
|
1158
1158
|
<% end %>
|
|
1159
|
+
<% if RailsErrorDashboard.configuration.enable_missing_translation_tracking %>
|
|
1160
|
+
<% diag_items << { path: missing_translations_errors_path(nav_params), icon: 'bi-translate', label: red_t("red.nav.missing_translations") } %>
|
|
1161
|
+
<% end %>
|
|
1162
|
+
<% if RailsErrorDashboard.configuration.enable_crash_capture %>
|
|
1163
|
+
<% diag_items << { path: boot_errors_errors_path(nav_params), icon: 'bi-power', label: red_t("red.nav.boot_errors") } %>
|
|
1164
|
+
<% end %>
|
|
1159
1165
|
|
|
1160
1166
|
<% if diag_items.any? %>
|
|
1161
1167
|
<div style="margin-bottom: var(--space-2);" id="navDiagSection">
|
|
@@ -1335,6 +1341,19 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1335
1341
|
</div>
|
|
1336
1342
|
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>try { if (sessionStorage.getItem('red_dismiss_creds_warning') === '1') { document.getElementById('security-warning').style.display = 'none'; } } catch(e) {}</script>
|
|
1337
1343
|
<% end %>
|
|
1344
|
+
<% solid_queue_problems = red_solid_queue_problems %>
|
|
1345
|
+
<% if solid_queue_problems.any? %>
|
|
1346
|
+
<div id="solid-queue-warning" class="alert alert-warning" style="display: flex; align-items: flex-start; gap: 10px; margin-top: var(--space-2); margin-bottom: var(--space-4); border-left: 4px solid var(--status-warning);">
|
|
1347
|
+
<i class="bi bi-exclamation-triangle-fill" style="font-size: 18px; flex-shrink: 0;"></i>
|
|
1348
|
+
<div style="flex: 1;">
|
|
1349
|
+
<strong><%= red_t("red.banners.solid_queue.label") %></strong> <%= red_t("red.banners.solid_queue.body_html") %>
|
|
1350
|
+
<ul style="margin: var(--space-2) 0; padding-left: var(--space-5); font-family: var(--font-mono); font-size: 12px;">
|
|
1351
|
+
<% solid_queue_problems.each do |problem| %><li><%= problem %></li><% end %>
|
|
1352
|
+
</ul>
|
|
1353
|
+
<a href="<%= RailsErrorDashboard::Services::SolidQueueConfigCheck::GUIDE_URL %>" target="_blank" rel="noopener"><%= red_t("red.banners.solid_queue.guide") %></a>
|
|
1354
|
+
</div>
|
|
1355
|
+
</div>
|
|
1356
|
+
<% end %>
|
|
1338
1357
|
<% if defined?(@storm_banner_event) && @storm_banner_event %>
|
|
1339
1358
|
<% storm_active = @storm_banner_event.active? %>
|
|
1340
1359
|
<div id="storm-banner" class="alert <%= storm_active ? 'alert-danger' : 'alert-info' %>" style="display: flex; align-items: center; gap: 10px; margin-top: var(--space-2); margin-bottom: var(--space-4); border-left: 4px solid <%= storm_active ? 'var(--status-critical)' : 'var(--status-info)' %>;">
|
|
@@ -1539,7 +1558,11 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1539
1558
|
});
|
|
1540
1559
|
|
|
1541
1560
|
if (typeof hljs.lineNumbersBlock === 'function') {
|
|
1542
|
-
|
|
1561
|
+
// The gutter shows the file's own line numbers: the excerpt starts at
|
|
1562
|
+
// data-start-line, not at 1.
|
|
1563
|
+
codeBlocks.forEach(function(block) {
|
|
1564
|
+
hljs.lineNumbersBlock(block, { startFrom: parseInt(block.dataset.startLine) || 1 });
|
|
1565
|
+
});
|
|
1543
1566
|
} else if (typeof hljs.initLineNumbersOnLoad === 'function') {
|
|
1544
1567
|
hljs.initLineNumbersOnLoad();
|
|
1545
1568
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<%# The locale is part of the cache key: this fragment now contains
|
|
2
2
|
translated text, and without it the first locale to render an error
|
|
3
3
|
would be served to every other one. %>
|
|
4
|
-
|
|
4
|
+
<%# Plus the source-integration configuration and a TTL, and not at all
|
|
5
|
+
while coverage is live: see BacktraceHelper#source_view_cache_key. %>
|
|
6
|
+
<% cache_if source_view_cacheable?, [error, 'error_details_v3', red_locale, source_view_cache_key], **source_view_cache_options do %>
|
|
5
7
|
<div class="card mb-4" id="section-error-info">
|
|
6
8
|
<div class="card-header bg-danger text-white">
|
|
7
9
|
<div class="d-flex justify-content-between align-items-center">
|
|
@@ -187,8 +187,10 @@
|
|
|
187
187
|
</small>
|
|
188
188
|
<% end %>
|
|
189
189
|
</div>
|
|
190
|
+
<%# data-red-action, not data-turbo-confirm: the layout runs with
|
|
191
|
+
Turbo off and loads no UJS, so nothing read the old attribute. %>
|
|
190
192
|
<%= button_to unassign_error_path(error), method: :post, class: "btn btn-sm btn-outline-secondary",
|
|
191
|
-
data: {
|
|
193
|
+
form: { data: { red_action: "confirm-submit", red_confirm_message: red_t("red.errors.sidebar.remove_assignment") } } do %>
|
|
192
194
|
<i class="bi bi-x"></i>
|
|
193
195
|
<% end %>
|
|
194
196
|
</div>
|
|
@@ -20,17 +20,11 @@
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<% if repo_link %>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# sentence around the name is translated.
|
|
26
|
-
repo_host = if repo_link.include?("github") then "GitHub"
|
|
27
|
-
elsif repo_link.include?("gitlab") then "GitLab"
|
|
28
|
-
else "Bitbucket"
|
|
29
|
-
end
|
|
30
|
-
%>
|
|
23
|
+
<%# Repository host names are brands and stay English; only the
|
|
24
|
+
sentence around the name is translated. %>
|
|
31
25
|
<%= link_to repo_link, target: "_blank", rel: "noopener",
|
|
32
26
|
class: "btn btn-sm btn-primary",
|
|
33
|
-
title: red_t("red.errors.source_code.view_on", host:
|
|
27
|
+
title: red_t("red.errors.source_code.view_on", host: repository_host_label(repo_link)) do %>
|
|
34
28
|
<i class="bi bi-box-arrow-up-right"></i> <%= red_t("red.errors.source_code.view_source") %>
|
|
35
29
|
<% end %>
|
|
36
30
|
<% end %>
|
|
@@ -20,6 +20,34 @@
|
|
|
20
20
|
</div>
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
|
+
<% probe = @storage_probe || {} %>
|
|
24
|
+
<% if probe[:status] && probe[:status] != "skipped" %>
|
|
25
|
+
<% reachable = probe[:status] == "ok" %>
|
|
26
|
+
<div class="card mb-4 border-<%= reachable ? "success" : "danger" %>">
|
|
27
|
+
<div class="card-body d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
28
|
+
<div>
|
|
29
|
+
<h6 class="mb-1">
|
|
30
|
+
<i class="bi <%= reachable ? "bi-check-circle-fill text-success" : "bi-x-circle-fill text-danger" %> me-1"></i>
|
|
31
|
+
<%= red_t("red.health.activestorage.probe.title") %>
|
|
32
|
+
<span class="badge <%= reachable ? "bg-success" : "bg-danger" %> ms-1">
|
|
33
|
+
<%= reachable ? red_t("red.health.activestorage.probe.reachable") : red_t("red.health.activestorage.probe.unreachable") %>
|
|
34
|
+
</span>
|
|
35
|
+
</h6>
|
|
36
|
+
<small class="text-muted">
|
|
37
|
+
<%# The service name comes from config/storage.yml and the error is an exception class: machine values, shown as-is. %>
|
|
38
|
+
<%= red_t("red.health.activestorage.probe.service", name: probe[:service].presence || "?") %>
|
|
39
|
+
<% if reachable %>
|
|
40
|
+
· <%= red_t("red.health.activestorage.probe.latency", ms: probe[:latency_ms]) %>
|
|
41
|
+
<% else %>
|
|
42
|
+
· <code><%= probe[:error] %></code>
|
|
43
|
+
<% end %>
|
|
44
|
+
</small>
|
|
45
|
+
</div>
|
|
46
|
+
<small class="text-muted"><%= red_t("red.health.activestorage.probe.how") %></small>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
23
51
|
<% if @unique_services == 0 %>
|
|
24
52
|
<div class="red-empty-state">
|
|
25
53
|
<i class="bi bi-cloud-arrow-up display-1 text-success mb-3"></i>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<% content_for :page_title, red_t("red.errors.boot_errors_page.title") %>
|
|
2
|
+
|
|
3
|
+
<div>
|
|
4
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
5
|
+
<h1 style="font-size: 20px; font-weight: 700; margin: 0;">
|
|
6
|
+
<i class="bi bi-power me-2"></i>
|
|
7
|
+
<%= red_t("red.errors.boot_errors_page.title") %>
|
|
8
|
+
</h1>
|
|
9
|
+
|
|
10
|
+
<div class="btn-group" role="group">
|
|
11
|
+
<%= link_to boot_errors_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
12
|
+
<%= red_t("red.common.days_filter.7") %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= link_to boot_errors_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
15
|
+
<%= red_t("red.common.days_filter.30") %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= link_to boot_errors_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
18
|
+
<%= red_t("red.common.days_filter.90") %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<% if @entry_count == 0 %>
|
|
24
|
+
<div class="red-empty-state">
|
|
25
|
+
<i class="bi bi-power display-1 text-success mb-3"></i>
|
|
26
|
+
<div class="red-empty-state-title"><%= red_t("red.errors.boot_errors_page.empty_title") %></div>
|
|
27
|
+
<p class="text-muted">
|
|
28
|
+
<%= red_t("red.errors.boot_errors_page.empty_body", days: @days) %>
|
|
29
|
+
</p>
|
|
30
|
+
<div class="card mx-auto" style="max-width: 560px;">
|
|
31
|
+
<div class="card-body text-start">
|
|
32
|
+
<h6><%= red_t("red.errors.boot_errors_page.how_title") %></h6>
|
|
33
|
+
<ul class="mb-0">
|
|
34
|
+
<li><%= red_t("red.errors.boot_errors_page.how_enabled_html", option: content_tag(:code, "enable_crash_capture = true")) %></li>
|
|
35
|
+
<li><%= red_t("red.errors.boot_errors_page.how_hooked") %></li>
|
|
36
|
+
<li><%= red_t("red.errors.boot_errors_page.how_captured_html", zeitwerk: content_tag(:code, "Zeitwerk::NameError"), syntax: content_tag(:code, "SyntaxError")) %></li>
|
|
37
|
+
<li><%= red_t("red.errors.boot_errors_page.how_imported") %></li>
|
|
38
|
+
<li><%= red_t("red.errors.boot_errors_page.how_never") %></li>
|
|
39
|
+
</ul>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<% else %>
|
|
44
|
+
<div class="row mb-4">
|
|
45
|
+
<div class="col-md-4">
|
|
46
|
+
<div class="card text-center">
|
|
47
|
+
<div class="card-body">
|
|
48
|
+
<div class="display-6 text-danger"><%= @unresolved_count %></div>
|
|
49
|
+
<small class="text-muted"><%= red_t("red.errors.boot_errors_page.unresolved") %></small>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="col-md-4">
|
|
54
|
+
<div class="card text-center">
|
|
55
|
+
<div class="card-body">
|
|
56
|
+
<div class="display-6 text-warning"><%= @entry_count %></div>
|
|
57
|
+
<small class="text-muted"><%= red_t("red.errors.boot_errors_page.boot_crashes") %></small>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="col-md-4">
|
|
62
|
+
<div class="card text-center">
|
|
63
|
+
<div class="card-body">
|
|
64
|
+
<div class="display-6 text-info"><%= @total_occurrences %></div>
|
|
65
|
+
<small class="text-muted"><%= red_t("red.errors.boot_errors_page.failed_boots") %></small>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="card mb-4">
|
|
72
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
73
|
+
<h5 class="mb-0">
|
|
74
|
+
<i class="bi bi-power text-danger me-2"></i>
|
|
75
|
+
<%= red_t("red.errors.boot_errors_page.all_title") %>
|
|
76
|
+
<span class="badge bg-danger"><%= @entry_count %></span>
|
|
77
|
+
</h5>
|
|
78
|
+
<small class="text-muted"><%== @pagy.info_tag %></small>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="card-body p-0">
|
|
81
|
+
<div class="table-responsive">
|
|
82
|
+
<table class="table table-hover mb-0">
|
|
83
|
+
<thead class="table-light">
|
|
84
|
+
<tr>
|
|
85
|
+
<th><%= red_t("red.errors.boot_errors_page.column_error") %></th>
|
|
86
|
+
<th><%= red_t("red.errors.boot_errors_page.column_file") %></th>
|
|
87
|
+
<th width="140"><%= red_t("red.errors.boot_errors_page.column_constant") %></th>
|
|
88
|
+
<th width="90"><%= red_t("red.errors.boot_errors_page.column_boots") %></th>
|
|
89
|
+
<th width="140"><%= red_t("red.errors.boot_errors_page.column_first_seen") %></th>
|
|
90
|
+
<th width="140"><%= red_t("red.errors.pages.column_last_seen") %></th>
|
|
91
|
+
<th width="100"><%= red_t("red.errors.boot_errors_page.column_status") %></th>
|
|
92
|
+
</tr>
|
|
93
|
+
</thead>
|
|
94
|
+
<tbody>
|
|
95
|
+
<% @entries.each do |entry| %>
|
|
96
|
+
<tr>
|
|
97
|
+
<td>
|
|
98
|
+
<%# Exception class and message are machine values from the crash file, shown as-is. %>
|
|
99
|
+
<%= link_to entry[:error_type], error_path(entry[:id]), class: "fw-semibold text-decoration-none" %>
|
|
100
|
+
<div class="text-muted small text-truncate" style="max-width: 420px;" title="<%= entry[:message] %>"><%= entry[:message] %></div>
|
|
101
|
+
</td>
|
|
102
|
+
<td><% if entry[:file].present? %><code><%= entry[:file] %></code><% else %><span class="text-muted">—</span><% end %></td>
|
|
103
|
+
<td><% if entry[:constant].present? %><code><%= entry[:constant] %></code><% else %><span class="text-muted">—</span><% end %></td>
|
|
104
|
+
<td><strong><%= entry[:count] %></strong></td>
|
|
105
|
+
<td><%= local_time_ago(entry[:first_seen]) %></td>
|
|
106
|
+
<td><%= local_time_ago(entry[:last_seen]) %></td>
|
|
107
|
+
<td>
|
|
108
|
+
<% if entry[:resolved] %>
|
|
109
|
+
<span class="badge bg-success"><%= red_t("red.errors.boot_errors_page.status_resolved") %></span>
|
|
110
|
+
<% else %>
|
|
111
|
+
<span class="badge bg-danger"><%= red_t("red.errors.boot_errors_page.status_open") %></span>
|
|
112
|
+
<% end %>
|
|
113
|
+
</td>
|
|
114
|
+
</tr>
|
|
115
|
+
<% end %>
|
|
116
|
+
</tbody>
|
|
117
|
+
</table>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="card-footer border-top d-flex justify-content-between align-items-center">
|
|
121
|
+
<small class="text-muted">
|
|
122
|
+
<i class="bi bi-lightbulb text-warning"></i> <%= red_t("red.errors.boot_errors_page.footer_tip") %>
|
|
123
|
+
</small>
|
|
124
|
+
<div>
|
|
125
|
+
<%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<% end %>
|
|
130
|
+
</div>
|
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
<%= button_tag type: "submit", name: "action_type", value: "resolve", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px;" do %>
|
|
273
273
|
<i class="bi bi-check-circle"></i> <%= red_t("red.errors.index.batch.resolve") %>
|
|
274
274
|
<% end %>
|
|
275
|
-
<%= button_tag type: "submit", name: "action_type", value: "delete", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px; color: var(--status-critical);", data: { confirm: red_t("red.errors.index.batch.confirm_delete") } do %>
|
|
275
|
+
<%= button_tag type: "submit", name: "action_type", value: "delete", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px; color: var(--status-critical);", data: { red_action: "confirm-submit", red_confirm_message: red_t("red.errors.index.batch.confirm_delete") } do %>
|
|
276
276
|
<i class="bi bi-trash"></i> <%= red_t("red.errors.index.batch.delete") %>
|
|
277
277
|
<% end %>
|
|
278
278
|
<% end %>
|