rails_error_dashboard 0.14.4 → 0.16.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 +19 -8
- data/app/controllers/rails_error_dashboard/application_controller.rb +28 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +97 -4
- data/app/controllers/rails_error_dashboard/health_controller.rb +38 -0
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +10 -9
- data/app/helpers/rails_error_dashboard/application_helper.rb +2 -2
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +63 -16
- data/app/helpers/rails_error_dashboard/overview_helper.rb +4 -0
- 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 +31 -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 +54 -20
- data/app/models/rails_error_dashboard/error_occurrence.rb +1 -0
- data/app/models/rails_error_dashboard/error_occurrence_context.rb +14 -0
- data/app/models/rails_error_dashboard/missing_translation.rb +43 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +14 -2
- data/app/views/rails_error_dashboard/errors/_captured_occurrences.html.erb +45 -0
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +12 -3
- data/app/views/rails_error_dashboard/errors/_occurrence_banner.html.erb +12 -0
- 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/overview.html.erb +10 -7
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +16 -11
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +7 -4
- data/app/views/rails_error_dashboard/errors/settings.html.erb +23 -8
- data/app/views/rails_error_dashboard/errors/show.html.erb +7 -4
- data/config/locales/de.yml +87 -5
- data/config/locales/en.yml +99 -4
- data/config/locales/es.yml +88 -5
- data/config/locales/fr.yml +88 -5
- data/config/locales/it.yml +88 -5
- data/config/locales/ja.yml +86 -4
- data/config/locales/pl.yml +89 -5
- data/config/locales/pt-BR.yml +88 -5
- data/config/locales/ru.yml +89 -5
- data/config/locales/uk.yml +89 -5
- data/config/locales/zh-CN.yml +86 -4
- data/config/routes.rb +6 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +10 -2
- data/db/migrate/20260106094256_backfill_application_for_existing_errors.rb +3 -3
- data/db/migrate/20261010000001_create_missing_translations.rb +50 -0
- data/db/migrate/20261015000001_create_rails_error_dashboard_error_occurrence_contexts.rb +35 -0
- data/db/migrate/20261015000002_add_capture_source_to_error_occurrences_and_error_logs.rb +46 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +9 -4
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +41 -5
- data/lib/rails_error_dashboard/commands/assign_error.rb +24 -1
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +11 -2
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +16 -3
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +7 -2
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +6 -0
- data/lib/rails_error_dashboard/commands/flush_missing_translations.rb +101 -0
- data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +2 -4
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +7 -6
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +2 -4
- data/lib/rails_error_dashboard/commands/log_error.rb +20 -8
- data/lib/rails_error_dashboard/commands/record_occurrence_context.rb +60 -0
- 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 +61 -4
- data/lib/rails_error_dashboard/engine.rb +42 -1
- data/lib/rails_error_dashboard/error_reporter.rb +4 -2
- data/lib/rails_error_dashboard/manual_error_reporter.rb +11 -1
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +18 -4
- 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/analytics_stats.rb +0 -12
- data/lib/rails_error_dashboard/queries/boot_errors.rb +76 -0
- data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +59 -33
- 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/queries/platform_comparison.rb +14 -6
- 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 +31 -7
- data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +30 -0
- data/lib/rails_error_dashboard/services/crash_capture.rb +90 -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 -14
- data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +15 -16
- 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/repository_url.rb +18 -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/similarity_calculator.rb +3 -9
- data/lib/rails_error_dashboard/services/source_code_reader.rb +23 -3
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +6 -3
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +6 -1
- 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/value_objects/occurrence_snapshot.rb +23 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +28 -0
- data/lib/tasks/error_dashboard.rake +2 -3
- metadata +29 -2
|
@@ -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>
|
|
@@ -201,14 +201,17 @@
|
|
|
201
201
|
morphology applied to an identifier — it renders "iOS" as
|
|
202
202
|
"IOS" in some engines and mangles the Turkish dotless i. %>
|
|
203
203
|
<span style="font-size: 13px; font-weight: 600;"><%= platform %></span>
|
|
204
|
-
<span class="badge bg-<%= health_status_color(health[:health_status]) %>"
|
|
205
|
-
</div>
|
|
206
|
-
<div style="margin-bottom: var(--space-2);">
|
|
207
|
-
<div style="font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums;"><%= red_t("red.errors.overview_page.stability_score_html", score: content_tag(:span, health[:stability_score])) %></div>
|
|
208
|
-
</div>
|
|
209
|
-
<div class="progress" style="height: 4px; margin-bottom: var(--space-3);">
|
|
210
|
-
<div class="progress-bar" style="width: <%= health[:stability_score] %>%; background: var(--status-<%= health[:health_status] == :healthy ? 'success' : health[:health_status] == :warning ? 'warning' : 'critical' %>);"></div>
|
|
204
|
+
<span class="badge bg-<%= health_status_color(health[:health_status]) %>"<% if health[:health_status] == :not_rated %> title="<%= red_t("red.common.health_status.not_rated_hint") %>"<% end %>><%= health_status_text(health[:health_status]) %></span>
|
|
211
205
|
</div>
|
|
206
|
+
<%# A lone platform has no score: a rating compares platforms %>
|
|
207
|
+
<% if health[:stability_score] %>
|
|
208
|
+
<div style="margin-bottom: var(--space-2);">
|
|
209
|
+
<div style="font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums;"><%= red_t("red.errors.overview_page.stability_score_html", score: content_tag(:span, health[:stability_score])) %></div>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="progress" style="height: 4px; margin-bottom: var(--space-3);">
|
|
212
|
+
<div class="progress-bar" style="width: <%= health[:stability_score] %>%; background: var(--status-<%= health[:health_status] == :healthy ? 'success' : health[:health_status] == :warning ? 'warning' : 'critical' %>);"></div>
|
|
213
|
+
</div>
|
|
214
|
+
<% end %>
|
|
212
215
|
<div style="font-size: 12px; color: var(--text-tertiary);">
|
|
213
216
|
<%= red_t("red.errors.overview_page.platform_summary", total: health[:total_errors], critical: health[:critical_errors], unresolved: health[:unresolved_errors]) %>
|
|
214
217
|
</div>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<div class="row mb-4">
|
|
37
37
|
<% @platform_health.each do |platform, health| %>
|
|
38
38
|
<div class="col-md-6 col-lg-3 mb-3">
|
|
39
|
-
<div class="card h-100 <%=
|
|
39
|
+
<div class="card h-100 <%= { healthy: 'border-success', warning: 'border-warning', not_rated: 'border-secondary' }.fetch(health[:health_status], 'border-danger') %>">
|
|
40
40
|
<div class="card-body">
|
|
41
41
|
<h5 class="card-title text-capitalize">
|
|
42
42
|
<%= platform || red_t("red.common.unknown") %>
|
|
@@ -44,23 +44,28 @@
|
|
|
44
44
|
<span class="badge bg-success float-end"><%= red_t("red.analytics.platform_comparison.health_card.healthy") %></span>
|
|
45
45
|
<% elsif health[:health_status] == :warning %>
|
|
46
46
|
<span class="badge bg-warning float-end"><%= red_t("red.analytics.platform_comparison.health_card.warning") %></span>
|
|
47
|
+
<% elsif health[:health_status] == :not_rated %>
|
|
48
|
+
<span class="badge bg-secondary float-end" title="<%= red_t("red.common.health_status.not_rated_hint") %>"><%= red_t("red.common.health_status.not_rated") %></span>
|
|
47
49
|
<% else %>
|
|
48
50
|
<span class="badge bg-danger float-end"><%= red_t("red.analytics.platform_comparison.health_card.critical") %></span>
|
|
49
51
|
<% end %>
|
|
50
52
|
</h5>
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
<%# A lone platform has no score: a rating compares platforms %>
|
|
55
|
+
<% if health[:stability_score] %>
|
|
56
|
+
<div class="mb-3">
|
|
57
|
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
58
|
+
<small class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.stability_score") %></small>
|
|
59
|
+
<strong><%= health[:stability_score] %>/100</strong>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="progress" style="height: 8px;">
|
|
62
|
+
<div class="progress-bar <%= health[:stability_score] >= 80 ? 'bg-success' : health[:stability_score] >= 60 ? 'bg-warning' : 'bg-danger' %>"
|
|
63
|
+
role="progressbar"
|
|
64
|
+
style="width: <%= health[:stability_score] %>%">
|
|
65
|
+
</div>
|
|
61
66
|
</div>
|
|
62
67
|
</div>
|
|
63
|
-
|
|
68
|
+
<% end %>
|
|
64
69
|
|
|
65
70
|
<div class="small">
|
|
66
71
|
<div class="d-flex justify-content-between mb-1">
|
|
@@ -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
|
<%
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
enable_middleware: { type: :boolean },
|
|
11
11
|
enable_error_subscriber: { type: :boolean },
|
|
12
12
|
retention_days: { type: :retention_days },
|
|
13
|
+
occurrence_context_limit: { type: :integer, unit: :occurrences },
|
|
13
14
|
max_backtrace_lines: { type: :integer, unit: :lines },
|
|
14
15
|
sampling_rate: { type: :percentage }
|
|
15
16
|
}
|
|
@@ -56,10 +57,15 @@
|
|
|
56
57
|
notification_email_from: { type: :string, show_if: :enable_email_notifications },
|
|
57
58
|
enable_discord_notifications: { type: :boolean },
|
|
58
59
|
discord_webhook_url: { type: :url, show_if: :enable_discord_notifications },
|
|
60
|
+
enable_telegram_notifications: { type: :boolean },
|
|
61
|
+
telegram_bot_token: { type: :secret, show_if: :enable_telegram_notifications },
|
|
62
|
+
telegram_chat_id: { type: :string, show_if: :enable_telegram_notifications },
|
|
63
|
+
telegram_api_base_url: { type: :url, show_if: :enable_telegram_notifications },
|
|
59
64
|
enable_pagerduty_notifications: { type: :boolean },
|
|
60
65
|
pagerduty_integration_key: { type: :string, show_if: :enable_pagerduty_notifications },
|
|
61
66
|
enable_webhook_notifications: { type: :boolean },
|
|
62
67
|
webhook_urls: { type: :array, show_if: :enable_webhook_notifications },
|
|
68
|
+
webhook_signing_secret: { type: :secret, show_if: :enable_webhook_notifications },
|
|
63
69
|
enable_scheduled_digests: { type: :boolean },
|
|
64
70
|
digest_frequency: { type: :symbol, show_if: :enable_scheduled_digests },
|
|
65
71
|
digest_recipients: { type: :array, show_if: :enable_scheduled_digests }
|
|
@@ -100,7 +106,7 @@
|
|
|
100
106
|
enable_breadcrumbs: { type: :boolean },
|
|
101
107
|
breadcrumb_buffer_size: { type: :integer, show_if: :enable_breadcrumbs, unit: :events },
|
|
102
108
|
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 }
|
|
109
|
+
n_plus_one_threshold: { type: :integer, show_if: [ :enable_breadcrumbs, :enable_n_plus_one_detection ], unit: :queries }
|
|
104
110
|
}
|
|
105
111
|
},
|
|
106
112
|
:system_health => {
|
|
@@ -149,6 +155,7 @@
|
|
|
149
155
|
color: "info",
|
|
150
156
|
settings: {
|
|
151
157
|
enable_rack_attack_tracking: { type: :boolean },
|
|
158
|
+
enable_missing_translation_tracking: { type: :boolean },
|
|
152
159
|
enable_actioncable_tracking: { type: :boolean },
|
|
153
160
|
enable_activestorage_tracking: { type: :boolean }
|
|
154
161
|
}
|
|
@@ -166,7 +173,9 @@
|
|
|
166
173
|
color: "light",
|
|
167
174
|
settings: {
|
|
168
175
|
enable_internal_logging: { type: :boolean },
|
|
169
|
-
|
|
176
|
+
# Not gated on enable_internal_logging: Logger.error honours log_level
|
|
177
|
+
# whether or not internal logging is on.
|
|
178
|
+
log_level: { type: :symbol }
|
|
170
179
|
}
|
|
171
180
|
}
|
|
172
181
|
}
|
|
@@ -187,27 +196,33 @@
|
|
|
187
196
|
|
|
188
197
|
<!-- Dynamically render setting groups -->
|
|
189
198
|
<% setting_groups.each do |group_slug, group_config| %>
|
|
199
|
+
<%# One flag, or every flag in an array, must be on for a row to show. The
|
|
200
|
+
count and the rows both use this, so the count says what is shown. %>
|
|
201
|
+
<% shown_settings = group_config[:settings].select { |_key, meta| Array(meta[:show_if]).all? { |flag| @config.send(flag) } } %>
|
|
190
202
|
<div class="card" style="margin-bottom: var(--space-4);">
|
|
191
203
|
<button type="button" data-red-action="toggle-next" data-red-display="block" style="display: flex; align-items: center; justify-content: space-between; width: 100%; padding: var(--space-4) var(--space-6); background: none; border: none; cursor: pointer; border-bottom: 1px solid var(--border-primary);">
|
|
192
204
|
<span style="font-size: 14px; font-weight: 600; color: var(--text-primary);"><i class="<%= group_config[:icon] %>" style="margin-right: 6px;"></i> <%= red_t("red.settings.groups.#{group_slug}") %></span>
|
|
193
205
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
194
|
-
<span style="font-size: 11px; color: var(--text-tertiary);"><%= red_tp("red.settings.option_count", count:
|
|
206
|
+
<span style="font-size: 11px; color: var(--text-tertiary);"><%= red_tp("red.settings.option_count", count: shown_settings.size) %></span>
|
|
195
207
|
<i class="bi bi-chevron-up chev" style="font-size: 12px; color: var(--text-tertiary);"></i>
|
|
196
208
|
</div>
|
|
197
209
|
</button>
|
|
198
210
|
<div>
|
|
199
|
-
<%
|
|
211
|
+
<% shown_settings.each do |setting_key, setting_meta| %>
|
|
200
212
|
<%
|
|
201
|
-
show_if_key = setting_meta[:show_if]
|
|
202
|
-
should_show = show_if_key.nil? || @config.send(show_if_key)
|
|
203
|
-
next unless should_show
|
|
204
213
|
value = @config.send(setting_meta[:method] || setting_key)
|
|
214
|
+
# A credential is shown as Set / Not set whatever shape it has. The
|
|
215
|
+
# per-row :type documents the value's shape; this one rule is what
|
|
216
|
+
# keeps a webhook URL or an API key off the page, the same list
|
|
217
|
+
# Configuration#inspect masks, so a new secret cannot leak by a
|
|
218
|
+
# forgotten per-row type.
|
|
219
|
+
badge_type = RailsErrorDashboard::Configuration::SECRET_ATTRIBUTES.include?(setting_key) ? :secret : setting_meta[:type]
|
|
205
220
|
%>
|
|
206
221
|
<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
222
|
<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
223
|
<span style="font-family: var(--font-mono); font-size: 12px; color: var(--text-primary); font-weight: 500; width: 180px; flex-shrink: 0;">
|
|
209
224
|
<%= render partial: 'rails_error_dashboard/errors/settings/value_badge',
|
|
210
|
-
locals: { value: value, type:
|
|
225
|
+
locals: { value: value, type: badge_type, unit: setting_meta[:unit] } %>
|
|
211
226
|
</span>
|
|
212
227
|
<%# Keyed by the config option's own name, so a key is greppable
|
|
213
228
|
from the initializer that sets it. %>
|
|
@@ -98,6 +98,8 @@
|
|
|
98
98
|
</div>
|
|
99
99
|
</div>
|
|
100
100
|
|
|
101
|
+
<%= render "occurrence_banner", error: @error, occurrence_context: @occurrence_context %>
|
|
102
|
+
|
|
101
103
|
<!-- Main content: tabs + sidebar -->
|
|
102
104
|
<div class="red-detail-grid">
|
|
103
105
|
<!-- Left: tabbed content -->
|
|
@@ -142,18 +144,19 @@
|
|
|
142
144
|
<% end %>
|
|
143
145
|
|
|
144
146
|
<%= render "error_info", error: @error %>
|
|
145
|
-
<%= render "local_variables", error: @
|
|
146
|
-
<%= render "instance_variables", error: @
|
|
147
|
+
<%= render "local_variables", error: @snapshot %>
|
|
148
|
+
<%= render "instance_variables", error: @snapshot %>
|
|
147
149
|
</div>
|
|
148
150
|
|
|
149
151
|
<!-- Tab: Context -->
|
|
150
152
|
<div id="panel-context">
|
|
151
153
|
<%= render "request_context", error: @error %>
|
|
152
|
-
<%= render "breadcrumbs_group", error: @
|
|
154
|
+
<%= render "breadcrumbs_group", error: @snapshot %>
|
|
153
155
|
</div>
|
|
154
156
|
|
|
155
157
|
<!-- Tab: History -->
|
|
156
158
|
<div id="panel-history">
|
|
159
|
+
<%= render "captured_occurrences", error: @error, occurrences: @captured_occurrences, current_id: @occurrence_context&.error_occurrence_id %>
|
|
157
160
|
<%= render "timeline" %>
|
|
158
161
|
<% unless RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
159
162
|
<%= render "discussion", error: @error %>
|
|
@@ -175,7 +178,7 @@
|
|
|
175
178
|
|
|
176
179
|
<!-- Right: sidebar -->
|
|
177
180
|
<div class="error-sidebar" style="display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; overflow-wrap: break-word;">
|
|
178
|
-
<%= render "sidebar_metadata", error: @
|
|
181
|
+
<%= render "sidebar_metadata", error: @snapshot, related_errors: @related_errors %>
|
|
179
182
|
|
|
180
183
|
<%= render "llm_summary", error: @error %>
|
|
181
184
|
|