rails_error_dashboard 0.14.4 → 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 +12 -5
- 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 +2 -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 +11 -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 +68 -5
- data/config/locales/en.yml +78 -4
- data/config/locales/es.yml +68 -5
- data/config/locales/fr.yml +68 -5
- data/config/locales/it.yml +68 -5
- data/config/locales/ja.yml +68 -4
- data/config/locales/pl.yml +68 -5
- data/config/locales/pt-BR.yml +68 -5
- data/config/locales/ru.yml +68 -5
- data/config/locales/uk.yml +68 -5
- data/config/locales/zh-CN.yml +68 -4
- data/config/routes.rb +6 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +10 -2
- data/db/migrate/20261010000001_create_missing_translations.rb +50 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +9 -4
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +27 -0
- 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 +1 -1
- 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 +42 -1
- data/lib/rails_error_dashboard/engine.rb +42 -1
- data/lib/rails_error_dashboard/middleware/rate_limiter.rb +7 -1
- data/lib/rails_error_dashboard/plugin.rb +19 -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 +8 -16
- 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/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/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
- metadata +21 -2
|
@@ -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">
|
|
@@ -1552,7 +1558,11 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1552
1558
|
});
|
|
1553
1559
|
|
|
1554
1560
|
if (typeof hljs.lineNumbersBlock === 'function') {
|
|
1555
|
-
|
|
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
|
+
});
|
|
1556
1566
|
} else if (typeof hljs.initLineNumbersOnLoad === 'function') {
|
|
1557
1567
|
hljs.initLineNumbersOnLoad();
|
|
1558
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 %>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<% content_for :page_title, red_t("red.errors.missing_translations_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-translate me-2"></i>
|
|
7
|
+
<%= red_t("red.errors.missing_translations_page.title") %>
|
|
8
|
+
</h1>
|
|
9
|
+
|
|
10
|
+
<div class="btn-group" role="group">
|
|
11
|
+
<%= link_to missing_translations_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 missing_translations_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 missing_translations_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-translate display-1 text-success mb-3"></i>
|
|
26
|
+
<div class="red-empty-state-title"><%= red_t("red.errors.missing_translations_page.empty_title") %></div>
|
|
27
|
+
<p class="text-muted">
|
|
28
|
+
<%= red_t("red.errors.missing_translations_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.missing_translations_page.how_title") %></h6>
|
|
33
|
+
<ul class="mb-0">
|
|
34
|
+
<li><%= red_t("red.errors.missing_translations_page.how_enabled_html", option: content_tag(:code, "enable_missing_translation_tracking = true")) %></li>
|
|
35
|
+
<li><%= red_t("red.errors.missing_translations_page.how_hooked") %></li>
|
|
36
|
+
<li><%= red_t("red.errors.missing_translations_page.how_recorded") %></li>
|
|
37
|
+
<li><%= red_t("red.errors.missing_translations_page.how_not_seen") %></li>
|
|
38
|
+
<li><%= red_t("red.errors.missing_translations_page.how_buffered") %></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-warning"><%= @unique_keys %></div>
|
|
49
|
+
<small class="text-muted"><%= red_t("red.errors.missing_translations_page.missing_keys") %></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-danger"><%= @total_misses %></div>
|
|
57
|
+
<small class="text-muted"><%= red_t("red.errors.missing_translations_page.total_misses") %></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"><%= @locale_count %></div>
|
|
65
|
+
<small class="text-muted"><%= red_t("red.errors.missing_translations_page.locales_affected") %></small>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<% if @overflow_count.to_i.positive? %>
|
|
72
|
+
<div class="alert alert-warning d-flex align-items-start" role="alert">
|
|
73
|
+
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
74
|
+
<div>
|
|
75
|
+
<%= red_t("red.errors.missing_translations_page.overflow_notice",
|
|
76
|
+
count: @overflow_count,
|
|
77
|
+
limit: RailsErrorDashboard::Services::MissingTranslationTracker::MAX_BUFFERED_KEYS) %>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<% end %>
|
|
81
|
+
|
|
82
|
+
<div class="card mb-4">
|
|
83
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
84
|
+
<h5 class="mb-0">
|
|
85
|
+
<i class="bi bi-translate text-warning me-2"></i>
|
|
86
|
+
<%= red_t("red.errors.missing_translations_page.all_title") %>
|
|
87
|
+
<span class="badge bg-warning text-dark"><%= @entry_count %></span>
|
|
88
|
+
</h5>
|
|
89
|
+
<small class="text-muted"><%== @pagy.info_tag %></small>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="card-body p-0">
|
|
92
|
+
<div class="table-responsive">
|
|
93
|
+
<table class="table table-hover mb-0">
|
|
94
|
+
<thead class="table-light">
|
|
95
|
+
<tr>
|
|
96
|
+
<th><%= red_t("red.errors.missing_translations_page.column_key") %></th>
|
|
97
|
+
<th width="90"><%= red_t("red.errors.missing_translations_page.column_locale") %></th>
|
|
98
|
+
<th width="90"><%= red_t("red.errors.missing_translations_page.column_misses") %></th>
|
|
99
|
+
<th><%= red_t("red.errors.missing_translations_page.column_source") %></th>
|
|
100
|
+
<th width="140"><%= red_t("red.errors.missing_translations_page.column_first_seen") %></th>
|
|
101
|
+
<th width="140"><%= red_t("red.errors.pages.column_last_seen") %></th>
|
|
102
|
+
</tr>
|
|
103
|
+
</thead>
|
|
104
|
+
<tbody>
|
|
105
|
+
<% @entries.each do |entry| %>
|
|
106
|
+
<tr>
|
|
107
|
+
<td><code><%= entry[:key] %></code></td>
|
|
108
|
+
<%# A locale tag is a machine value, shown as-is. %>
|
|
109
|
+
<td><span class="badge bg-light text-dark"><%= entry[:locale] %></span></td>
|
|
110
|
+
<td><strong><%= entry[:count] %></strong></td>
|
|
111
|
+
<td>
|
|
112
|
+
<% if entry[:source].present? %>
|
|
113
|
+
<code><%= entry[:source] %></code>
|
|
114
|
+
<% else %>
|
|
115
|
+
<span class="text-muted">—</span>
|
|
116
|
+
<% end %>
|
|
117
|
+
</td>
|
|
118
|
+
<td><%= local_time_ago(entry[:first_seen]) %></td>
|
|
119
|
+
<td><%= local_time_ago(entry[:last_seen]) %></td>
|
|
120
|
+
</tr>
|
|
121
|
+
<% end %>
|
|
122
|
+
</tbody>
|
|
123
|
+
</table>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="card-footer border-top d-flex justify-content-between align-items-center">
|
|
127
|
+
<div>
|
|
128
|
+
<small class="text-muted">
|
|
129
|
+
<i class="bi bi-lightbulb text-warning"></i> <%= red_t("red.errors.missing_translations_page.footer_tip") %>
|
|
130
|
+
</small>
|
|
131
|
+
<small class="ms-3">
|
|
132
|
+
<a href="https://guides.rubyonrails.org/i18n.html" target="_blank" rel="noopener" class="text-decoration-none">
|
|
133
|
+
<i class="bi bi-book"></i> <%= red_t("red.errors.missing_translations_page.docs") %> <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
|
|
134
|
+
</a>
|
|
135
|
+
</small>
|
|
136
|
+
</div>
|
|
137
|
+
<div>
|
|
138
|
+
<%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
<% end %>
|
|
143
|
+
</div>
|
|
@@ -265,16 +265,19 @@
|
|
|
265
265
|
end
|
|
266
266
|
|
|
267
267
|
when :retention_days
|
|
268
|
-
#
|
|
268
|
+
# Retention is enforced by RetentionCleanupJob, which the host schedules
|
|
269
|
+
# (docs/PRODUCTION.md). The badge used to claim "manual cleanup only" and
|
|
270
|
+
# cite cleanup_resolved, a different task that deletes only resolved
|
|
271
|
+
# errors and ignores this setting.
|
|
269
272
|
if value.present?
|
|
270
273
|
%>
|
|
271
274
|
<div>
|
|
272
275
|
<span class="badge bg-warning text-dark fs-6"><%= red_tp("red.settings.values.days", count: value) %></span>
|
|
273
|
-
<small class="text-
|
|
274
|
-
<i class="bi bi-
|
|
276
|
+
<small class="text-muted d-block">
|
|
277
|
+
<i class="bi bi-clock-history"></i> <%= red_t("red.settings.values.retention_enforced_by_job") %>
|
|
275
278
|
</small>
|
|
276
279
|
<small class="text-muted d-block mt-1">
|
|
277
|
-
<%= red_t("red.settings.values.run_command") %> <code class="small">
|
|
280
|
+
<%= red_t("red.settings.values.run_command") %> <code class="small">RailsErrorDashboard::RetentionCleanupJob.perform_now</code>
|
|
278
281
|
</small>
|
|
279
282
|
</div>
|
|
280
283
|
<%
|
|
@@ -56,10 +56,15 @@
|
|
|
56
56
|
notification_email_from: { type: :string, show_if: :enable_email_notifications },
|
|
57
57
|
enable_discord_notifications: { type: :boolean },
|
|
58
58
|
discord_webhook_url: { type: :url, show_if: :enable_discord_notifications },
|
|
59
|
+
enable_telegram_notifications: { type: :boolean },
|
|
60
|
+
telegram_bot_token: { type: :secret, show_if: :enable_telegram_notifications },
|
|
61
|
+
telegram_chat_id: { type: :string, show_if: :enable_telegram_notifications },
|
|
62
|
+
telegram_api_base_url: { type: :url, show_if: :enable_telegram_notifications },
|
|
59
63
|
enable_pagerduty_notifications: { type: :boolean },
|
|
60
64
|
pagerduty_integration_key: { type: :string, show_if: :enable_pagerduty_notifications },
|
|
61
65
|
enable_webhook_notifications: { type: :boolean },
|
|
62
66
|
webhook_urls: { type: :array, show_if: :enable_webhook_notifications },
|
|
67
|
+
webhook_signing_secret: { type: :secret, show_if: :enable_webhook_notifications },
|
|
63
68
|
enable_scheduled_digests: { type: :boolean },
|
|
64
69
|
digest_frequency: { type: :symbol, show_if: :enable_scheduled_digests },
|
|
65
70
|
digest_recipients: { type: :array, show_if: :enable_scheduled_digests }
|
|
@@ -100,7 +105,7 @@
|
|
|
100
105
|
enable_breadcrumbs: { type: :boolean },
|
|
101
106
|
breadcrumb_buffer_size: { type: :integer, show_if: :enable_breadcrumbs, unit: :events },
|
|
102
107
|
enable_n_plus_one_detection: { type: :boolean, show_if: :enable_breadcrumbs },
|
|
103
|
-
n_plus_one_threshold: { type: :integer, show_if: :enable_n_plus_one_detection, unit: :queries }
|
|
108
|
+
n_plus_one_threshold: { type: :integer, show_if: [ :enable_breadcrumbs, :enable_n_plus_one_detection ], unit: :queries }
|
|
104
109
|
}
|
|
105
110
|
},
|
|
106
111
|
:system_health => {
|
|
@@ -149,6 +154,7 @@
|
|
|
149
154
|
color: "info",
|
|
150
155
|
settings: {
|
|
151
156
|
enable_rack_attack_tracking: { type: :boolean },
|
|
157
|
+
enable_missing_translation_tracking: { type: :boolean },
|
|
152
158
|
enable_actioncable_tracking: { type: :boolean },
|
|
153
159
|
enable_activestorage_tracking: { type: :boolean }
|
|
154
160
|
}
|
|
@@ -166,7 +172,9 @@
|
|
|
166
172
|
color: "light",
|
|
167
173
|
settings: {
|
|
168
174
|
enable_internal_logging: { type: :boolean },
|
|
169
|
-
|
|
175
|
+
# Not gated on enable_internal_logging: Logger.error honours log_level
|
|
176
|
+
# whether or not internal logging is on.
|
|
177
|
+
log_level: { type: :symbol }
|
|
170
178
|
}
|
|
171
179
|
}
|
|
172
180
|
}
|
|
@@ -198,16 +206,22 @@
|
|
|
198
206
|
<div>
|
|
199
207
|
<% group_config[:settings].each do |setting_key, setting_meta| %>
|
|
200
208
|
<%
|
|
201
|
-
|
|
202
|
-
should_show =
|
|
209
|
+
# One flag, or every flag in an array, must be on for the row to show.
|
|
210
|
+
should_show = Array(setting_meta[:show_if]).all? { |flag| @config.send(flag) }
|
|
203
211
|
next unless should_show
|
|
204
212
|
value = @config.send(setting_meta[:method] || setting_key)
|
|
213
|
+
# A credential is shown as Set / Not set whatever shape it has. The
|
|
214
|
+
# per-row :type documents the value's shape; this one rule is what
|
|
215
|
+
# keeps a webhook URL or an API key off the page, the same list
|
|
216
|
+
# Configuration#inspect masks, so a new secret cannot leak by a
|
|
217
|
+
# forgotten per-row type.
|
|
218
|
+
badge_type = RailsErrorDashboard::Configuration::SECRET_ATTRIBUTES.include?(setting_key) ? :secret : setting_meta[:type]
|
|
205
219
|
%>
|
|
206
220
|
<div style="display: flex; align-items: center; gap: var(--space-4); padding: var(--space-3) var(--space-6); border-bottom: 1px solid var(--border-primary); font-size: 13px;">
|
|
207
221
|
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--accent); font-weight: 500; width: 240px; flex-shrink: 0;"><%= setting_key %></span>
|
|
208
222
|
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--text-primary); font-weight: 500; width: 180px; flex-shrink: 0;">
|
|
209
223
|
<%= render partial: 'rails_error_dashboard/errors/settings/value_badge',
|
|
210
|
-
locals: { value: value, type:
|
|
224
|
+
locals: { value: value, type: badge_type, unit: setting_meta[:unit] } %>
|
|
211
225
|
</span>
|
|
212
226
|
<%# Keyed by the config option's own name, so a key is greppable
|
|
213
227
|
from the initializer that sets it. %>
|