rails_vitals 0.2.0 → 0.3.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 +6 -0
- data/app/assets/javascripts/rails_vitals/application.js +161 -0
- data/app/assets/stylesheets/rails_vitals/application.css +175 -0
- data/app/controllers/rails_vitals/explains_controller.rb +16 -0
- data/app/controllers/rails_vitals/n_plus_ones_controller.rb +0 -1
- data/app/controllers/rails_vitals/requests_controller.rb +1 -1
- data/app/helpers/rails_vitals/application_helper.rb +28 -0
- data/app/views/layouts/rails_vitals/application.html.erb +1 -0
- data/app/views/rails_vitals/associations/index.html.erb +41 -189
- data/app/views/rails_vitals/dashboard/index.html.erb +6 -6
- data/app/views/rails_vitals/explains/_plan_node.html.erb +137 -0
- data/app/views/rails_vitals/explains/show.html.erb +186 -0
- data/app/views/rails_vitals/heatmap/index.html.erb +7 -7
- data/app/views/rails_vitals/models/index.html.erb +19 -36
- data/app/views/rails_vitals/n_plus_ones/index.html.erb +9 -9
- data/app/views/rails_vitals/n_plus_ones/show.html.erb +30 -76
- data/app/views/rails_vitals/requests/index.html.erb +5 -5
- data/app/views/rails_vitals/requests/show.html.erb +82 -165
- data/config/routes.rb +1 -0
- data/lib/rails_vitals/analyzers/explain_analyzer.rb +347 -0
- data/lib/rails_vitals/collector.rb +2 -1
- data/lib/rails_vitals/notifications/subscriber.rb +2 -1
- data/lib/rails_vitals/version.rb +1 -1
- data/lib/rails_vitals.rb +1 -0
- metadata +8 -9
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.plan-node {
|
|
3
|
+
border-left: 2px solid #4a5568;
|
|
4
|
+
margin-left: 16px;
|
|
5
|
+
padding-left: 16px;
|
|
6
|
+
margin-top: 8px;
|
|
7
|
+
}
|
|
8
|
+
.plan-node:first-child {
|
|
9
|
+
margin-left: 0;
|
|
10
|
+
padding-left: 0;
|
|
11
|
+
border-left: none;
|
|
12
|
+
}
|
|
13
|
+
.node-box {
|
|
14
|
+
border-radius: 6px;
|
|
15
|
+
padding: 12px 16px;
|
|
16
|
+
margin-bottom: 4px;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
transition: opacity 0.15s;
|
|
19
|
+
}
|
|
20
|
+
.node-box:hover { opacity: 0.85; }
|
|
21
|
+
</style>
|
|
22
|
+
|
|
23
|
+
<%# Header %>
|
|
24
|
+
<div class="mb-20">
|
|
25
|
+
<% if @record %>
|
|
26
|
+
<a href="<%= rails_vitals.request_path(@record.id) %>" class="text-blue" style="font-size:13px;">
|
|
27
|
+
← Back to Request Detail
|
|
28
|
+
</a>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<h2 class="page-heading" style="margin:8px 0;">
|
|
32
|
+
EXPLAIN Visualizer
|
|
33
|
+
</h2>
|
|
34
|
+
|
|
35
|
+
<div class="mono text-grey text-sm word-break">
|
|
36
|
+
<%= @sql %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<% if @result.error %>
|
|
41
|
+
<div
|
|
42
|
+
class="text-danger"
|
|
43
|
+
style="background:#2d1515;border:1px solid #fc818166;border-radius:6px;padding:16px;font-size:13px;"
|
|
44
|
+
>
|
|
45
|
+
<%= @result.error %>
|
|
46
|
+
</div>
|
|
47
|
+
<% else %>
|
|
48
|
+
<%# Summary stats row %>
|
|
49
|
+
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:24px;">
|
|
50
|
+
<% stats = [
|
|
51
|
+
{ label: "Total Cost", value: @result.total_cost, color: cost_color(@result.total_cost) },
|
|
52
|
+
{ label: "Actual Time", value: "#{@result.actual_time_ms}ms", color: time_color(@result.actual_time_ms) },
|
|
53
|
+
{ label: "Rows Examined", value: @result.rows_examined, color: rows_color(@result.rows_examined) },
|
|
54
|
+
{ label: "Warnings", value: @result.warnings.size, color: @result.warnings.any? ? "#fc8181" : "#68d391" },
|
|
55
|
+
{ label: "Row Width", value: "#{@result.plan&.plan_width}B", color: @result.plan&.plan_width.to_i > 200 ? "#f6ad55" : "#68d391" }
|
|
56
|
+
] %>
|
|
57
|
+
|
|
58
|
+
<% stats.each do |s| %>
|
|
59
|
+
<div class="stat-card-dark">
|
|
60
|
+
<div class="bold mono text-24" style="color:<%= s[:color] %>;">
|
|
61
|
+
<%= s[:value] %>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="text-muted text-sm mt-4">
|
|
64
|
+
<%= s[:label] %>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<%# Warnings %>
|
|
71
|
+
<% if @result.warnings.any? %>
|
|
72
|
+
<div class="card mb-20">
|
|
73
|
+
<div class="card-title text-danger">
|
|
74
|
+
⚠ Warnings (<%= @result.warnings.size %>)
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<% @result.warnings.each do |w| %>
|
|
78
|
+
<% severity_color = w[:severity] == :danger ? "#fc8181" : "#f6ad55" %>
|
|
79
|
+
|
|
80
|
+
<div
|
|
81
|
+
style="background:#1a202c;border-left:3px solid <%= severity_color %>;
|
|
82
|
+
border-radius:4px;padding:12px 16px;margin-bottom:8px;font-size:13px;"
|
|
83
|
+
>
|
|
84
|
+
<% case w[:type] %>
|
|
85
|
+
<% when :sequential_scan %>
|
|
86
|
+
<div class="bold mb-4" style="color:<%= severity_color %>;">
|
|
87
|
+
Sequential Scan on
|
|
88
|
+
<span class="mono"><%= w[:table] %></span>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="text-muted">
|
|
91
|
+
Scanned <%= (w[:rows].to_i + w[:removed].to_i).to_s %> rows, returned
|
|
92
|
+
<%= w[:rows] %>, removed <%= w[:removed] || 0 %> by filter.
|
|
93
|
+
<% if w[:filter] %>
|
|
94
|
+
<span class="mono text-grey text-sm">Filter: <%= w[:filter] %></span>
|
|
95
|
+
<% end %>
|
|
96
|
+
</div>
|
|
97
|
+
<% when :sort_without_index %>
|
|
98
|
+
<div class="bold mb-4" style="color:<%= severity_color %>;">Sort without index</div>
|
|
99
|
+
<div class="text-muted">
|
|
100
|
+
PostgreSQL sorted rows in memory — ORDER BY column may lack an index.
|
|
101
|
+
</div>
|
|
102
|
+
<% when :large_nested_loop %>
|
|
103
|
+
<div class="bold mb-4" style="color:<%= severity_color %>;">
|
|
104
|
+
Large Nested Loop (<%= w[:rows] %> rows)
|
|
105
|
+
</div>
|
|
106
|
+
<div class="text-muted">
|
|
107
|
+
Nested Loop processed a large number of rows. Verify join columns are indexed.
|
|
108
|
+
</div>
|
|
109
|
+
<% end %>
|
|
110
|
+
</div>
|
|
111
|
+
<% end %>
|
|
112
|
+
</div>
|
|
113
|
+
<% end %>
|
|
114
|
+
|
|
115
|
+
<%# Fix suggestions %>
|
|
116
|
+
<% if @result.suggestions.any? %>
|
|
117
|
+
<div class="card mb-20">
|
|
118
|
+
<div class="card-title text-healthy">💡 Fix Suggestions</div>
|
|
119
|
+
|
|
120
|
+
<% @result.suggestions.each do |s| %>
|
|
121
|
+
<% s_color = s[:severity] == :danger ? "#fc8181" : "#f6ad55" %>
|
|
122
|
+
|
|
123
|
+
<div class="info-block mb-12">
|
|
124
|
+
<div class="bold mb-8" style="color:<%= s_color %>;font-size:13px;">
|
|
125
|
+
<%= s[:title] %>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="text-muted mb-12 line-relaxed" style="font-size:13px;">
|
|
129
|
+
<%= s[:body] %>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<% if s[:migration] %>
|
|
133
|
+
<div class="mb-6">
|
|
134
|
+
<div class="mini-label">Migration</div>
|
|
135
|
+
<div class="code-block-dark text-healthy"><%= s[:migration] %></div>
|
|
136
|
+
</div>
|
|
137
|
+
<% end %>
|
|
138
|
+
|
|
139
|
+
<% if s[:command] %>
|
|
140
|
+
<div>
|
|
141
|
+
<div class="mini-label">Generate</div>
|
|
142
|
+
<div class="code-block-dark text-blue">$ <%= s[:command] %></div>
|
|
143
|
+
</div>
|
|
144
|
+
<% end %>
|
|
145
|
+
</div>
|
|
146
|
+
<% end %>
|
|
147
|
+
</div>
|
|
148
|
+
<% end %>
|
|
149
|
+
|
|
150
|
+
<%# Interpretation summary %>
|
|
151
|
+
<% if @result.interpretation %>
|
|
152
|
+
<div
|
|
153
|
+
class="mb-16 line-relaxed"
|
|
154
|
+
style="
|
|
155
|
+
background:#1a202c;
|
|
156
|
+
border-radius:6px;
|
|
157
|
+
padding:12px 16px;
|
|
158
|
+
font-size:13px;
|
|
159
|
+
color:<%= @result.warnings.any? ? '#f6ad55' : '#68d391' %>;
|
|
160
|
+
border-left:3px solid <%= @result.warnings.any? ? '#f6ad55' : '#68d391' %>;
|
|
161
|
+
"
|
|
162
|
+
>
|
|
163
|
+
<%= @result.interpretation %>
|
|
164
|
+
</div>
|
|
165
|
+
<% end %>
|
|
166
|
+
|
|
167
|
+
<%# Plan tree %>
|
|
168
|
+
<div class="card">
|
|
169
|
+
<div class="card-title">
|
|
170
|
+
Execution Plan
|
|
171
|
+
<span class="text-muted text-sm ml-8" style="font-weight:normal;">click any node to expand explanation</span>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div style="padding:8px 0;">
|
|
175
|
+
<%= render partial: "rails_vitals/explains/plan_node",
|
|
176
|
+
locals: { node: @result.plan, depth: 0 } %>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<% end %>
|
|
180
|
+
|
|
181
|
+
<%# Push current URL into history so back-nav reuses the cached EXPLAIN result %>
|
|
182
|
+
<script>
|
|
183
|
+
if (window.history && window.history.replaceState) {
|
|
184
|
+
window.history.replaceState(null, "", window.location.href);
|
|
185
|
+
}
|
|
186
|
+
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<h1
|
|
3
|
-
<p
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h1 class="page-heading">Endpoint Heatmap</h1>
|
|
3
|
+
<p class="page-subtitle">
|
|
4
4
|
<%= @total %> requests recorded across <%= @heatmap.size %> endpoints
|
|
5
5
|
</p>
|
|
6
6
|
</div>
|
|
@@ -23,17 +23,17 @@
|
|
|
23
23
|
<% @heatmap.each do |row| %>
|
|
24
24
|
<% color = score_label_to_color(row[:avg_score]) %>
|
|
25
25
|
<tr>
|
|
26
|
-
<td
|
|
26
|
+
<td class="bold">
|
|
27
27
|
<%= link_to row[:endpoint],
|
|
28
28
|
requests_path(endpoint: row[:endpoint]),
|
|
29
|
-
|
|
29
|
+
class: "text-accent" %>
|
|
30
30
|
</td>
|
|
31
31
|
<td>
|
|
32
32
|
<span class="badge badge-<%= color %>">
|
|
33
33
|
<%= row[:avg_score] %>
|
|
34
34
|
</span>
|
|
35
35
|
</td>
|
|
36
|
-
<td
|
|
36
|
+
<td class="text-muted"><%= row[:hits] %></td>
|
|
37
37
|
<td>
|
|
38
38
|
<span style="color:<%= query_heat_color(row[:avg_queries]) %>">
|
|
39
39
|
<%= row[:avg_queries] %>
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
</table>
|
|
61
61
|
</div>
|
|
62
62
|
<% else %>
|
|
63
|
-
<div class="card
|
|
63
|
+
<div class="card empty-state">
|
|
64
64
|
No requests recorded yet. Visit your app to generate data.
|
|
65
65
|
</div>
|
|
66
66
|
<% end %>
|
|
@@ -1,42 +1,35 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<h1
|
|
3
|
-
<p
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h1 class="page-heading">Per-Model Breakdown</h1>
|
|
3
|
+
<p class="page-subtitle">
|
|
4
4
|
Query activity aggregated across <%= @total_requests %> recorded requests
|
|
5
5
|
</p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
<% if @breakdown.any? %>
|
|
9
9
|
<% @breakdown.each do |row| %>
|
|
10
|
-
<div class="card
|
|
10
|
+
<div class="card mb-16">
|
|
11
11
|
|
|
12
12
|
<%# Model header %>
|
|
13
|
-
<div
|
|
14
|
-
|
|
15
|
-
justify-content:space-between;
|
|
16
|
-
align-items:center;
|
|
17
|
-
margin-bottom:16px;
|
|
18
|
-
padding-bottom:12px;
|
|
19
|
-
border-bottom:1px solid #2d3748;
|
|
20
|
-
">
|
|
21
|
-
<span style="font-size:16px;font-weight:bold;color:#90cdf4;">
|
|
13
|
+
<div class="flex-between mb-16 section-divider">
|
|
14
|
+
<span class="text-accent bold text-16">
|
|
22
15
|
<%= row[:model] %>
|
|
23
16
|
</span>
|
|
24
|
-
<div style="
|
|
17
|
+
<div class="flex" style="gap:24px;">
|
|
25
18
|
<span>
|
|
26
|
-
<span
|
|
27
|
-
<span
|
|
19
|
+
<span class="text-muted">Queries</span>
|
|
20
|
+
<span class="text-primary bold ml-6">
|
|
28
21
|
<%= row[:query_count] %>
|
|
29
22
|
</span>
|
|
30
23
|
</span>
|
|
31
24
|
<span>
|
|
32
|
-
<span
|
|
33
|
-
<span style="color:<%= time_heat_color(row[:total_time_ms]) %>;
|
|
25
|
+
<span class="text-muted">Total DB Time</span>
|
|
26
|
+
<span class="bold ml-6" style="color:<%= time_heat_color(row[:total_time_ms]) %>;">
|
|
34
27
|
<%= row[:total_time_ms] %>ms
|
|
35
28
|
</span>
|
|
36
29
|
</span>
|
|
37
30
|
<span>
|
|
38
|
-
<span
|
|
39
|
-
<span style="color:<%= time_heat_color(row[:avg_time_ms]) %>;
|
|
31
|
+
<span class="text-muted">Avg Query Time</span>
|
|
32
|
+
<span class="bold ml-6" style="color:<%= time_heat_color(row[:avg_time_ms]) %>;">
|
|
40
33
|
<%= row[:avg_time_ms] %>ms
|
|
41
34
|
</span>
|
|
42
35
|
</span>
|
|
@@ -47,9 +40,7 @@
|
|
|
47
40
|
|
|
48
41
|
<%# Endpoints that triggered this model %>
|
|
49
42
|
<div>
|
|
50
|
-
<div
|
|
51
|
-
Triggered By
|
|
52
|
-
</div>
|
|
43
|
+
<div class="section-label">Triggered By</div>
|
|
53
44
|
<table>
|
|
54
45
|
<thead>
|
|
55
46
|
<tr>
|
|
@@ -63,9 +54,9 @@
|
|
|
63
54
|
<td>
|
|
64
55
|
<%= link_to endpoint,
|
|
65
56
|
requests_path(endpoint: endpoint),
|
|
66
|
-
|
|
57
|
+
class: "text-accent" %>
|
|
67
58
|
</td>
|
|
68
|
-
<td
|
|
59
|
+
<td class="text-muted"><%= count %></td>
|
|
69
60
|
</tr>
|
|
70
61
|
<% end %>
|
|
71
62
|
</tbody>
|
|
@@ -75,9 +66,7 @@
|
|
|
75
66
|
<%# Callback types — only if present %>
|
|
76
67
|
<% if row[:callbacks].any? %>
|
|
77
68
|
<div>
|
|
78
|
-
<div
|
|
79
|
-
Callbacks
|
|
80
|
-
</div>
|
|
69
|
+
<div class="section-label">Callbacks</div>
|
|
81
70
|
<table>
|
|
82
71
|
<thead>
|
|
83
72
|
<tr>
|
|
@@ -89,13 +78,7 @@
|
|
|
89
78
|
<% row[:callbacks].each do |type, count| %>
|
|
90
79
|
<tr>
|
|
91
80
|
<td>
|
|
92
|
-
<span style="
|
|
93
|
-
background:<%= callback_color(type) %>;
|
|
94
|
-
color:#fff;
|
|
95
|
-
padding:1px 6px;
|
|
96
|
-
border-radius:3px;
|
|
97
|
-
font-size:11px;
|
|
98
|
-
">
|
|
81
|
+
<span class="callback-badge" style="background:<%= callback_color(type) %>;">
|
|
99
82
|
<%= type %>
|
|
100
83
|
</span>
|
|
101
84
|
</td>
|
|
@@ -111,7 +94,7 @@
|
|
|
111
94
|
</div>
|
|
112
95
|
<% end %>
|
|
113
96
|
<% else %>
|
|
114
|
-
<div class="card
|
|
97
|
+
<div class="card empty-state">
|
|
115
98
|
No data recorded yet. Visit your app to generate data.
|
|
116
99
|
</div>
|
|
117
100
|
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<h1
|
|
3
|
-
<p
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h1 class="page-heading">N+1 Patterns</h1>
|
|
3
|
+
<p class="page-subtitle">
|
|
4
4
|
Aggregated across <%= @total_requests %> recorded requests
|
|
5
5
|
</p>
|
|
6
6
|
</div>
|
|
@@ -20,22 +20,22 @@
|
|
|
20
20
|
<tbody>
|
|
21
21
|
<% @patterns.each do |p| %>
|
|
22
22
|
<tr>
|
|
23
|
-
<td
|
|
23
|
+
<td class="mono text-sm text-muted truncate" style="max-width:300px;">
|
|
24
24
|
<%= p[:pattern] %>
|
|
25
25
|
</td>
|
|
26
26
|
<td>
|
|
27
|
-
<span
|
|
27
|
+
<span class="text-danger bold"><%= p[:occurrences] %>x</span>
|
|
28
28
|
</td>
|
|
29
|
-
<td
|
|
29
|
+
<td class="text-muted">
|
|
30
30
|
<%= p[:endpoints].keys.join(", ") %>
|
|
31
31
|
</td>
|
|
32
|
-
<td
|
|
32
|
+
<td class="mono text-sm text-healthy">
|
|
33
33
|
<%= p[:fix_suggestion][:code] %>
|
|
34
34
|
</td>
|
|
35
35
|
<td>
|
|
36
36
|
<%= link_to "Simulate →",
|
|
37
37
|
n_plus_one_path(pattern_id(p)),
|
|
38
|
-
|
|
38
|
+
class: "text-accent" %>
|
|
39
39
|
</td>
|
|
40
40
|
</tr>
|
|
41
41
|
<% end %>
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
</table>
|
|
44
44
|
</div>
|
|
45
45
|
<% else %>
|
|
46
|
-
<div class="card
|
|
46
|
+
<div class="card empty-state">
|
|
47
47
|
No N+1 patterns detected yet.
|
|
48
48
|
</div>
|
|
49
49
|
<% end %>
|
|
@@ -1,112 +1,67 @@
|
|
|
1
1
|
<%# app/views/rails_vitals/n_plus_one/show.html.erb %>
|
|
2
|
-
<div
|
|
3
|
-
<div
|
|
4
|
-
<%= link_to "← N+1 Patterns", n_plus_ones_path,
|
|
5
|
-
style: "color:#a0aec0;font-size:13px;" %>
|
|
2
|
+
<div class="page-header">
|
|
3
|
+
<div class="mb-8">
|
|
4
|
+
<%= link_to "← N+1 Patterns", n_plus_ones_path, class: "back-link" %>
|
|
6
5
|
</div>
|
|
7
|
-
<h1
|
|
6
|
+
<h1 class="page-heading">Impact Simulator</h1>
|
|
8
7
|
</div>
|
|
9
8
|
|
|
10
9
|
<%# Pattern card %>
|
|
11
|
-
<div class="card
|
|
10
|
+
<div class="card mb-16">
|
|
12
11
|
<div class="card-title">Detected Pattern</div>
|
|
13
|
-
<pre
|
|
14
|
-
background:#1a202c;
|
|
15
|
-
padding:12px;
|
|
16
|
-
border-radius:6px;
|
|
17
|
-
font-size:12px;
|
|
18
|
-
color:#a0aec0;
|
|
19
|
-
white-space:pre-wrap;
|
|
20
|
-
word-break:break-all;
|
|
21
|
-
margin-bottom:16px;
|
|
22
|
-
"><%= @pattern[:pattern] %></pre>
|
|
12
|
+
<pre class="code-block text-muted mb-16"><%= @pattern[:pattern] %></pre>
|
|
23
13
|
|
|
24
14
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;font-size:13px;">
|
|
25
15
|
<div>
|
|
26
|
-
<div
|
|
27
|
-
<div
|
|
16
|
+
<div class="text-muted mb-4">Total Occurrences</div>
|
|
17
|
+
<div class="text-danger stat-value-lg"><%= @pattern[:occurrences] %>x</div>
|
|
28
18
|
</div>
|
|
29
19
|
<div>
|
|
30
|
-
<div
|
|
31
|
-
<div
|
|
20
|
+
<div class="text-muted mb-4">Estimated Time Wasted</div>
|
|
21
|
+
<div class="text-danger stat-value-lg"><%= @estimated_saving_ms %>ms</div>
|
|
32
22
|
</div>
|
|
33
23
|
<div>
|
|
34
|
-
<div
|
|
35
|
-
<div
|
|
24
|
+
<div class="text-muted mb-4">Affected Requests</div>
|
|
25
|
+
<div class="text-primary stat-value-lg"><%= @affected_requests.size %></div>
|
|
36
26
|
</div>
|
|
37
27
|
</div>
|
|
38
28
|
</div>
|
|
39
29
|
|
|
40
30
|
<%# Fix suggestion card %>
|
|
41
|
-
<div class="card
|
|
31
|
+
<div class="card mb-16">
|
|
42
32
|
<div class="card-title">Suggested Fix</div>
|
|
43
|
-
<div
|
|
33
|
+
<div class="text-muted mb-12">
|
|
44
34
|
<%= @pattern[:fix_suggestion][:description] %>
|
|
45
35
|
</div>
|
|
46
|
-
<pre
|
|
47
|
-
background:#1a202c;
|
|
48
|
-
padding:12px;
|
|
49
|
-
border-radius:6px;
|
|
50
|
-
font-size:13px;
|
|
51
|
-
color:#68d391;
|
|
52
|
-
"><%= @pattern[:fix_suggestion][:code] %></pre>
|
|
36
|
+
<pre class="code-block text-healthy"><%= @pattern[:fix_suggestion][:code] %></pre>
|
|
53
37
|
</div>
|
|
54
38
|
|
|
55
39
|
<%# Impact simulator card %>
|
|
56
|
-
<div class="card
|
|
40
|
+
<div class="card mb-16">
|
|
57
41
|
<div class="card-title">
|
|
58
42
|
Impact Simulator
|
|
59
|
-
<span style="
|
|
43
|
+
<span class="text-muted text-12 ml-8" style="font-weight:normal;">
|
|
60
44
|
if you fix this N+1
|
|
61
45
|
</span>
|
|
62
46
|
</div>
|
|
63
47
|
|
|
64
|
-
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;
|
|
65
|
-
<div
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
padding:16px;
|
|
69
|
-
text-align:center;
|
|
70
|
-
">
|
|
71
|
-
<div style="color:#a0aec0;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;">
|
|
72
|
-
Est. Saving Per Request
|
|
73
|
-
</div>
|
|
74
|
-
<div style="color:#68d391;font-size:28px;font-weight:bold;">
|
|
75
|
-
<%= @avg_saving_per_request %>ms
|
|
76
|
-
</div>
|
|
48
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;" class="mb-24">
|
|
49
|
+
<div class="impact-box">
|
|
50
|
+
<div class="section-label">Est. Saving Per Request</div>
|
|
51
|
+
<div class="text-healthy stat-value-xl"><%= @avg_saving_per_request %>ms</div>
|
|
77
52
|
</div>
|
|
78
|
-
<div
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
padding:16px;
|
|
82
|
-
text-align:center;
|
|
83
|
-
">
|
|
84
|
-
<div style="color:#a0aec0;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;">
|
|
85
|
-
Total Time Recovered
|
|
86
|
-
</div>
|
|
87
|
-
<div style="color:#68d391;font-size:28px;font-weight:bold;">
|
|
88
|
-
<%= @estimated_saving_ms %>ms
|
|
89
|
-
</div>
|
|
53
|
+
<div class="impact-box">
|
|
54
|
+
<div class="section-label">Total Time Recovered</div>
|
|
55
|
+
<div class="text-healthy stat-value-xl"><%= @estimated_saving_ms %>ms</div>
|
|
90
56
|
</div>
|
|
91
|
-
<div
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
padding:16px;
|
|
95
|
-
text-align:center;
|
|
96
|
-
">
|
|
97
|
-
<div style="color:#a0aec0;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;">
|
|
98
|
-
Requests Affected
|
|
99
|
-
</div>
|
|
100
|
-
<div style="color:#e2e8f0;font-size:28px;font-weight:bold;">
|
|
101
|
-
<%= @affected_requests.size %>
|
|
102
|
-
</div>
|
|
57
|
+
<div class="impact-box">
|
|
58
|
+
<div class="section-label">Requests Affected</div>
|
|
59
|
+
<div class="text-primary stat-value-xl"><%= @affected_requests.size %></div>
|
|
103
60
|
</div>
|
|
104
61
|
</div>
|
|
105
62
|
|
|
106
63
|
<%# Affected requests list %>
|
|
107
|
-
<div
|
|
108
|
-
Affected Requests
|
|
109
|
-
</div>
|
|
64
|
+
<div class="section-label">Affected Requests</div>
|
|
110
65
|
<table>
|
|
111
66
|
<thead>
|
|
112
67
|
<tr>
|
|
@@ -127,10 +82,9 @@
|
|
|
127
82
|
</span>
|
|
128
83
|
</td>
|
|
129
84
|
<td><%= r.total_query_count %></td>
|
|
130
|
-
<td
|
|
85
|
+
<td class="text-muted"><%= r.duration_ms.round(1) %>ms</td>
|
|
131
86
|
<td>
|
|
132
|
-
<%= link_to "→", request_path(r.id),
|
|
133
|
-
style: "color:#90cdf4;" %>
|
|
87
|
+
<%= link_to "→", request_path(r.id), class: "text-accent" %>
|
|
134
88
|
</td>
|
|
135
89
|
</tr>
|
|
136
90
|
<% end %>
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
12
|
<% if params[:endpoint].present? %>
|
|
13
|
-
<div style="margin:20px 0;
|
|
13
|
+
<div class="text-muted" style="margin:20px 0;">
|
|
14
14
|
Filtering by endpoint:
|
|
15
|
-
<span
|
|
15
|
+
<span class="text-accent"><%= params[:endpoint] %></span>
|
|
16
16
|
|
|
17
|
-
<%= link_to "✕ Clear", requests_path,
|
|
17
|
+
<%= link_to "✕ Clear", requests_path, class: "text-danger" %>
|
|
18
18
|
</div>
|
|
19
19
|
<% end %>
|
|
20
20
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<tbody>
|
|
36
36
|
<% @records.each do |r| %>
|
|
37
37
|
<tr>
|
|
38
|
-
<td
|
|
38
|
+
<td class="text-muted text-sm"><%= r.recorded_at.strftime("%H:%M:%S") %></td>
|
|
39
39
|
<td><%= r.endpoint %></td>
|
|
40
40
|
<td>
|
|
41
41
|
<span class="badge badge-<%= r.color %>">
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<% if r.n_plus_one_patterns.any? %>
|
|
49
49
|
<span class="n1-badge"><%= r.n_plus_one_patterns.size %></span>
|
|
50
50
|
<% else %>
|
|
51
|
-
<span
|
|
51
|
+
<span class="text-healthy">—</span>
|
|
52
52
|
<% end %>
|
|
53
53
|
</td>
|
|
54
54
|
<td><%= r.duration_ms&.round(1) %>ms</td>
|