rails_pulse 0.1.3 → 0.1.4
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 +56 -16
- data/Rakefile +169 -86
- data/app/controllers/rails_pulse/queries_controller.rb +14 -20
- data/app/controllers/rails_pulse/requests_controller.rb +43 -30
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +1 -1
- data/app/helpers/rails_pulse/chart_helper.rb +1 -1
- data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
- data/app/javascript/rails_pulse/controllers/index_controller.js +11 -3
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +1 -1
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +56 -17
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +3 -7
- data/app/models/rails_pulse/request.rb +1 -1
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +2 -2
- data/app/models/rails_pulse/requests/tables/index.rb +77 -0
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +16 -5
- data/app/models/rails_pulse/routes/tables/index.rb +4 -2
- data/app/models/rails_pulse/summary.rb +7 -7
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +11 -3
- data/app/views/rails_pulse/components/_metric_card.html.erb +2 -2
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/_analysis_results.html.erb +53 -23
- data/app/views/rails_pulse/queries/_show_table.html.erb +33 -5
- data/app/views/rails_pulse/queries/_table.html.erb +3 -7
- data/app/views/rails_pulse/requests/_table.html.erb +30 -19
- data/app/views/rails_pulse/requests/index.html.erb +8 -0
- data/app/views/rails_pulse/requests/show.html.erb +0 -2
- data/app/views/rails_pulse/routes/_requests_table.html.erb +39 -0
- data/app/views/rails_pulse/routes/_table.html.erb +3 -9
- data/app/views/rails_pulse/routes/show.html.erb +3 -5
- data/config/initializers/rails_charts_csp_patch.rb +32 -40
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
- data/db/rails_pulse_schema.rb +1 -1
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +25 -9
- data/lib/generators/rails_pulse/install_generator.rb +9 -5
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +72 -2
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +3 -2
- data/lib/generators/rails_pulse/upgrade_generator.rb +2 -1
- data/lib/rails_pulse/engine.rb +21 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +1 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +2 -2
- metadata +5 -4
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +0 -54
- data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +0 -13
@@ -1,9 +1,7 @@
|
|
1
1
|
<% columns = [
|
2
2
|
{ field: :normalized_sql, label: 'Query', class: 'w-auto' },
|
3
|
-
{ field: :
|
4
|
-
{ field: :
|
5
|
-
{ field: :total_time_consumed_sort, label: 'Total Time', class: 'w-28' },
|
6
|
-
{ field: :performance_status, label: 'Status', class: 'w-16', sortable: false }
|
3
|
+
{ field: :avg_duration_sort, label: 'Average Query Time', class: 'w-44' },
|
4
|
+
{ field: :execution_count_sort, label: 'Executions', class: 'w-24' }
|
7
5
|
] %>
|
8
6
|
|
9
7
|
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
@@ -17,10 +15,8 @@
|
|
17
15
|
<%= link_to html_escape(truncate_sql(summary.normalized_sql)), query_path(summary.query_id), data: { turbo_frame: '_top', } %>
|
18
16
|
</div>
|
19
17
|
</td>
|
20
|
-
<td class="whitespace-nowrap"><%= number_with_delimiter summary.execution_count %></td>
|
21
18
|
<td class="whitespace-nowrap"><%= summary.avg_duration.to_i %> ms</td>
|
22
|
-
<td class="whitespace-nowrap"><%= number_with_delimiter summary.
|
23
|
-
<td class="whitespace-nowrap text-center"><%= query_status_indicator(summary.avg_duration) %></td>
|
19
|
+
<td class="whitespace-nowrap"><%= number_with_delimiter summary.execution_count %></td>
|
24
20
|
</tr>
|
25
21
|
<% end %>
|
26
22
|
</tbody>
|
@@ -1,28 +1,39 @@
|
|
1
|
-
<%
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
{ field: :duration, label: 'Response Time', class: 'w-24' },
|
8
|
-
{ field: :status, label: 'HTTP Status', class: 'w-20' },
|
9
|
-
{ field: :status_indicator, label: 'Status', class: 'w-16' }
|
10
|
-
]
|
11
|
-
%>
|
1
|
+
<% columns = [
|
2
|
+
{ field: :occurred_at, label: 'Timestamp', class: 'w-36' },
|
3
|
+
{ field: :route_path, label: 'Route', class: 'w-auto' },
|
4
|
+
{ field: :duration, label: 'Response Time', class: 'w-36' },
|
5
|
+
{ field: :status, label: 'Status', class: 'w-20' }
|
6
|
+
] %>
|
12
7
|
|
13
8
|
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
14
9
|
<%= render "rails_pulse/components/table_head", columns: columns %>
|
15
10
|
|
16
11
|
<tbody>
|
17
|
-
<% @table_data.each do |
|
12
|
+
<% @table_data.each do |request| %>
|
13
|
+
<%
|
14
|
+
# Determine performance class based on request duration
|
15
|
+
performance_class = case request.duration
|
16
|
+
when 0..100 then "text-green-600"
|
17
|
+
when 100..300 then "text-yellow-600"
|
18
|
+
when 300..1000 then "text-orange-600"
|
19
|
+
else "text-red-600"
|
20
|
+
end
|
21
|
+
%>
|
18
22
|
<tr>
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
<td class="whitespace-nowrap"
|
23
|
-
|
24
|
-
|
25
|
-
<td class="whitespace-nowrap
|
23
|
+
<td class="whitespace-nowrap">
|
24
|
+
<%= link_to human_readable_occurred_at(request.occurred_at), request_path(request), data: { turbo_frame: '_top' } %>
|
25
|
+
</td>
|
26
|
+
<td class="whitespace-nowrap">
|
27
|
+
<%= link_to "#{request.route.path} #{request.route.method}", route_path(request.route), data: { turbo_frame: '_top' } %>
|
28
|
+
</td>
|
29
|
+
<td class="whitespace-nowrap">
|
30
|
+
<span class="<%= performance_class %> font-medium">
|
31
|
+
<%= request.duration.round(2) %> ms
|
32
|
+
</span>
|
33
|
+
</td>
|
34
|
+
<td class="whitespace-nowrap">
|
35
|
+
<span class="text-green-600"><%= request.status %></span>
|
36
|
+
</td>
|
26
37
|
</tr>
|
27
38
|
<% end %>
|
28
39
|
</tbody>
|
@@ -55,8 +55,16 @@
|
|
55
55
|
)
|
56
56
|
) %>
|
57
57
|
</div>
|
58
|
+
<% else %>
|
59
|
+
<div class="p-4 text-center text-muted">
|
60
|
+
No response time data available for the selected filters.
|
61
|
+
</div>
|
58
62
|
<% end %>
|
59
63
|
|
64
|
+
<div class="alert flex items-start mbs-8" role="alert">
|
65
|
+
<p>The chart above shows aggregated average response times grouped by time periods while the table below shows specific request details.</p>
|
66
|
+
</div>
|
67
|
+
|
60
68
|
<%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
|
61
69
|
<%= render 'rails_pulse/requests/table' %>
|
62
70
|
<% end %>
|
@@ -8,8 +8,6 @@
|
|
8
8
|
<dd><%= link_to @request.route.path_and_method, route_path(@request.route) %></dd>
|
9
9
|
<dt>Timestamp</dt>
|
10
10
|
<dd><%= human_readable_occurred_at(@request.occurred_at) %></dd>
|
11
|
-
<dt>Request UUID</dt>
|
12
|
-
<dd><code><%= @request.request_uuid %></code></dd>
|
13
11
|
<dt>Duration</dt>
|
14
12
|
<dd><%= @request.duration.round(2) %> ms</dd>
|
15
13
|
<dt>Status</dt>
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<% columns = [
|
2
|
+
{ field: :occurred_at, label: 'Timestamp', class: 'w-auto' },
|
3
|
+
{ field: :duration, label: 'Response Time', class: 'w-36' },
|
4
|
+
{ field: :status, label: 'Status', class: 'w-20' }
|
5
|
+
] %>
|
6
|
+
|
7
|
+
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
8
|
+
<%= render "rails_pulse/components/table_head", columns: columns %>
|
9
|
+
|
10
|
+
<tbody>
|
11
|
+
<% @table_data.each do |request| %>
|
12
|
+
<tr>
|
13
|
+
<td class="whitespace-nowrap">
|
14
|
+
<%= link_to human_readable_occurred_at(request.occurred_at), request_path(request), data: { turbo_frame: '_top' } %>
|
15
|
+
</td>
|
16
|
+
<td class="whitespace-nowrap">
|
17
|
+
<% performance_class = case request.duration
|
18
|
+
when 0..100 then "text-green-600"
|
19
|
+
when 100..300 then "text-yellow-600"
|
20
|
+
when 300..1000 then "text-orange-600"
|
21
|
+
else "text-red-600"
|
22
|
+
end %>
|
23
|
+
<span class="<%= performance_class %> font-medium">
|
24
|
+
<%= request.duration.round(2) %> ms
|
25
|
+
</span>
|
26
|
+
</td>
|
27
|
+
<td class="whitespace-nowrap">
|
28
|
+
<% if request.is_error? %>
|
29
|
+
<span class="text-red-600">Error (<%= request.status %>)</span>
|
30
|
+
<% else %>
|
31
|
+
<span class="text-green-600"><%= request.status %></span>
|
32
|
+
<% end %>
|
33
|
+
</td>
|
34
|
+
</tr>
|
35
|
+
<% end %>
|
36
|
+
</tbody>
|
37
|
+
</table>
|
38
|
+
|
39
|
+
<%= render "rails_pulse/components/table_pagination" %>
|
@@ -1,11 +1,8 @@
|
|
1
1
|
<% columns = [
|
2
2
|
{ field: :route_path, label: 'Route', class: 'w-auto' },
|
3
|
-
{ field: :avg_duration_sort, label: 'Average Response Time', class: 'w-
|
4
|
-
{ field: :max_duration_sort, label: 'Max Response Time', class: 'w-
|
5
|
-
{ field: :count_sort, label: 'Requests', class: 'w-24' }
|
6
|
-
{ field: :requests_per_minute, label: 'Requests Per Minute', class: 'w-28' },
|
7
|
-
{ field: :error_rate_percentage, label: 'Error Rate (%)', class: 'w-20' },
|
8
|
-
{ field: :status_indicator, label: 'Status', class: 'w-16', sortable: false }
|
3
|
+
{ field: :avg_duration_sort, label: 'Average Response Time', class: 'w-48' },
|
4
|
+
{ field: :max_duration_sort, label: 'Max Response Time', class: 'w-44' },
|
5
|
+
{ field: :count_sort, label: 'Requests', class: 'w-24' }
|
9
6
|
] %>
|
10
7
|
|
11
8
|
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
@@ -18,9 +15,6 @@
|
|
18
15
|
<td class="whitespace-nowrap"><%= summary.avg_duration.to_i %> ms</td>
|
19
16
|
<td class="whitespace-nowrap"><%= summary.max_duration.to_i %> ms</td>
|
20
17
|
<td class="whitespace-nowrap"><%= number_with_delimiter summary.count %></td>
|
21
|
-
<td class="whitespace-nowrap"><%= summary.count < 1 ? '< 1' : (summary.count / 60.0).round(2) %></td>
|
22
|
-
<td class="whitespace-nowrap"><%= ((summary.error_count.to_f / summary.count) * 100).round(2) %>%</td>
|
23
|
-
<td class="whitespace-nowrap text-center"><%= route_status_indicator(summary.avg_duration >= 500 ? 1 : 0) %></td>
|
24
18
|
</tr>
|
25
19
|
<% end %>
|
26
20
|
</tbody>
|
@@ -1,7 +1,5 @@
|
|
1
1
|
<%= render 'rails_pulse/components/breadcrumbs' %>
|
2
2
|
|
3
|
-
<h1 class="text-2xl mis-2"><%= @route.path_and_method %></h1>
|
4
|
-
|
5
3
|
<% unless turbo_frame_request? %>
|
6
4
|
<div class="row">
|
7
5
|
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
|
@@ -14,7 +12,7 @@
|
|
14
12
|
<div
|
15
13
|
class="row"
|
16
14
|
data-controller="rails-pulse--index"
|
17
|
-
data-rails-pulse--index-chart-id-value="
|
15
|
+
data-rails-pulse--index-chart-id-value="route_responses_chart"
|
18
16
|
>
|
19
17
|
<div class="grid-item">
|
20
18
|
<%= render 'rails_pulse/components/panel', { title: 'Route Reqeusts', } do %>
|
@@ -44,7 +42,7 @@
|
|
44
42
|
<%= bar_chart(
|
45
43
|
@chart_data,
|
46
44
|
code: false,
|
47
|
-
id: "
|
45
|
+
id: "route_responses_chart",
|
48
46
|
height: "100%",
|
49
47
|
options: bar_chart_options(
|
50
48
|
units: "ms",
|
@@ -62,7 +60,7 @@
|
|
62
60
|
<% end %>
|
63
61
|
|
64
62
|
<%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
|
65
|
-
<%= render 'rails_pulse/
|
63
|
+
<%= render 'rails_pulse/routes/requests_table' %>
|
66
64
|
<% end %>
|
67
65
|
<% else %>
|
68
66
|
<%= render 'rails_pulse/components/empty_state',
|
@@ -1,62 +1,54 @@
|
|
1
|
-
#
|
2
|
-
#
|
1
|
+
# CSP patch for RailsCharts gem
|
2
|
+
# Adds nonce attributes to script tags generated by RailsCharts for CSP compliance
|
3
3
|
|
4
4
|
if defined?(RailsCharts)
|
5
5
|
module RailsCharts
|
6
6
|
module CspPatch
|
7
7
|
def line_chart(data_source, options = {})
|
8
|
-
# Get the original chart HTML
|
9
8
|
chart_html = super(data_source, options)
|
9
|
+
add_csp_nonce_to_chart(chart_html)
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
if nonce.present? && chart_html.present?
|
15
|
-
# Add nonce to all script tags in the chart HTML
|
16
|
-
chart_html = add_nonce_to_scripts(chart_html.to_s, nonce)
|
17
|
-
# Ensure the HTML is marked as safe for Rails to render
|
18
|
-
chart_html = chart_html.html_safe if chart_html.respond_to?(:html_safe)
|
19
|
-
end
|
20
|
-
|
21
|
-
chart_html
|
12
|
+
def bar_chart(data_source, options = {})
|
13
|
+
chart_html = super(data_source, options)
|
14
|
+
add_csp_nonce_to_chart(chart_html)
|
22
15
|
end
|
23
16
|
|
24
17
|
private
|
25
18
|
|
26
|
-
def
|
27
|
-
|
28
|
-
nonce = nil
|
19
|
+
def add_csp_nonce_to_chart(chart_html)
|
20
|
+
return chart_html unless chart_html.present?
|
29
21
|
|
30
|
-
|
31
|
-
|
32
|
-
nonce = content_security_policy_nonce
|
33
|
-
end
|
34
|
-
|
35
|
-
# Method 2: Check for custom csp_nonce helper
|
36
|
-
if nonce.blank? && respond_to?(:csp_nonce)
|
37
|
-
nonce = csp_nonce
|
38
|
-
end
|
22
|
+
nonce = get_csp_nonce
|
23
|
+
return chart_html unless nonce.present?
|
39
24
|
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
request.env["csp_nonce"]
|
45
|
-
end
|
25
|
+
# Add nonce to script tags and mark as safe
|
26
|
+
modified_html = add_nonce_to_scripts(chart_html.to_s, nonce)
|
27
|
+
modified_html.html_safe if modified_html.respond_to?(:html_safe)
|
28
|
+
end
|
46
29
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
30
|
+
def get_csp_nonce
|
31
|
+
# Try common CSP nonce sources in order of preference
|
32
|
+
if respond_to?(:content_security_policy_nonce)
|
33
|
+
content_security_policy_nonce
|
34
|
+
elsif respond_to?(:csp_nonce)
|
35
|
+
csp_nonce
|
36
|
+
elsif defined?(request) && request
|
37
|
+
request.env["action_dispatch.content_security_policy_nonce"] ||
|
38
|
+
request.env["secure_headers.content_security_policy_nonce"] ||
|
39
|
+
request.env["csp_nonce"]
|
40
|
+
elsif respond_to?(:controller) && controller.respond_to?(:content_security_policy_nonce)
|
41
|
+
controller.content_security_policy_nonce
|
42
|
+
elsif defined?(@view_context) && @view_context.respond_to?(:content_security_policy_nonce)
|
43
|
+
@view_context.content_security_policy_nonce
|
44
|
+
else
|
45
|
+
Thread.current[:rails_pulse_csp_nonce] ||
|
46
|
+
(defined?(RequestStore) && RequestStore.store[:rails_pulse_csp_nonce])
|
51
47
|
end
|
52
|
-
|
53
|
-
nonce.presence
|
54
48
|
end
|
55
49
|
|
56
50
|
def add_nonce_to_scripts(html, nonce)
|
57
|
-
# Use regex to add nonce to script tags that don't already have one
|
58
51
|
html.gsub(/<script(?![^>]*\snonce=)([^>]*)>/i) do |match|
|
59
|
-
# Insert nonce attribute before the closing >
|
60
52
|
attributes = $1
|
61
53
|
if attributes.strip.empty?
|
62
54
|
"<script nonce=\"#{nonce}\">"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Generated from Rails Pulse schema - automatically loads current schema definition
|
2
|
+
class InstallRailsPulseTables < ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}".to_f]
|
3
|
+
def change
|
4
|
+
# Load and execute the Rails Pulse schema directly
|
5
|
+
# This ensures the migration is always in sync with the schema file
|
6
|
+
schema_file = File.join(File.dirname(__FILE__), "..", "rails_pulse_schema.rb")
|
7
|
+
|
8
|
+
if File.exist?(schema_file)
|
9
|
+
say "Loading Rails Pulse schema from db/rails_pulse_schema.rb"
|
10
|
+
|
11
|
+
# Load the schema file to define RailsPulse::Schema
|
12
|
+
load schema_file
|
13
|
+
|
14
|
+
# Execute the schema in the context of this migration
|
15
|
+
RailsPulse::Schema.call(connection)
|
16
|
+
|
17
|
+
say "Rails Pulse tables created successfully"
|
18
|
+
say "The schema file db/rails_pulse_schema.rb remains as your single source of truth"
|
19
|
+
else
|
20
|
+
raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/db/rails_pulse_schema.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
RailsPulse::Schema = lambda do |connection|
|
6
6
|
# Skip if all tables already exist to prevent conflicts
|
7
|
-
required_tables = [:rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_summaries]
|
7
|
+
required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_summaries ]
|
8
8
|
|
9
9
|
if ENV["CI"] == "true"
|
10
10
|
existing_tables = required_tables.select { |table| connection.table_exists?(table) }
|
@@ -13,18 +13,32 @@ module RailsPulse
|
|
13
13
|
|
14
14
|
def check_schema_file
|
15
15
|
unless File.exist?("db/rails_pulse_schema.rb")
|
16
|
-
|
17
|
-
|
16
|
+
# Only show message in non-test environments to reduce test noise
|
17
|
+
unless Rails.env.test?
|
18
|
+
say "No db/rails_pulse_schema.rb file found. Run 'rails generate rails_pulse:install' first.", :red
|
19
|
+
exit 1
|
20
|
+
else
|
21
|
+
return false
|
22
|
+
end
|
18
23
|
end
|
19
24
|
|
20
25
|
if rails_pulse_tables_exist?
|
21
|
-
|
22
|
-
|
23
|
-
|
26
|
+
unless Rails.env.test?
|
27
|
+
say "Rails Pulse tables already exist. No conversion needed.", :yellow
|
28
|
+
say "Use 'rails generate rails_pulse:upgrade' to update existing installation.", :blue
|
29
|
+
exit 0
|
30
|
+
else
|
31
|
+
return false
|
32
|
+
end
|
24
33
|
end
|
34
|
+
|
35
|
+
true
|
25
36
|
end
|
26
37
|
|
27
38
|
def create_conversion_migration
|
39
|
+
# Only create migration if schema file check passes
|
40
|
+
return unless check_schema_file
|
41
|
+
|
28
42
|
say "Converting db/rails_pulse_schema.rb to migration...", :green
|
29
43
|
|
30
44
|
migration_template(
|
@@ -34,17 +48,19 @@ module RailsPulse
|
|
34
48
|
end
|
35
49
|
|
36
50
|
def display_completion_message
|
51
|
+
# Only display completion message if migration was created
|
52
|
+
return unless File.exist?("db/rails_pulse_schema.rb")
|
53
|
+
|
37
54
|
say <<~MESSAGE
|
38
55
|
|
39
56
|
Conversion complete!
|
40
57
|
|
41
58
|
Next steps:
|
42
59
|
1. Run: rails db:migrate
|
43
|
-
2.
|
44
|
-
3. Remove db/rails_pulse_migrate/ directory if it exists
|
45
|
-
4. Restart your Rails server
|
60
|
+
2. Restart your Rails server
|
46
61
|
|
47
|
-
|
62
|
+
The schema file db/rails_pulse_schema.rb remains as your single source of truth.
|
63
|
+
Future Rails Pulse updates will come as regular migrations in db/migrate/
|
48
64
|
|
49
65
|
MESSAGE
|
50
66
|
end
|
@@ -18,6 +18,10 @@ module RailsPulse
|
|
18
18
|
copy_file "db/rails_pulse_schema.rb", "db/rails_pulse_schema.rb"
|
19
19
|
end
|
20
20
|
|
21
|
+
def create_migration_directory
|
22
|
+
create_file "db/rails_pulse_migrate/.keep"
|
23
|
+
end
|
24
|
+
|
21
25
|
def copy_initializer
|
22
26
|
copy_file "rails_pulse.rb", "config/initializers/rails_pulse.rb"
|
23
27
|
end
|
@@ -45,10 +49,9 @@ module RailsPulse
|
|
45
49
|
end
|
46
50
|
|
47
51
|
def create_separate_database_setup
|
48
|
-
create_file "db/rails_pulse_migrate/.keep"
|
49
|
-
|
50
52
|
say "Setting up separate database configuration...", :green
|
51
53
|
|
54
|
+
# Migration directory already created by create_migration_directory
|
52
55
|
# Could add database.yml configuration here if needed
|
53
56
|
# For now, users will configure manually
|
54
57
|
end
|
@@ -80,6 +83,7 @@ module RailsPulse
|
|
80
83
|
2. Run: rails db:prepare (creates database and loads schema)
|
81
84
|
3. Restart your Rails server
|
82
85
|
|
86
|
+
The schema file db/rails_pulse_schema.rb is your single source of truth.
|
83
87
|
Future schema changes will come as regular migrations in db/rails_pulse_migrate/
|
84
88
|
|
85
89
|
MESSAGE
|
@@ -92,12 +96,12 @@ module RailsPulse
|
|
92
96
|
|
93
97
|
Next steps:
|
94
98
|
1. Run: rails db:migrate (creates Rails Pulse tables in your main database)
|
95
|
-
2.
|
96
|
-
3. Restart your Rails server
|
99
|
+
2. Restart your Rails server
|
97
100
|
|
101
|
+
The schema file db/rails_pulse_schema.rb is your single source of truth.
|
98
102
|
Future schema changes will come as regular migrations in db/migrate/
|
99
103
|
|
100
|
-
Note: The installation migration
|
104
|
+
Note: The installation migration loads from db/rails_pulse_schema.rb
|
101
105
|
and includes all current Rails Pulse tables and columns.
|
102
106
|
|
103
107
|
MESSAGE
|
@@ -3,8 +3,17 @@
|
|
3
3
|
# Load with: rails db:schema:load:rails_pulse or db:prepare
|
4
4
|
|
5
5
|
RailsPulse::Schema = lambda do |connection|
|
6
|
-
# Skip if tables already exist to prevent conflicts
|
7
|
-
|
6
|
+
# Skip if all tables already exist to prevent conflicts
|
7
|
+
required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_summaries ]
|
8
|
+
|
9
|
+
if ENV["CI"] == "true"
|
10
|
+
existing_tables = required_tables.select { |table| connection.table_exists?(table) }
|
11
|
+
missing_tables = required_tables - existing_tables
|
12
|
+
puts "[RailsPulse::Schema] Existing tables: #{existing_tables.join(', ')}" if existing_tables.any?
|
13
|
+
puts "[RailsPulse::Schema] Missing tables: #{missing_tables.join(', ')}" if missing_tables.any?
|
14
|
+
end
|
15
|
+
|
16
|
+
return if required_tables.all? { |table| connection.table_exists?(table) }
|
8
17
|
|
9
18
|
connection.create_table :rails_pulse_routes do |t|
|
10
19
|
t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
|
@@ -16,6 +25,15 @@ RailsPulse::Schema = lambda do |connection|
|
|
16
25
|
|
17
26
|
connection.create_table :rails_pulse_queries do |t|
|
18
27
|
t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
|
28
|
+
t.datetime :analyzed_at, comment: "When query analysis was last performed"
|
29
|
+
t.text :explain_plan, comment: "EXPLAIN output from actual SQL execution"
|
30
|
+
t.text :issues, comment: "JSON array of detected performance issues"
|
31
|
+
t.text :metadata, comment: "JSON object containing query complexity metrics"
|
32
|
+
t.text :query_stats, comment: "JSON object with query characteristics analysis"
|
33
|
+
t.text :backtrace_analysis, comment: "JSON object with call chain and N+1 detection"
|
34
|
+
t.text :index_recommendations, comment: "JSON array of database index recommendations"
|
35
|
+
t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
|
36
|
+
t.text :suggestions, comment: "JSON array of optimization recommendations"
|
19
37
|
t.timestamps
|
20
38
|
end
|
21
39
|
|
@@ -53,6 +71,58 @@ RailsPulse::Schema = lambda do |connection|
|
|
53
71
|
connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
|
54
72
|
connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
|
55
73
|
connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
|
74
|
+
|
75
|
+
connection.create_table :rails_pulse_summaries do |t|
|
76
|
+
# Time fields
|
77
|
+
t.datetime :period_start, null: false, comment: "Start of the aggregation period"
|
78
|
+
t.datetime :period_end, null: false, comment: "End of the aggregation period"
|
79
|
+
t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
|
80
|
+
|
81
|
+
# Polymorphic association to handle both routes and queries
|
82
|
+
t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
|
83
|
+
# This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
|
84
|
+
# and summarizable_id (route_id or query_id)
|
85
|
+
|
86
|
+
# Universal metrics
|
87
|
+
t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
|
88
|
+
t.float :avg_duration, comment: "Average duration in milliseconds"
|
89
|
+
t.float :min_duration, comment: "Minimum duration in milliseconds"
|
90
|
+
t.float :max_duration, comment: "Maximum duration in milliseconds"
|
91
|
+
t.float :p50_duration, comment: "50th percentile duration"
|
92
|
+
t.float :p95_duration, comment: "95th percentile duration"
|
93
|
+
t.float :p99_duration, comment: "99th percentile duration"
|
94
|
+
t.float :total_duration, comment: "Total duration in milliseconds"
|
95
|
+
t.float :stddev_duration, comment: "Standard deviation of duration"
|
96
|
+
|
97
|
+
# Request/Route specific metrics
|
98
|
+
t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
|
99
|
+
t.integer :success_count, default: 0, comment: "Number of successful responses"
|
100
|
+
t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
|
101
|
+
t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
|
102
|
+
t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
|
103
|
+
t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
|
104
|
+
|
105
|
+
t.timestamps
|
106
|
+
end
|
107
|
+
|
108
|
+
# Unique constraint and indexes for summaries
|
109
|
+
connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
|
110
|
+
unique: true,
|
111
|
+
name: "idx_pulse_summaries_unique"
|
112
|
+
connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
|
113
|
+
connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
|
114
|
+
|
115
|
+
# Add indexes to existing tables for efficient aggregation
|
116
|
+
connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
|
117
|
+
connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
|
118
|
+
|
119
|
+
connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
|
120
|
+
connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
|
121
|
+
|
122
|
+
if ENV["CI"] == "true"
|
123
|
+
created_tables = required_tables.select { |table| connection.table_exists?(table) }
|
124
|
+
puts "[RailsPulse::Schema] Successfully created tables: #{created_tables.join(', ')}"
|
125
|
+
end
|
56
126
|
end
|
57
127
|
|
58
128
|
if defined?(RailsPulse::ApplicationRecord)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Generated from Rails Pulse schema - automatically loads current schema definition
|
2
|
-
class InstallRailsPulseTables < ActiveRecord::Migration[<%=
|
2
|
+
class InstallRailsPulseTables < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
3
3
|
def change
|
4
4
|
# Load and execute the Rails Pulse schema directly
|
5
5
|
# This ensures the migration is always in sync with the schema file
|
@@ -15,8 +15,9 @@ class InstallRailsPulseTables < ActiveRecord::Migration[<%= ActiveRecord::Migrat
|
|
15
15
|
RailsPulse::Schema.call(connection)
|
16
16
|
|
17
17
|
say "Rails Pulse tables created successfully"
|
18
|
+
say "The schema file db/rails_pulse_schema.rb remains as your single source of truth"
|
18
19
|
else
|
19
20
|
raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb"
|
20
21
|
end
|
21
22
|
end
|
22
|
-
end
|
23
|
+
end
|
@@ -132,7 +132,8 @@ module RailsPulse
|
|
132
132
|
To convert to single database setup:
|
133
133
|
1. Run: rails generate rails_pulse:convert_to_migrations
|
134
134
|
2. Run: rails db:migrate
|
135
|
-
|
135
|
+
|
136
|
+
The schema file db/rails_pulse_schema.rb will remain as your single source of truth.
|
136
137
|
|
137
138
|
MESSAGE
|
138
139
|
end
|
data/lib/rails_pulse/engine.rb
CHANGED
@@ -66,6 +66,27 @@ module RailsPulse
|
|
66
66
|
# Instead, we explicitly use time_zone: "UTC" in all groupdate calls
|
67
67
|
end
|
68
68
|
|
69
|
+
initializer "rails_pulse.disable_turbo" do
|
70
|
+
# Disable Turbo navigation globally for Rails Pulse to avoid CSP issues with charts
|
71
|
+
# This ensures all navigation within Rails Pulse uses full page refreshes
|
72
|
+
ActiveSupport.on_load(:action_view) do
|
73
|
+
ActionView::Helpers::UrlHelper.module_eval do
|
74
|
+
alias_method :original_link_to, :link_to
|
75
|
+
|
76
|
+
def link_to(*args, &block)
|
77
|
+
# Only modify links within Rails Pulse namespace
|
78
|
+
if respond_to?(:controller) && controller.class.name.start_with?("RailsPulse::")
|
79
|
+
options = args.extract_options!
|
80
|
+
options[:data] ||= {}
|
81
|
+
options[:data][:turbo] = false unless options[:data].key?(:turbo)
|
82
|
+
args << options
|
83
|
+
end
|
84
|
+
original_link_to(*args, &block)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
69
90
|
# CSP helper methods
|
70
91
|
def self.csp_sources
|
71
92
|
{
|
data/lib/rails_pulse/version.rb
CHANGED