rails_pulse 0.1.3 → 0.2.2
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 +134 -16
- data/Rakefile +315 -83
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/stylesheets/rails_pulse/components/datepicker.css +191 -0
- data/app/assets/stylesheets/rails_pulse/components/switch.css +36 -0
- data/app/assets/stylesheets/rails_pulse/components/tags.css +98 -0
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +26 -0
- data/app/controllers/concerns/response_range_concern.rb +15 -2
- data/app/controllers/concerns/tag_filter_concern.rb +26 -0
- data/app/controllers/concerns/time_range_concern.rb +27 -8
- data/app/controllers/rails_pulse/application_controller.rb +73 -0
- data/app/controllers/rails_pulse/queries_controller.rb +18 -21
- data/app/controllers/rails_pulse/requests_controller.rb +80 -35
- data/app/controllers/rails_pulse/routes_controller.rb +4 -2
- data/app/controllers/rails_pulse/tags_controller.rb +51 -0
- data/app/helpers/rails_pulse/application_helper.rb +2 -0
- 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/form_helper.rb +75 -0
- data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
- data/app/helpers/rails_pulse/tags_helper.rb +29 -0
- data/app/javascript/rails_pulse/application.js +6 -0
- data/app/javascript/rails_pulse/controllers/custom_range_controller.js +115 -0
- data/app/javascript/rails_pulse/controllers/datepicker_controller.js +48 -0
- data/app/javascript/rails_pulse/controllers/global_filters_controller.js +110 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +11 -3
- data/app/models/concerns/taggable.rb +61 -0
- 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/queries/tables/index.rb +10 -2
- data/app/models/rails_pulse/query.rb +2 -0
- data/app/models/rails_pulse/request.rb +10 -2
- 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/route.rb +2 -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 +14 -4
- data/app/models/rails_pulse/summary.rb +7 -7
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +11 -3
- data/app/services/rails_pulse/summary_service.rb +2 -0
- data/app/views/layouts/rails_pulse/_global_filters.html.erb +84 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +5 -5
- data/app/views/layouts/rails_pulse/application.html.erb +8 -5
- 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/_page_header.html.erb +20 -0
- 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/operations/show.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 +4 -6
- data/app/views/rails_pulse/queries/index.html.erb +3 -7
- data/app/views/rails_pulse/queries/show.html.erb +3 -7
- data/app/views/rails_pulse/requests/_table.html.erb +32 -19
- data/app/views/rails_pulse/requests/index.html.erb +45 -55
- data/app/views/rails_pulse/requests/show.html.erb +1 -3
- data/app/views/rails_pulse/routes/_requests_table.html.erb +41 -0
- data/app/views/rails_pulse/routes/_table.html.erb +4 -8
- data/app/views/rails_pulse/routes/index.html.erb +4 -8
- data/app/views/rails_pulse/routes/show.html.erb +6 -12
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +73 -0
- data/config/initializers/rails_charts_csp_patch.rb +32 -40
- data/config/routes.rb +5 -0
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
- data/db/rails_pulse_schema.rb +4 -1
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +25 -9
- data/lib/generators/rails_pulse/install_generator.rb +30 -7
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +75 -2
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +3 -2
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +21 -0
- data/lib/generators/rails_pulse/upgrade_generator.rb +147 -30
- data/lib/rails_pulse/configuration.rb +16 -1
- data/lib/rails_pulse/engine.rb +21 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/public/rails-pulse-assets/rails-pulse-icons.js +16 -15
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +73 -69
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- metadata +20 -5
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +0 -12
- 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,4 +1,4 @@
|
|
|
1
|
-
<%= render 'rails_pulse/components/
|
|
1
|
+
<%= render 'rails_pulse/components/page_header' %>
|
|
2
2
|
|
|
3
3
|
<% unless turbo_frame_request? %>
|
|
4
4
|
<div class="row">
|
|
@@ -9,61 +9,51 @@
|
|
|
9
9
|
</div>
|
|
10
10
|
<% end %>
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
data
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
<%= render 'rails_pulse/components/panel', { title: 'Requests', } do %>
|
|
13
|
+
<%= search_form_for @ransack_query, url: requests_path, class: "flex items-center justify-between gap mb-4", data: { controller: "rails-pulse--custom-range" } do |form| %>
|
|
14
|
+
<div class="flex items-center grow gap">
|
|
15
|
+
<%= time_range_selector(form,
|
|
16
|
+
time_range_options: RailsPulse::RequestsController::TIME_RANGE_OPTIONS,
|
|
17
|
+
selected_time_range: @selected_time_range,
|
|
18
|
+
mode: :recent_custom
|
|
19
|
+
) %>
|
|
20
|
+
|
|
21
|
+
<%= form.search_field :route_path_cont,
|
|
22
|
+
placeholder: "Filter by route",
|
|
23
|
+
autocomplete: "off",
|
|
24
|
+
class: "input"
|
|
25
|
+
%>
|
|
26
|
+
|
|
27
|
+
<%= form.select :status_category_eq,
|
|
28
|
+
[
|
|
29
|
+
["All statuses", ""],
|
|
30
|
+
["2xx Success", "2"],
|
|
31
|
+
["3xx Redirect", "3"],
|
|
32
|
+
["4xx Client Error", "4"],
|
|
33
|
+
["5xx Server Error", "5"]
|
|
34
|
+
],
|
|
35
|
+
{},
|
|
36
|
+
{ class: "input" }
|
|
37
|
+
%>
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
>
|
|
40
|
-
<%= bar_chart(
|
|
41
|
-
@chart_data,
|
|
42
|
-
code: false,
|
|
43
|
-
id: "average_response_times_chart",
|
|
44
|
-
height: "100%",
|
|
45
|
-
options: bar_chart_options(
|
|
46
|
-
units: "ms",
|
|
47
|
-
zoom: true,
|
|
48
|
-
chart_start: 0,
|
|
49
|
-
chart_end: @chart_data.length - 1,
|
|
50
|
-
xaxis_formatter: @xaxis_formatter,
|
|
51
|
-
tooltip_formatter: @tooltip_formatter,
|
|
52
|
-
zoom_start: @zoom_start,
|
|
53
|
-
zoom_end: @zoom_end,
|
|
54
|
-
chart_data: @chart_data
|
|
55
|
-
)
|
|
56
|
-
) %>
|
|
57
|
-
</div>
|
|
58
|
-
<% end %>
|
|
39
|
+
<%= form.select :duration_gteq,
|
|
40
|
+
duration_options(:request),
|
|
41
|
+
{ selected: @selected_response_range, prompt: "Min duration" },
|
|
42
|
+
{ class: "input" }
|
|
43
|
+
%>
|
|
59
44
|
|
|
60
|
-
<%=
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
45
|
+
<%= link_to "Reset", requests_path, class: "btn btn--borderless show@md" if params.has_key?(:q) %>
|
|
46
|
+
<%= form.submit "Search", class: "btn show@sm" %>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
49
|
+
|
|
50
|
+
<% if @has_data %>
|
|
51
|
+
<%= turbo_frame_tag :index_table do %>
|
|
52
|
+
<%= render 'rails_pulse/requests/table' %>
|
|
67
53
|
<% end %>
|
|
54
|
+
<% else %>
|
|
55
|
+
<%= render 'rails_pulse/components/empty_state',
|
|
56
|
+
title: 'No request data found for the selected filters.',
|
|
57
|
+
description: 'Try adjusting your time range or filters to see results.' %>
|
|
68
58
|
<% end %>
|
|
69
|
-
|
|
59
|
+
<% end %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%= render 'rails_pulse/components/
|
|
1
|
+
<%= render 'rails_pulse/components/page_header', taggable: @request %>
|
|
2
2
|
|
|
3
3
|
<div class="row">
|
|
4
4
|
<div class="grid-item">
|
|
@@ -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,41 @@
|
|
|
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
|
+
{ field: nil, label: 'Tags', class: 'w-32' }
|
|
6
|
+
] %>
|
|
7
|
+
|
|
8
|
+
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
|
9
|
+
<%= render "rails_pulse/components/table_head", columns: columns %>
|
|
10
|
+
|
|
11
|
+
<tbody>
|
|
12
|
+
<% @table_data.each do |request| %>
|
|
13
|
+
<tr>
|
|
14
|
+
<td class="whitespace-nowrap">
|
|
15
|
+
<%= link_to human_readable_occurred_at(request.occurred_at), request_path(request), data: { turbo_frame: '_top' } %>
|
|
16
|
+
</td>
|
|
17
|
+
<td class="whitespace-nowrap">
|
|
18
|
+
<% performance_class = case request.duration
|
|
19
|
+
when 0..100 then "text-green-600"
|
|
20
|
+
when 100..300 then "text-yellow-600"
|
|
21
|
+
when 300..1000 then "text-orange-600"
|
|
22
|
+
else "text-red-600"
|
|
23
|
+
end %>
|
|
24
|
+
<span class="<%= performance_class %> font-medium">
|
|
25
|
+
<%= request.duration.round(2) %> ms
|
|
26
|
+
</span>
|
|
27
|
+
</td>
|
|
28
|
+
<td class="whitespace-nowrap">
|
|
29
|
+
<% if request.is_error? %>
|
|
30
|
+
<span class="text-red-600">Error (<%= request.status %>)</span>
|
|
31
|
+
<% else %>
|
|
32
|
+
<span class="text-green-600"><%= request.status %></span>
|
|
33
|
+
<% end %>
|
|
34
|
+
</td>
|
|
35
|
+
<td class="whitespace-nowrap"><%= display_tag_badges(request) %></td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
|
|
41
|
+
<%= render "rails_pulse/components/table_pagination" %>
|
|
@@ -1,11 +1,9 @@
|
|
|
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-
|
|
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
5
|
{ field: :count_sort, label: 'Requests', class: 'w-24' },
|
|
6
|
-
{ field:
|
|
7
|
-
{ field: :error_rate_percentage, label: 'Error Rate (%)', class: 'w-20' },
|
|
8
|
-
{ field: :status_indicator, label: 'Status', class: 'w-16', sortable: false }
|
|
6
|
+
{ field: nil, label: 'Tags', class: 'w-32' }
|
|
9
7
|
] %>
|
|
10
8
|
|
|
11
9
|
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
|
@@ -18,9 +16,7 @@
|
|
|
18
16
|
<td class="whitespace-nowrap"><%= summary.avg_duration.to_i %> ms</td>
|
|
19
17
|
<td class="whitespace-nowrap"><%= summary.max_duration.to_i %> ms</td>
|
|
20
18
|
<td class="whitespace-nowrap"><%= number_with_delimiter summary.count %></td>
|
|
21
|
-
<td class="whitespace-nowrap"><%=
|
|
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>
|
|
19
|
+
<td class="whitespace-nowrap"><%= display_tag_badges(summary.tags) %></td>
|
|
24
20
|
</tr>
|
|
25
21
|
<% end %>
|
|
26
22
|
</tbody>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%= render 'rails_pulse/components/
|
|
1
|
+
<%= render 'rails_pulse/components/page_header' %>
|
|
2
2
|
|
|
3
3
|
<% unless turbo_frame_request? %>
|
|
4
4
|
<div class="row">
|
|
@@ -14,14 +14,10 @@
|
|
|
14
14
|
data-rails-pulse--index-chart-id-value="average_response_times_chart"
|
|
15
15
|
>
|
|
16
16
|
<%= render 'rails_pulse/components/panel', { title: 'Average Response Time', card_classes: 'b-full' } do %>
|
|
17
|
-
<%= search_form_for @ransack_query, url: routes_path, class: "flex items-center justify-between gap mb-4" do |form| %>
|
|
17
|
+
<%= search_form_for @ransack_query, url: routes_path, class: "flex items-center justify-between gap mb-4", data: { controller: "rails-pulse--custom-range" } do |form| %>
|
|
18
18
|
<div class="flex items-center grow gap">
|
|
19
|
-
<%= form
|
|
20
|
-
<%= form.
|
|
21
|
-
RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
|
|
22
|
-
{ selected: @selected_time_range },
|
|
23
|
-
{ class: "input" }
|
|
24
|
-
%>
|
|
19
|
+
<%= time_range_selector(form, time_range_options: RailsPulse::RoutesController::TIME_RANGE_OPTIONS, selected_time_range: @selected_time_range) %>
|
|
20
|
+
<%= form.search_field :route_path_cont, placeholder: "Filter by route", autocomplete: "off", class: "input" %>
|
|
25
21
|
<%= form.select :avg_duration,
|
|
26
22
|
duration_options(:route),
|
|
27
23
|
{ selected: @selected_response_range },
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
<%= render 'rails_pulse/components/
|
|
2
|
-
|
|
3
|
-
<h1 class="text-2xl mis-2"><%= @route.path_and_method %></h1>
|
|
1
|
+
<%= render 'rails_pulse/components/page_header', taggable: @route %>
|
|
4
2
|
|
|
5
3
|
<% unless turbo_frame_request? %>
|
|
6
4
|
<div class="row">
|
|
@@ -14,17 +12,13 @@
|
|
|
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 %>
|
|
21
|
-
<%= search_form_for @ransack_query, url: route_path(@route), class: "flex items-center justify-between gap mb-4" do |form| %>
|
|
19
|
+
<%= search_form_for @ransack_query, url: route_path(@route), class: "flex items-center justify-between gap mb-4", data: { controller: "rails-pulse--custom-range" } do |form| %>
|
|
22
20
|
<div class="flex items-center grow gap">
|
|
23
|
-
<%= form
|
|
24
|
-
RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
|
|
25
|
-
{ selected: @selected_time_range },
|
|
26
|
-
{ class: "input" }
|
|
27
|
-
%>
|
|
21
|
+
<%= time_range_selector(form, time_range_options: RailsPulse::RoutesController::TIME_RANGE_OPTIONS, selected_time_range: @selected_time_range) %>
|
|
28
22
|
<%= form.select :avg_duration,
|
|
29
23
|
duration_options(:route),
|
|
30
24
|
{ selected: @selected_response_range },
|
|
@@ -44,7 +38,7 @@
|
|
|
44
38
|
<%= bar_chart(
|
|
45
39
|
@chart_data,
|
|
46
40
|
code: false,
|
|
47
|
-
id: "
|
|
41
|
+
id: "route_responses_chart",
|
|
48
42
|
height: "100%",
|
|
49
43
|
options: bar_chart_options(
|
|
50
44
|
units: "ms",
|
|
@@ -62,7 +56,7 @@
|
|
|
62
56
|
<% end %>
|
|
63
57
|
|
|
64
58
|
<%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
|
|
65
|
-
<%= render 'rails_pulse/
|
|
59
|
+
<%= render 'rails_pulse/routes/requests_table' %>
|
|
66
60
|
<% end %>
|
|
67
61
|
<% else %>
|
|
68
62
|
<%= render 'rails_pulse/components/empty_state',
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<%
|
|
2
|
+
taggable_type = taggable.class.name.demodulize.underscore
|
|
3
|
+
available_tags = RailsPulse.configuration.tags
|
|
4
|
+
current_tags = taggable.tag_list
|
|
5
|
+
available_to_add = available_tags - current_tags
|
|
6
|
+
%>
|
|
7
|
+
|
|
8
|
+
<div
|
|
9
|
+
id="tag_manager_<%= taggable_type %>_<%= taggable.id %>"
|
|
10
|
+
class="tag-manager"
|
|
11
|
+
>
|
|
12
|
+
<div class="tag-list">
|
|
13
|
+
<% current_tags.each do |tag| %>
|
|
14
|
+
<span class="tag">
|
|
15
|
+
<%= tag %>
|
|
16
|
+
<%= button_to remove_tag_path(taggable_type, taggable.id, tag: tag),
|
|
17
|
+
method: :delete,
|
|
18
|
+
class: "tag-remove",
|
|
19
|
+
data: {
|
|
20
|
+
turbo_frame: "_top"
|
|
21
|
+
} do %>
|
|
22
|
+
<span aria-hidden="true">×</span>
|
|
23
|
+
<% end %>
|
|
24
|
+
</span>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<% if available_to_add.any? %>
|
|
28
|
+
<div class="tag-add-container" data-controller="rails-pulse--popover" data-rails-pulse--popover-placement-value="bottom-start">
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
id="tag_menu_button_<%= taggable_type %>_<%= taggable.id %>"
|
|
32
|
+
class="btn btn--sm tag-add-button"
|
|
33
|
+
data-rails-pulse--popover-target="button"
|
|
34
|
+
data-action="rails-pulse--popover#toggle"
|
|
35
|
+
aria-haspopup="true"
|
|
36
|
+
aria-controls="tag_menu_<%= taggable_type %>_<%= taggable.id %>"
|
|
37
|
+
>
|
|
38
|
+
+ tag
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<div
|
|
42
|
+
popover
|
|
43
|
+
class="popover"
|
|
44
|
+
style="--popover-size: 12rem;"
|
|
45
|
+
data-rails-pulse--popover-target="menu"
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
id="tag_menu_<%= taggable_type %>_<%= taggable.id %>"
|
|
49
|
+
class="menu"
|
|
50
|
+
data-controller="rails-pulse--menu"
|
|
51
|
+
data-action="keydown.up->rails-pulse--menu#prev keydown.down->rails-pulse--menu#next"
|
|
52
|
+
role="menu"
|
|
53
|
+
aria-labelledby="tag_menu_button_<%= taggable_type %>_<%= taggable.id %>"
|
|
54
|
+
>
|
|
55
|
+
<% available_to_add.each do |tag| %>
|
|
56
|
+
<%= button_to add_tag_path(taggable_type, taggable.id, tag: tag),
|
|
57
|
+
method: :post,
|
|
58
|
+
class: "btn menu__item i-full",
|
|
59
|
+
data: {
|
|
60
|
+
turbo_frame: "_top",
|
|
61
|
+
rails_pulse__menu_target: "item"
|
|
62
|
+
},
|
|
63
|
+
style: "cursor: pointer;",
|
|
64
|
+
role: "menuitem" do %>
|
|
65
|
+
<%= tag %>
|
|
66
|
+
<% end %>
|
|
67
|
+
<% end %>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
@@ -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}\">"
|
data/config/routes.rb
CHANGED
|
@@ -11,6 +11,11 @@ RailsPulse::Engine.routes.draw do
|
|
|
11
11
|
resources :operations, only: %i[show]
|
|
12
12
|
resources :caches, only: %i[show], as: :cache
|
|
13
13
|
patch "pagination/limit", to: "application#set_pagination_limit"
|
|
14
|
+
patch "settings/global_filters", to: "application#set_global_filters"
|
|
15
|
+
|
|
16
|
+
# Tag management
|
|
17
|
+
post "tags/:taggable_type/:taggable_id/add", to: "tags#create", as: :add_tag
|
|
18
|
+
delete "tags/:taggable_type/:taggable_id/remove", to: "tags#destroy", as: :remove_tag
|
|
14
19
|
|
|
15
20
|
# CSP compliance testing
|
|
16
21
|
get "csp_test", to: "csp_test#show", as: :csp_test
|
|
@@ -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) }
|
|
@@ -18,6 +18,7 @@ RailsPulse::Schema = lambda do |connection|
|
|
|
18
18
|
connection.create_table :rails_pulse_routes do |t|
|
|
19
19
|
t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
|
|
20
20
|
t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
|
|
21
|
+
t.text :tags, comment: "JSON array of tags for filtering and categorization"
|
|
21
22
|
t.timestamps
|
|
22
23
|
end
|
|
23
24
|
|
|
@@ -34,6 +35,7 @@ RailsPulse::Schema = lambda do |connection|
|
|
|
34
35
|
t.text :index_recommendations, comment: "JSON array of database index recommendations"
|
|
35
36
|
t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
|
|
36
37
|
t.text :suggestions, comment: "JSON array of optimization recommendations"
|
|
38
|
+
t.text :tags, comment: "JSON array of tags for filtering and categorization"
|
|
37
39
|
t.timestamps
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -47,6 +49,7 @@ RailsPulse::Schema = lambda do |connection|
|
|
|
47
49
|
t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
|
|
48
50
|
t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
|
|
49
51
|
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
|
52
|
+
t.text :tags, comment: "JSON array of tags for filtering and categorization"
|
|
50
53
|
t.timestamps
|
|
51
54
|
end
|
|
52
55
|
|
|
@@ -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,29 @@ 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
|
+
|
|
25
|
+
def copy_gem_migrations
|
|
26
|
+
gem_migrations_path = File.expand_path("../../../db/rails_pulse_migrate", __dir__)
|
|
27
|
+
destination_dir = separate_database? ? "db/rails_pulse_migrate" : "db/migrate"
|
|
28
|
+
|
|
29
|
+
if File.directory?(gem_migrations_path)
|
|
30
|
+
Dir.glob("#{gem_migrations_path}/*.rb").each do |migration_file|
|
|
31
|
+
migration_name = File.basename(migration_file)
|
|
32
|
+
destination_path = File.join(destination_dir, migration_name)
|
|
33
|
+
|
|
34
|
+
# Only copy if it doesn't already exist in the destination
|
|
35
|
+
# Use File.join with destination_root to check the actual location
|
|
36
|
+
full_destination_path = File.join(destination_root, destination_path)
|
|
37
|
+
unless File.exist?(full_destination_path)
|
|
38
|
+
copy_file migration_file, destination_path
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
21
44
|
def copy_initializer
|
|
22
45
|
copy_file "rails_pulse.rb", "config/initializers/rails_pulse.rb"
|
|
23
46
|
end
|
|
@@ -45,10 +68,9 @@ module RailsPulse
|
|
|
45
68
|
end
|
|
46
69
|
|
|
47
70
|
def create_separate_database_setup
|
|
48
|
-
create_file "db/rails_pulse_migrate/.keep"
|
|
49
|
-
|
|
50
71
|
say "Setting up separate database configuration...", :green
|
|
51
72
|
|
|
73
|
+
# Migration directory already created by create_migration_directory
|
|
52
74
|
# Could add database.yml configuration here if needed
|
|
53
75
|
# For now, users will configure manually
|
|
54
76
|
end
|
|
@@ -80,7 +102,8 @@ module RailsPulse
|
|
|
80
102
|
2. Run: rails db:prepare (creates database and loads schema)
|
|
81
103
|
3. Restart your Rails server
|
|
82
104
|
|
|
83
|
-
|
|
105
|
+
The schema file db/rails_pulse_schema.rb is your single source of truth.
|
|
106
|
+
Future upgrades will automatically copy new migrations to db/rails_pulse_migrate/
|
|
84
107
|
|
|
85
108
|
MESSAGE
|
|
86
109
|
end
|
|
@@ -92,12 +115,12 @@ module RailsPulse
|
|
|
92
115
|
|
|
93
116
|
Next steps:
|
|
94
117
|
1. Run: rails db:migrate (creates Rails Pulse tables in your main database)
|
|
95
|
-
2.
|
|
96
|
-
3. Restart your Rails server
|
|
118
|
+
2. Restart your Rails server
|
|
97
119
|
|
|
98
|
-
|
|
120
|
+
The schema file db/rails_pulse_schema.rb is your single source of truth.
|
|
121
|
+
Future upgrades will automatically copy new migrations to db/migrate/
|
|
99
122
|
|
|
100
|
-
Note: The installation migration
|
|
123
|
+
Note: The installation migration loads from db/rails_pulse_schema.rb
|
|
101
124
|
and includes all current Rails Pulse tables and columns.
|
|
102
125
|
|
|
103
126
|
MESSAGE
|