rails_error_dashboard 0.6.1 → 0.6.3
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/errors_controller.rb +6 -3
- data/app/helpers/rails_error_dashboard/application_helper.rb +27 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +123 -14
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +3 -3
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +12 -14
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +6 -6
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +3 -3
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +11 -11
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/index.html.erb +31 -34
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/settings.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/show.html.erb +13 -13
- data/db/migrate/20260503000001_backfill_resolved_status.rb +36 -0
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +6 -4
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -1
- data/lib/rails_error_dashboard/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 938ab24cad38b8b20bc0dd0f2a1eb6e5c9e46a4634fb97e2c01b3487221c9fc3
|
|
4
|
+
data.tar.gz: 6e053a3f8c41e3c3f75f95ab1f9bd8908e9bcea4c473e2d1d3a0c11b7bdf0bdb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b84b4a4f917863e7577e253f2324100a06b6fef6f33d56cef211f28a4f1dc4688932f618489f19924baa8aa065135cfcaeffb2c7431f15bea898d3e334395b7
|
|
7
|
+
data.tar.gz: 4149d8da317c77751e04db12b46f02924fe46e1a420075232e3fa7c96f12218166249596b33a55f1790e696b5fca3580cd5da62d38a39585e356660a702a518c
|
|
@@ -60,8 +60,11 @@ module RailsErrorDashboard
|
|
|
60
60
|
# Use Query to get filtered errors
|
|
61
61
|
errors_query = Queries::ErrorsList.call(filter_params)
|
|
62
62
|
|
|
63
|
-
# Paginate with Pagy
|
|
64
|
-
|
|
63
|
+
# Paginate with Pagy. raise_range_error makes pagy 43.x raise Pagy::RangeError
|
|
64
|
+
# on out-of-range pages (e.g. ?page=999999) so application_controller.rb's
|
|
65
|
+
# rescue_from can redirect to a valid page. Without this, pagy 43.x silently
|
|
66
|
+
# returns an empty result and users see "All clear!" instead of their data.
|
|
67
|
+
@pagy, @errors = pagy(:offset, errors_query, limit: params[:per_page] || 25, raise_range_error: true)
|
|
65
68
|
|
|
66
69
|
# Get dashboard stats using Query (pass application filter)
|
|
67
70
|
@stats = Queries::DashboardStats.call(application_id: @current_application_id)
|
|
@@ -237,7 +240,7 @@ module RailsErrorDashboard
|
|
|
237
240
|
resolution_comment: params[:resolution_comment]
|
|
238
241
|
)
|
|
239
242
|
when "mute"
|
|
240
|
-
Commands::BatchMuteErrors.call(error_ids, muted_by: params[:muted_by])
|
|
243
|
+
Commands::BatchMuteErrors.call(error_ids, muted_by: params[:muted_by], reason: params[:reason])
|
|
241
244
|
when "unmute"
|
|
242
245
|
Commands::BatchUnmuteErrors.call(error_ids)
|
|
243
246
|
when "delete"
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
module RailsErrorDashboard
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
# Returns the host app's CSP nonce (if any) so inline <script> tags pass strict CSP.
|
|
4
|
+
# Falls back to nil when the host has no CSP configured — in that case the script tag
|
|
5
|
+
# works without a nonce attribute. Strict CSPs (script-src 'self' 'nonce-...') require
|
|
6
|
+
# this; without it the script is blocked.
|
|
7
|
+
def red_csp_nonce
|
|
8
|
+
return nil unless respond_to?(:content_security_policy_nonce)
|
|
9
|
+
content_security_policy_nonce
|
|
10
|
+
rescue StandardError
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Wraps an inline <script> block with the host app's CSP nonce when available.
|
|
15
|
+
# Use everywhere we have <script>...</script> in our views so they pass strict CSP.
|
|
16
|
+
#
|
|
17
|
+
# <%= red_javascript_tag do %>
|
|
18
|
+
# console.log('hi');
|
|
19
|
+
# <% end %>
|
|
20
|
+
def red_javascript_tag(&block)
|
|
21
|
+
nonce = red_csp_nonce
|
|
22
|
+
content = capture(&block)
|
|
23
|
+
if nonce
|
|
24
|
+
content_tag(:script, content.html_safe, nonce: nonce)
|
|
25
|
+
else
|
|
26
|
+
content_tag(:script, content.html_safe)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
3
30
|
# Returns Bootstrap color class for error severity
|
|
4
31
|
# Uses Catppuccin Mocha colors in dark theme via CSS variables
|
|
5
32
|
# @param severity [Symbol] The severity level (:critical, :high, :medium, :low, :info)
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<% end %>
|
|
40
40
|
|
|
41
41
|
<!-- Early theme detection (prevents flash of wrong theme) -->
|
|
42
|
-
<script
|
|
42
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
43
43
|
(function(){
|
|
44
44
|
var t = localStorage.getItem('red-theme');
|
|
45
45
|
if (t === 'dark' || (!t && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
@@ -880,6 +880,12 @@ main { animation: contentFadeIn 0.15s ease; }
|
|
|
880
880
|
details { background: var(--surface-primary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); }
|
|
881
881
|
summary { padding: var(--space-3) var(--space-4); cursor: pointer; font-weight: 500; color: var(--text-primary); background: var(--surface-hover); }
|
|
882
882
|
details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
883
|
+
|
|
884
|
+
/* Error row hover (replaces inline onmouseenter/onmouseleave) */
|
|
885
|
+
tr[data-red-row-href] { transition: background 0.1s ease; }
|
|
886
|
+
tr[data-red-row-href]:hover { background: var(--surface-hover); }
|
|
887
|
+
tr[data-red-row-href] .sev-bar { transition: opacity 0.1s ease; }
|
|
888
|
+
tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
883
889
|
</style>
|
|
884
890
|
</head>
|
|
885
891
|
|
|
@@ -957,7 +963,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
957
963
|
|
|
958
964
|
<% if health_items.any? %>
|
|
959
965
|
<div style="margin-bottom: var(--space-2);" id="navHealthSection">
|
|
960
|
-
<button class="red-sidebar-section-label"
|
|
966
|
+
<button type="button" class="red-sidebar-section-label" data-red-action="toggle-nav-section" data-red-target="navHealthItems">
|
|
961
967
|
Health
|
|
962
968
|
<i class="bi bi-chevron-down" style="font-size: 10px;" id="navHealthChevron"></i>
|
|
963
969
|
</button>
|
|
@@ -988,7 +994,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
988
994
|
|
|
989
995
|
<% if diag_items.any? %>
|
|
990
996
|
<div style="margin-bottom: var(--space-2);" id="navDiagSection">
|
|
991
|
-
<button class="red-sidebar-section-label"
|
|
997
|
+
<button type="button" class="red-sidebar-section-label" data-red-action="toggle-nav-section" data-red-target="navDiagItems">
|
|
992
998
|
Diagnostics
|
|
993
999
|
<i class="bi bi-chevron-down" style="font-size: 10px;" id="navDiagChevron"></i>
|
|
994
1000
|
</button>
|
|
@@ -1006,7 +1012,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1006
1012
|
|
|
1007
1013
|
<!-- INSIGHTS section -->
|
|
1008
1014
|
<div style="margin-bottom: var(--space-2);" id="navInsightsSection">
|
|
1009
|
-
<button class="red-sidebar-section-label"
|
|
1015
|
+
<button type="button" class="red-sidebar-section-label" data-red-action="toggle-nav-section" data-red-target="navInsightsItems">
|
|
1010
1016
|
Insights
|
|
1011
1017
|
<i class="bi bi-chevron-down" style="font-size: 10px;" id="navInsightsChevron"></i>
|
|
1012
1018
|
</button>
|
|
@@ -1120,9 +1126,9 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1120
1126
|
Set <code>ERROR_DASHBOARD_USER</code> and <code>ERROR_DASHBOARD_PASSWORD</code> environment variables,
|
|
1121
1127
|
or configure <code>authenticate_with</code> in your initializer.
|
|
1122
1128
|
</div>
|
|
1123
|
-
<button type="button"
|
|
1129
|
+
<button type="button" data-red-action="dismiss-creds-warning" style="background: none; border: none; cursor: pointer; color: var(--text-secondary); font-size: 18px; padding: 0 4px; flex-shrink: 0; line-height: 1;" title="Dismiss for this session">×</button>
|
|
1124
1130
|
</div>
|
|
1125
|
-
<script
|
|
1131
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>try { if (sessionStorage.getItem('red_dismiss_creds_warning') === '1') { document.getElementById('security-warning').style.display = 'none'; } } catch(e) {}</script>
|
|
1126
1132
|
<% end %>
|
|
1127
1133
|
<%= yield %>
|
|
1128
1134
|
</main>
|
|
@@ -1211,7 +1217,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1211
1217
|
<script src="https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.umd.js"></script>
|
|
1212
1218
|
|
|
1213
1219
|
<!-- Loading State Controller -->
|
|
1214
|
-
<script
|
|
1220
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1215
1221
|
(function() {
|
|
1216
1222
|
'use strict';
|
|
1217
1223
|
if (typeof Stimulus === 'undefined') return;
|
|
@@ -1290,7 +1296,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1290
1296
|
</script>
|
|
1291
1297
|
|
|
1292
1298
|
<!-- Syntax Highlighting -->
|
|
1293
|
-
<script
|
|
1299
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1294
1300
|
(function() {
|
|
1295
1301
|
'use strict';
|
|
1296
1302
|
if (typeof hljs === 'undefined') return;
|
|
@@ -1334,7 +1340,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1334
1340
|
</script>
|
|
1335
1341
|
|
|
1336
1342
|
<!-- Theme Toggle + Chart Theme -->
|
|
1337
|
-
<script
|
|
1343
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1338
1344
|
document.addEventListener('DOMContentLoaded', function() {
|
|
1339
1345
|
var themeToggle = document.getElementById('themeToggle');
|
|
1340
1346
|
var themeIcon = document.getElementById('themeIcon');
|
|
@@ -1421,7 +1427,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1421
1427
|
</script>
|
|
1422
1428
|
|
|
1423
1429
|
<!-- Utilities (tooltips, clipboard, time conversion, keyboard shortcuts) -->
|
|
1424
|
-
<script
|
|
1430
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1425
1431
|
document.addEventListener('DOMContentLoaded', function() {
|
|
1426
1432
|
|
|
1427
1433
|
// Tooltips
|
|
@@ -1591,11 +1597,114 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1591
1597
|
window.location.href = '<%= analytics_errors_path %>';
|
|
1592
1598
|
}
|
|
1593
1599
|
});
|
|
1600
|
+
|
|
1601
|
+
// ==========================================================================
|
|
1602
|
+
// Delegated event handlers for [data-red-action]
|
|
1603
|
+
// Replaces inline event handlers (which strict CSP blocks).
|
|
1604
|
+
// ==========================================================================
|
|
1605
|
+
function findActionEl(target) {
|
|
1606
|
+
var el = target;
|
|
1607
|
+
while (el && el !== document.body) {
|
|
1608
|
+
if (el.dataset && el.dataset.redAction) return el;
|
|
1609
|
+
el = el.parentElement;
|
|
1610
|
+
}
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
document.addEventListener('click', function(e) {
|
|
1615
|
+
// Row navigation: <tr data-red-row-href="/path"> — clicking anywhere on the row
|
|
1616
|
+
// navigates, except when clicking inside a checkbox cell or interactive elements.
|
|
1617
|
+
var rowEl = e.target.closest && e.target.closest('[data-red-row-href]');
|
|
1618
|
+
if (rowEl && !e.defaultPrevented) {
|
|
1619
|
+
var inIgnore = e.target.closest('input, button, a, label, [data-red-stop]');
|
|
1620
|
+
if (!inIgnore) {
|
|
1621
|
+
window.location = rowEl.dataset.redRowHref;
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
var actionEl = findActionEl(e.target);
|
|
1627
|
+
if (!actionEl) return;
|
|
1628
|
+
var action = actionEl.dataset.redAction;
|
|
1629
|
+
|
|
1630
|
+
switch (action) {
|
|
1631
|
+
case 'navigate':
|
|
1632
|
+
if (actionEl.dataset.redHref) {
|
|
1633
|
+
window.location = actionEl.dataset.redHref;
|
|
1634
|
+
}
|
|
1635
|
+
break;
|
|
1636
|
+
case 'copy':
|
|
1637
|
+
if (typeof window.copyToClipboard === 'function') {
|
|
1638
|
+
var text = actionEl.dataset.redCopyText;
|
|
1639
|
+
if (text == null && actionEl.dataset.redCopyFrom) {
|
|
1640
|
+
text = actionEl.dataset[actionEl.dataset.redCopyFrom];
|
|
1641
|
+
}
|
|
1642
|
+
window.copyToClipboard(text || '', actionEl);
|
|
1643
|
+
}
|
|
1644
|
+
break;
|
|
1645
|
+
case 'copy-markdown':
|
|
1646
|
+
if (typeof window.copyToClipboard === 'function') {
|
|
1647
|
+
var md = (actionEl.dataset.markdown || '').replace(/\\(.)/g, function(m, c) {
|
|
1648
|
+
return c === 'n' ? '\n' : c;
|
|
1649
|
+
});
|
|
1650
|
+
window.copyToClipboard(md, actionEl);
|
|
1651
|
+
}
|
|
1652
|
+
break;
|
|
1653
|
+
case 'download-json':
|
|
1654
|
+
if (typeof window.downloadErrorJSON === 'function') {
|
|
1655
|
+
// Provide currentTarget pointing at the button so the handler can show feedback.
|
|
1656
|
+
window.downloadErrorJSON({ currentTarget: actionEl });
|
|
1657
|
+
}
|
|
1658
|
+
break;
|
|
1659
|
+
case 'switch-tab':
|
|
1660
|
+
if (typeof window.switchTab === 'function' && actionEl.dataset.redTab) {
|
|
1661
|
+
window.switchTab(actionEl.dataset.redTab);
|
|
1662
|
+
}
|
|
1663
|
+
break;
|
|
1664
|
+
case 'toggle-display':
|
|
1665
|
+
var tgtSel = actionEl.dataset.redTarget;
|
|
1666
|
+
if (tgtSel) {
|
|
1667
|
+
var tgt = document.querySelector(tgtSel);
|
|
1668
|
+
if (tgt) {
|
|
1669
|
+
var openDisplay = actionEl.dataset.redDisplay || 'block';
|
|
1670
|
+
tgt.style.display = (tgt.style.display === 'none' || tgt.style.display === '') ? openDisplay : 'none';
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
break;
|
|
1674
|
+
case 'toggle-next':
|
|
1675
|
+
var nextEl = actionEl.nextElementSibling;
|
|
1676
|
+
if (nextEl) {
|
|
1677
|
+
var isHidden = nextEl.style.display === 'none' || nextEl.style.display === '';
|
|
1678
|
+
nextEl.style.display = isHidden ? (actionEl.dataset.redDisplay || 'block') : 'none';
|
|
1679
|
+
var chev = actionEl.querySelector('.chev');
|
|
1680
|
+
if (chev) {
|
|
1681
|
+
chev.className = isHidden ? 'bi bi-chevron-up chev' : 'bi bi-chevron-down chev';
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
break;
|
|
1685
|
+
case 'dismiss-creds-warning':
|
|
1686
|
+
if (actionEl.parentElement) actionEl.parentElement.style.display = 'none';
|
|
1687
|
+
try { sessionStorage.setItem('red_dismiss_creds_warning', '1'); } catch (err) {}
|
|
1688
|
+
break;
|
|
1689
|
+
case 'toggle-nav-section':
|
|
1690
|
+
if (typeof window.toggleNavSection === 'function' && actionEl.dataset.redTarget) {
|
|
1691
|
+
window.toggleNavSection(actionEl.dataset.redTarget);
|
|
1692
|
+
}
|
|
1693
|
+
break;
|
|
1694
|
+
case 'confirm-submit':
|
|
1695
|
+
var msg = actionEl.dataset.redConfirmMessage || 'Are you sure?';
|
|
1696
|
+
if (!window.confirm(msg)) {
|
|
1697
|
+
e.preventDefault();
|
|
1698
|
+
e.stopPropagation();
|
|
1699
|
+
}
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1594
1703
|
});
|
|
1595
1704
|
</script>
|
|
1596
1705
|
|
|
1597
1706
|
<!-- Sidebar Toggle + Nav Section Toggle -->
|
|
1598
|
-
<script
|
|
1707
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1599
1708
|
document.addEventListener('DOMContentLoaded', function() {
|
|
1600
1709
|
var sidebarToggle = document.getElementById('sidebarToggle');
|
|
1601
1710
|
var sidebar = document.getElementById('sidebar');
|
|
@@ -1622,7 +1731,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1622
1731
|
});
|
|
1623
1732
|
|
|
1624
1733
|
// Nav section collapse/expand
|
|
1625
|
-
function
|
|
1734
|
+
window.toggleNavSection = function(itemsId) {
|
|
1626
1735
|
var items = document.getElementById(itemsId);
|
|
1627
1736
|
var chevron = document.getElementById(itemsId.replace('Items', 'Chevron'));
|
|
1628
1737
|
if (items) {
|
|
@@ -1631,7 +1740,7 @@ function toggleNavSection(itemsId) {
|
|
|
1631
1740
|
if (chevron) chevron.className = hidden ? 'bi bi-chevron-down' : 'bi bi-chevron-right';
|
|
1632
1741
|
localStorage.setItem('red_nav_' + itemsId, hidden ? 'open' : 'closed');
|
|
1633
1742
|
}
|
|
1634
|
-
}
|
|
1743
|
+
};
|
|
1635
1744
|
|
|
1636
1745
|
// Restore nav section states
|
|
1637
1746
|
document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -1648,7 +1757,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1648
1757
|
</script>
|
|
1649
1758
|
|
|
1650
1759
|
<!-- Flash Messages -->
|
|
1651
|
-
<script
|
|
1760
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1652
1761
|
<% if defined?(flash) && flash.present? %>
|
|
1653
1762
|
<% if flash[:notice] %>
|
|
1654
1763
|
showToast('<%= j flash[:notice] %>', 'success');
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</span>
|
|
14
14
|
<% end %>
|
|
15
15
|
</h5>
|
|
16
|
-
<button class="btn btn-sm btn-outline-light"
|
|
16
|
+
<button type="button" class="btn btn-sm btn-outline-light" data-red-action="copy" data-red-copy-text="<%= j error.error_type %>" title="Copy error type">
|
|
17
17
|
<i class="bi bi-clipboard"></i>
|
|
18
18
|
</button>
|
|
19
19
|
</div>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<div class="card-body">
|
|
22
22
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
23
23
|
<h6 class="text-muted mb-0">Error Message:</h6>
|
|
24
|
-
<button class="btn btn-sm btn-outline-secondary"
|
|
24
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= j error.message %>" title="Copy error message">
|
|
25
25
|
<i class="bi bi-clipboard"></i> Copy
|
|
26
26
|
</button>
|
|
27
27
|
</div>
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
<% end %>
|
|
96
96
|
</h6>
|
|
97
97
|
<% if error.backtrace.present? %>
|
|
98
|
-
<button class="btn btn-sm btn-outline-secondary"
|
|
98
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= j error.backtrace %>" title="Copy full backtrace">
|
|
99
99
|
<i class="bi bi-clipboard"></i> Copy Full Backtrace
|
|
100
100
|
</button>
|
|
101
101
|
<% end %>
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
<tr id="error_<%= error.id %>"
|
|
2
|
-
|
|
3
|
-
style="border-bottom: 1px solid var(--border-primary); cursor: pointer;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<span class="sev-bar" style="position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: <%= severity_color_var(error.severity) %>; opacity: 0; transition: opacity 0.1s ease; border-radius: 0 2px 2px 0;"></span>
|
|
8
|
-
<input type="checkbox" class="error-checkbox form-check-input" value="<%= error.id %>" data-error-id="<%= error.id %>" onclick="event.stopPropagation();" style="accent-color: var(--accent);">
|
|
2
|
+
data-red-row-href="<%= error_path(error, **app_context) %>"
|
|
3
|
+
style="border-bottom: 1px solid var(--border-primary); cursor: pointer;">
|
|
4
|
+
<td style="padding: var(--space-3) var(--space-4); text-align: center; position: relative;" data-red-stop>
|
|
5
|
+
<span class="sev-bar" style="position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: <%= severity_color_var(error.severity) %>; opacity: 0; border-radius: 0 2px 2px 0;"></span>
|
|
6
|
+
<input type="checkbox" class="error-checkbox form-check-input" value="<%= error.id %>" data-error-id="<%= error.id %>" data-red-stop style="accent-color: var(--accent);">
|
|
9
7
|
</td>
|
|
10
|
-
<td style="padding: var(--space-3) var(--space-4);"
|
|
8
|
+
<td style="padding: var(--space-3) var(--space-4);">
|
|
11
9
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
12
10
|
<span class="badge bg-<%= severity_color(error.severity) %>" style="display: inline-flex; align-items: center; gap: 4px;">
|
|
13
11
|
<span style="width: 6px; height: 6px; border-radius: 50%; background: currentColor;"></span>
|
|
@@ -24,7 +22,7 @@
|
|
|
24
22
|
</div>
|
|
25
23
|
</div>
|
|
26
24
|
</td>
|
|
27
|
-
<td style="padding: var(--space-3) var(--space-4);"
|
|
25
|
+
<td style="padding: var(--space-3) var(--space-4);">
|
|
28
26
|
<%
|
|
29
27
|
# Use the model's status field if it has a meaningful workflow status
|
|
30
28
|
raw_status = error.respond_to?(:status) ? error.status : nil
|
|
@@ -58,26 +56,26 @@
|
|
|
58
56
|
<i class="bi bi-arrow-counterclockwise" style="color: var(--status-warning); margin-left: 4px; font-size: 12px;" title="Reopened"></i>
|
|
59
57
|
<% end %>
|
|
60
58
|
</td>
|
|
61
|
-
<td style="padding: var(--space-3) var(--space-4); text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-primary);"
|
|
59
|
+
<td style="padding: var(--space-3) var(--space-4); text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-primary);">
|
|
62
60
|
<%= error.occurrence_count.to_s(:delimited) rescue error.occurrence_count %>
|
|
63
61
|
</td>
|
|
64
|
-
<td style="padding: var(--space-3) var(--space-4); text-align: right; font-variant-numeric: tabular-nums; color: var(--text-secondary);"
|
|
62
|
+
<td style="padding: var(--space-3) var(--space-4); text-align: right; font-variant-numeric: tabular-nums; color: var(--text-secondary);">
|
|
65
63
|
<% if error.user_id %>
|
|
66
64
|
<%= error.user_id %>
|
|
67
65
|
<% else %>
|
|
68
66
|
<span style="color: var(--text-tertiary);">—</span>
|
|
69
67
|
<% end %>
|
|
70
68
|
</td>
|
|
71
|
-
<td style="padding: var(--space-3) var(--space-4); text-align: right; color: var(--text-tertiary); font-size: 12px;"
|
|
69
|
+
<td style="padding: var(--space-3) var(--space-4); text-align: right; color: var(--text-tertiary); font-size: 12px;">
|
|
72
70
|
<%= local_time_ago(error.last_seen_at) %>
|
|
73
71
|
</td>
|
|
74
72
|
<% if local_assigns[:show_application] %>
|
|
75
|
-
<td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= error.application&.name || 'Unknown' %>"
|
|
73
|
+
<td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= error.application&.name || 'Unknown' %>">
|
|
76
74
|
<span class="badge bg-info"><%= error.application&.name || 'Unknown' %></span>
|
|
77
75
|
</td>
|
|
78
76
|
<% end %>
|
|
79
77
|
<% if local_assigns[:show_platform] %>
|
|
80
|
-
<td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= error.platform || 'API' %>"
|
|
78
|
+
<td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= error.platform || 'API' %>">
|
|
81
79
|
<% if error.platform == 'iOS' %>
|
|
82
80
|
<span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
|
|
83
81
|
<% elsif error.platform == 'Android' %>
|
|
@@ -84,17 +84,17 @@
|
|
|
84
84
|
<th>Replay Request:</th>
|
|
85
85
|
<td>
|
|
86
86
|
<% if curl_cmd.present? %>
|
|
87
|
-
<button class="btn btn-sm btn-outline-primary me-2"
|
|
88
|
-
|
|
89
|
-
data-
|
|
87
|
+
<button type="button" class="btn btn-sm btn-outline-primary me-2"
|
|
88
|
+
data-red-action="copy"
|
|
89
|
+
data-red-copy-text="<%= h curl_cmd %>"
|
|
90
90
|
title="Copy curl command to clipboard">
|
|
91
91
|
<i class="bi bi-terminal"></i> Copy as curl
|
|
92
92
|
</button>
|
|
93
93
|
<% end %>
|
|
94
94
|
<% if rspec_cmd.present? %>
|
|
95
|
-
<button class="btn btn-sm btn-outline-success"
|
|
96
|
-
|
|
97
|
-
data-
|
|
95
|
+
<button type="button" class="btn btn-sm btn-outline-success"
|
|
96
|
+
data-red-action="copy"
|
|
97
|
+
data-red-copy-text="<%= h rspec_cmd %>"
|
|
98
98
|
title="Copy RSpec request spec to clipboard">
|
|
99
99
|
<i class="bi bi-code-slash"></i> Copy as RSpec
|
|
100
100
|
</button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<script
|
|
2
|
-
function
|
|
1
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
2
|
+
window.downloadErrorJSON = function(event) {
|
|
3
3
|
const errorData = {
|
|
4
4
|
id: <%= raw error.id.to_json %>,
|
|
5
5
|
error_type: <%= raw error.error_type.to_json %>,
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
button.classList.remove('btn-success');
|
|
53
53
|
button.classList.add('btn-outline-secondary');
|
|
54
54
|
}, 2000);
|
|
55
|
-
}
|
|
55
|
+
};
|
|
56
56
|
|
|
57
57
|
// Section Navigation — pill bar with scroll spy
|
|
58
58
|
document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -526,7 +526,7 @@
|
|
|
526
526
|
<small class="metadata-label d-block mb-1">Error ID</small>
|
|
527
527
|
<div class="d-flex align-items-center gap-2">
|
|
528
528
|
<code><%= error.id %></code>
|
|
529
|
-
<button class="btn btn-sm btn-outline-secondary"
|
|
529
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= error.id %>" title="Copy error ID">
|
|
530
530
|
<i class="bi bi-clipboard"></i>
|
|
531
531
|
</button>
|
|
532
532
|
</div>
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
</td>
|
|
62
62
|
<% end %>
|
|
63
63
|
<td>
|
|
64
|
-
<%= link_to "View Errors", errors_path(user_id: user_data[:user_id]), class: "btn btn-sm btn-outline-primary" %>
|
|
64
|
+
<%= link_to "View Errors", errors_path(user_id: user_data[:user_id], unresolved: '0'), class: "btn btn-sm btn-outline-primary" %>
|
|
65
65
|
</td>
|
|
66
66
|
</tr>
|
|
67
67
|
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% content_for :page_title, "Analytics" %>
|
|
2
2
|
|
|
3
|
-
<script
|
|
3
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
4
4
|
// Dynamic chart colors based on theme
|
|
5
5
|
window.getChartColors = function() {
|
|
6
6
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
</div>
|
|
68
68
|
<div class="card-body">
|
|
69
69
|
<div id="errors-over-time-chart"></div>
|
|
70
|
-
<script
|
|
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
73
|
new Chartkick.LineChart("errors-over-time-chart", <%= raw @errors_over_time.to_json %>, {
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
</div>
|
|
113
113
|
<div class="card-body">
|
|
114
114
|
<div id="errors-by-platform-chart"></div>
|
|
115
|
-
<script
|
|
115
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
116
116
|
document.addEventListener('DOMContentLoaded', function() {
|
|
117
117
|
const colors = window.getChartColors();
|
|
118
118
|
// Distinct, accessible colors for 5 platforms with good contrast
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
<div class="d-flex gap-2 flex-wrap">
|
|
149
149
|
<small class="text-muted me-2">Quick Links:</small>
|
|
150
150
|
<% @errors_by_platform.keys.each do |platform| %>
|
|
151
|
-
<%= link_to platform, errors_path(platform: platform), class: "btn btn-sm btn-outline-secondary" %>
|
|
151
|
+
<%= link_to platform, errors_path(platform: platform, unresolved: '0'), class: "btn btn-sm btn-outline-secondary" %>
|
|
152
152
|
<% end %>
|
|
153
153
|
</div>
|
|
154
154
|
</div>
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
</div>
|
|
167
167
|
<div class="card-body">
|
|
168
168
|
<div id="errors-by-type-chart"></div>
|
|
169
|
-
<script
|
|
169
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
170
170
|
document.addEventListener('DOMContentLoaded', function() {
|
|
171
171
|
const colors = window.getChartColors();
|
|
172
172
|
new Chartkick.BarChart("errors-by-type-chart", <%= raw @errors_by_type.to_json %>, {
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
</div>
|
|
209
209
|
<div class="card-body">
|
|
210
210
|
<div id="errors-by-hour-chart"></div>
|
|
211
|
-
<script
|
|
211
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
212
212
|
document.addEventListener('DOMContentLoaded', function() {
|
|
213
213
|
const colors = window.getChartColors();
|
|
214
214
|
new Chartkick.ColumnChart("errors-by-hour-chart", <%= raw @errors_by_hour.to_json %>, {
|
|
@@ -304,7 +304,7 @@
|
|
|
304
304
|
</div>
|
|
305
305
|
</td>
|
|
306
306
|
<td>
|
|
307
|
-
<%= link_to "View Errors", errors_path(error_type: error_type), class: "btn btn-sm btn-outline-primary" %>
|
|
307
|
+
<%= link_to "View Errors", errors_path(error_type: error_type, unresolved: '0'), class: "btn btn-sm btn-outline-primary" %>
|
|
308
308
|
</td>
|
|
309
309
|
</tr>
|
|
310
310
|
<% end %>
|
|
@@ -410,7 +410,7 @@
|
|
|
410
410
|
</div>
|
|
411
411
|
<div class="card-body">
|
|
412
412
|
<div id="errors-by-version-chart"></div>
|
|
413
|
-
<script
|
|
413
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
414
414
|
document.addEventListener('DOMContentLoaded', function() {
|
|
415
415
|
const versionData = <%= raw @errors_by_version.transform_values { |v| v[:count] }.to_json %>;
|
|
416
416
|
const colors = window.getChartColors();
|
|
@@ -549,7 +549,7 @@
|
|
|
549
549
|
<div class="mt-4">
|
|
550
550
|
<h6 class="text-muted mb-3">MTTR by Platform</h6>
|
|
551
551
|
<div id="mttr-by-platform-chart"></div>
|
|
552
|
-
<script
|
|
552
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
553
553
|
document.addEventListener('DOMContentLoaded', function() {
|
|
554
554
|
const colors = window.getChartColors();
|
|
555
555
|
new Chartkick.BarChart("mttr-by-platform-chart",
|
|
@@ -587,7 +587,7 @@
|
|
|
587
587
|
</td>
|
|
588
588
|
<td><strong><%= hours %> hours</strong></td>
|
|
589
589
|
<td>
|
|
590
|
-
<%= link_to "View", errors_path(severity: severity), class: "btn btn-sm btn-outline-primary" %>
|
|
590
|
+
<%= link_to "View", errors_path(severity: severity, unresolved: '0'), class: "btn btn-sm btn-outline-primary" %>
|
|
591
591
|
</td>
|
|
592
592
|
</tr>
|
|
593
593
|
<% end %>
|
|
@@ -602,7 +602,7 @@
|
|
|
602
602
|
<div class="mt-4">
|
|
603
603
|
<h6 class="text-muted mb-3">MTTR Trend (Weekly)</h6>
|
|
604
604
|
<div id="mttr-trend-chart"></div>
|
|
605
|
-
<script
|
|
605
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
606
606
|
document.addEventListener('DOMContentLoaded', function() {
|
|
607
607
|
const colors = window.getChartColors();
|
|
608
608
|
new Chartkick.LineChart("mttr-trend-chart",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
<% end %>
|
|
106
106
|
</td>
|
|
107
107
|
<td>
|
|
108
|
-
<%= link_to "View", errors_path(search: release[:version]), class: "btn btn-sm btn-outline-primary" %>
|
|
108
|
+
<%= link_to "View", errors_path(search: release[:version], unresolved: '0'), class: "btn btn-sm btn-outline-primary" %>
|
|
109
109
|
</td>
|
|
110
110
|
</tr>
|
|
111
111
|
<% end %>
|
|
@@ -86,11 +86,11 @@
|
|
|
86
86
|
current_status = params[:status]
|
|
87
87
|
is_unresolved_only = params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
88
88
|
%>
|
|
89
|
-
<%= link_to errors_path(app_context), class: "btn filter-pill #{current_status.blank? && !is_unresolved_only ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>All<% end %>
|
|
89
|
+
<%= link_to errors_path(app_context.merge(unresolved: '0')), class: "btn filter-pill #{current_status.blank? && !is_unresolved_only ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>All<% end %>
|
|
90
90
|
<%= link_to errors_path(app_context.merge(unresolved: '1')), class: "btn filter-pill #{is_unresolved_only && current_status.blank? ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Unresolved <span style="font-size: 11px; opacity: 0.7;"><%= @stats[:unresolved] %></span><% end %>
|
|
91
91
|
<%= link_to errors_path(app_context.merge(status: 'resolved', unresolved: '0')), class: "btn filter-pill #{current_status == 'resolved' ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Resolved<% end %>
|
|
92
|
-
<%= link_to errors_path(app_context.merge(assigned_to: '__assigned__')), class: "btn filter-pill #{params[:assigned_to] == '__assigned__' ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Assigned<% end %>
|
|
93
|
-
<%= link_to errors_path(app_context.merge(reopened: 'true')), class: "btn filter-pill #{params[:reopened] == 'true' ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Reopened<% end %>
|
|
92
|
+
<%= link_to errors_path(app_context.merge(assigned_to: '__assigned__', unresolved: '0')), class: "btn filter-pill #{params[:assigned_to] == '__assigned__' ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Assigned<% end %>
|
|
93
|
+
<%= link_to errors_path(app_context.merge(reopened: 'true', unresolved: '0')), class: "btn filter-pill #{params[:reopened] == 'true' ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Reopened<% end %>
|
|
94
94
|
</div>
|
|
95
95
|
|
|
96
96
|
<div style="width: 1px; height: 24px; background: var(--border-primary);"></div>
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
|
|
107
107
|
<!-- Advanced filters toggle -->
|
|
108
108
|
<div style="width: 1px; height: 24px; background: var(--border-primary);"></div>
|
|
109
|
-
<button type="button"
|
|
109
|
+
<button type="button" data-red-action="toggle-display" data-red-target="#advanced-filters" data-red-display="flex" class="filter-pill" style="border: none;">
|
|
110
110
|
<i class="bi bi-sliders"></i> More filters
|
|
111
111
|
</button>
|
|
112
112
|
</div>
|
|
@@ -194,6 +194,23 @@
|
|
|
194
194
|
<!-- Error Table -->
|
|
195
195
|
<div class="card" style="overflow: hidden;">
|
|
196
196
|
<% if @errors.any? %>
|
|
197
|
+
<!-- Batch action toolbar (shown when one or more errors are selected). Sits above the table
|
|
198
|
+
so it doesn't disrupt the table's column widths the way a colspan thead row does. -->
|
|
199
|
+
<div id="batch-toolbar" style="display: none; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border-primary); background: var(--surface-secondary);">
|
|
200
|
+
<%= form_with url: batch_action_errors_path, method: :post, id: "batch-form", style: "display: flex; gap: 8px; align-items: center; flex-wrap: wrap;" do |f| %>
|
|
201
|
+
<% if params[:application_id].present? %>
|
|
202
|
+
<%= hidden_field_tag :application_id, params[:application_id] %>
|
|
203
|
+
<% end %>
|
|
204
|
+
<input type="checkbox" id="select-all-batch" class="form-check-input" style="accent-color: var(--accent); margin: 0;" checked>
|
|
205
|
+
<span id="selected-count" style="font-size: 13px; font-weight: 600; color: var(--text-primary); margin-right: var(--space-2);"></span>
|
|
206
|
+
<%= button_tag type: "submit", name: "action_type", value: "resolve", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px;" do %>
|
|
207
|
+
<i class="bi bi-check-circle"></i> Resolve
|
|
208
|
+
<% end %>
|
|
209
|
+
<%= 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: "Are you sure you want to delete the selected errors?" } do %>
|
|
210
|
+
<i class="bi bi-trash"></i> Delete
|
|
211
|
+
<% end %>
|
|
212
|
+
<% end %>
|
|
213
|
+
</div>
|
|
197
214
|
<div data-loading-target="content">
|
|
198
215
|
<div style="overflow-x: auto;">
|
|
199
216
|
<table class="table table-hover" style="margin-bottom: 0; width: 100%; table-layout: fixed;">
|
|
@@ -214,25 +231,6 @@
|
|
|
214
231
|
<th style="padding: var(--space-3) var(--space-4); width: 100px;">Platform</th>
|
|
215
232
|
<% end %>
|
|
216
233
|
</tr>
|
|
217
|
-
<tr id="thead-batch" style="display: none; border-bottom: 1px solid var(--border-primary); background: var(--surface-secondary);">
|
|
218
|
-
<th style="width: 40px; padding: var(--space-3) var(--space-4); text-align: center;">
|
|
219
|
-
<input type="checkbox" id="select-all-batch" class="form-check-input" style="accent-color: var(--accent);" checked>
|
|
220
|
-
</th>
|
|
221
|
-
<th colspan="99" style="padding: var(--space-3) var(--space-4);">
|
|
222
|
-
<%= form_with url: batch_action_errors_path, method: :post, id: "batch-form", style: "display: flex; gap: 8px; align-items: center;" do |f| %>
|
|
223
|
-
<% if params[:application_id].present? %>
|
|
224
|
-
<%= hidden_field_tag :application_id, params[:application_id] %>
|
|
225
|
-
<% end %>
|
|
226
|
-
<span id="selected-count" style="font-size: 13px; font-weight: 600; color: var(--text-primary);"></span>
|
|
227
|
-
<%= button_tag type: "submit", name: "action_type", value: "resolve", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px;" do %>
|
|
228
|
-
<i class="bi bi-check-circle"></i> Resolve
|
|
229
|
-
<% end %>
|
|
230
|
-
<%= 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: "Are you sure you want to delete the selected errors?" } do %>
|
|
231
|
-
<i class="bi bi-trash"></i> Delete
|
|
232
|
-
<% end %>
|
|
233
|
-
<% end %>
|
|
234
|
-
</th>
|
|
235
|
-
</tr>
|
|
236
234
|
</thead>
|
|
237
235
|
<tbody id="error_list">
|
|
238
236
|
<% @errors.each do |error| %>
|
|
@@ -276,12 +274,11 @@
|
|
|
276
274
|
</div>
|
|
277
275
|
</div>
|
|
278
276
|
|
|
279
|
-
|
|
277
|
+
<%= red_javascript_tag do %>
|
|
280
278
|
document.addEventListener('DOMContentLoaded', function() {
|
|
281
279
|
var selectAllCheckbox = document.getElementById('select-all');
|
|
282
280
|
var selectAllBatchCheckbox = document.getElementById('select-all-batch');
|
|
283
|
-
var
|
|
284
|
-
var theadBatch = document.getElementById('thead-batch');
|
|
281
|
+
var batchToolbar = document.getElementById('batch-toolbar');
|
|
285
282
|
var selectedCountSpan = document.getElementById('selected-count');
|
|
286
283
|
var batchForm = document.getElementById('batch-form');
|
|
287
284
|
|
|
@@ -297,14 +294,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
297
294
|
var someChecked = Array.from(boxes).some(function(cb) { return cb.checked; });
|
|
298
295
|
|
|
299
296
|
if (count > 0) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
297
|
+
if (batchToolbar) batchToolbar.style.display = '';
|
|
298
|
+
if (selectedCountSpan) selectedCountSpan.textContent = count + ' selected';
|
|
299
|
+
if (selectAllBatchCheckbox) {
|
|
300
|
+
selectAllBatchCheckbox.checked = allChecked;
|
|
301
|
+
selectAllBatchCheckbox.indeterminate = someChecked && !allChecked;
|
|
302
|
+
}
|
|
305
303
|
} else {
|
|
306
|
-
|
|
307
|
-
theadBatch.style.display = 'none';
|
|
304
|
+
if (batchToolbar) batchToolbar.style.display = 'none';
|
|
308
305
|
}
|
|
309
306
|
|
|
310
307
|
if (selectAllCheckbox) {
|
|
@@ -394,4 +391,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
394
391
|
});
|
|
395
392
|
}
|
|
396
393
|
});
|
|
397
|
-
|
|
394
|
+
<% end %>
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
</div>
|
|
89
89
|
</div>
|
|
90
90
|
<div class="card-footer border-top">
|
|
91
|
-
<%= link_to "View #{platform} Errors", errors_path(platform: platform), class: "btn btn-sm btn-outline-primary w-100" %>
|
|
91
|
+
<%= link_to "View #{platform} Errors", errors_path(platform: platform, unresolved: '0'), class: "btn btn-sm btn-outline-primary w-100" %>
|
|
92
92
|
</div>
|
|
93
93
|
</div>
|
|
94
94
|
</div>
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
<% end %>
|
|
291
291
|
</div>
|
|
292
292
|
|
|
293
|
-
<script
|
|
293
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
294
294
|
// Platform color mapping - consistent with analytics page
|
|
295
295
|
const platformColorMap = {
|
|
296
296
|
'api': '#2563EB', // Blue
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
<!-- Dynamically render setting groups -->
|
|
182
182
|
<% setting_groups.each do |group_name, group_config| %>
|
|
183
183
|
<div class="card" style="margin-bottom: var(--space-4);">
|
|
184
|
-
<button
|
|
184
|
+
<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);">
|
|
185
185
|
<span style="font-size: 14px; font-weight: 600; color: var(--text-primary);"><i class="<%= group_config[:icon] %>" style="margin-right: 6px;"></i> <%= group_name %></span>
|
|
186
186
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
187
187
|
<span style="font-size: 11px; color: var(--text-tertiary);"><%= group_config[:settings].size %> options</span>
|
|
@@ -270,7 +270,7 @@
|
|
|
270
270
|
The test error is clearly marked and safe to resolve or delete.
|
|
271
271
|
</p>
|
|
272
272
|
<%= form_tag test_error_errors_path(application_id: @current_application_id.presence), method: :post, style: "display: inline;" do %>
|
|
273
|
-
<button type="submit" class="btn btn-outline"
|
|
273
|
+
<button type="submit" class="btn btn-outline" data-red-action="confirm-submit" data-red-confirm-message="This will create a test error and send notifications to all configured channels. Continue?">
|
|
274
274
|
<i class="bi bi-send" style="margin-right: 4px;"></i> Send Test Error
|
|
275
275
|
</button>
|
|
276
276
|
<% end %>
|
|
@@ -77,10 +77,10 @@
|
|
|
77
77
|
<button type="button" class="btn" data-bs-toggle="modal" data-bs-target="#assignModal" id="hero-assign-btn">
|
|
78
78
|
Assign
|
|
79
79
|
</button>
|
|
80
|
-
<button type="button" class="btn"
|
|
80
|
+
<button type="button" class="btn" data-red-action="copy-markdown" data-markdown="<%= j @error_markdown %>">
|
|
81
81
|
<i class="bi bi-clipboard" style="margin-right: 4px;"></i>Copy for LLM
|
|
82
82
|
</button>
|
|
83
|
-
<button type="button" class="btn"
|
|
83
|
+
<button type="button" class="btn" data-red-action="download-json" title="Export JSON">
|
|
84
84
|
<i class="bi bi-download"></i>
|
|
85
85
|
</button>
|
|
86
86
|
</div>
|
|
@@ -92,17 +92,17 @@
|
|
|
92
92
|
<div>
|
|
93
93
|
<!-- Tab bar -->
|
|
94
94
|
<div style="display: flex; gap: 0; border-bottom: 1px solid var(--border-primary); margin-bottom: var(--space-4);">
|
|
95
|
-
<button
|
|
95
|
+
<button type="button" data-red-action="switch-tab" data-red-tab="details" class="red-tab active" id="tab-details" style="padding: 10px 16px; font-size: 13px; font-weight: 600; color: var(--accent); background: none; border: none; border-bottom: 2px solid var(--accent); margin-bottom: -1px; cursor: pointer;">
|
|
96
96
|
<i class="bi bi-code-slash" style="margin-right: 6px;"></i>Details
|
|
97
97
|
</button>
|
|
98
|
-
<button
|
|
98
|
+
<button type="button" data-red-action="switch-tab" data-red-tab="context" class="red-tab" id="tab-context" style="padding: 10px 16px; font-size: 13px; font-weight: 400; color: var(--text-secondary); background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer;">
|
|
99
99
|
<i class="bi bi-layers" style="margin-right: 6px;"></i>Context
|
|
100
100
|
</button>
|
|
101
|
-
<button
|
|
101
|
+
<button type="button" data-red-action="switch-tab" data-red-tab="history" class="red-tab" id="tab-history" style="padding: 10px 16px; font-size: 13px; font-weight: 400; color: var(--text-secondary); background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer;">
|
|
102
102
|
<i class="bi bi-clock-history" style="margin-right: 6px;"></i>History
|
|
103
103
|
</button>
|
|
104
104
|
<% if RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
105
|
-
<button
|
|
105
|
+
<button type="button" data-red-action="switch-tab" data-red-tab="issues" class="red-tab" id="tab-issues" style="padding: 10px 16px; font-size: 13px; font-weight: 400; color: var(--text-secondary); background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer;">
|
|
106
106
|
<i class="bi bi-github" style="margin-right: 6px;"></i>Issues
|
|
107
107
|
</button>
|
|
108
108
|
<% end %>
|
|
@@ -203,15 +203,15 @@
|
|
|
203
203
|
<span style="font-size: 13px; font-weight: 600;">Quick Actions</span>
|
|
204
204
|
</div>
|
|
205
205
|
<div style="padding: var(--space-3) var(--space-5); display: flex; flex-direction: column; gap: var(--space-2);">
|
|
206
|
-
<%= link_to errors_path(app_context.merge(error_type: @error.error_type)), class: "btn btn-sm" do %>
|
|
206
|
+
<%= link_to errors_path(app_context.merge(error_type: @error.error_type, unresolved: '0')), class: "btn btn-sm" do %>
|
|
207
207
|
<i class="bi bi-filter"></i> View Similar Errors
|
|
208
208
|
<% end %>
|
|
209
209
|
<% if @error.user_id %>
|
|
210
|
-
<%= link_to errors_path(app_context.merge(user_id: @error.user_id)), class: "btn btn-sm" do %>
|
|
210
|
+
<%= link_to errors_path(app_context.merge(user_id: @error.user_id, unresolved: '0')), class: "btn btn-sm" do %>
|
|
211
211
|
<i class="bi bi-person"></i> View User's Errors
|
|
212
212
|
<% end %>
|
|
213
213
|
<% end %>
|
|
214
|
-
<%= link_to errors_path(app_context.merge(platform: @error.platform)), class: "btn btn-sm" do %>
|
|
214
|
+
<%= link_to errors_path(app_context.merge(platform: @error.platform, unresolved: '0')), class: "btn btn-sm" do %>
|
|
215
215
|
<i class="bi bi-phone"></i> View <%= @error.platform %> Errors
|
|
216
216
|
<% end %>
|
|
217
217
|
<%= link_to analytics_errors_path(app_context), class: "btn btn-sm" do %>
|
|
@@ -225,8 +225,8 @@
|
|
|
225
225
|
|
|
226
226
|
<%= render "modals", error: @error %>
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
function
|
|
228
|
+
<%= red_javascript_tag do %>
|
|
229
|
+
window.switchTab = function(tabId) {
|
|
230
230
|
// Deactivate all tabs
|
|
231
231
|
document.querySelectorAll('.red-tab').forEach(function(el) {
|
|
232
232
|
el.style.color = 'var(--text-secondary)';
|
|
@@ -243,5 +243,5 @@ function switchTab(tabId) {
|
|
|
243
243
|
// Scroll to selected panel
|
|
244
244
|
var panel = document.getElementById('panel-' + tabId);
|
|
245
245
|
if (panel) { panel.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
|
246
|
-
}
|
|
247
|
-
|
|
246
|
+
};
|
|
247
|
+
<% end %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Backfill `status` column for errors that were bulk-resolved before v0.6.3.
|
|
4
|
+
#
|
|
5
|
+
# Versions 0.6.0 through 0.6.2 had a bug in BatchResolveErrors that set
|
|
6
|
+
# `resolved: true` and `resolved_at` but skipped the `status` column. The
|
|
7
|
+
# errors-index "Resolved" filter pill queries `where(status: 'resolved')`,
|
|
8
|
+
# so bulk-resolved errors silently disappeared from that view even though
|
|
9
|
+
# they were marked resolved.
|
|
10
|
+
#
|
|
11
|
+
# This migration is idempotent: it only updates rows that are out-of-sync
|
|
12
|
+
# (resolved but not status='resolved'). Running it twice is a no-op.
|
|
13
|
+
#
|
|
14
|
+
# See: https://github.com/AnjanJ/rails_error_dashboard
|
|
15
|
+
class BackfillResolvedStatus < ActiveRecord::Migration[7.0]
|
|
16
|
+
def up
|
|
17
|
+
return unless table_exists?(:rails_error_dashboard_error_logs)
|
|
18
|
+
return unless column_exists?(:rails_error_dashboard_error_logs, :status)
|
|
19
|
+
return unless column_exists?(:rails_error_dashboard_error_logs, :resolved)
|
|
20
|
+
|
|
21
|
+
# Use ActiveRecord update_all so the count is portable across adapters
|
|
22
|
+
# (PostgreSQL, MySQL, SQLite all return the affected row count).
|
|
23
|
+
table = ActiveRecord::Base.connection.quote_table_name("rails_error_dashboard_error_logs")
|
|
24
|
+
klass = Class.new(ActiveRecord::Base) { self.table_name = "rails_error_dashboard_error_logs" }
|
|
25
|
+
updated = klass.where(resolved: true).where("status IS NULL OR status != ?", "resolved")
|
|
26
|
+
.update_all(status: "resolved")
|
|
27
|
+
|
|
28
|
+
say "Backfilled status='resolved' on #{updated} error log(s) that were bulk-resolved on v0.6.0–v0.6.2."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def down
|
|
32
|
+
# No-op: we cannot reliably distinguish errors that were bulk-resolved
|
|
33
|
+
# before the fix from errors that are legitimately resolved now. Leaving
|
|
34
|
+
# status='resolved' is the safe choice on rollback.
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -4,13 +4,14 @@ module RailsErrorDashboard
|
|
|
4
4
|
module Commands
|
|
5
5
|
# Command: Mute multiple errors at once
|
|
6
6
|
class BatchMuteErrors
|
|
7
|
-
def self.call(error_ids, muted_by: nil)
|
|
8
|
-
new(error_ids, muted_by).call
|
|
7
|
+
def self.call(error_ids, muted_by: nil, reason: nil)
|
|
8
|
+
new(error_ids, muted_by, reason).call
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def initialize(error_ids, muted_by = nil)
|
|
11
|
+
def initialize(error_ids, muted_by = nil, reason = nil)
|
|
12
12
|
@error_ids = Array(error_ids).compact
|
|
13
13
|
@muted_by = muted_by
|
|
14
|
+
@reason = reason
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def call
|
|
@@ -27,7 +28,8 @@ module RailsErrorDashboard
|
|
|
27
28
|
error.update!(
|
|
28
29
|
muted: true,
|
|
29
30
|
muted_at: Time.current,
|
|
30
|
-
muted_by: @muted_by
|
|
31
|
+
muted_by: @muted_by,
|
|
32
|
+
muted_reason: @reason
|
|
31
33
|
)
|
|
32
34
|
muted_count += 1
|
|
33
35
|
muted_errors << error
|
|
@@ -31,7 +31,8 @@ module RailsErrorDashboard
|
|
|
31
31
|
resolved: true,
|
|
32
32
|
resolved_at: Time.current,
|
|
33
33
|
resolved_by_name: @resolved_by_name,
|
|
34
|
-
resolution_comment: @resolution_comment
|
|
34
|
+
resolution_comment: @resolution_comment,
|
|
35
|
+
status: "resolved"
|
|
35
36
|
)
|
|
36
37
|
resolved_count += 1
|
|
37
38
|
resolved_errors << error
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_error_dashboard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -355,6 +355,7 @@ files:
|
|
|
355
355
|
- db/migrate/20260323000001_add_muted_to_error_logs.rb
|
|
356
356
|
- db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb
|
|
357
357
|
- db/migrate/20260326000001_add_issue_tracking_to_error_logs.rb
|
|
358
|
+
- db/migrate/20260503000001_backfill_resolved_status.rb
|
|
358
359
|
- lib/generators/rails_error_dashboard/install/install_generator.rb
|
|
359
360
|
- lib/generators/rails_error_dashboard/install/templates/README
|
|
360
361
|
- lib/generators/rails_error_dashboard/install/templates/initializer.rb
|
|
@@ -497,7 +498,7 @@ metadata:
|
|
|
497
498
|
funding_uri: https://github.com/sponsors/AnjanJ
|
|
498
499
|
post_install_message: |
|
|
499
500
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
500
|
-
RED (Rails Error Dashboard) v0.6.
|
|
501
|
+
RED (Rails Error Dashboard) v0.6.3
|
|
501
502
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
502
503
|
|
|
503
504
|
First install:
|