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
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<%= link_to "← Back", rails_vitals.requests_path,
|
|
1
|
+
<div class="flex-center mb-20" style="gap:16px;">
|
|
2
|
+
<%= link_to "← Back", rails_vitals.requests_path, class: "back-link" %>
|
|
3
3
|
<div class="page-title" style="margin:0;">Request Detail</div>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<%# Header %>
|
|
7
|
-
<div class="card
|
|
7
|
+
<div class="card flex-between">
|
|
8
8
|
<div>
|
|
9
|
-
<div style="font-size:42px;
|
|
9
|
+
<div class="bold" style="font-size:42px;color:<%= score_color(@record.color) %>;">
|
|
10
10
|
<%= @record.score %>
|
|
11
|
-
<span style="font-size:16px;
|
|
11
|
+
<span class="text-muted" style="font-size:16px;">/ 100</span>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
|
-
<div
|
|
14
|
+
<div class="text-right">
|
|
15
15
|
<span class="badge badge-<%= @record.color %>" style="font-size:13px;padding:4px 14px;">
|
|
16
16
|
<%= @record.label %>
|
|
17
17
|
</span>
|
|
18
|
-
<div
|
|
18
|
+
<div class="text-muted text-sm mt-6"><%= @record.recorded_at.strftime("%Y-%m-%d %H:%M:%S") %></div>
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
<div class="card-title">Request Info</div>
|
|
25
25
|
<table>
|
|
26
26
|
<tbody>
|
|
27
|
-
<tr><td style="
|
|
28
|
-
<tr><td
|
|
29
|
-
<tr><td
|
|
30
|
-
<tr><td
|
|
27
|
+
<tr><td class="text-muted" style="width:140px;">Endpoint</td><td><%= @record.endpoint %></td></tr>
|
|
28
|
+
<tr><td class="text-muted">Method</td> <td><%= @record.http_method %></td></tr>
|
|
29
|
+
<tr><td class="text-muted">Status</td> <td><%= @record.response_status %></td></tr>
|
|
30
|
+
<tr><td class="text-muted">Duration</td><td><%= @record.duration_ms&.round(1) %>ms</td></tr>
|
|
31
31
|
</tbody>
|
|
32
32
|
</table>
|
|
33
33
|
</div>
|
|
@@ -37,15 +37,15 @@
|
|
|
37
37
|
<div class="card-title">Query Summary</div>
|
|
38
38
|
<table>
|
|
39
39
|
<tbody>
|
|
40
|
-
<tr><td style="
|
|
41
|
-
<tr><td
|
|
40
|
+
<tr><td class="text-muted" style="width:140px;">Total Queries</td><td><%= @record.total_query_count %></td></tr>
|
|
41
|
+
<tr><td class="text-muted">DB Time</td> <td><%= @record.total_db_time_ms.round(1) %>ms</td></tr>
|
|
42
42
|
<tr>
|
|
43
|
-
<td
|
|
43
|
+
<td class="text-muted">N+1 Patterns</td>
|
|
44
44
|
<td>
|
|
45
45
|
<% if @record.n_plus_one_patterns.any? %>
|
|
46
46
|
<span class="n1-badge"><%= @record.n_plus_one_patterns.size %> detected</span>
|
|
47
47
|
<% else %>
|
|
48
|
-
<span
|
|
48
|
+
<span class="text-healthy">None detected</span>
|
|
49
49
|
<% end %>
|
|
50
50
|
</td>
|
|
51
51
|
</tr>
|
|
@@ -78,17 +78,13 @@
|
|
|
78
78
|
|
|
79
79
|
<%# All Queries %>
|
|
80
80
|
<div class="card">
|
|
81
|
-
<div
|
|
82
|
-
class="
|
|
83
|
-
onclick="toggleCard('all_queries_table', 'all_queries_chevron_simple')"
|
|
84
|
-
style="cursor:pointer;"
|
|
85
|
-
>
|
|
86
|
-
<div style="display:flex;justify-content:space-between;align-items:center;">
|
|
81
|
+
<div class="card-title cursor-pointer" onclick="toggleCard('all_queries_table', 'all_queries_chevron_simple')">
|
|
82
|
+
<div class="flex-between">
|
|
87
83
|
<span>All Queries (<%= @record.queries.size %>)</span>
|
|
88
84
|
<span style="font-size:14px;" id="all_queries_chevron_simple">▼</span>
|
|
89
85
|
</div>
|
|
90
86
|
</div>
|
|
91
|
-
<table id="all_queries_table">
|
|
87
|
+
<table id="all_queries_table" class="d-none">
|
|
92
88
|
<thead>
|
|
93
89
|
<tr>
|
|
94
90
|
<th>SQL</th>
|
|
@@ -99,7 +95,7 @@
|
|
|
99
95
|
<% @record.queries.sort_by { |q| -q[:duration_ms] }.each do |q| %>
|
|
100
96
|
<tr>
|
|
101
97
|
<td class="sql"><%= q[:sql] %></td>
|
|
102
|
-
<td
|
|
98
|
+
<td class="text-nowrap"><%= q[:duration_ms].round(1) %>ms</td>
|
|
103
99
|
</tr>
|
|
104
100
|
<% end %>
|
|
105
101
|
</tbody>
|
|
@@ -111,23 +107,16 @@
|
|
|
111
107
|
<div class="card">
|
|
112
108
|
<div class="card-title">
|
|
113
109
|
Callback Map
|
|
114
|
-
<span style="
|
|
110
|
+
<span class="text-muted ml-8" style="font-weight:normal;">
|
|
115
111
|
<%= @record.callbacks.size %> callbacks —
|
|
116
112
|
<%= @record.total_callback_time_ms.round(1) %>ms total
|
|
117
113
|
</span>
|
|
118
114
|
</div>
|
|
119
115
|
<% @record.callbacks.group_by { |c| c[:model] }.each do |model, callbacks| %>
|
|
120
|
-
<div
|
|
121
|
-
<div style="
|
|
122
|
-
color:#90cdf4;
|
|
123
|
-
font-size:12px;
|
|
124
|
-
font-weight:bold;
|
|
125
|
-
margin-bottom:6px;
|
|
126
|
-
padding-bottom:4px;
|
|
127
|
-
border-bottom:1px solid #2d3748;
|
|
128
|
-
">
|
|
116
|
+
<div class="mb-16">
|
|
117
|
+
<div class="text-accent bold text-12 mb-6" style="padding-bottom:4px;border-bottom:1px solid #2d3748;">
|
|
129
118
|
<%= model %>
|
|
130
|
-
<span style="
|
|
119
|
+
<span class="text-muted" style="font-weight:normal;">
|
|
131
120
|
(<%= callbacks.size %> callbacks,
|
|
132
121
|
<%= callbacks.sum { |c| c[:duration_ms] }.round(1) %>ms)
|
|
133
122
|
</span>
|
|
@@ -144,18 +133,12 @@
|
|
|
144
133
|
<% callbacks.sort_by { |c| -c[:duration_ms] }.each do |cb| %>
|
|
145
134
|
<tr>
|
|
146
135
|
<td>
|
|
147
|
-
<span style="
|
|
148
|
-
background:<%= callback_color(cb[:kind]) %>;
|
|
149
|
-
color:#fff;
|
|
150
|
-
padding:1px 6px;
|
|
151
|
-
border-radius:3px;
|
|
152
|
-
font-size:11px;
|
|
153
|
-
">
|
|
136
|
+
<span class="callback-badge" style="background:<%= callback_color(cb[:kind]) %>;">
|
|
154
137
|
<%= cb[:kind] %>
|
|
155
138
|
</span>
|
|
156
139
|
</td>
|
|
157
140
|
<td><%= cb[:duration_ms] %>ms</td>
|
|
158
|
-
<td
|
|
141
|
+
<td class="text-muted">
|
|
159
142
|
<% pct = @record.total_callback_time_ms > 0 ?
|
|
160
143
|
((cb[:duration_ms] / @record.total_callback_time_ms) * 100).round(1) : 0 %>
|
|
161
144
|
<%= pct %>%
|
|
@@ -171,22 +154,16 @@
|
|
|
171
154
|
|
|
172
155
|
<%# All Queries with DNA %>
|
|
173
156
|
<div class="card">
|
|
174
|
-
<div
|
|
175
|
-
class="
|
|
176
|
-
onclick="toggleCard('all_queries_table_dna', 'all_queries_chevron_dna')"
|
|
177
|
-
style="cursor:pointer;"
|
|
178
|
-
>
|
|
179
|
-
<div style="display:flex;justify-content:space-between;align-items:center;">
|
|
157
|
+
<div class="card-title cursor-pointer" onclick="toggleCard('all_queries_table_dna', 'all_queries_chevron_dna')">
|
|
158
|
+
<div class="flex-between">
|
|
180
159
|
<p>
|
|
181
160
|
All Queries (<%= @record.queries.size %>)
|
|
182
|
-
<span class="card-title-description">
|
|
183
|
-
click any query to expand DNA
|
|
184
|
-
</span>
|
|
161
|
+
<span class="card-title-description">click any query to expand DNA</span>
|
|
185
162
|
</p>
|
|
186
163
|
<span style="font-size:14px;" id="all_queries_chevron_dna">▼</span>
|
|
187
164
|
</div>
|
|
188
165
|
</div>
|
|
189
|
-
<table id="all_queries_table_dna">
|
|
166
|
+
<table id="all_queries_table_dna" class="d-none">
|
|
190
167
|
<thead>
|
|
191
168
|
<tr>
|
|
192
169
|
<th>#</th>
|
|
@@ -203,68 +180,63 @@
|
|
|
203
180
|
<% dna_id = "dna_#{i}" %>
|
|
204
181
|
|
|
205
182
|
<%# Query row — clickable %>
|
|
206
|
-
<tr
|
|
207
|
-
|
|
208
|
-
style="
|
|
209
|
-
>
|
|
210
|
-
<td style="color:#a0aec0;"><%= i + 1 %></td>
|
|
211
|
-
<td style="font-family:monospace;font-size:11px;color:#e2e8f0;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
|
183
|
+
<tr onclick="toggleDna('<%= dna_id %>')" class="cursor-pointer">
|
|
184
|
+
<td class="text-muted"><%= i + 1 %></td>
|
|
185
|
+
<td class="mono text-sm text-primary truncate" style="max-width:400px;">
|
|
212
186
|
<%= q[:sql] %>
|
|
213
187
|
</td>
|
|
214
188
|
<td style="color:<%= time_heat_color(q[:duration_ms]) %>;">
|
|
215
189
|
<%= q[:duration_ms].round(2) %>ms
|
|
216
190
|
</td>
|
|
217
191
|
<td>
|
|
218
|
-
<span style="color:<%= dna.complexity_label[:color] %>;
|
|
192
|
+
<span class="text-12" style="color:<%= dna.complexity_label[:color] %>;">
|
|
219
193
|
<%= dna.complexity_label[:label] %>
|
|
220
|
-
<span style="
|
|
194
|
+
<span class="text-muted" style="font-size:10px;">(<%= dna.complexity %>/10)</span>
|
|
221
195
|
</span>
|
|
222
196
|
</td>
|
|
223
197
|
<td>
|
|
224
198
|
<% risk_colors = { healthy: "#68d391", neutral: "#a0aec0", warning: "#f6ad55", danger: "#fc8181" } %>
|
|
225
|
-
<span style="color:<%= risk_colors[dna.risk] %>;
|
|
199
|
+
<span class="text-12" style="color:<%= risk_colors[dna.risk] %>;text-transform:capitalize;">
|
|
226
200
|
<%= dna.risk %>
|
|
227
201
|
</span>
|
|
228
202
|
</td>
|
|
203
|
+
<td></td>
|
|
229
204
|
</tr>
|
|
230
205
|
|
|
231
206
|
<%# DNA panel — hidden by default %>
|
|
232
|
-
<tr id="<%= dna_id %>"
|
|
207
|
+
<tr id="<%= dna_id %>" class="d-none">
|
|
233
208
|
<td colspan="5" style="padding:0;">
|
|
234
|
-
<div
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
209
|
+
<div class="dna-panel">
|
|
210
|
+
<% if q[:sql].strip.match?(/\ASELECT/i) %>
|
|
211
|
+
<div class="flex mb-8" style="justify-content:flex-end;">
|
|
212
|
+
<a
|
|
213
|
+
href="<%= rails_vitals.explain_path(
|
|
214
|
+
request_id: @record.id,
|
|
215
|
+
query_index: i
|
|
216
|
+
) %>"
|
|
217
|
+
target="_blank"
|
|
218
|
+
class="text-blue text-nowrap explain-btn"
|
|
219
|
+
>
|
|
220
|
+
EXPLAIN →
|
|
221
|
+
</a>
|
|
222
|
+
</div>
|
|
223
|
+
<% end %>
|
|
240
224
|
|
|
241
225
|
<%# Full SQL %>
|
|
242
|
-
<div
|
|
243
|
-
<div
|
|
244
|
-
|
|
245
|
-
</div>
|
|
246
|
-
<pre style="color:#e2e8f0;font-size:11px;white-space:pre-wrap;word-break:break-all;margin:0;"><%= q[:sql] %></pre>
|
|
226
|
+
<div class="mb-16">
|
|
227
|
+
<div class="mini-label">Full SQL</div>
|
|
228
|
+
<pre class="text-primary text-sm word-break"><%= q[:sql] %></pre>
|
|
247
229
|
</div>
|
|
248
230
|
|
|
249
231
|
<%# Token strip %>
|
|
250
|
-
<div
|
|
251
|
-
<div
|
|
252
|
-
|
|
253
|
-
</div>
|
|
254
|
-
<div style="display:flex;flex-wrap:wrap;gap:6px;">
|
|
232
|
+
<div class="mb-16">
|
|
233
|
+
<div class="mini-label">Query DNA</div>
|
|
234
|
+
<div class="flex flex-wrap" style="gap:6px;">
|
|
255
235
|
<% dna.tokens.each do |token| %>
|
|
256
236
|
<span
|
|
257
237
|
onclick="toggleCard('<%= "card_#{dna_id}_#{token[:type]}" %>'); event.stopPropagation();"
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
color:<%= token[:color] %>;
|
|
261
|
-
border:1px solid <%= token[:color] %>66;
|
|
262
|
-
padding:3px 10px;
|
|
263
|
-
border-radius:4px;
|
|
264
|
-
font-size:12px;
|
|
265
|
-
font-family:monospace;
|
|
266
|
-
cursor:pointer;
|
|
267
|
-
"
|
|
238
|
+
class="mono cursor-pointer"
|
|
239
|
+
style="background:<%= token[:color] %>22;color:<%= token[:color] %>;border:1px solid <%= token[:color] %>66;padding:3px 10px;border-radius:4px;font-size:12px;"
|
|
268
240
|
>
|
|
269
241
|
<%= token[:label] %>
|
|
270
242
|
</span>
|
|
@@ -274,20 +246,14 @@
|
|
|
274
246
|
|
|
275
247
|
<%# Repetition bar %>
|
|
276
248
|
<% if dna.repetition_bar.is_a?(Hash) && dna.repetition_count > 1 %>
|
|
277
|
-
<div
|
|
278
|
-
<div
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
<span style="color:#fc8181;">
|
|
284
|
-
<%= "█" * dna.repetition_bar[:filled] %>
|
|
285
|
-
</span>
|
|
286
|
-
<span style="color:#2d3748;">
|
|
287
|
-
<%= "█" * dna.repetition_bar[:empty] %>
|
|
288
|
-
</span>
|
|
249
|
+
<div class="mb-16">
|
|
250
|
+
<div class="mini-label">Repetition — <%= dna.repetition_count %>x in this request</div>
|
|
251
|
+
<div class="flex-center" style="gap:8px;">
|
|
252
|
+
<div class="mono" style="font-size:13px;">
|
|
253
|
+
<span class="text-danger"><%= "█" * dna.repetition_bar[:filled] %></span>
|
|
254
|
+
<span style="color:#2d3748;"><%= "█" * dna.repetition_bar[:empty] %></span>
|
|
289
255
|
</div>
|
|
290
|
-
<span
|
|
256
|
+
<span class="text-danger bold text-12">
|
|
291
257
|
<%= dna.repetition_count %>x — N+1 pattern
|
|
292
258
|
</span>
|
|
293
259
|
</div>
|
|
@@ -295,23 +261,23 @@
|
|
|
295
261
|
<% end %>
|
|
296
262
|
|
|
297
263
|
<%# Complexity + metadata row %>
|
|
298
|
-
<div style="
|
|
264
|
+
<div class="flex mb-16 text-12" style="gap:24px;">
|
|
299
265
|
<div>
|
|
300
|
-
<span
|
|
301
|
-
<span style="color:<%= dna.complexity_label[:color] %>;
|
|
266
|
+
<span class="text-muted">Complexity</span>
|
|
267
|
+
<span class="bold ml-6" style="color:<%= dna.complexity_label[:color] %>;">
|
|
302
268
|
<%= dna.complexity_label[:label] %> (<%= dna.complexity %>/10)
|
|
303
269
|
</span>
|
|
304
270
|
</div>
|
|
305
271
|
<div>
|
|
306
|
-
<span
|
|
272
|
+
<span class="text-muted">Risk</span>
|
|
307
273
|
<% risk_colors = { healthy: "#68d391", neutral: "#a0aec0", warning: "#f6ad55", danger: "#fc8181" } %>
|
|
308
|
-
<span style="color:<%= risk_colors[dna.risk] %>;
|
|
274
|
+
<span class="bold ml-6" style="color:<%= risk_colors[dna.risk] %>;text-transform:capitalize;">
|
|
309
275
|
<%= dna.risk %>
|
|
310
276
|
</span>
|
|
311
277
|
</div>
|
|
312
278
|
<div>
|
|
313
|
-
<span
|
|
314
|
-
<span style="color:<%= time_heat_color(q[:duration_ms]) %>;
|
|
279
|
+
<span class="text-muted">Duration</span>
|
|
280
|
+
<span class="bold ml-6" style="color:<%= time_heat_color(q[:duration_ms]) %>;">
|
|
315
281
|
<%= q[:duration_ms].round(2) %>ms
|
|
316
282
|
</span>
|
|
317
283
|
</div>
|
|
@@ -319,38 +285,18 @@
|
|
|
319
285
|
|
|
320
286
|
<%# Education cards — one per token, hidden by default %>
|
|
321
287
|
<% dna.tokens.each do |token| %>
|
|
322
|
-
<div
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
>
|
|
326
|
-
<div style="
|
|
327
|
-
background:#2d3748;
|
|
328
|
-
border-left:3px solid <%= token[:color] %>;
|
|
329
|
-
border-radius:4px;
|
|
330
|
-
padding:12px 16px;
|
|
331
|
-
">
|
|
332
|
-
<div style="
|
|
333
|
-
color:<%= token[:color] %>;
|
|
334
|
-
font-size:12px;
|
|
335
|
-
font-weight:bold;
|
|
336
|
-
font-family:monospace;
|
|
337
|
-
margin-bottom:6px;
|
|
338
|
-
">
|
|
288
|
+
<div id="card_<%= dna_id %>_<%= token[:type] %>" class="d-none mb-8">
|
|
289
|
+
<div style="background:#2d3748;border-left:3px solid <%= token[:color] %>;border-radius:4px;padding:12px 16px;">
|
|
290
|
+
<div class="bold mono mb-6" style="color:<%= token[:color] %>;font-size:12px;">
|
|
339
291
|
💡 <%= token[:label] %>
|
|
340
|
-
<span
|
|
341
|
-
|
|
342
|
-
color:<%= token[:color] %>;
|
|
343
|
-
|
|
344
|
-
padding:1px 6px;
|
|
345
|
-
border-radius:3px;
|
|
346
|
-
margin-left:6px;
|
|
347
|
-
text-transform:uppercase;
|
|
348
|
-
font-family:Arial,sans-serif;
|
|
349
|
-
">
|
|
292
|
+
<span
|
|
293
|
+
class="text-upper"
|
|
294
|
+
style="background:<%= token[:color] %>33;color:<%= token[:color] %>;font-size:10px;padding:1px 6px;border-radius:3px;margin-left:6px;font-family:Arial,sans-serif;"
|
|
295
|
+
>
|
|
350
296
|
<%= token[:risk] %>
|
|
351
297
|
</span>
|
|
352
298
|
</div>
|
|
353
|
-
<div
|
|
299
|
+
<div class="text-primary line-relaxed">
|
|
354
300
|
<%= token[:explanation] %>
|
|
355
301
|
</div>
|
|
356
302
|
</div>
|
|
@@ -365,32 +311,3 @@
|
|
|
365
311
|
</tbody>
|
|
366
312
|
</table>
|
|
367
313
|
</div>
|
|
368
|
-
|
|
369
|
-
<%# Inline JS for toggle behavior %>
|
|
370
|
-
<script>
|
|
371
|
-
function toggleDna(id) {
|
|
372
|
-
var row = document.getElementById(id);
|
|
373
|
-
if (row) {
|
|
374
|
-
row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function toggleCard(id, chevronId) {
|
|
379
|
-
var card = document.getElementById(id);
|
|
380
|
-
if (card) {
|
|
381
|
-
if (card.style.display === 'none') {
|
|
382
|
-
card.style.display = card.tagName === 'TABLE' ? 'table' : 'block';
|
|
383
|
-
} else {
|
|
384
|
-
card.style.display = 'none';
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Toggle chevron icon if provided
|
|
389
|
-
if (chevronId) {
|
|
390
|
-
var chevron = document.getElementById(chevronId);
|
|
391
|
-
if (chevron) {
|
|
392
|
-
chevron.textContent = chevron.textContent === '▼' ? '▶' : '▼';
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
</script>
|
data/config/routes.rb
CHANGED