rails_pulse 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +638 -0
- data/Rakefile +207 -0
- data/app/assets/images/rails_pulse/dashboard.png +0 -0
- data/app/assets/images/rails_pulse/menu.svg +1 -0
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/images/rails_pulse/request.png +0 -0
- data/app/assets/images/rails_pulse/routes.png +0 -0
- data/app/assets/stylesheets/rails_pulse/application.css +102 -0
- data/app/assets/stylesheets/rails_pulse/components/alert.css +24 -0
- data/app/assets/stylesheets/rails_pulse/components/badge.css +58 -0
- data/app/assets/stylesheets/rails_pulse/components/base.css +79 -0
- data/app/assets/stylesheets/rails_pulse/components/breadcrumb.css +31 -0
- data/app/assets/stylesheets/rails_pulse/components/button.css +99 -0
- data/app/assets/stylesheets/rails_pulse/components/card.css +19 -0
- data/app/assets/stylesheets/rails_pulse/components/chart.css +18 -0
- data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +86 -0
- data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +9 -0
- data/app/assets/stylesheets/rails_pulse/components/dialog.css +56 -0
- data/app/assets/stylesheets/rails_pulse/components/flash.css +47 -0
- data/app/assets/stylesheets/rails_pulse/components/input.css +80 -0
- data/app/assets/stylesheets/rails_pulse/components/layouts.css +63 -0
- data/app/assets/stylesheets/rails_pulse/components/menu.css +43 -0
- data/app/assets/stylesheets/rails_pulse/components/popover.css +36 -0
- data/app/assets/stylesheets/rails_pulse/components/prose.css +144 -0
- data/app/assets/stylesheets/rails_pulse/components/row.css +24 -0
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +79 -0
- data/app/assets/stylesheets/rails_pulse/components/skeleton.css +5 -0
- data/app/assets/stylesheets/rails_pulse/components/table.css +37 -0
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +36 -0
- data/app/controllers/concerns/chart_table_concern.rb +82 -0
- data/app/controllers/concerns/response_range_concern.rb +24 -0
- data/app/controllers/concerns/time_range_concern.rb +67 -0
- data/app/controllers/concerns/zoom_range_concern.rb +40 -0
- data/app/controllers/rails_pulse/application_controller.rb +67 -0
- data/app/controllers/rails_pulse/assets_controller.rb +33 -0
- data/app/controllers/rails_pulse/caches_controller.rb +115 -0
- data/app/controllers/rails_pulse/csp_test_controller.rb +57 -0
- data/app/controllers/rails_pulse/dashboard_controller.rb +6 -0
- data/app/controllers/rails_pulse/operations_controller.rb +219 -0
- data/app/controllers/rails_pulse/queries_controller.rb +121 -0
- data/app/controllers/rails_pulse/requests_controller.rb +69 -0
- data/app/controllers/rails_pulse/routes_controller.rb +99 -0
- data/app/helpers/rails_pulse/application_helper.rb +111 -0
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +62 -0
- data/app/helpers/rails_pulse/cached_component_helper.rb +73 -0
- data/app/helpers/rails_pulse/chart_formatters.rb +43 -0
- data/app/helpers/rails_pulse/chart_helper.rb +140 -0
- data/app/helpers/rails_pulse/formatting_helper.rb +29 -0
- data/app/helpers/rails_pulse/status_helper.rb +279 -0
- data/app/helpers/rails_pulse/table_helper.rb +54 -0
- data/app/javascript/rails_pulse/application.js +119 -0
- data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +20 -0
- data/app/javascript/rails_pulse/controllers/context_menu_controller.js +16 -0
- data/app/javascript/rails_pulse/controllers/dialog_controller.js +21 -0
- data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +67 -0
- data/app/javascript/rails_pulse/controllers/form_controller.js +39 -0
- data/app/javascript/rails_pulse/controllers/icon_controller.js +170 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +230 -0
- data/app/javascript/rails_pulse/controllers/menu_controller.js +60 -0
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +69 -0
- data/app/javascript/rails_pulse/controllers/popover_controller.js +91 -0
- data/app/javascript/rails_pulse/controllers/timezone_controller.js +106 -0
- data/app/javascript/rails_pulse/theme.js +416 -0
- data/app/jobs/rails_pulse/application_job.rb +4 -0
- data/app/jobs/rails_pulse/cleanup_job.rb +21 -0
- data/app/mailers/rails_pulse/application_mailer.rb +6 -0
- data/app/models/rails_pulse/application_record.rb +7 -0
- data/app/models/rails_pulse/component_cache_key.rb +33 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +27 -0
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +37 -0
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +59 -0
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +45 -0
- data/app/models/rails_pulse/operation.rb +87 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +52 -0
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +57 -0
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +71 -0
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +112 -0
- data/app/models/rails_pulse/query.rb +58 -0
- data/app/models/rails_pulse/request.rb +64 -0
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +99 -0
- data/app/models/rails_pulse/requests/charts/operations_chart.rb +35 -0
- data/app/models/rails_pulse/route.rb +77 -0
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +54 -0
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +73 -0
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +73 -0
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +59 -0
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +115 -0
- data/app/models/rails_pulse/routes/tables/index.rb +63 -0
- data/app/services/rails_pulse/sql_query_normalizer.rb +124 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +19 -0
- data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +44 -0
- data/app/views/layouts/rails_pulse/application.html.erb +72 -0
- data/app/views/rails_pulse/caches/show.html.erb +9 -0
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +12 -0
- data/app/views/rails_pulse/components/_code_panel.html.erb +12 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +55 -0
- data/app/views/rails_pulse/components/_metric_row.html.erb +9 -0
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +241 -0
- data/app/views/rails_pulse/components/_panel.html.erb +56 -0
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +15 -0
- data/app/views/rails_pulse/components/_table.html.erb +50 -0
- data/app/views/rails_pulse/components/_table_head.html.erb +20 -0
- data/app/views/rails_pulse/components/_table_pagination.html.erb +45 -0
- data/app/views/rails_pulse/components/_time_period.html.erb +16 -0
- data/app/views/rails_pulse/csp_test/show.html.erb +207 -0
- data/app/views/rails_pulse/dashboard/charts/_bar_chart.html.erb +1 -0
- data/app/views/rails_pulse/dashboard/index.html.erb +64 -0
- data/app/views/rails_pulse/dashboard/tables/_routes_table.html.erb +32 -0
- data/app/views/rails_pulse/dashboard/tables/_standard_table.html.erb +1 -0
- data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +43 -0
- data/app/views/rails_pulse/operations/_operation_analysis_database.html.erb +12 -0
- data/app/views/rails_pulse/operations/_operation_analysis_generic.html.erb +15 -0
- data/app/views/rails_pulse/operations/_operation_analysis_other.html.erb +69 -0
- data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +39 -0
- data/app/views/rails_pulse/operations/show.html.erb +79 -0
- data/app/views/rails_pulse/queries/_show_table.html.erb +19 -0
- data/app/views/rails_pulse/queries/_table.html.erb +31 -0
- data/app/views/rails_pulse/queries/index.html.erb +64 -0
- data/app/views/rails_pulse/queries/show.html.erb +86 -0
- data/app/views/rails_pulse/requests/_operations.html.erb +85 -0
- data/app/views/rails_pulse/requests/_table.html.erb +31 -0
- data/app/views/rails_pulse/requests/index.html.erb +64 -0
- data/app/views/rails_pulse/requests/show.html.erb +44 -0
- data/app/views/rails_pulse/routes/_table.html.erb +29 -0
- data/app/views/rails_pulse/routes/index.html.erb +65 -0
- data/app/views/rails_pulse/routes/show.html.erb +67 -0
- data/app/views/rails_pulse/skeletons/_chart.html.erb +3 -0
- data/app/views/rails_pulse/skeletons/_metric_card.html.erb +20 -0
- data/app/views/rails_pulse/skeletons/_panel.html.erb +19 -0
- data/app/views/rails_pulse/skeletons/_table.html.erb +8 -0
- data/config/importmap.rb +12 -0
- data/config/initializers/rails_charts_csp_patch.rb +83 -0
- data/config/initializers/rails_pulse.rb +198 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20250227235904_create_routes.rb +12 -0
- data/db/migrate/20250227235915_create_requests.rb +19 -0
- data/db/migrate/20250228000000_create_queries.rb +14 -0
- data/db/migrate/20250228000056_create_operations.rb +24 -0
- data/lib/generators/rails_pulse/install_generator.rb +17 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +198 -0
- data/lib/rails_pulse/cleanup_service.rb +212 -0
- data/lib/rails_pulse/configuration.rb +176 -0
- data/lib/rails_pulse/engine.rb +88 -0
- data/lib/rails_pulse/middleware/asset_server.rb +84 -0
- data/lib/rails_pulse/middleware/request_collector.rb +120 -0
- data/lib/rails_pulse/migration.rb +29 -0
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +280 -0
- data/lib/rails_pulse/version.rb +3 -0
- data/lib/rails_pulse.rb +38 -0
- data/lib/tasks/rails_pulse_tasks.rake +138 -0
- data/public/rails-pulse-assets/csp-test.js +110 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +89 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +13 -0
- data/public/rails-pulse-assets/rails-pulse.css +1 -0
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -0
- data/public/rails-pulse-assets/rails-pulse.js +183 -0
- data/public/rails-pulse-assets/rails-pulse.js.map +7 -0
- metadata +339 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
<div class="flex gap items-center justify-end mbe-4">
|
2
|
+
<p>Time period</p>
|
3
|
+
<div data-controller="rails-pulse--popover" data-rails-pulse--popover-placement-value="bottom-start">
|
4
|
+
<button type="button" id="menu_button" class="btn" data-rails-pulse--popover-target="button" data-action="rails-pulse--popover#toggle" aria-haspopup="true" aria-controls="menu">
|
5
|
+
1 month
|
6
|
+
<%= rails_pulse_icon 'chevron-down' %>
|
7
|
+
</button>
|
8
|
+
<div popover class="popover" style="--popover-size: 8rem;" data-rails-pulse--popover-target="menu">
|
9
|
+
<div id="menu" class="menu" data-controller="rails-pulse--menu" data-action="keydown.up->rails-pulse--menu#prev keydown.down->rails-pulse--menu#next" role="menu" aria-labelledby="menu_button">
|
10
|
+
<a class="btn menu__item" data-rails-pulse--menu-target="item" role="menuitem" href="">1 week</a>
|
11
|
+
<a class="btn menu__item" data-rails-pulse--menu-target="item" role="menuitem" href="">1 month</a>
|
12
|
+
<a class="btn menu__item" data-rails-pulse--menu-target="item" role="menuitem" href="">3 months</a>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
</div>
|
16
|
+
</div>
|
@@ -0,0 +1,207 @@
|
|
1
|
+
<% content_for :title, "CSP Compliance Test" %>
|
2
|
+
|
3
|
+
<div class="p-6">
|
4
|
+
<h1 class="text-2xl font-bold mb-4">Content Security Policy Compliance Test</h1>
|
5
|
+
|
6
|
+
<p class="text-subtle mb-6">This page tests Rails Pulse components under strict CSP policies to ensure all assets and functionality work safely.</p>
|
7
|
+
|
8
|
+
<div class="flex flex-col gap">
|
9
|
+
|
10
|
+
<!-- Icon Controller Test -->
|
11
|
+
<div class="card">
|
12
|
+
<div class="p-4">
|
13
|
+
<h2 class="text-lg font-semibold mb-3">Icon Controller Test</h2>
|
14
|
+
<p class="text-subtle mb-4">Testing CSP-safe icon rendering with pre-compiled assets:</p>
|
15
|
+
|
16
|
+
<div class="flex gap items-center mb-3">
|
17
|
+
<span
|
18
|
+
data-controller="rails-pulse--icon"
|
19
|
+
data-rails-pulse--icon-name-value="menu"
|
20
|
+
data-rails-pulse--icon-width-value="24"
|
21
|
+
data-rails-pulse--icon-height-value="24"
|
22
|
+
class="loading error loaded"
|
23
|
+
></span>
|
24
|
+
<span class="mis-2">Menu Icon</span>
|
25
|
+
</div>
|
26
|
+
|
27
|
+
<div class="flex gap items-center mb-3">
|
28
|
+
<span
|
29
|
+
data-controller="rails-pulse--icon"
|
30
|
+
data-rails-pulse--icon-name-value="check"
|
31
|
+
data-rails-pulse--icon-width-value="20"
|
32
|
+
data-rails-pulse--icon-height-value="20"
|
33
|
+
></span>
|
34
|
+
<span class="mis-2">Check Icon</span>
|
35
|
+
</div>
|
36
|
+
|
37
|
+
<div class="flex gap items-center">
|
38
|
+
<span
|
39
|
+
data-controller="rails-pulse--icon"
|
40
|
+
data-rails-pulse--icon-name-value="nonexistent"
|
41
|
+
data-rails-pulse--icon-width-value="16"
|
42
|
+
data-rails-pulse--icon-height-value="16"
|
43
|
+
></span>
|
44
|
+
<span class="mis-2">Missing Icon (should show placeholder)</span>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<!-- Popover Controller Test -->
|
50
|
+
<div class="card">
|
51
|
+
<div class="p-4">
|
52
|
+
<h2 class="text-lg font-semibold mb-3">Popover Controller Test</h2>
|
53
|
+
<p class="text-subtle mb-4">Testing CSP-safe popover positioning:</p>
|
54
|
+
|
55
|
+
<div data-controller="rails-pulse--popover">
|
56
|
+
<button
|
57
|
+
data-rails-pulse--popover-target="button"
|
58
|
+
data-action="click->rails-pulse--popover#toggle"
|
59
|
+
class="btn btn--primary"
|
60
|
+
>
|
61
|
+
Show Popover
|
62
|
+
</button>
|
63
|
+
|
64
|
+
<div
|
65
|
+
data-rails-pulse--popover-target="menu"
|
66
|
+
popover="auto"
|
67
|
+
class="bg-white border rounded-md shadow-md p-3"
|
68
|
+
>
|
69
|
+
<p class="mb-2">This popover uses CSS custom properties for positioning (CSP-safe)</p>
|
70
|
+
<p class="text-subtle">No inline styles are used.</p>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
</div>
|
75
|
+
|
76
|
+
<!-- Context Menu Controller Test -->
|
77
|
+
<div class="card">
|
78
|
+
<div class="p-4">
|
79
|
+
<h2 class="text-lg font-semibold mb-3">Context Menu Controller Test</h2>
|
80
|
+
<p class="text-subtle mb-4">Testing CSP-safe context menu positioning:</p>
|
81
|
+
|
82
|
+
<div data-controller="rails-pulse--context-menu">
|
83
|
+
<div
|
84
|
+
class="card p-4 border border-dark bg-shade text-center"
|
85
|
+
data-action="contextmenu->rails-pulse--context-menu#show"
|
86
|
+
>
|
87
|
+
Right-click me to show context menu
|
88
|
+
</div>
|
89
|
+
|
90
|
+
<div
|
91
|
+
data-rails-pulse--context-menu-target="menu"
|
92
|
+
popover="auto"
|
93
|
+
class="bg-white border rounded-md shadow-md"
|
94
|
+
>
|
95
|
+
<div class="p-2">
|
96
|
+
<button class="block i-full text-start p-2 hover:bg-shade">Copy</button>
|
97
|
+
<button class="block i-full text-start p-2 hover:bg-shade">Paste</button>
|
98
|
+
<button class="block i-full text-start p-2 hover:bg-shade">Delete</button>
|
99
|
+
</div>
|
100
|
+
</div>
|
101
|
+
</div>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
|
105
|
+
<!-- Index Controller Test -->
|
106
|
+
<div class="card">
|
107
|
+
<div class="p-4">
|
108
|
+
<h2 class="text-lg font-semibold mb-3">Index Controller Test (Turbo Frame)</h2>
|
109
|
+
<p class="text-subtle mb-4">Testing CSP-safe DOM manipulation for Turbo Frame updates:</p>
|
110
|
+
|
111
|
+
<turbo-frame id="csp-test-frame">
|
112
|
+
<div class="bg-shade border border-main rounded-md p-4">
|
113
|
+
<p class="mb-2">This content can be updated via Turbo Frame requests using CSP-safe DOM methods.</p>
|
114
|
+
<p class="text-subtle">No innerHTML usage - only DOM parser and appendChild methods.</p>
|
115
|
+
</div>
|
116
|
+
</turbo-frame>
|
117
|
+
</div>
|
118
|
+
</div>
|
119
|
+
|
120
|
+
<!-- Asset Loading Test -->
|
121
|
+
<div class="card">
|
122
|
+
<div class="p-4">
|
123
|
+
<h2 class="text-lg font-semibold mb-3">Asset Loading Test</h2>
|
124
|
+
<p class="text-subtle mb-4">Verifying all Rails Pulse assets load correctly under strict CSP:</p>
|
125
|
+
|
126
|
+
<div class="flex flex-col gap">
|
127
|
+
<div class="flex justify-between items-center">
|
128
|
+
<span>CSS Bundle (rails-pulse.css):</span>
|
129
|
+
<span id="css-status" class="badge badge--secondary">Loading...</span>
|
130
|
+
</div>
|
131
|
+
|
132
|
+
<div class="flex justify-between items-center">
|
133
|
+
<span>JavaScript Bundle (rails-pulse.js):</span>
|
134
|
+
<span id="js-status" class="badge badge--secondary">Loading...</span>
|
135
|
+
</div>
|
136
|
+
|
137
|
+
<div class="flex justify-between items-center">
|
138
|
+
<span>Icons Bundle (rails-pulse-icons.js):</span>
|
139
|
+
<span id="icons-status" class="badge badge--secondary">Loading...</span>
|
140
|
+
</div>
|
141
|
+
|
142
|
+
<div class="flex justify-between items-center">
|
143
|
+
<span>Stimulus Controllers:</span>
|
144
|
+
<span id="stimulus-status" class="badge badge--secondary">Loading...</span>
|
145
|
+
</div>
|
146
|
+
</div>
|
147
|
+
</div>
|
148
|
+
</div>
|
149
|
+
|
150
|
+
<!-- AJAX Request Test -->
|
151
|
+
<div class="card">
|
152
|
+
<div class="p-4">
|
153
|
+
<h2 class="text-lg font-semibold mb-3">AJAX Request Test</h2>
|
154
|
+
<p class="text-subtle mb-4">Testing CSP compliance for dynamic content loading:</p>
|
155
|
+
|
156
|
+
<button
|
157
|
+
id="ajax-test-btn"
|
158
|
+
class="btn btn--primary mb-3"
|
159
|
+
data-testid="ajax-test-button"
|
160
|
+
>
|
161
|
+
Test AJAX Loading
|
162
|
+
</button>
|
163
|
+
|
164
|
+
<div id="ajax-result" class="bg-shade border border-main rounded-md p-4">
|
165
|
+
<span class="text-subtle">Click button to test AJAX request...</span>
|
166
|
+
</div>
|
167
|
+
</div>
|
168
|
+
</div>
|
169
|
+
|
170
|
+
<!-- Chart Component Test -->
|
171
|
+
<div class="card">
|
172
|
+
<div class="p-4">
|
173
|
+
<h2 class="text-lg font-semibold mb-3">Chart Component Test</h2>
|
174
|
+
<p class="text-subtle mb-4">Testing rails_charts CSP compliance:</p>
|
175
|
+
|
176
|
+
<div id="chart-container" class="bg-shade border border-main rounded-md p-4 text-center">
|
177
|
+
<span class="text-subtle">Chart components would render here in a real dashboard</span>
|
178
|
+
</div>
|
179
|
+
</div>
|
180
|
+
</div>
|
181
|
+
|
182
|
+
<!-- CSP Violation Test -->
|
183
|
+
<div class="card">
|
184
|
+
<div class="p-4">
|
185
|
+
<h2 class="text-lg font-semibold mb-3">CSP Violation Summary</h2>
|
186
|
+
<p class="mb-2">Check the browser console for any CSP violations.</p>
|
187
|
+
<p class="text-subtle mb-4">A properly implemented CSP-safe system should show zero violations.</p>
|
188
|
+
|
189
|
+
<div class="bg-shade border border-main rounded-md p-4">
|
190
|
+
<div class="mb-2">
|
191
|
+
<strong class="font-semibold">Expected Result:</strong>
|
192
|
+
<span class="text-subtle">No CSP violations in browser console</span>
|
193
|
+
</div>
|
194
|
+
<div>
|
195
|
+
<strong class="font-semibold">Violation Count:</strong>
|
196
|
+
<span id="violation-count" class="badge badge--secondary">0</span>
|
197
|
+
</div>
|
198
|
+
</div>
|
199
|
+
</div>
|
200
|
+
</div>
|
201
|
+
|
202
|
+
</div>
|
203
|
+
</div>
|
204
|
+
|
205
|
+
<%= content_for :head do %>
|
206
|
+
<script src="/rails-pulse-assets/csp-test.js" nonce="<%= request_nonce %>"></script>
|
207
|
+
<% end %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= bar_chart @component_data, height: "100%", options: bar_chart_options(units: "ms") %>
|
@@ -0,0 +1,64 @@
|
|
1
|
+
<div class="row">
|
2
|
+
<%= cached_component(component: "metric_card", id: "average_response_times", class: "grid-item block") %>
|
3
|
+
<%= cached_component(component: "metric_card", id: "percentile_response_times", class: "grid-item block") %>
|
4
|
+
<%= cached_component(component: "metric_card", id: "request_count_totals", class: "grid-item block") %>
|
5
|
+
<%= cached_component(component: "metric_card", id: "error_rate_per_route", class: "grid-item block") %>
|
6
|
+
</div>
|
7
|
+
|
8
|
+
<div class="row">
|
9
|
+
<div class="grid-item" style="height:300px">
|
10
|
+
<%= cached_component(
|
11
|
+
component: "panel",
|
12
|
+
id: "dashboard_average_response_time",
|
13
|
+
card_classes: 'b-full',
|
14
|
+
title: 'Average Response Time',
|
15
|
+
help_heading: 'Average Response Time',
|
16
|
+
help_text: 'This panel measures the average server-side response time in milliseconds for all HTTP requests processed by your application. This is the time it takes from the server receiving a request until the request is returned to the client in milliseconds.',
|
17
|
+
actions: [{ url: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
|
18
|
+
refresh_action: true,
|
19
|
+
content_partial: 'rails_pulse/dashboard/charts/bar_chart')
|
20
|
+
%>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div class="grid-item">
|
24
|
+
<%= cached_component(
|
25
|
+
component: "panel",
|
26
|
+
id: "dashboard_p95_response_time",
|
27
|
+
card_classes: 'b-full',
|
28
|
+
title: 'Query Performance',
|
29
|
+
help_heading: 'Query Performance',
|
30
|
+
help_text: 'This panel measures the 95th percentile response time in milliseconds for queries in your application. This represents the query performance below which 95% of queries fall, indicating the upper-bound performance for most database operations.',
|
31
|
+
actions: [{ url: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
|
32
|
+
refresh_action: true,
|
33
|
+
content_partial: 'rails_pulse/dashboard/charts/bar_chart')
|
34
|
+
%>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
|
38
|
+
<div class="row">
|
39
|
+
<div class="grid-item">
|
40
|
+
<%= cached_component(
|
41
|
+
component: "panel",
|
42
|
+
id: "dashboard_slow_routes",
|
43
|
+
title: 'Slowest Routes This Week',
|
44
|
+
help_heading: 'Slowest Routes',
|
45
|
+
help_text: 'This panel shows the slowest routes in your application this week compared to last week, including average response time, week-over-week change, and total request count.',
|
46
|
+
refresh_action: true,
|
47
|
+
content_partial: 'rails_pulse/dashboard/tables/standard_table',
|
48
|
+
class: "table-container")
|
49
|
+
%>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
<div class="grid-item">
|
53
|
+
<%= cached_component(
|
54
|
+
component: "panel",
|
55
|
+
id: "dashboard_slow_queries",
|
56
|
+
title: 'Slowest Queries This Week',
|
57
|
+
help_heading: 'Slowest Queries',
|
58
|
+
help_text: 'This panel shows the slowest database queries in your application this week, including average execution time and when they were last seen.',
|
59
|
+
refresh_action: true,
|
60
|
+
content_partial: 'rails_pulse/dashboard/tables/standard_table',
|
61
|
+
class: "table-container")
|
62
|
+
%>
|
63
|
+
</div>
|
64
|
+
</div>
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<table class="table mbs-4">
|
2
|
+
<thead>
|
3
|
+
<tr>
|
4
|
+
<th>Route</th>
|
5
|
+
<th>This Week Avg (ms)</th>
|
6
|
+
<th>Last Week Avg (ms)</th>
|
7
|
+
<th>Change</th>
|
8
|
+
<th>Requests</th>
|
9
|
+
</tr>
|
10
|
+
</thead>
|
11
|
+
<tbody>
|
12
|
+
<% if table_data.any? %>
|
13
|
+
<% table_data.each do |route_data| %>
|
14
|
+
<tr>
|
15
|
+
<td><%= route_data[:route_path] %></td>
|
16
|
+
<td class="<%= route_data[:trend] == 'worse' ? 'highlight-red' : (route_data[:trend] == 'better' ? 'highlight-green' : '') %>">
|
17
|
+
<%= route_data[:this_week_avg] %>
|
18
|
+
</td>
|
19
|
+
<td><%= route_data[:last_week_avg] %></td>
|
20
|
+
<td class="<%= route_data[:percentage_change] > 5 ? 'highlight-red' : (route_data[:percentage_change] < -5 ? 'highlight-green' : '') %>">
|
21
|
+
<%= route_data[:percentage_change] > 0 ? '+' : '' %><%= route_data[:percentage_change] %>%
|
22
|
+
</td>
|
23
|
+
<td><%= route_data[:request_count] %></td>
|
24
|
+
</tr>
|
25
|
+
<% end %>
|
26
|
+
<% else %>
|
27
|
+
<tr>
|
28
|
+
<td colspan="5" class="text-center text-subtle">No route data available for the selected time period</td>
|
29
|
+
</tr>
|
30
|
+
<% end %>
|
31
|
+
</tbody>
|
32
|
+
</table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render 'rails_pulse/components/table', table_data: @component_data %>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<% if operation.label&.match?(/(\w+)#(\w+)/) %>
|
2
|
+
<% controller, action = operation.label.match(/(\w+)#(\w+)/)&.captures %>
|
3
|
+
<dt>Controller</dt>
|
4
|
+
<dd><%= html_escape(controller) %></dd>
|
5
|
+
<dt>Action</dt>
|
6
|
+
<dd><%= html_escape(action) %></dd>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<% total_db_time = operation.request.operations
|
10
|
+
.where(operation_type: ["sql"])
|
11
|
+
.sum(:duration) %>
|
12
|
+
<% total_view_time = operation.request.operations
|
13
|
+
.where(operation_type: ["template", "partial", "layout", "collection"])
|
14
|
+
.sum(:duration) %>
|
15
|
+
<% pure_controller_time = operation.duration - total_db_time - total_view_time %>
|
16
|
+
|
17
|
+
<% if pure_controller_time > 0 %>
|
18
|
+
<dt>Pure Logic Time</dt>
|
19
|
+
<dd>
|
20
|
+
<%= pure_controller_time.round(1) %>ms
|
21
|
+
<% if pure_controller_time > 100 %>
|
22
|
+
- consider optimization
|
23
|
+
<% end %>
|
24
|
+
</dd>
|
25
|
+
<% end %>
|
26
|
+
|
27
|
+
<% db_operations_count = operation.request.operations
|
28
|
+
.where(operation_type: ["sql"])
|
29
|
+
.count %>
|
30
|
+
<% if db_operations_count > 0 %>
|
31
|
+
<dt>Database Queries</dt>
|
32
|
+
<dd>
|
33
|
+
<%= db_operations_count %>
|
34
|
+
<% if db_operations_count > 10 %>
|
35
|
+
- potential N+1 queries
|
36
|
+
<% end %>
|
37
|
+
</dd>
|
38
|
+
<% end %>
|
39
|
+
|
40
|
+
<% if operation.codebase_location.present? %>
|
41
|
+
<dt>Source</dt>
|
42
|
+
<dd><code><%= html_escape(operation.codebase_location.split('/').last(3).join('/')) %></code></dd>
|
43
|
+
<% end %>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<dt>SQL Analysis</dt>
|
2
|
+
<dd>
|
3
|
+
<% if operation.duration > RailsPulse.configuration.query_thresholds[:critical] %>
|
4
|
+
Critically slow query (><%= RailsPulse.configuration.query_thresholds[:critical] %> ms)
|
5
|
+
<% elsif operation.duration > RailsPulse.configuration.query_thresholds[:very_slow] %>
|
6
|
+
Very slow query (><%= RailsPulse.configuration.query_thresholds[:very_slow] %> ms)
|
7
|
+
<% elsif operation.duration > RailsPulse.configuration.query_thresholds[:slow] %>
|
8
|
+
Slow query (><%= RailsPulse.configuration.query_thresholds[:slow] %> ms)
|
9
|
+
<% else %>
|
10
|
+
Normal query (<<%= RailsPulse.configuration.query_thresholds[:slow] %> ms)
|
11
|
+
<% end %>
|
12
|
+
</dd>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<dt>Duration</dt>
|
2
|
+
<dd><%= operation.duration.round(2) %>ms</dd>
|
3
|
+
|
4
|
+
<dt>Operation Type</dt>
|
5
|
+
<dd><%= operation.operation_type.humanize %></dd>
|
6
|
+
|
7
|
+
<% if operation.label.present? && operation.label != operation.operation_type %>
|
8
|
+
<dt>Operation</dt>
|
9
|
+
<dd><%= html_escape(truncate(operation.label, length: 150)) %></dd>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<% if operation.codebase_location.present? %>
|
13
|
+
<dt>Source</dt>
|
14
|
+
<dd><code><%= html_escape(operation.codebase_location.split('/').last(3).join('/')) %></code></dd>
|
15
|
+
<% end %>
|
@@ -0,0 +1,69 @@
|
|
1
|
+
<dt>Operation Performance</dt>
|
2
|
+
<dd>
|
3
|
+
<% case operation.operation_type %>
|
4
|
+
<% when "cache_read", "cache_write" %>
|
5
|
+
<% if operation.operation_type == "cache_read" %>
|
6
|
+
<% if operation.duration > 10 %>
|
7
|
+
Slow cache read (>10ms)
|
8
|
+
<% else %>
|
9
|
+
Fast cache read
|
10
|
+
<% end %>
|
11
|
+
<% else %>
|
12
|
+
<% if operation.duration > 50 %>
|
13
|
+
Slow cache write (>50ms)
|
14
|
+
<% else %>
|
15
|
+
Fast cache write
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
<% when "http" %>
|
19
|
+
<% if operation.duration > 1000 %>
|
20
|
+
Very slow request (>1s)
|
21
|
+
<% elsif operation.duration > 500 %>
|
22
|
+
Slow request (>500ms)
|
23
|
+
<% else %>
|
24
|
+
Fast request
|
25
|
+
<% end %>
|
26
|
+
<% when "job" %>
|
27
|
+
<% if operation.duration > 5000 %>
|
28
|
+
Long-running job (>5s)
|
29
|
+
<% elsif operation.duration > 1000 %>
|
30
|
+
Moderate job (>1s)
|
31
|
+
<% else %>
|
32
|
+
Quick job
|
33
|
+
<% end %>
|
34
|
+
<% when "mailer" %>
|
35
|
+
<% if operation.duration > 2000 %>
|
36
|
+
Slow email delivery (>2s)
|
37
|
+
<% elsif operation.duration > 500 %>
|
38
|
+
Moderate delivery (>500ms)
|
39
|
+
<% else %>
|
40
|
+
Fast delivery
|
41
|
+
<% end %>
|
42
|
+
<% when "storage" %>
|
43
|
+
<% if operation.duration > 1000 %>
|
44
|
+
Slow storage operation (>1s)
|
45
|
+
<% elsif operation.duration > 500 %>
|
46
|
+
Moderate storage (>500ms)
|
47
|
+
<% else %>
|
48
|
+
Fast storage
|
49
|
+
<% end %>
|
50
|
+
<% else %>
|
51
|
+
Duration: <%= operation.duration.round(2) %>ms
|
52
|
+
<% end %>
|
53
|
+
</dd>
|
54
|
+
|
55
|
+
<% if operation.operation_type == "http" && operation.label&.match?(/https?:\/\/([^\/\s]+)/) %>
|
56
|
+
<% domain = operation.label.match(/https?:\/\/([^\/\s]+)/)&.captures&.first %>
|
57
|
+
<dt>External Service</dt>
|
58
|
+
<dd><%= html_escape(domain) %></dd>
|
59
|
+
<% end %>
|
60
|
+
|
61
|
+
<% if operation.label.present? && operation.label != operation.operation_type %>
|
62
|
+
<dt>Details</dt>
|
63
|
+
<dd><%= html_escape(truncate(operation.label, length: 100)) %></dd>
|
64
|
+
<% end %>
|
65
|
+
|
66
|
+
<% if operation.codebase_location.present? %>
|
67
|
+
<dt>Source</dt>
|
68
|
+
<dd><code><%= html_escape(operation.codebase_location.split('/').last(3).join('/')) %></code></dd>
|
69
|
+
<% end %>
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<dt>Code Location</dt>
|
2
|
+
<dd>
|
3
|
+
<code>
|
4
|
+
<%= html_escape(operation.codebase_location.present? ? operation.codebase_location.split('/').last(3).join('/') : 'N/A') %>
|
5
|
+
</code>
|
6
|
+
</dd>
|
7
|
+
|
8
|
+
<dt>View Performance</dt>
|
9
|
+
<dd>
|
10
|
+
<% if operation.duration > 100 %>
|
11
|
+
Slow render (>100ms)
|
12
|
+
<% elsif operation.duration > 50 %>
|
13
|
+
Moderate render (>50ms)
|
14
|
+
<% else %>
|
15
|
+
Fast render
|
16
|
+
<% end %>
|
17
|
+
</dd>
|
18
|
+
|
19
|
+
<% if operation.label&.include?('_') %>
|
20
|
+
<dt>Template Type</dt>
|
21
|
+
<dd>Partial template - ensure data is pre-loaded in controller</dd>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
<% view_start = operation.occurred_at %>
|
25
|
+
<% view_end = operation.occurred_at + operation.duration %>
|
26
|
+
<% concurrent_db_ops = operation.request.operations
|
27
|
+
.where(operation_type: ["sql"])
|
28
|
+
.where("occurred_at >= ? AND occurred_at <= ?", view_start, view_end)
|
29
|
+
.count %>
|
30
|
+
|
31
|
+
<% if concurrent_db_ops > 0 %>
|
32
|
+
<dt>Queries During Rendering</dt>
|
33
|
+
<dd><%= concurrent_db_ops %> database <%= 'query'.pluralize(concurrent_db_ops) %></dd>
|
34
|
+
<% end %>
|
35
|
+
|
36
|
+
<% if operation.duration > 50 %>
|
37
|
+
<dt>Optimization Suggestion</dt>
|
38
|
+
<dd>Consider fragment caching: <code><% cache do %></code></dd>
|
39
|
+
<% end %>
|
@@ -0,0 +1,79 @@
|
|
1
|
+
<%= render 'rails_pulse/components/breadcrumbs' %>
|
2
|
+
|
3
|
+
<h1>Operation <%= @operation.id %></h1>
|
4
|
+
|
5
|
+
<div class="card">
|
6
|
+
<%= turbo_frame_tag "operation_#{@operation.id}_details" do %>
|
7
|
+
<dl class="descriptive-list">
|
8
|
+
<dt>
|
9
|
+
<% if @operation.operation_type == "sql" %>
|
10
|
+
Query
|
11
|
+
<% elsif ["template", "partial", "layout", "collection"].include?(@operation.operation_type) %>
|
12
|
+
View Render
|
13
|
+
<% else %>
|
14
|
+
<%= html_escape(@operation.label) %>
|
15
|
+
<% end %>
|
16
|
+
</dt>
|
17
|
+
<dd>
|
18
|
+
<% if @operation.operation_type == "sql" %>
|
19
|
+
<%= render 'rails_pulse/components/code_panel', { value: @operation.label } %>
|
20
|
+
<% else %>
|
21
|
+
<%= html_escape(@operation.label) %>
|
22
|
+
<% end %>
|
23
|
+
</dd>
|
24
|
+
|
25
|
+
<dt>Operation Type</dt>
|
26
|
+
<dd><pre><%= @operation.operation_type %></pre></dd>
|
27
|
+
|
28
|
+
<dt>Duration</dt>
|
29
|
+
<dd>
|
30
|
+
<%= @operation.duration.round(2) %> ms
|
31
|
+
</dd>
|
32
|
+
|
33
|
+
<dt>Request Impact</dt>
|
34
|
+
<dd>
|
35
|
+
<% impact = (@operation.duration / @request.duration * 100).round(1) %>
|
36
|
+
<%= impact %>% of total request time
|
37
|
+
</dd>
|
38
|
+
|
39
|
+
<dt>Occurred At</dt>
|
40
|
+
<dd><%= human_readable_occurred_at @operation.occurred_at %></dd>
|
41
|
+
|
42
|
+
<% begin %>
|
43
|
+
<%= render partial: "operation_analysis_#{categorize_operation(@operation.operation_type)}", locals: { operation: @operation } %>
|
44
|
+
<% rescue ActionView::MissingTemplate %>
|
45
|
+
<%= render partial: "operation_analysis_generic", locals: { operation: @operation } %>
|
46
|
+
<% end %>
|
47
|
+
|
48
|
+
<% if @performance_context.any? %>
|
49
|
+
<dt>Performance Comparison</dt>
|
50
|
+
<dd>
|
51
|
+
Average: <%= @performance_context[:average].round(1) %>ms<br>
|
52
|
+
95th Percentile: <%= @performance_context[:percentile_95].round(1) %>ms<br>
|
53
|
+
Based on <%= @performance_context[:count] %> recent operations
|
54
|
+
</dd>
|
55
|
+
<% end %>
|
56
|
+
|
57
|
+
<% if @related_operations.any? %>
|
58
|
+
<dt>Related Operations</dt>
|
59
|
+
<dd>
|
60
|
+
<%= pluralize(@related_operations.count, 'similar operation') %> in this request
|
61
|
+
<% if @related_operations.count > 2 %>
|
62
|
+
<br>Potential N+1 query pattern detected
|
63
|
+
<% end %>
|
64
|
+
</dd>
|
65
|
+
<% end %>
|
66
|
+
|
67
|
+
<% if @optimization_suggestions.any? %>
|
68
|
+
<dt>Optimization Tips</dt>
|
69
|
+
<dd>
|
70
|
+
<% @optimization_suggestions.each do |suggestion| %>
|
71
|
+
<div>
|
72
|
+
<%= suggestion[:title] %> - <%= suggestion[:description] %>
|
73
|
+
</div>
|
74
|
+
<% end %>
|
75
|
+
</dd>
|
76
|
+
<% end %>
|
77
|
+
</dl>
|
78
|
+
<% end %>
|
79
|
+
</div>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<% columns = [
|
2
|
+
{ field: :occurred_at, label: 'Occurred At', class: 'w-auto' },
|
3
|
+
{ field: :duration, label: 'Duration', class: 'w-32'}
|
4
|
+
] %>
|
5
|
+
|
6
|
+
<table class="table mbs-4">
|
7
|
+
<%= render "rails_pulse/components/table_head", columns: columns %>
|
8
|
+
|
9
|
+
<tbody>
|
10
|
+
<% @table_data.each do |query| %>
|
11
|
+
<tr>
|
12
|
+
<td class="whitespace-nowrap"><%= human_readable_occurred_at(query.occurred_at) %></td>
|
13
|
+
<td class="whitespace-nowrap"><%= query.duration.round(2) %> ms</td>
|
14
|
+
</tr>
|
15
|
+
<% end %>
|
16
|
+
</tbody>
|
17
|
+
</table>
|
18
|
+
|
19
|
+
<%= render "rails_pulse/components/table_pagination" %>
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<% columns = [
|
2
|
+
{ field: :normalized_sql, label: 'Query', class: 'w-auto' },
|
3
|
+
{ field: :execution_count, label: 'Executions', class: 'w-24' },
|
4
|
+
{ field: :average_query_time_ms, label: 'Avg Time', class: 'w-24' },
|
5
|
+
{ field: :total_time_consumed, label: 'Total Time', class: 'w-28' },
|
6
|
+
{ field: :performance_status, label: 'Status', class: 'w-16' },
|
7
|
+
{ field: :occurred_at, label: 'Last Seen', class: 'w-32' }
|
8
|
+
] %>
|
9
|
+
|
10
|
+
<table class="table mbs-4">
|
11
|
+
<%= render "rails_pulse/components/table_head", columns: columns %>
|
12
|
+
|
13
|
+
<tbody>
|
14
|
+
<% @table_data.each do |query| %>
|
15
|
+
<tr>
|
16
|
+
<td class="whitespace-nowrap">
|
17
|
+
<div>
|
18
|
+
<%= link_to html_escape(truncate_sql(query.normalized_sql)), query_path(query), data: { turbo_frame: '_top', } %>
|
19
|
+
</div>
|
20
|
+
</td>
|
21
|
+
<td class="whitespace-nowrap"><%= number_with_delimiter query.execution_count %></td>
|
22
|
+
<td class="whitespace-nowrap"><%= query.average_query_time_ms.to_i %> ms</td>
|
23
|
+
<td class="whitespace-nowrap"><%= number_with_delimiter query.total_time_consumed.to_i %> ms</td>
|
24
|
+
<td class="whitespace-nowrap text-center"><%= query_status_indicator(query.average_query_time_ms) %></td>
|
25
|
+
<td class="whitespace-nowrap"><%= human_readable_occurred_at(query.occurred_at) if query.occurred_at %></td>
|
26
|
+
</tr>
|
27
|
+
<% end %>
|
28
|
+
</tbody>
|
29
|
+
</table>
|
30
|
+
|
31
|
+
<%= render "rails_pulse/components/table_pagination" %>
|