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.
@@ -1,21 +1,21 @@
1
- <div style="display:flex;align-items:center;gap:16px;margin-bottom:20px;">
2
- <%= link_to "← Back", rails_vitals.requests_path, style: "color:#a0aec0;font-size:12px;" %>
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" style="display:flex;align-items:center;justify-content:space-between;">
7
+ <div class="card flex-between">
8
8
  <div>
9
- <div style="font-size:42px;font-weight:bold;color:<%= score_color(@record.color) %>;">
9
+ <div class="bold" style="font-size:42px;color:<%= score_color(@record.color) %>;">
10
10
  <%= @record.score %>
11
- <span style="font-size:16px;color:#a0aec0;">/ 100</span>
11
+ <span class="text-muted" style="font-size:16px;">/ 100</span>
12
12
  </div>
13
13
  </div>
14
- <div style="text-align:right;">
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 style="color:#a0aec0;font-size:11px;margin-top:6px;"><%= @record.recorded_at.strftime("%Y-%m-%d %H:%M:%S") %></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="color:#a0aec0;width:140px;">Endpoint</td><td><%= @record.endpoint %></td></tr>
28
- <tr><td style="color:#a0aec0;">Method</td> <td><%= @record.http_method %></td></tr>
29
- <tr><td style="color:#a0aec0;">Status</td> <td><%= @record.response_status %></td></tr>
30
- <tr><td style="color:#a0aec0;">Duration</td><td><%= @record.duration_ms&.round(1) %>ms</td></tr>
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="color:#a0aec0;width:140px;">Total Queries</td><td><%= @record.total_query_count %></td></tr>
41
- <tr><td style="color:#a0aec0;">DB Time</td> <td><%= @record.total_db_time_ms.round(1) %>ms</td></tr>
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 style="color:#a0aec0;">N+1 Patterns</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 style="color:#68d391;">None detected</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="card-title"
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 style="white-space:nowrap;"><%= q[:duration_ms].round(1) %>ms</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="color:#a0aec0;font-weight:normal;margin-left:8px;">
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 style="margin-bottom:16px;">
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="color:#a0aec0;font-weight:normal;">
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 style="color:#a0aec0;">
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="card-title"
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
- onclick="toggleDna('<%= dna_id %>')"
208
- style="cursor:pointer;"
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] %>;font-size:12px;">
192
+ <span class="text-12" style="color:<%= dna.complexity_label[:color] %>;">
219
193
  <%= dna.complexity_label[:label] %>
220
- <span style="color:#a0aec0;font-size:10px;">(<%= dna.complexity %>/10)</span>
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] %>;font-size:12px;text-transform:capitalize;">
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 %>" style="display:none;">
207
+ <tr id="<%= dna_id %>" class="d-none">
233
208
  <td colspan="5" style="padding:0;">
234
- <div style="
235
- background:#1a202c;
236
- border-left:3px solid #4299e1;
237
- padding:16px;
238
- margin:4px 0;
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 style="margin-bottom:16px;">
243
- <div style="color:#a0aec0;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:6px;">
244
- Full SQL
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 style="margin-bottom:16px;">
251
- <div style="color:#a0aec0;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;">
252
- Query DNA
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
- style="
259
- background:<%= token[:color] %>22;
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 style="margin-bottom:16px;">
278
- <div style="color:#a0aec0;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:6px;">
279
- Repetition <%= dna.repetition_count %>x in this request
280
- </div>
281
- <div style="display:flex;align-items:center;gap:8px;">
282
- <div style="font-family:monospace;font-size:13px;">
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 style="color:#fc8181;font-size:12px;font-weight:bold;">
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="display:flex;gap:24px;margin-bottom:16px;font-size:12px;">
264
+ <div class="flex mb-16 text-12" style="gap:24px;">
299
265
  <div>
300
- <span style="color:#a0aec0;">Complexity</span>
301
- <span style="color:<%= dna.complexity_label[:color] %>;margin-left:6px;font-weight:bold;">
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 style="color:#a0aec0;">Risk</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] %>;margin-left:6px;font-weight:bold;text-transform:capitalize;">
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 style="color:#a0aec0;">Duration</span>
314
- <span style="color:<%= time_heat_color(q[:duration_ms]) %>;margin-left:6px;font-weight:bold;">
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
- id="card_<%= dna_id %>_<%= token[:type] %>"
324
- style="display:none;margin-bottom:8px;"
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 style="
341
- background:<%= token[:color] %>33;
342
- color:<%= token[:color] %>;
343
- font-size:10px;
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 style="color:#e2e8f0;font-size:13px;line-height:1.6;">
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
@@ -6,4 +6,5 @@ RailsVitals::Engine.routes.draw do
6
6
  resources :n_plus_ones, only: [ :index, :show ]
7
7
  resources :associations, only: [ :index ]
8
8
  get "heatmap", to: "heatmap#index", as: :heatmap
9
+ get "requests/:request_id/explain/:query_index", to: "explains#show", as: :explain
9
10
  end