rails_vitals 0.4.0 → 0.4.1
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 +10 -1
- data/app/assets/stylesheets/rails_vitals/application.css +43 -0
- data/app/helpers/rails_vitals/application_helper.rb +23 -1
- data/app/views/rails_vitals/dashboard/index.html.erb +8 -16
- data/app/views/rails_vitals/explains/show.html.erb +9 -40
- data/app/views/rails_vitals/heatmap/index.html.erb +3 -4
- data/app/views/rails_vitals/models/index.html.erb +2 -3
- data/app/views/rails_vitals/n_plus_ones/index.html.erb +1 -3
- data/app/views/rails_vitals/n_plus_ones/show.html.erb +7 -8
- data/app/views/rails_vitals/requests/index.html.erb +6 -12
- data/app/views/rails_vitals/requests/show.html.erb +23 -25
- data/app/views/rails_vitals/shared/_empty_state.html.erb +3 -0
- data/app/views/rails_vitals/shared/_n1_indicator.html.erb +9 -0
- data/app/views/rails_vitals/shared/_page_header.html.erb +15 -0
- data/app/views/rails_vitals/shared/_score_badge.html.erb +3 -0
- data/lib/rails_vitals/analyzers/association_mapper.rb +1 -1
- data/lib/rails_vitals/analyzers/explain_analyzer.rb +59 -57
- data/lib/rails_vitals/analyzers/n_plus_one_aggregator.rb +13 -13
- data/lib/rails_vitals/analyzers/sql_tokenizer.rb +81 -81
- data/lib/rails_vitals/collector.rb +18 -18
- data/lib/rails_vitals/instrumentation/callback_instrumentation.rb +3 -3
- data/lib/rails_vitals/notifications/subscriber.rb +3 -3
- data/lib/rails_vitals/panel_renderer.rb +7 -11
- data/lib/rails_vitals/playground/sandbox.rb +2 -1
- data/lib/rails_vitals/request_record.rb +12 -12
- data/lib/rails_vitals/scorers/base_scorer.rb +3 -3
- data/lib/rails_vitals/scorers/composite_scorer.rb +3 -3
- data/lib/rails_vitals/scorers/query_scorer.rb +3 -3
- data/lib/rails_vitals/store.rb +2 -2
- data/lib/rails_vitals/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e310ba646b2f5168dec1765754ead900d1341fcfb229820afc9c75e4e6be8fb
|
|
4
|
+
data.tar.gz: a38f8d572a5b5a00a45d21239d9b07669c9dca818f7d75bcd9d7d9cfeea06e79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5708c964e928d6400375f2e1d98024a05f9e089066fb3fb65c32ae61c39cbb69a4589d4ed901cbb6a8e491657ec438ec60b419dbff27bbdfbef9d8517c789b3
|
|
7
|
+
data.tar.gz: 88155cc28f07138987841604882c728068c9031fb25566ac8fc00efc824e3139a77352fd109a42b1d6861db466d1aae9dab2920412a860947de651456fb0d486
|
data/README.md
CHANGED
|
@@ -282,12 +282,21 @@ rails_vitals/
|
|
|
282
282
|
├── models/
|
|
283
283
|
├── n_plus_ones/
|
|
284
284
|
├── associations/
|
|
285
|
-
|
|
285
|
+
├── explains/
|
|
286
|
+
├── playgrounds/
|
|
287
|
+
└── shared/ # Reusable partials
|
|
288
|
+
├── _page_header.html.erb
|
|
289
|
+
├── _empty_state.html.erb
|
|
290
|
+
├── _score_badge.html.erb
|
|
291
|
+
└── _n1_indicator.html.erb
|
|
286
292
|
```
|
|
287
293
|
|
|
288
294
|
**Key architectural decisions:**
|
|
289
295
|
|
|
290
296
|
- **Zero JS dependencies** — no Chartkick, no D3, no Chart.js. Tables for data, SVG for diagrams, vanilla JS for interactions.
|
|
297
|
+
- **Single CSS/JS asset file** — all styles in `app/assets/stylesheets/rails_vitals/application.css`, all behaviour in `app/assets/javascripts/rails_vitals/application.js`. No inline `<style>` or `<script>` blocks in views.
|
|
298
|
+
- **Shared view partials** — common UI patterns (`_page_header`, `_empty_state`, `_score_badge`, `_n1_indicator`) live in `app/views/rails_vitals/shared/` and are reused across all views.
|
|
299
|
+
- **Centralised helpers** — all color logic, heat colors, and formatting live in `ApplicationHelper`. Views never define inline color hashes or call `.round(1).to_s + "ms"` directly; they use `format_ms()`, `risk_color()`, `badge_class()`, etc.
|
|
291
300
|
- **Thread-local Collector** — instrumentation state is stored per-thread, never shared between concurrent requests.
|
|
292
301
|
- **In-memory ring buffer** — the Store keeps the last N requests in memory. No database writes, no schema migrations.
|
|
293
302
|
- **Module prepend for callbacks** — callback instrumentation wraps `ActiveRecord::Base#run_callbacks` via `Module#prepend`. No TracePoint, no monkey-patching.
|
|
@@ -364,6 +364,13 @@ tr:hover td { background: #1e2535; }
|
|
|
364
364
|
.gap-24 { gap: 24px; }
|
|
365
365
|
|
|
366
366
|
/* ─── Grid utilities ─────────────────────────────────────────────────────────── */
|
|
367
|
+
.grid-5 {
|
|
368
|
+
display: grid;
|
|
369
|
+
grid-template-columns: repeat(5, 1fr);
|
|
370
|
+
gap: 12px;
|
|
371
|
+
margin-bottom: 24px;
|
|
372
|
+
}
|
|
373
|
+
|
|
367
374
|
.grid-4 {
|
|
368
375
|
display: grid;
|
|
369
376
|
grid-template-columns: repeat(4, 1fr);
|
|
@@ -499,3 +506,39 @@ tr:hover td { background: #1e2535; }
|
|
|
499
506
|
padding: 12px 16px;
|
|
500
507
|
margin-bottom: 20px;
|
|
501
508
|
}
|
|
509
|
+
|
|
510
|
+
/* ─── Score display (big number on request detail) ───────────────────────────── */
|
|
511
|
+
.score-display {
|
|
512
|
+
font-size: 42px;
|
|
513
|
+
font-weight: bold;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* ─── EXPLAIN plan tree ──────────────────────────────────────────────────────── */
|
|
517
|
+
.plan-node {
|
|
518
|
+
border-left: 2px solid #4a5568;
|
|
519
|
+
margin-left: 16px;
|
|
520
|
+
padding-left: 16px;
|
|
521
|
+
margin-top: 8px;
|
|
522
|
+
}
|
|
523
|
+
.plan-node:first-child {
|
|
524
|
+
margin-left: 0;
|
|
525
|
+
padding-left: 0;
|
|
526
|
+
border-left: none;
|
|
527
|
+
}
|
|
528
|
+
.node-box {
|
|
529
|
+
border-radius: 6px;
|
|
530
|
+
padding: 12px 16px;
|
|
531
|
+
margin-bottom: 4px;
|
|
532
|
+
cursor: pointer;
|
|
533
|
+
transition: opacity 0.15s;
|
|
534
|
+
}
|
|
535
|
+
.node-box:hover { opacity: 0.85; }
|
|
536
|
+
|
|
537
|
+
/* ─── EXPLAIN warning/interpretation block ───────────────────────────────────── */
|
|
538
|
+
.interpret-block {
|
|
539
|
+
border-radius: 6px;
|
|
540
|
+
padding: 12px 16px;
|
|
541
|
+
font-size: 13px;
|
|
542
|
+
background: #1a202c;
|
|
543
|
+
margin-bottom: 16px;
|
|
544
|
+
}
|
|
@@ -11,7 +11,9 @@ module RailsVitals
|
|
|
11
11
|
COLOR_ORANGE = "#f6ad55"
|
|
12
12
|
COLOR_LIGHT_GREEN = "#68d391"
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# Module-level helper so plain Ruby classes (e.g. PanelRenderer) can resolve
|
|
15
|
+
# a score color without needing a helper instance.
|
|
16
|
+
def self.score_color_for(color)
|
|
15
17
|
case color
|
|
16
18
|
when "green" then COLOR_GREEN
|
|
17
19
|
when "blue" then COLOR_BLUE
|
|
@@ -20,6 +22,10 @@ module RailsVitals
|
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
def score_color(color)
|
|
26
|
+
RailsVitals::ApplicationHelper.score_color_for(color)
|
|
27
|
+
end
|
|
28
|
+
|
|
23
29
|
def score_label_to_color(score)
|
|
24
30
|
case score
|
|
25
31
|
when 90..100 then "healthy"
|
|
@@ -107,5 +113,21 @@ module RailsVitals
|
|
|
107
113
|
else COLOR_LIGHT_RED
|
|
108
114
|
end
|
|
109
115
|
end
|
|
116
|
+
|
|
117
|
+
# Returns the full badge CSS class string for a score color label.
|
|
118
|
+
def badge_class(color)
|
|
119
|
+
"badge badge-#{color}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def format_ms(value)
|
|
123
|
+
return "0ms" unless value
|
|
124
|
+
|
|
125
|
+
"#{value.to_f.round(1)}ms"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Calculates a percentage of count over total, returning 0 when total is zero.
|
|
129
|
+
def percentage(count, total)
|
|
130
|
+
total.to_f > 0 ? ((count.to_f / total) * 100).round(1) : 0
|
|
131
|
+
end
|
|
110
132
|
end
|
|
111
133
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
<%= render "rails_vitals/shared/page_header", title: "Dashboard" %>
|
|
2
2
|
|
|
3
3
|
<div class="grid-3">
|
|
4
4
|
<div class="stat-card">
|
|
@@ -34,20 +34,14 @@
|
|
|
34
34
|
<tr>
|
|
35
35
|
<td><%= r.endpoint %></td>
|
|
36
36
|
<td>
|
|
37
|
-
|
|
38
|
-
<%= r.score %> <%= r.label %>
|
|
39
|
-
</span>
|
|
37
|
+
<%= render "rails_vitals/shared/score_badge", color: r.color, score: r.score, label: r.label %>
|
|
40
38
|
</td>
|
|
41
39
|
<td><%= r.total_query_count %></td>
|
|
42
|
-
<td><%= r.total_db_time_ms
|
|
40
|
+
<td><%= format_ms(r.total_db_time_ms) %></td>
|
|
43
41
|
<td>
|
|
44
|
-
|
|
45
|
-
<span class="n1-badge"><%= r.n_plus_one_patterns.size %></span>
|
|
46
|
-
<% else %>
|
|
47
|
-
<span class="text-healthy">None</span>
|
|
48
|
-
<% end %>
|
|
42
|
+
<%= render "rails_vitals/shared/n1_indicator", patterns: r.n_plus_one_patterns %>
|
|
49
43
|
</td>
|
|
50
|
-
<td><%= r.duration_ms
|
|
44
|
+
<td><%= format_ms(r.duration_ms) %></td>
|
|
51
45
|
<td><%= link_to "→", rails_vitals.request_path(r.id) %></td>
|
|
52
46
|
</tr>
|
|
53
47
|
<% end %>
|
|
@@ -74,7 +68,7 @@
|
|
|
74
68
|
<td><%= stats[:count] %></td>
|
|
75
69
|
<td><%= stats[:avg_score] %></td>
|
|
76
70
|
<td><%= stats[:avg_queries] %></td>
|
|
77
|
-
<td><%= stats[:avg_db_time_ms]
|
|
71
|
+
<td><%= format_ms(stats[:avg_db_time_ms]) %></td>
|
|
78
72
|
</tr>
|
|
79
73
|
<% end %>
|
|
80
74
|
</tbody>
|
|
@@ -100,7 +94,7 @@
|
|
|
100
94
|
<td><%= label %></td>
|
|
101
95
|
<td><%= count %></td>
|
|
102
96
|
<td class="text-muted">
|
|
103
|
-
<%=
|
|
97
|
+
<%= percentage(count, @total) %>%
|
|
104
98
|
</td>
|
|
105
99
|
</tr>
|
|
106
100
|
<% end %>
|
|
@@ -122,9 +116,7 @@
|
|
|
122
116
|
<tr>
|
|
123
117
|
<td class="text-muted text-sm"><%= endpoint %></td>
|
|
124
118
|
<td>
|
|
125
|
-
|
|
126
|
-
<%= score %>
|
|
127
|
-
</span>
|
|
119
|
+
<%= render "rails_vitals/shared/score_badge", color: score_label_to_color(score), score: score %>
|
|
128
120
|
</td>
|
|
129
121
|
</tr>
|
|
130
122
|
<% end %>
|
|
@@ -1,34 +1,12 @@
|
|
|
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
1
|
<%# Header %>
|
|
24
2
|
<div class="mb-20">
|
|
25
3
|
<% if @record %>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
4
|
+
<%= link_to "← Back to Request Detail",
|
|
5
|
+
rails_vitals.request_path(@record.id),
|
|
6
|
+
class: "back-link text-sm" %>
|
|
29
7
|
<% end %>
|
|
30
8
|
|
|
31
|
-
<h2 class="page-heading
|
|
9
|
+
<h2 class="page-heading mb-8">
|
|
32
10
|
EXPLAIN Visualizer
|
|
33
11
|
</h2>
|
|
34
12
|
|
|
@@ -38,15 +16,12 @@
|
|
|
38
16
|
</div>
|
|
39
17
|
|
|
40
18
|
<% 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
|
-
>
|
|
19
|
+
<div class="error-card">
|
|
45
20
|
<%= @result.error %>
|
|
46
21
|
</div>
|
|
47
22
|
<% else %>
|
|
48
23
|
<%# Summary stats row %>
|
|
49
|
-
<div
|
|
24
|
+
<div class="grid-5">
|
|
50
25
|
<% stats = [
|
|
51
26
|
{ label: "Total Cost", value: @result.total_cost, color: cost_color(@result.total_cost) },
|
|
52
27
|
{ label: "Actual Time", value: "#{@result.actual_time_ms}ms", color: time_color(@result.actual_time_ms) },
|
|
@@ -149,16 +124,10 @@
|
|
|
149
124
|
|
|
150
125
|
<%# Interpretation summary %>
|
|
151
126
|
<% if @result.interpretation %>
|
|
127
|
+
<% interp_color = @result.warnings.any? ? "#f6ad55" : "#68d391" %>
|
|
152
128
|
<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
|
-
"
|
|
129
|
+
class="interpret-block mb-16 line-relaxed"
|
|
130
|
+
style="color:<%= interp_color %>;border-left:3px solid <%= interp_color %>;"
|
|
162
131
|
>
|
|
163
132
|
<%= @result.interpretation %>
|
|
164
133
|
</div>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
class: "text-accent" %>
|
|
30
30
|
</td>
|
|
31
31
|
<td>
|
|
32
|
-
<span class="
|
|
32
|
+
<span class="<%= badge_class(color) %>">
|
|
33
33
|
<%= row[:avg_score] %>
|
|
34
34
|
</span>
|
|
35
35
|
</td>
|
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
</table>
|
|
61
61
|
</div>
|
|
62
62
|
<% else %>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</div>
|
|
63
|
+
<%= render "rails_vitals/shared/empty_state",
|
|
64
|
+
message: "No requests recorded yet. Visit your app to generate data." %>
|
|
66
65
|
<% end %>
|
|
@@ -94,7 +94,6 @@
|
|
|
94
94
|
</div>
|
|
95
95
|
<% end %>
|
|
96
96
|
<% else %>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
</div>
|
|
97
|
+
<%= render "rails_vitals/shared/empty_state",
|
|
98
|
+
message: "No data recorded yet. Visit your app to generate data." %>
|
|
100
99
|
<% end %>
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
<div class="card-title">Detected Pattern</div>
|
|
12
12
|
<pre class="code-block text-muted mb-16"><%= @pattern[:pattern] %></pre>
|
|
13
13
|
|
|
14
|
-
<div
|
|
14
|
+
<div class="grid-3 mb-16" style="font-size:13px;">
|
|
15
15
|
<div>
|
|
16
16
|
<div class="text-muted mb-4">Total Occurrences</div>
|
|
17
17
|
<div class="text-danger stat-value-lg"><%= @pattern[:occurrences] %>x</div>
|
|
18
18
|
</div>
|
|
19
19
|
<div>
|
|
20
20
|
<div class="text-muted mb-4">Estimated Time Wasted</div>
|
|
21
|
-
<div class="text-danger stat-value-lg"><%= @estimated_saving_ms
|
|
21
|
+
<div class="text-danger stat-value-lg"><%= format_ms(@estimated_saving_ms) %></div>
|
|
22
22
|
</div>
|
|
23
23
|
<div>
|
|
24
24
|
<div class="text-muted mb-4">Affected Requests</div>
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;" class="mb-24">
|
|
49
49
|
<div class="impact-box">
|
|
50
50
|
<div class="section-label">Est. Saving Per Request</div>
|
|
51
|
-
<div class="text-healthy stat-value-xl"><%= @avg_saving_per_request
|
|
51
|
+
<div class="text-healthy stat-value-xl"><%= format_ms(@avg_saving_per_request) %></div>
|
|
52
52
|
</div>
|
|
53
53
|
<div class="impact-box">
|
|
54
54
|
<div class="section-label">Total Time Recovered</div>
|
|
55
|
-
<div class="text-healthy stat-value-xl"><%= @estimated_saving_ms
|
|
55
|
+
<div class="text-healthy stat-value-xl"><%= format_ms(@estimated_saving_ms) %></div>
|
|
56
56
|
</div>
|
|
57
57
|
<div class="impact-box">
|
|
58
58
|
<div class="section-label">Requests Affected</div>
|
|
@@ -77,12 +77,11 @@
|
|
|
77
77
|
<tr>
|
|
78
78
|
<td><%= r.endpoint %></td>
|
|
79
79
|
<td>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</span>
|
|
80
|
+
<%= render "rails_vitals/shared/score_badge",
|
|
81
|
+
color: score_label_to_color(r.score), score: r.score %>
|
|
83
82
|
</td>
|
|
84
83
|
<td><%= r.total_query_count %></td>
|
|
85
|
-
<td class="text-muted"><%= r.duration_ms
|
|
84
|
+
<td class="text-muted"><%= format_ms(r.duration_ms) %></td>
|
|
86
85
|
<td>
|
|
87
86
|
<%= link_to "→", request_path(r.id), class: "text-accent" %>
|
|
88
87
|
</td>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
<%= render "rails_vitals/shared/page_header", title: "Request History" %>
|
|
2
2
|
|
|
3
3
|
<div class="filter-bar">
|
|
4
4
|
<%= link_to "All", rails_vitals.requests_path, class: params[:score].blank? && params[:n_plus_one].blank? ? "active" : "" %>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
12
|
<% if params[:endpoint].present? %>
|
|
13
|
-
|
|
13
|
+
<div class="text-muted mb-16">
|
|
14
14
|
Filtering by endpoint:
|
|
15
15
|
<span class="text-accent"><%= params[:endpoint] %></span>
|
|
16
16
|
|
|
@@ -38,20 +38,14 @@
|
|
|
38
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
|
-
|
|
42
|
-
<%= r.score %>
|
|
43
|
-
</span>
|
|
41
|
+
<%= render "rails_vitals/shared/score_badge", color: r.color, score: r.score %>
|
|
44
42
|
</td>
|
|
45
43
|
<td><%= r.total_query_count %></td>
|
|
46
|
-
<td><%= r.total_db_time_ms
|
|
44
|
+
<td><%= format_ms(r.total_db_time_ms) %></td>
|
|
47
45
|
<td>
|
|
48
|
-
|
|
49
|
-
<span class="n1-badge"><%= r.n_plus_one_patterns.size %></span>
|
|
50
|
-
<% else %>
|
|
51
|
-
<span class="text-healthy">—</span>
|
|
52
|
-
<% end %>
|
|
46
|
+
<%= render "rails_vitals/shared/n1_indicator", patterns: r.n_plus_one_patterns, none_label: "—" %>
|
|
53
47
|
</td>
|
|
54
|
-
<td><%= r.duration_ms
|
|
48
|
+
<td><%= format_ms(r.duration_ms) %></td>
|
|
55
49
|
<td><%= link_to "→", rails_vitals.request_path(r.id) %></td>
|
|
56
50
|
</tr>
|
|
57
51
|
<% end %>
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
<div class="
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<div class="mb-8">
|
|
3
|
+
<%= link_to "← Back", rails_vitals.requests_path, class: "back-link" %>
|
|
4
|
+
</div>
|
|
5
|
+
<h1 class="page-heading">Request Detail</h1>
|
|
4
6
|
</div>
|
|
5
7
|
|
|
6
8
|
<%# Header %>
|
|
7
9
|
<div class="card flex-between">
|
|
8
10
|
<div>
|
|
9
|
-
<div class="
|
|
11
|
+
<div class="score-display" style="color:<%= score_color(@record.color) %>;">
|
|
10
12
|
<%= @record.score %>
|
|
11
13
|
<span class="text-muted" style="font-size:16px;">/ 100</span>
|
|
12
14
|
</div>
|
|
13
15
|
</div>
|
|
14
16
|
<div class="text-right">
|
|
15
|
-
<span class="<%=
|
|
17
|
+
<span class="<%= badge_class(@record.color) %>" style="font-size:13px;padding:4px 14px;">
|
|
16
18
|
<%= @record.label %>
|
|
17
19
|
</span>
|
|
18
20
|
<div class="text-muted text-sm mt-6"><%= @record.recorded_at.strftime("%Y-%m-%d %H:%M:%S") %></div>
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
<tr><td class="text-muted" style="width:140px;">Endpoint</td><td><%= @record.endpoint %></td></tr>
|
|
28
30
|
<tr><td class="text-muted">Method</td> <td><%= @record.http_method %></td></tr>
|
|
29
31
|
<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
|
|
32
|
+
<tr><td class="text-muted">Duration</td><td><%= format_ms(@record.duration_ms) %></td></tr>
|
|
31
33
|
</tbody>
|
|
32
34
|
</table>
|
|
33
35
|
</div>
|
|
@@ -38,15 +40,14 @@
|
|
|
38
40
|
<table>
|
|
39
41
|
<tbody>
|
|
40
42
|
<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
|
|
43
|
+
<tr><td class="text-muted">DB Time</td> <td><%= format_ms(@record.total_db_time_ms) %></td></tr>
|
|
42
44
|
<tr>
|
|
43
45
|
<td class="text-muted">N+1 Patterns</td>
|
|
44
46
|
<td>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<% end %>
|
|
47
|
+
<%= render "rails_vitals/shared/n1_indicator",
|
|
48
|
+
patterns: @record.n_plus_one_patterns,
|
|
49
|
+
suffix: "detected",
|
|
50
|
+
none_label: "None detected" %>
|
|
50
51
|
</td>
|
|
51
52
|
</tr>
|
|
52
53
|
</tbody>
|
|
@@ -95,7 +96,7 @@
|
|
|
95
96
|
<% @record.queries.sort_by { |q| -q[:duration_ms] }.each do |q| %>
|
|
96
97
|
<tr>
|
|
97
98
|
<td class="sql"><%= q[:sql] %></td>
|
|
98
|
-
<td class="text-nowrap"><%= q[:duration_ms]
|
|
99
|
+
<td class="text-nowrap"><%= format_ms(q[:duration_ms]) %></td>
|
|
99
100
|
</tr>
|
|
100
101
|
<% end %>
|
|
101
102
|
</tbody>
|
|
@@ -109,7 +110,7 @@
|
|
|
109
110
|
Callback Map
|
|
110
111
|
<span class="text-muted ml-8" style="font-weight:normal;">
|
|
111
112
|
<%= @record.callbacks.size %> callbacks —
|
|
112
|
-
<%= @record.total_callback_time_ms
|
|
113
|
+
<%= format_ms(@record.total_callback_time_ms) %> total
|
|
113
114
|
</span>
|
|
114
115
|
</div>
|
|
115
116
|
<% @record.callbacks.group_by { |c| c[:model] }.each do |model, callbacks| %>
|
|
@@ -117,8 +118,9 @@
|
|
|
117
118
|
<div class="text-accent bold text-12 mb-6" style="padding-bottom:4px;border-bottom:1px solid #2d3748;">
|
|
118
119
|
<%= model %>
|
|
119
120
|
<span class="text-muted" style="font-weight:normal;">
|
|
121
|
+
<td class="text-muted ml-6">
|
|
120
122
|
(<%= callbacks.size %> callbacks,
|
|
121
|
-
<%= callbacks.sum { |c| c[:duration_ms] }
|
|
123
|
+
<%= format_ms(callbacks.sum { |c| c[:duration_ms] }) %>)
|
|
122
124
|
</span>
|
|
123
125
|
</div>
|
|
124
126
|
<table>
|
|
@@ -137,11 +139,9 @@
|
|
|
137
139
|
<%= cb[:kind] %>
|
|
138
140
|
</span>
|
|
139
141
|
</td>
|
|
140
|
-
<td><%= cb[:duration_ms]
|
|
142
|
+
<td><%= format_ms(cb[:duration_ms]) %></td>
|
|
141
143
|
<td class="text-muted">
|
|
142
|
-
|
|
143
|
-
((cb[:duration_ms] / @record.total_callback_time_ms) * 100).round(1) : 0 %>
|
|
144
|
-
<%= pct %>%
|
|
144
|
+
<%= percentage(cb[:duration_ms], @record.total_callback_time_ms) %>%
|
|
145
145
|
</td>
|
|
146
146
|
</tr>
|
|
147
147
|
<% end %>
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
<% dna_id = "dna_#{i}" %>
|
|
181
181
|
|
|
182
182
|
<%# Query row — clickable %>
|
|
183
|
-
<tr onclick="toggleDna(
|
|
183
|
+
<tr onclick="toggleDna(<%= dna_id.to_json %>)" class="cursor-pointer">
|
|
184
184
|
<td class="text-muted"><%= i + 1 %></td>
|
|
185
185
|
<td class="mono text-sm text-primary truncate" style="max-width:400px;">
|
|
186
186
|
<%= q[:sql] %>
|
|
@@ -195,8 +195,7 @@
|
|
|
195
195
|
</span>
|
|
196
196
|
</td>
|
|
197
197
|
<td>
|
|
198
|
-
|
|
199
|
-
<span class="text-12" style="color:<%= risk_colors[dna.risk] %>;text-transform:capitalize;">
|
|
198
|
+
<span class="text-12" style="color:<%= risk_color(dna.risk) %>;text-transform:capitalize;">
|
|
200
199
|
<%= dna.risk %>
|
|
201
200
|
</span>
|
|
202
201
|
</td>
|
|
@@ -234,7 +233,7 @@
|
|
|
234
233
|
<div class="flex flex-wrap" style="gap:6px;">
|
|
235
234
|
<% dna.tokens.each do |token| %>
|
|
236
235
|
<span
|
|
237
|
-
onclick="toggleCard(
|
|
236
|
+
onclick="toggleCard(<%= "card_#{dna_id}_#{token[:type]}".to_json %>); event.stopPropagation();"
|
|
238
237
|
class="mono cursor-pointer"
|
|
239
238
|
style="background:<%= token[:color] %>22;color:<%= token[:color] %>;border:1px solid <%= token[:color] %>66;padding:3px 10px;border-radius:4px;font-size:12px;"
|
|
240
239
|
>
|
|
@@ -270,8 +269,7 @@
|
|
|
270
269
|
</div>
|
|
271
270
|
<div>
|
|
272
271
|
<span class="text-muted">Risk</span>
|
|
273
|
-
|
|
274
|
-
<span class="bold ml-6" style="color:<%= risk_colors[dna.risk] %>;text-transform:capitalize;">
|
|
272
|
+
<span class="bold ml-6" style="color:<%= risk_color(dna.risk) %>;text-transform:capitalize;">
|
|
275
273
|
<%= dna.risk %>
|
|
276
274
|
</span>
|
|
277
275
|
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<% if local_assigns[:back_path] %>
|
|
3
|
+
<div class="mb-8">
|
|
4
|
+
<%= link_to local_assigns.fetch(:back_label, "← Back"), back_path, class: "back-link" %>
|
|
5
|
+
</div>
|
|
6
|
+
<% end %>
|
|
7
|
+
<h1 class="page-heading">
|
|
8
|
+
<%= title %>
|
|
9
|
+
</h1>
|
|
10
|
+
<% if local_assigns[:subtitle] %>
|
|
11
|
+
<p class="page-subtitle">
|
|
12
|
+
<%= subtitle %>
|
|
13
|
+
</p>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
@@ -104,7 +104,7 @@ module RailsVitals
|
|
|
104
104
|
canvas_h = 120 + (by_depth.keys.max || 0) * 160
|
|
105
105
|
|
|
106
106
|
by_depth.each do |depth, layer_nodes|
|
|
107
|
-
count
|
|
107
|
+
count = layer_nodes.size
|
|
108
108
|
x_step = canvas_w / (count + 1)
|
|
109
109
|
layer_nodes.each_with_index do |node, i|
|
|
110
110
|
node.position = {
|