rails_error_dashboard 0.12.1 → 0.14.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/app/controllers/rails_error_dashboard/application_controller.rb +92 -11
- data/app/controllers/rails_error_dashboard/errors_controller.rb +111 -34
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +3 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +46 -1
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +8 -0
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +10 -0
- data/app/jobs/rails_error_dashboard/concerns/plain_channel_message.rb +71 -0
- data/app/jobs/rails_error_dashboard/notification_burst_summary_job.rb +55 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +122 -1
- data/app/jobs/rails_error_dashboard/storm_flush_job.rb +7 -4
- data/app/jobs/rails_error_dashboard/storm_notification_job.rb +8 -44
- data/app/models/rails_error_dashboard/error_baseline.rb +12 -9
- data/app/models/rails_error_dashboard/error_comment.rb +0 -5
- data/app/models/rails_error_dashboard/error_log.rb +19 -3
- data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
- data/app/models/rails_error_dashboard/error_occurrence.rb +9 -1
- data/app/models/rails_error_dashboard/event_count.rb +132 -0
- data/app/models/rails_error_dashboard/event_timing_gap.rb +55 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +58 -5
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +18 -11
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +22 -8
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +2 -0
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +15 -6
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +8 -8
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/index.html.erb +6 -1
- data/app/views/rails_error_dashboard/errors/overview.html.erb +12 -0
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/releases.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/settings.html.erb +2 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +3 -3
- data/config/locales/de.yml +30 -0
- data/config/locales/en.yml +37 -0
- data/config/locales/es.yml +30 -0
- data/config/locales/fr.yml +30 -0
- data/config/locales/it.yml +30 -0
- data/config/locales/ja.yml +30 -0
- data/config/locales/pl.yml +30 -0
- data/config/locales/pt-BR.yml +30 -0
- data/config/locales/ru.yml +30 -0
- data/config/locales/uk.yml +30 -0
- data/config/locales/zh-CN.yml +30 -0
- data/db/migrate/20260917000001_add_last_notified_at_to_error_logs.rb +40 -0
- data/db/migrate/20260919000001_create_event_counts.rb +71 -0
- data/db/migrate/20260920000001_add_buckets_incomplete_to_storm_events.rb +25 -0
- data/db/migrate/20260920000002_create_event_timing_gaps.rb +55 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +12 -1
- data/lib/rails_error_dashboard/commands/assign_error.rb +12 -2
- data/lib/rails_error_dashboard/commands/backfill_environments.rb +2 -0
- data/lib/rails_error_dashboard/commands/backfill_resolved_at.rb +42 -0
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -0
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +2 -0
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -0
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +2 -0
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +109 -11
- data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +3 -1
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +252 -12
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +5 -2
- data/lib/rails_error_dashboard/commands/link_existing_issue.rb +1 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +291 -40
- data/lib/rails_error_dashboard/commands/mute_error.rb +1 -0
- data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -0
- data/lib/rails_error_dashboard/commands/scrub_invalid_encoding.rb +104 -0
- data/lib/rails_error_dashboard/commands/snooze_error.rb +34 -10
- data/lib/rails_error_dashboard/commands/unmute_error.rb +1 -0
- data/lib/rails_error_dashboard/commands/update_error_priority.rb +23 -2
- data/lib/rails_error_dashboard/commands/update_error_status.rb +33 -6
- data/lib/rails_error_dashboard/configuration.rb +39 -1
- data/lib/rails_error_dashboard/engine.rb +28 -0
- data/lib/rails_error_dashboard/manual_error_reporter.rb +16 -5
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +89 -30
- data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +216 -74
- data/lib/rails_error_dashboard/queries/error_correlation.rb +6 -3
- data/lib/rails_error_dashboard/queries/errors_list.rb +15 -2
- data/lib/rails_error_dashboard/queries/event_volume.rb +503 -0
- data/lib/rails_error_dashboard/queries/similar_errors.rb +1 -1
- data/lib/rails_error_dashboard/services/analytics_cache_manager.rb +43 -18
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +3 -1
- data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +23 -0
- data/lib/rails_error_dashboard/services/cause_chain_extractor.rb +3 -1
- data/lib/rails_error_dashboard/services/codeberg_issue_client.rb +13 -2
- data/lib/rails_error_dashboard/services/diagnostic_dump_generator.rb +5 -3
- data/lib/rails_error_dashboard/services/encoding_sanitizer.rb +80 -0
- data/lib/rails_error_dashboard/services/error_broadcaster.rb +180 -32
- data/lib/rails_error_dashboard/services/error_hash_generator.rb +9 -3
- data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +13 -0
- data/lib/rails_error_dashboard/services/exception_filter.rb +55 -0
- data/lib/rails_error_dashboard/services/git_head_reader.rb +102 -0
- data/lib/rails_error_dashboard/services/notification_throttler.rb +172 -28
- data/lib/rails_error_dashboard/services/sensitive_data_filter.rb +47 -1
- data/lib/rails_error_dashboard/services/storm_protection/circuit_breaker.rb +59 -3
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +64 -6
- data/lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb +25 -2
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +32 -5
- data/lib/rails_error_dashboard/services/swallowed_exception_tracker.rb +99 -23
- data/lib/rails_error_dashboard/services/url_safety.rb +40 -0
- data/lib/rails_error_dashboard/services/variable_serializer.rb +125 -10
- data/lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb +111 -0
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +11 -2
- data/lib/rails_error_dashboard/value_objects/error_context.rb +40 -3
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +34 -0
- data/lib/tasks/error_dashboard.rake +54 -4
- metadata +16 -2
|
@@ -744,9 +744,54 @@ dd { color: var(--text-primary); }
|
|
|
744
744
|
}
|
|
745
745
|
.red-stat-grid > * { min-width: 0; }
|
|
746
746
|
|
|
747
|
-
/* Error detail
|
|
747
|
+
/* Error detail hero
|
|
748
|
+
Same failure as the stat grids above: an inline `display: flex` cannot carry
|
|
749
|
+
a media query. The action row held `flex-shrink: 0` while the title column
|
|
750
|
+
was plain `flex: 1`, so at 390px up to five buttons kept their full width
|
|
751
|
+
and the title absorbed every pixel of the squeeze — "NoMethodError" wrapped
|
|
752
|
+
to roughly one character per line, making a very tall header before the
|
|
753
|
+
backtrace on the one screen an on-call engineer actually reads.
|
|
754
|
+
|
|
755
|
+
`flex: 1 1 260px` is the load-bearing part: the 260px basis stops the text
|
|
756
|
+
column collapsing toward zero. Dropping flex-shrink from the actions lets
|
|
757
|
+
them wrap onto their own line instead. */
|
|
758
|
+
.red-error-hero {
|
|
759
|
+
display: flex;
|
|
760
|
+
align-items: flex-start;
|
|
761
|
+
justify-content: space-between;
|
|
762
|
+
gap: var(--space-4);
|
|
763
|
+
flex-wrap: wrap;
|
|
764
|
+
}
|
|
765
|
+
.red-error-hero-text { flex: 1 1 260px; min-width: 0; }
|
|
766
|
+
.red-error-hero-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
767
|
+
@media (max-width: 575.98px) {
|
|
768
|
+
/* Full-width buttons once stacked: a wrapped row of five reads as rubble. */
|
|
769
|
+
.red-error-hero-actions { width: 100%; }
|
|
770
|
+
.red-error-hero-actions .btn { flex: 1 1 auto; }
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/* Error detail responsive grid
|
|
774
|
+
The single-column rule below stacks the panels, but `1fr` is min-content by
|
|
775
|
+
default, so a track still refused to shrink below its widest child: at 390px
|
|
776
|
+
the grid measured 334px while its content needed 419px, and the page scrolled
|
|
777
|
+
sideways independently of the hero. Same fix as the stat grids: minmax's 0
|
|
778
|
+
floor lets the track shrink, and min-width:0 lets its children do the same. */
|
|
748
779
|
.red-detail-grid { display: grid; grid-template-columns: 1fr 260px; gap: var(--space-4); }
|
|
749
|
-
|
|
780
|
+
.red-detail-grid > * { min-width: 0; }
|
|
781
|
+
@media (max-width: 1023.98px) { .red-detail-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
782
|
+
|
|
783
|
+
/* Detail tables inside those panels (Request Context, Metadata).
|
|
784
|
+
Their label column is a fixed `width="200"` and their values are URLs and
|
|
785
|
+
JSON, so the table's intrinsic width stayed ~416px however narrow the panel
|
|
786
|
+
became — the last thing still pushing the detail page sideways on a phone
|
|
787
|
+
once the grid and the hero were fixed. Below the stacking breakpoint the
|
|
788
|
+
label column narrows and long values wrap on any character, so the table
|
|
789
|
+
fits its panel instead of widening the page. */
|
|
790
|
+
@media (max-width: 1023.98px) {
|
|
791
|
+
.red-detail-grid .card-body > .table { table-layout: fixed; width: 100%; }
|
|
792
|
+
.red-detail-grid .card-body > .table th { width: 40%; }
|
|
793
|
+
.red-detail-grid .card-body > .table td { overflow-wrap: anywhere; }
|
|
794
|
+
}
|
|
750
795
|
|
|
751
796
|
/* Env badge */
|
|
752
797
|
.red-env-badge {
|
|
@@ -1182,10 +1227,18 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1182
1227
|
<i class="bi bi-layout-sidebar-inset"></i>
|
|
1183
1228
|
</button>
|
|
1184
1229
|
<!-- Search -->
|
|
1185
|
-
|
|
1230
|
+
<%# A plain GET form to the errors index, using the same "search"
|
|
1231
|
+
parameter as the index's own field. Hand-written rather than
|
|
1232
|
+
form_with: a GET form must not put an authenticity token or a
|
|
1233
|
+
utf8 field into the query string. Only String parameters are
|
|
1234
|
+
echoed back; a nested one is ignored. %>
|
|
1235
|
+
<form action="<%= errors_path %>" method="get" role="search" style="position: relative; margin: 0;">
|
|
1186
1236
|
<i class="bi bi-search" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 13px; color: var(--text-tertiary);"></i>
|
|
1187
|
-
<input class="red-search-input" placeholder="<%= red_t("red.navbar.search_placeholder") %>" id="globalSearch" />
|
|
1188
|
-
|
|
1237
|
+
<input class="red-search-input" type="text" name="search" value="<%= params[:search] if params[:search].is_a?(String) %>" placeholder="<%= red_t("red.navbar.search_placeholder") %>" aria-label="<%= red_t("red.navbar.search_placeholder") %>" id="globalSearch" />
|
|
1238
|
+
<% if params[:application_id].is_a?(String) && params[:application_id].present? %>
|
|
1239
|
+
<input type="hidden" name="application_id" value="<%= params[:application_id] %>" />
|
|
1240
|
+
<% end %>
|
|
1241
|
+
</form>
|
|
1189
1242
|
</div>
|
|
1190
1243
|
<div style="display: flex; align-items: center; gap: var(--space-3);">
|
|
1191
1244
|
<!-- Application Switcher (multi-app) -->
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<% has_linked_issue = error.respond_to?(:external_issue_url) && error.external_issue_url.present? %>
|
|
2
|
+
<%# nil when the stored value is not plain http(s); the button is then omitted
|
|
3
|
+
(the issue card explains why). See safe_external_url. %>
|
|
4
|
+
<% discussion_issue_url = has_linked_issue ? safe_external_url(error.external_issue_url) : nil %>
|
|
2
5
|
<% has_audit_comments = error.respond_to?(:comments) && error.comments.any? %>
|
|
3
6
|
<% platform_comments = @platform_comments || [] %>
|
|
4
7
|
<% issue_tracking_enabled = RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
@@ -30,9 +33,11 @@
|
|
|
30
33
|
<span class="badge bg-secondary ms-2"><%= platform_comments.size %></span>
|
|
31
34
|
<% end %>
|
|
32
35
|
</h5>
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
+
<% if discussion_issue_url %>
|
|
37
|
+
<a href="<%= discussion_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
|
|
38
|
+
<i class="bi bi-box-arrow-up-right me-1"></i> <%= red_t("red.errors.discussion.reply_on", provider: discussion_provider) %>
|
|
39
|
+
</a>
|
|
40
|
+
<% end %>
|
|
36
41
|
</div>
|
|
37
42
|
<div class="card-body p-0">
|
|
38
43
|
<% if platform_comments.any? %>
|
|
@@ -40,14 +45,14 @@
|
|
|
40
45
|
<% platform_comments.each_with_index do |comment, index| %>
|
|
41
46
|
<div class="px-3 py-3 <%= index < platform_comments.size - 1 ? 'border-bottom' : '' %>">
|
|
42
47
|
<div class="d-flex align-items-start gap-2 mb-2">
|
|
43
|
-
<% if comment[:avatar_url]
|
|
44
|
-
<img src="<%=
|
|
48
|
+
<% if (avatar = safe_external_url(comment[:avatar_url])) %>
|
|
49
|
+
<img src="<%= avatar %>" alt="<%= comment[:author] %>" width="24" height="24" class="rounded-circle" style="flex-shrink: 0;">
|
|
45
50
|
<% else %>
|
|
46
51
|
<i class="bi bi-person-circle" style="font-size: 1.3em; flex-shrink: 0;"></i>
|
|
47
52
|
<% end %>
|
|
48
53
|
<div>
|
|
49
54
|
<strong class="text-primary"><%= comment[:author] || red_t("red.common.unknown") %></strong>
|
|
50
|
-
<small class="text-muted ms-2"><%= comment[:created_at]
|
|
55
|
+
<small class="text-muted ms-2"><%= local_time(safe_parse_time(comment[:created_at]), format: :datetime, fallback: "") %></small>
|
|
51
56
|
</div>
|
|
52
57
|
</div>
|
|
53
58
|
<div class="ms-4 text-break" style="font-size: 0.9em;">
|
|
@@ -68,7 +73,7 @@
|
|
|
68
73
|
<div class="d-flex justify-content-between align-items-start">
|
|
69
74
|
<div>
|
|
70
75
|
<small class="text-primary fw-bold"><i class="bi bi-person-circle"></i> <%= comment.author_name %></small>
|
|
71
|
-
<small class="text-muted ms-2"><%= comment.
|
|
76
|
+
<small class="text-muted ms-2"><%= local_time(comment.created_at, format: :datetime) %></small>
|
|
72
77
|
</div>
|
|
73
78
|
</div>
|
|
74
79
|
<small class="text-muted text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></small>
|
|
@@ -84,9 +89,11 @@
|
|
|
84
89
|
<h5 class="mb-0">
|
|
85
90
|
<i class="bi bi-chat-dots"></i> <%= red_t("red.errors.discussion.title") %>
|
|
86
91
|
</h5>
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
92
|
+
<% if discussion_issue_url %>
|
|
93
|
+
<a href="<%= discussion_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
|
|
94
|
+
<i class="bi bi-box-arrow-up-right me-1"></i> <%= red_t("red.errors.discussion.start_on", provider: discussion_provider) %>
|
|
95
|
+
</a>
|
|
96
|
+
<% end %>
|
|
90
97
|
</div>
|
|
91
98
|
<div class="card-body text-center py-3">
|
|
92
99
|
<p class="text-muted small mb-0"><%= red_t("red.errors.discussion.empty", provider: discussion_provider_lc) %></p>
|
|
@@ -106,7 +113,7 @@
|
|
|
106
113
|
<div class="d-flex justify-content-between align-items-start">
|
|
107
114
|
<div>
|
|
108
115
|
<small class="text-primary fw-bold"><i class="bi bi-person-circle"></i> <%= comment.author_name %></small>
|
|
109
|
-
<small class="text-muted ms-2"><%= comment.
|
|
116
|
+
<small class="text-muted ms-2"><%= local_time(comment.created_at, format: :datetime) %></small>
|
|
110
117
|
</div>
|
|
111
118
|
</div>
|
|
112
119
|
<small class="text-muted text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></small>
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
<i class="bi bi-link-45deg me-2"></i>
|
|
6
6
|
<%= red_t("red.errors.issue_section.title") %>
|
|
7
7
|
</h5>
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
<%# nil unless the stored value is plain http(s) — see safe_external_url. %>
|
|
9
|
+
<% issue_url = safe_external_url(@error.external_issue_url) %>
|
|
10
|
+
<% if issue_url %>
|
|
11
|
+
<a href="<%= issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
|
|
10
12
|
<i class="bi bi-box-arrow-up-right me-1"></i> <%= red_t("red.errors.issue_section.view_issue") %>
|
|
11
13
|
</a>
|
|
12
14
|
<% end %>
|
|
@@ -23,6 +25,15 @@
|
|
|
23
25
|
end %>
|
|
24
26
|
<% issue = @platform_issue %>
|
|
25
27
|
|
|
28
|
+
<% unless issue_url %>
|
|
29
|
+
<%# Stored, but not plain http(s): shown as text, never as a link. %>
|
|
30
|
+
<div class="alert alert-warning py-2 px-3 mb-3" style="font-size: 0.85em;">
|
|
31
|
+
<i class="bi bi-exclamation-triangle me-1"></i>
|
|
32
|
+
<%= red_t("red.errors.issue_section.unsafe_link") %>
|
|
33
|
+
<code class="d-block text-break mt-1"><%= @error.external_issue_url.to_s.truncate(200) %></code>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
26
37
|
<!-- Issue badge + status -->
|
|
27
38
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
28
39
|
<span class="badge bg-success">
|
|
@@ -50,8 +61,8 @@
|
|
|
50
61
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
51
62
|
<% issue[:assignees].each do |assignee| %>
|
|
52
63
|
<span class="d-inline-flex align-items-center gap-1 border rounded-pill px-2 py-1" style="font-size: 0.85em;">
|
|
53
|
-
<% if assignee[:avatar_url]
|
|
54
|
-
<img src="<%=
|
|
64
|
+
<% if (avatar = safe_external_url(assignee[:avatar_url])) %>
|
|
65
|
+
<img src="<%= avatar %>" width="18" height="18" class="rounded-circle">
|
|
55
66
|
<% end %>
|
|
56
67
|
<%= assignee[:login] %>
|
|
57
68
|
</span>
|
|
@@ -68,8 +79,9 @@
|
|
|
68
79
|
<% if issue[:labels].present? && issue[:labels].any? %>
|
|
69
80
|
<div class="d-flex align-items-center gap-1 flex-wrap">
|
|
70
81
|
<% issue[:labels].each do |label| %>
|
|
71
|
-
|
|
72
|
-
<%
|
|
82
|
+
<%# Third-party value going into style=: validated, never interpolated raw. %>
|
|
83
|
+
<% bg_color = safe_label_color(label[:color]) %>
|
|
84
|
+
<% text_color = label_text_color(bg_color) %>
|
|
73
85
|
<span class="badge rounded-pill" style="background-color: <%= bg_color %>; color: <%= text_color %>; font-size: 0.78em;">
|
|
74
86
|
<%= label[:name] %>
|
|
75
87
|
</span>
|
|
@@ -118,10 +130,12 @@
|
|
|
118
130
|
</div>
|
|
119
131
|
</div>
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
<%# The URL comes from the forge's API response; window.open would run a
|
|
134
|
+
javascript: target, so it gets the same http(s) check as the links. %>
|
|
135
|
+
<% if (new_issue_url = safe_external_url(flash[:new_issue_url])) %>
|
|
122
136
|
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
123
137
|
(function() {
|
|
124
|
-
window.open(
|
|
138
|
+
window.open(<%= js_safe_json(new_issue_url) %>, '_blank');
|
|
125
139
|
})();
|
|
126
140
|
</script>
|
|
127
141
|
<% end %>
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
<div><small class="text-muted"><%= red_t("red.errors.request_context.snapshot_fidelity_lite") %></small></div>
|
|
31
31
|
<% when "minimal" %>
|
|
32
32
|
<div><small class="text-muted"><%= red_t("red.errors.request_context.snapshot_fidelity_minimal") %></small></div>
|
|
33
|
+
<% when "partial" %>
|
|
34
|
+
<div><small class="text-muted"><%= red_t("red.errors.request_context.snapshot_fidelity_partial") %></small></div>
|
|
33
35
|
<% end %>
|
|
34
36
|
<% end %>
|
|
35
37
|
</td>
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
<% if error.respond_to?(:environment) && error.environment.present? %>
|
|
74
74
|
<div class="mb-3">
|
|
75
|
-
<small class="metadata-label d-block mb-1"><%= red_t("red.errors.sidebar.environment") %></small>
|
|
75
|
+
<small class="metadata-label d-block mb-1"><%= red_t("red.errors.sidebar.environment.title") %></small>
|
|
76
76
|
<%= link_to errors_path(environment: error.environment, unresolved: '0'), class: "text-decoration-none", title: red_t("red.errors.sidebar.view_all_environment", environment: error.environment) do %>
|
|
77
77
|
<span class="badge badge-api" data-environment-badge="<%= error.environment %>"><i class="bi bi-hdd-stack"></i> <%= error.environment %></span>
|
|
78
78
|
<% end %>
|
|
@@ -106,12 +106,21 @@
|
|
|
106
106
|
The <a> is built here rather than living in the key. The key
|
|
107
107
|
is _html and therefore html_safe, so a raw URL interpolated
|
|
108
108
|
into an href inside it would not be escaped and a crafted
|
|
109
|
-
external_issue_url could inject an event handler.
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
external_issue_url could inject an event handler.
|
|
110
|
+
|
|
111
|
+
link_to escapes the URL, which stops attribute BREAKOUT. It
|
|
112
|
+
does not reject a scheme: link_to("x", "javascript:...")
|
|
113
|
+
is a working link. That protection comes from
|
|
114
|
+
safe_external_url, which returns nil for anything that is
|
|
115
|
+
not plain http(s). %>
|
|
116
|
+
<% stored_issue_url = error.respond_to?(:external_issue_url) ? error.external_issue_url : nil %>
|
|
117
|
+
<% sidebar_issue_url = safe_external_url(stored_issue_url) %>
|
|
118
|
+
<% if sidebar_issue_url %>
|
|
112
119
|
<%= red_t("red.errors.sidebar.issue_tracking_linked_html",
|
|
113
120
|
link: link_to(red_t("red.errors.sidebar.issue_tracking_link_text"),
|
|
114
|
-
|
|
121
|
+
sidebar_issue_url, target: "_blank", rel: "noopener")) %>
|
|
122
|
+
<% elsif stored_issue_url.present? %>
|
|
123
|
+
<%= red_t("red.errors.issue_section.unsafe_link") %>
|
|
115
124
|
<% else %>
|
|
116
125
|
<%= red_t("red.errors.sidebar.issue_tracking_unlinked") %>
|
|
117
126
|
<% end %>
|
|
@@ -504,7 +513,7 @@
|
|
|
504
513
|
<% if health[:job_queue] %>
|
|
505
514
|
<% jq = health[:job_queue] %>
|
|
506
515
|
<div class="mb-1">
|
|
507
|
-
<small class="text-muted"
|
|
516
|
+
<small class="text-muted"><%= red_t("red.errors.sidebar.health.job_queue", adapter: jq[:adapter]) %></small>
|
|
508
517
|
<code class="ms-1">
|
|
509
518
|
<% case jq[:adapter] %>
|
|
510
519
|
<% when "sidekiq" %>
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
71
71
|
document.addEventListener('DOMContentLoaded', function() {
|
|
72
72
|
const colors = window.getChartColors();
|
|
73
|
-
new Chartkick.LineChart("errors-over-time-chart", <%=
|
|
73
|
+
new Chartkick.LineChart("errors-over-time-chart", <%= js_safe_json(@errors_over_time) %>, {
|
|
74
74
|
color: "#DC2626",
|
|
75
75
|
curve: false,
|
|
76
76
|
points: true,
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"#EF4444" // Red - iOS
|
|
123
123
|
];
|
|
124
124
|
|
|
125
|
-
new Chartkick.PieChart("errors-by-platform-chart", <%=
|
|
125
|
+
new Chartkick.PieChart("errors-by-platform-chart", <%= js_safe_json(@errors_by_platform) %>, {
|
|
126
126
|
colors: platformColors,
|
|
127
127
|
height: "300px",
|
|
128
128
|
legend: "bottom",
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
const colors = window.getChartColors();
|
|
174
174
|
const environmentColors = ["#EF4444", "#F59E0B", "#2563EB", "#10B981", "#8B5CF6", "#6B7280"];
|
|
175
175
|
|
|
176
|
-
new Chartkick.PieChart("errors-by-environment-chart", <%=
|
|
176
|
+
new Chartkick.PieChart("errors-by-environment-chart", <%= js_safe_json(environment_counts) %>, {
|
|
177
177
|
colors: environmentColors,
|
|
178
178
|
height: "300px",
|
|
179
179
|
legend: "bottom",
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
218
218
|
document.addEventListener('DOMContentLoaded', function() {
|
|
219
219
|
const colors = window.getChartColors();
|
|
220
|
-
new Chartkick.BarChart("errors-by-type-chart", <%=
|
|
220
|
+
new Chartkick.BarChart("errors-by-type-chart", <%= js_safe_json(@errors_by_type) %>, {
|
|
221
221
|
color: "#EF4444",
|
|
222
222
|
height: "400px",
|
|
223
223
|
library: {
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
261
261
|
document.addEventListener('DOMContentLoaded', function() {
|
|
262
262
|
const colors = window.getChartColors();
|
|
263
|
-
new Chartkick.ColumnChart("errors-by-hour-chart", <%=
|
|
263
|
+
new Chartkick.ColumnChart("errors-by-hour-chart", <%= js_safe_json(@errors_by_hour) %>, {
|
|
264
264
|
color: "#DC2626",
|
|
265
265
|
height: "300px",
|
|
266
266
|
library: {
|
|
@@ -469,7 +469,7 @@
|
|
|
469
469
|
<div id="errors-by-version-chart"></div>
|
|
470
470
|
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
471
471
|
document.addEventListener('DOMContentLoaded', function() {
|
|
472
|
-
const versionData = <%=
|
|
472
|
+
const versionData = <%= js_safe_json(@errors_by_version.transform_values { |v| v[:count] }) %>;
|
|
473
473
|
const colors = window.getChartColors();
|
|
474
474
|
|
|
475
475
|
new Chartkick.ColumnChart("errors-by-version-chart", versionData, {
|
|
@@ -608,7 +608,7 @@
|
|
|
608
608
|
document.addEventListener('DOMContentLoaded', function() {
|
|
609
609
|
const colors = window.getChartColors();
|
|
610
610
|
new Chartkick.BarChart("mttr-by-platform-chart",
|
|
611
|
-
<%=
|
|
611
|
+
<%= js_safe_json(@mttr_by_platform) %>, {
|
|
612
612
|
suffix: '<%= red_js_t("red.ui_js.charts.suffix_hours") %>',
|
|
613
613
|
height: "200px",
|
|
614
614
|
colors: ["#8B5CF6"],
|
|
@@ -661,7 +661,7 @@
|
|
|
661
661
|
document.addEventListener('DOMContentLoaded', function() {
|
|
662
662
|
const colors = window.getChartColors();
|
|
663
663
|
new Chartkick.LineChart("mttr-trend-chart",
|
|
664
|
-
<%=
|
|
664
|
+
<%= js_safe_json(@mttr_stats[:mttr_trend]) %>, {
|
|
665
665
|
suffix: '<%= red_js_t("red.ui_js.charts.suffix_hours") %>',
|
|
666
666
|
height: "250px",
|
|
667
667
|
colors: ["#10B981"],
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
<div class="card text-center">
|
|
50
50
|
<div class="card-body">
|
|
51
51
|
<%
|
|
52
|
-
latest = @
|
|
52
|
+
latest = @latest_dump
|
|
53
53
|
latest_data = begin; JSON.parse(latest.dump_data); rescue; {}; end if latest
|
|
54
54
|
thread_count = latest_data&.dig("system_health", "thread_count")
|
|
55
55
|
%>
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- Subscribe to Turbo Stream updates (only if Turbo Streams + ActionCable pubsub are available) -->
|
|
4
4
|
<% if defined?(Turbo::StreamsHelper) && defined?(ActionCable) && respond_to?(:turbo_stream_from) && RailsErrorDashboard::Services::ErrorBroadcaster.available? %>
|
|
5
|
-
|
|
5
|
+
<%# Names come from ErrorBroadcaster (one source for both ends): this page's
|
|
6
|
+
application or the global streams, without the new-row stream when a
|
|
7
|
+
filter, a sort or a later page means a new row does not belong on top. %>
|
|
8
|
+
<% (@live_streams || []).each do |stream| %>
|
|
9
|
+
<%= turbo_stream_from stream %>
|
|
10
|
+
<% end %>
|
|
6
11
|
<% end %>
|
|
7
12
|
|
|
8
13
|
<%
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
</div>
|
|
17
17
|
<% end %>
|
|
18
18
|
|
|
19
|
+
<%# A storm episode degraded without writing its time buckets, so the
|
|
20
|
+
time-window figures below rest on each group's first-seen timestamp
|
|
21
|
+
rather than on per-event records. The COUNTS are still exact -- only
|
|
22
|
+
their placement in time is approximate, and saying so beats presenting
|
|
23
|
+
a figure of unknown completeness as fact. %>
|
|
24
|
+
<% if @stats[:event_timing_incomplete] %>
|
|
25
|
+
<div class="alert-warning" style="display: flex; align-items: center; gap: 10px; padding: 10px var(--space-5); background: var(--status-warning-bg); border-radius: var(--radius-md); border: 1px solid var(--status-warning); margin-bottom: var(--space-6); font-size: 13px; color: var(--status-warning);">
|
|
26
|
+
<i class="bi bi-clock-history"></i>
|
|
27
|
+
<strong><%= red_t("red.errors.overview_page.event_timing_incomplete") %></strong>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
19
31
|
<!-- Spike / Critical Alerts Banner -->
|
|
20
32
|
<% if @critical_alerts.any? %>
|
|
21
33
|
<%
|
|
@@ -313,10 +313,10 @@
|
|
|
313
313
|
new Chart(errorRateCtx, {
|
|
314
314
|
type: 'bar',
|
|
315
315
|
data: {
|
|
316
|
-
labels: <%=
|
|
316
|
+
labels: <%= js_safe_json(@error_rate_by_platform.keys.map { |k| k.to_s.capitalize }) %>,
|
|
317
317
|
datasets: [{
|
|
318
318
|
label: '<%= red_js_t("red.ui_js.charts.series_total_errors") %>',
|
|
319
|
-
data: <%=
|
|
319
|
+
data: <%= js_safe_json(@error_rate_by_platform.values) %>,
|
|
320
320
|
backgroundColor: 'rgba(37, 99, 235, 0.5)', // Blue with transparency
|
|
321
321
|
borderColor: '#2563EB', // Blue
|
|
322
322
|
borderWidth: 1
|
|
@@ -338,14 +338,14 @@
|
|
|
338
338
|
// Daily Trend Chart
|
|
339
339
|
const dailyTrendCtx = document.getElementById('dailyTrendChart');
|
|
340
340
|
if (dailyTrendCtx) {
|
|
341
|
-
const datasets = <%=
|
|
341
|
+
const datasets = <%= js_safe_json(@daily_trends.map { |platform, data|
|
|
342
342
|
{
|
|
343
343
|
label: platform.to_s.capitalize,
|
|
344
344
|
data: data.values,
|
|
345
345
|
platform: platform.to_s.downcase,
|
|
346
346
|
tension: 0.4
|
|
347
347
|
}
|
|
348
|
-
}
|
|
348
|
+
}) %>;
|
|
349
349
|
|
|
350
350
|
// Apply platform-specific colors
|
|
351
351
|
datasets.forEach(ds => {
|
|
@@ -358,7 +358,7 @@
|
|
|
358
358
|
new Chart(dailyTrendCtx, {
|
|
359
359
|
type: 'line',
|
|
360
360
|
data: {
|
|
361
|
-
labels: <%=
|
|
361
|
+
labels: <%= js_safe_json(@daily_trends.values.first&.keys&.map { |d| red_chart_date(d) } || []) %>,
|
|
362
362
|
datasets: datasets
|
|
363
363
|
},
|
|
364
364
|
options: {
|
|
@@ -377,8 +377,8 @@
|
|
|
377
377
|
// Resolution Time Chart
|
|
378
378
|
const resolutionTimeCtx = document.getElementById('resolutionTimeChart');
|
|
379
379
|
if (resolutionTimeCtx) {
|
|
380
|
-
const platforms = <%=
|
|
381
|
-
const times = <%=
|
|
380
|
+
const platforms = <%= js_safe_json(@resolution_times.keys.map { |k| k.to_s.capitalize }) %>;
|
|
381
|
+
const times = <%= js_safe_json(@resolution_times.values.map { |v| v || 0 }) %>;
|
|
382
382
|
|
|
383
383
|
new Chart(resolutionTimeCtx, {
|
|
384
384
|
type: 'bar',
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
</div>
|
|
44
44
|
<% else %>
|
|
45
45
|
<%
|
|
46
|
-
current_release = @
|
|
46
|
+
current_release = @current_release
|
|
47
47
|
%>
|
|
48
48
|
|
|
49
49
|
<!-- Summary Cards -->
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
<div class="card text-center">
|
|
77
77
|
<div class="card-body">
|
|
78
78
|
<%
|
|
79
|
-
problematic_count = @
|
|
79
|
+
problematic_count = @problematic_release_count
|
|
80
80
|
%>
|
|
81
81
|
<div class="display-6 <%= problematic_count > 0 ? 'text-danger' : 'text-success' %>"><%= problematic_count %></div>
|
|
82
82
|
<small class="text-muted"><%= red_t("red.analytics.releases.summary.problematic") %></small>
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
color: "info",
|
|
48
48
|
settings: {
|
|
49
49
|
notification_environments: { type: :array },
|
|
50
|
+
notification_burst_limit: { type: :integer },
|
|
51
|
+
notification_burst_window_seconds: { type: :integer, unit: :seconds },
|
|
50
52
|
enable_slack_notifications: { type: :boolean },
|
|
51
53
|
slack_webhook_url: { type: :url, show_if: :enable_slack_notifications },
|
|
52
54
|
enable_email_notifications: { type: :boolean },
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
15
|
<!-- Hero card -->
|
|
16
|
-
<div
|
|
17
|
-
<div
|
|
16
|
+
<div class="red-error-hero" style="padding: var(--space-6); background: var(--surface-primary); border-radius: var(--radius-md); border: 1px solid var(--border-primary); margin-bottom: var(--space-4);">
|
|
17
|
+
<div class="red-error-hero-text">
|
|
18
18
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
|
19
19
|
<span class="badge bg-<%= severity_color(@error.severity) %>" style="display: inline-flex; align-items: center; gap: 4px;">
|
|
20
20
|
<span style="width: 6px; height: 6px; border-radius: 50%; background: currentColor;"></span>
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
<span><%= red_t("red.errors.show.hero.last_seen_html", time: local_time_ago(@error.last_seen_at)) %></span>
|
|
76
76
|
</div>
|
|
77
77
|
</div>
|
|
78
|
-
<div
|
|
78
|
+
<div class="red-error-hero-actions">
|
|
79
79
|
<% unless @error.resolved? %>
|
|
80
80
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#resolveModal" style="font-weight: 600;">
|
|
81
81
|
<%= red_t("red.errors.show.actions.resolve") %>
|
data/config/locales/de.yml
CHANGED
|
@@ -420,6 +420,7 @@ de:
|
|
|
420
420
|
issue_section:
|
|
421
421
|
title: Issue-Tracker
|
|
422
422
|
view_issue: Issue ansehen
|
|
423
|
+
unsafe_link: "Dieser Issue-Link ist keine http(s)-URL, daher wird er als Text angezeigt und kann nicht geöffnet werden."
|
|
423
424
|
linked: Verknüpft
|
|
424
425
|
issue_reference: "%{provider} #%{number}"
|
|
425
426
|
states:
|
|
@@ -582,6 +583,7 @@ de:
|
|
|
582
583
|
snapshot_stale: aus einem früheren Auftreten — spätere lieferten keinen Kontext
|
|
583
584
|
snapshot_fidelity_lite: reduzierte Erfassung (der Storm-Schutz hat die Kontext-Payloads verworfen)
|
|
584
585
|
snapshot_fidelity_minimal: während eines Storms gezählt — es wurde kein Kontext erfasst
|
|
586
|
+
snapshot_fidelity_partial: gemischter Snapshot — einige Felder stammen aus einem früheren Vorkommen
|
|
585
587
|
snapshot_unknown: erfasst, bevor RED die Herkunft von Momentaufnahmen aufzeichnete
|
|
586
588
|
similar_errors:
|
|
587
589
|
title: Ähnliche Fehler
|
|
@@ -939,6 +941,7 @@ de:
|
|
|
939
941
|
rate_per_hour: "%{value}/Std."
|
|
940
942
|
affected_users_incomplete: mindestens — der Storm-Schutz hat Detaildaten pro Ereignis verworfen
|
|
941
943
|
data_unavailable: Statistiken sind derzeit nicht verfügbar — die Zahlen unten sind keine Messung.
|
|
944
|
+
event_timing_incomplete: "Zeitangaben sind unvollständig — der Sturmschutz hat Zeitstempel verworfen; die Gesamtzahlen stimmen."
|
|
942
945
|
unresolved: Offen
|
|
943
946
|
unresolved_hint: Behebung ausstehend
|
|
944
947
|
resolution_rate: Behebungsquote
|
|
@@ -972,6 +975,16 @@ de:
|
|
|
972
975
|
types:
|
|
973
976
|
one: "%{count} Typ"
|
|
974
977
|
other: "%{count} Typen"
|
|
978
|
+
errors_page:
|
|
979
|
+
csrf:
|
|
980
|
+
title: "Ihr Sitzungstoken ist abgelaufen"
|
|
981
|
+
message: "Gehen Sie zurück, laden Sie die Seite neu und versuchen Sie es erneut. Es wurde nichts geändert."
|
|
982
|
+
bad_request:
|
|
983
|
+
title: "Diese Anfrage konnte nicht verarbeitet werden"
|
|
984
|
+
message: "Ein erforderlicher Wert fehlte oder war fehlerhaft. Gehen Sie zurück und versuchen Sie es erneut."
|
|
985
|
+
not_acceptable:
|
|
986
|
+
title: "Dieses Format ist nicht verfügbar"
|
|
987
|
+
message: "Das Dashboard kann das angeforderte Format nicht erzeugen."
|
|
975
988
|
settings:
|
|
976
989
|
title: Einstellungen
|
|
977
990
|
version: v%{version}
|
|
@@ -1086,6 +1099,8 @@ de:
|
|
|
1086
1099
|
gesetzt)
|
|
1087
1100
|
environment: Umgebung, der Fehler zugeordnet werden (Rails.env, falls nicht
|
|
1088
1101
|
gesetzt)
|
|
1102
|
+
notification_burst_limit: "Benachrichtigungen über neue Fehler pro Zeitfenster und Prozess (0 = keine Obergrenze)"
|
|
1103
|
+
notification_burst_window_seconds: "Länge des Zeitfensters für Benachrichtigungen über neue Fehler"
|
|
1089
1104
|
notification_environments: Nur für diese Umgebungen benachrichtigen (alle,
|
|
1090
1105
|
falls nicht gesetzt)
|
|
1091
1106
|
database: Aktive Datenbank für Fehlerprotokolle
|
|
@@ -1193,6 +1208,16 @@ de:
|
|
|
1193
1208
|
linked: Issue erfolgreich verknüpft
|
|
1194
1209
|
create_failed: 'Issue konnte nicht erstellt werden: %{reason}'
|
|
1195
1210
|
link_failed: 'Issue konnte nicht verknüpft werden: %{reason}'
|
|
1211
|
+
status:
|
|
1212
|
+
updated: "Status geändert zu %{status}"
|
|
1213
|
+
invalid_transition: "Der Status kann nicht von %{from} zu %{to} geändert werden"
|
|
1214
|
+
unknown: "Diesen Status gibt es nicht"
|
|
1215
|
+
snooze:
|
|
1216
|
+
invalid_hours: "Die Zurückstelldauer muss eine ganze Zahl von Stunden zwischen 1 und %{max} sein"
|
|
1217
|
+
priority:
|
|
1218
|
+
invalid: "Diese Prioritätsstufe gibt es nicht"
|
|
1219
|
+
assign:
|
|
1220
|
+
blank: "Geben Sie einen Namen ein, dem dieser Fehler zugewiesen werden soll"
|
|
1196
1221
|
not_enabled: "%{feature} ist nicht aktiviert. Aktivieren Sie es in %{file}"
|
|
1197
1222
|
not_enabled_plural: "%{feature} sind nicht aktiviert. Aktivieren Sie sie in
|
|
1198
1223
|
%{file}"
|
|
@@ -1238,6 +1263,7 @@ de:
|
|
|
1238
1263
|
not_configured: Issue-Tracking ist nicht konfiguriert
|
|
1239
1264
|
already_linked: 'Der Fehler hat bereits ein verknüpftes Issue: %{url}'
|
|
1240
1265
|
url_required: Die Issue-URL ist erforderlich
|
|
1266
|
+
url_invalid: Die Issue-URL muss mit http:// oder https:// beginnen
|
|
1241
1267
|
error_not_found: 'Fehler nicht gefunden: %{id}'
|
|
1242
1268
|
batch_failed:
|
|
1243
1269
|
mute:
|
|
@@ -1362,6 +1388,10 @@ de:
|
|
|
1362
1388
|
one: "%{value} Fehler"
|
|
1363
1389
|
other: "%{value} Fehler"
|
|
1364
1390
|
view_in_dashboard: Im Dashboard ansehen
|
|
1391
|
+
burst:
|
|
1392
|
+
suppressed: "In %{application} wurden innerhalb von %{window} Sekunden mehr als %{limit} neue Fehler gemeldet."
|
|
1393
|
+
still_recorded: "Benachrichtigungen über weitere neue Fehler sind bis zum Ende dieses Zeitfensters pausiert; jeder Fehler wird weiterhin erfasst."
|
|
1394
|
+
dashboard: "Dashboard: %{url}"
|
|
1365
1395
|
storm:
|
|
1366
1396
|
detected: Fehlersturm in %{application} um %{started_at} erkannt.
|
|
1367
1397
|
mode:
|