rails_pulse 0.1.1 → 0.1.3
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 +79 -177
- data/Rakefile +77 -2
- data/app/assets/images/rails_pulse/dashboard.png +0 -0
- data/app/assets/images/rails_pulse/request.png +0 -0
- data/app/assets/stylesheets/rails_pulse/application.css +28 -17
- data/app/assets/stylesheets/rails_pulse/components/badge.css +13 -0
- data/app/assets/stylesheets/rails_pulse/components/base.css +12 -2
- data/app/assets/stylesheets/rails_pulse/components/collapsible.css +30 -0
- data/app/assets/stylesheets/rails_pulse/components/popover.css +0 -1
- data/app/assets/stylesheets/rails_pulse/components/row.css +55 -3
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +23 -0
- data/app/controllers/concerns/chart_table_concern.rb +21 -4
- data/app/controllers/concerns/response_range_concern.rb +6 -3
- data/app/controllers/concerns/time_range_concern.rb +5 -10
- data/app/controllers/concerns/zoom_range_concern.rb +32 -1
- data/app/controllers/rails_pulse/application_controller.rb +13 -5
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -0
- data/app/controllers/rails_pulse/queries_controller.rb +111 -51
- data/app/controllers/rails_pulse/requests_controller.rb +37 -12
- data/app/controllers/rails_pulse/routes_controller.rb +98 -24
- data/app/helpers/rails_pulse/application_helper.rb +0 -1
- data/app/helpers/rails_pulse/chart_formatters.rb +3 -3
- data/app/helpers/rails_pulse/chart_helper.rb +21 -9
- data/app/helpers/rails_pulse/status_helper.rb +10 -4
- data/app/javascript/rails_pulse/application.js +34 -3
- data/app/javascript/rails_pulse/controllers/collapsible_controller.js +32 -0
- data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +2 -1
- data/app/javascript/rails_pulse/controllers/expandable_rows_controller.js +58 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +353 -39
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +17 -27
- data/app/javascript/rails_pulse/controllers/popover_controller.js +28 -4
- data/app/javascript/rails_pulse/controllers/table_sort_controller.js +14 -0
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +41 -0
- data/app/jobs/rails_pulse/summary_job.rb +53 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +28 -7
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +18 -22
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +18 -7
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +34 -41
- data/app/models/rails_pulse/operation.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +49 -25
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +40 -28
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +37 -43
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +23 -97
- data/app/models/rails_pulse/queries/tables/index.rb +74 -0
- data/app/models/rails_pulse/query.rb +47 -0
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +23 -84
- data/app/models/rails_pulse/route.rb +1 -6
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +45 -25
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +43 -45
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +36 -44
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +37 -27
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +23 -100
- data/app/models/rails_pulse/routes/tables/index.rb +57 -40
- data/app/models/rails_pulse/summary.rb +143 -0
- data/app/services/rails_pulse/analysis/backtrace_analyzer.rb +256 -0
- data/app/services/rails_pulse/analysis/base_analyzer.rb +67 -0
- data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +206 -0
- data/app/services/rails_pulse/analysis/index_recommendation_engine.rb +326 -0
- data/app/services/rails_pulse/analysis/n_plus_one_detector.rb +241 -0
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +146 -0
- data/app/services/rails_pulse/analysis/suggestion_generator.rb +217 -0
- data/app/services/rails_pulse/query_analysis_service.rb +125 -0
- data/app/services/rails_pulse/summary_service.rb +199 -0
- data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +0 -1
- data/app/views/layouts/rails_pulse/application.html.erb +4 -6
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +1 -1
- data/app/views/rails_pulse/components/_code_panel.html.erb +17 -3
- data/app/views/rails_pulse/components/_empty_state.html.erb +11 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +37 -28
- data/app/views/rails_pulse/components/_panel.html.erb +1 -1
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +5 -7
- data/app/views/rails_pulse/components/_table_head.html.erb +6 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +55 -37
- data/app/views/rails_pulse/operations/show.html.erb +17 -15
- data/app/views/rails_pulse/queries/_analysis_error.html.erb +15 -0
- data/app/views/rails_pulse/queries/_analysis_prompt.html.erb +27 -0
- data/app/views/rails_pulse/queries/_analysis_results.html.erb +87 -0
- data/app/views/rails_pulse/queries/_analysis_section.html.erb +39 -0
- data/app/views/rails_pulse/queries/_show_table.html.erb +2 -2
- data/app/views/rails_pulse/queries/_table.html.erb +11 -13
- data/app/views/rails_pulse/queries/index.html.erb +32 -28
- data/app/views/rails_pulse/queries/show.html.erb +45 -34
- data/app/views/rails_pulse/requests/_operations.html.erb +38 -45
- data/app/views/rails_pulse/requests/_table.html.erb +3 -3
- data/app/views/rails_pulse/requests/index.html.erb +33 -28
- data/app/views/rails_pulse/routes/_table.html.erb +14 -14
- data/app/views/rails_pulse/routes/index.html.erb +34 -29
- data/app/views/rails_pulse/routes/show.html.erb +43 -36
- data/config/initializers/rails_pulse.rb +0 -12
- data/config/routes.rb +5 -1
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +54 -0
- data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +13 -0
- data/db/rails_pulse_schema.rb +130 -0
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +65 -0
- data/lib/generators/rails_pulse/install_generator.rb +94 -4
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +60 -0
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +22 -0
- data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +19 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +0 -12
- data/lib/generators/rails_pulse/upgrade_generator.rb +225 -0
- data/lib/rails_pulse/configuration.rb +0 -11
- data/lib/rails_pulse/engine.rb +0 -1
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/tasks/rails_pulse.rake +77 -0
- 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 +53 -53
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- data/public/rails-pulse-assets/search.svg +43 -0
- metadata +48 -14
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/images/rails_pulse/routes.png +0 -0
- data/app/controllers/rails_pulse/caches_controller.rb +0 -115
- data/app/helpers/rails_pulse/cached_component_helper.rb +0 -73
- data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +0 -67
- data/app/models/rails_pulse/component_cache_key.rb +0 -33
- data/app/views/rails_pulse/caches/show.html.erb +0 -9
- data/db/migrate/20250227235904_create_routes.rb +0 -12
- data/db/migrate/20250227235915_create_requests.rb +0 -19
- data/db/migrate/20250228000000_create_queries.rb +0 -14
- data/db/migrate/20250228000056_create_operations.rb +0 -24
- data/lib/rails_pulse/migration.rb +0 -29
@@ -1,22 +1,45 @@
|
|
1
1
|
<%
|
2
|
-
|
3
|
-
id
|
4
|
-
context
|
5
|
-
title
|
6
|
-
summary
|
7
|
-
line_chart_data
|
8
|
-
trend_icon
|
9
|
-
trend_amount
|
10
|
-
trend_text
|
2
|
+
# Use the passed data object directly
|
3
|
+
id = data[:id]
|
4
|
+
context = data[:context]
|
5
|
+
title = data[:title]
|
6
|
+
summary = data[:summary]
|
7
|
+
line_chart_data = data[:line_chart_data]
|
8
|
+
trend_icon = data[:trend_icon]
|
9
|
+
trend_amount = data[:trend_amount]
|
10
|
+
trend_text = data[:trend_text]
|
11
11
|
%>
|
12
|
-
<div class="grid-item" data-controller="rails-pulse--chart">
|
12
|
+
<div class="grid-item" data-controller="rails-pulse--chart" id="<%= id %>">
|
13
13
|
<%= render 'rails_pulse/components/panel', { title: title, card_classes: 'card-compact' } do %>
|
14
|
-
<div class="row mbs-2" style="
|
14
|
+
<div class="row mbs-2" style="--columns: 2; align-items: center; margin-bottom: 0;">
|
15
15
|
<div class="grid-item">
|
16
16
|
<h4 class="text-xl mbs-1 font-bold"><%= summary %></h4>
|
17
17
|
</div>
|
18
18
|
<div class="grid-item">
|
19
|
-
|
19
|
+
<div class="chart-container chart-container--slim">
|
20
|
+
<%
|
21
|
+
# Match chart color to trending icon - use hex equivalents of CSS variables
|
22
|
+
# These colors match the badge colors exactly
|
23
|
+
chart_color = case trend_icon
|
24
|
+
when "trending-up"
|
25
|
+
"#dc2626" # equivalent to --red-600 (negative trend)
|
26
|
+
when "trending-down"
|
27
|
+
"#16a34a" # equivalent to --green-600 (positive trend)
|
28
|
+
else
|
29
|
+
"#ffc91f" # equivalent to --zinc-900 (neutral/primary)
|
30
|
+
end
|
31
|
+
|
32
|
+
chart_options = sparkline_chart_options.deep_merge(
|
33
|
+
color: [chart_color],
|
34
|
+
series: {
|
35
|
+
itemStyle: {
|
36
|
+
color: chart_color
|
37
|
+
}
|
38
|
+
}
|
39
|
+
)
|
40
|
+
%>
|
41
|
+
<%= bar_chart line_chart_data, height: "100%", options: chart_options %>
|
42
|
+
</div>
|
20
43
|
</div>
|
21
44
|
</div>
|
22
45
|
<div class="mbs-2" style="height: 10px;">
|
@@ -30,25 +53,11 @@
|
|
30
53
|
end
|
31
54
|
%>
|
32
55
|
<div class="flex items-center justify-between">
|
33
|
-
<span class="badge <%= badge %> p-0">
|
56
|
+
<span class="badge <%= badge %> badge--trend p-0">
|
34
57
|
<%= rails_pulse_icon trend_icon, height: "15px", width: "15px", class: "mie-2" %>
|
35
|
-
|
58
|
+
<span class="badge__trend-amount"><%= trend_amount %></span>
|
36
59
|
<p class="mis-2 text-subtle text-xs"><%= trend_text %></p>
|
37
60
|
</span>
|
38
|
-
<%
|
39
|
-
refresh_path = rails_pulse.cache_path(id: id, context: context, refresh: true)
|
40
|
-
cached_iso = @cached_at ? @cached_at.iso8601 : nil
|
41
|
-
%>
|
42
|
-
<%= link_to refresh_path,
|
43
|
-
data: {
|
44
|
-
controller: "rails-pulse--timezone",
|
45
|
-
rails_pulse__timezone_target_frame_value: "#{id}_card",
|
46
|
-
rails_pulse__timezone_cached_at_value: cached_iso,
|
47
|
-
turbo_frame: "#{id}_card",
|
48
|
-
turbo_prefetch: "false"
|
49
|
-
} do %>
|
50
|
-
<%= rails_pulse_icon 'refresh-cw', height: "15px", width: "15px" %>
|
51
|
-
<% end %>
|
52
61
|
</div>
|
53
62
|
</div>
|
54
63
|
<% end %>
|
@@ -15,7 +15,7 @@
|
|
15
15
|
<% end %>
|
16
16
|
<div class="flex gap items-center">
|
17
17
|
<% if help_heading %>
|
18
|
-
<div data-controller="rails-pulse--popover" data-rails-pulse--popover-placement-value="bottom-
|
18
|
+
<div data-controller="rails-pulse--popover" data-rails-pulse--popover-placement-value="bottom-start">
|
19
19
|
<a href="#"
|
20
20
|
data-rails-pulse--popover-target="button"
|
21
21
|
data-action="rails-pulse--popover#toggle"
|
@@ -1,10 +1,8 @@
|
|
1
|
-
<div class="
|
2
|
-
<
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
<%= line_chart line_chart_data, height: "100%", options: sparkline_chart_options %>
|
7
|
-
</div>
|
1
|
+
<div class="mbs-2">
|
2
|
+
<h4 class="text-xl mbs-1 font-bold"><%= summary %></h4>
|
3
|
+
</div>
|
4
|
+
<div class="chart-container chart-container--slim">
|
5
|
+
<%= bar_chart line_chart_data, height: "100%", options: sparkline_chart_options %>
|
8
6
|
</div>
|
9
7
|
<div>
|
10
8
|
<span class="badge badge--<%= trend_direction == "down" ? "positive" : "negative" %>-inverse p-0">
|
@@ -8,10 +8,15 @@
|
|
8
8
|
<% sort_params = {} %>
|
9
9
|
<% sort_params[:zoom_start_time] = @zoom_start if @zoom_start.present? %>
|
10
10
|
<% sort_params[:zoom_end_time] = @zoom_end if @zoom_end.present? %>
|
11
|
+
<% sort_params[:selected_column_time] = params[:selected_column_time] if params[:selected_column_time].present? %>
|
11
12
|
<%= sort_link @ransack_query, column[:field], column[:label],
|
12
13
|
sort_params.merge(
|
13
14
|
class: "flex items-center",
|
14
|
-
data: {
|
15
|
+
data: {
|
16
|
+
turbo_prefetch: "false",
|
17
|
+
turbo_frame: "index_table",
|
18
|
+
action: "click->rails-pulse--table-sort#updateUrl"
|
19
|
+
}
|
15
20
|
) %>
|
16
21
|
<% end %>
|
17
22
|
</th>
|
@@ -1,64 +1,82 @@
|
|
1
1
|
<div class="row">
|
2
|
-
<%=
|
3
|
-
<%=
|
4
|
-
<%=
|
5
|
-
<%=
|
2
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
|
3
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_response_times_metric_card } %>
|
4
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @request_count_totals_metric_card } %>
|
5
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @error_rate_per_route_metric_card } %>
|
6
6
|
</div>
|
7
7
|
|
8
8
|
<div class="row">
|
9
|
-
<div class="grid-item"
|
10
|
-
<%=
|
11
|
-
component: "panel",
|
12
|
-
id: "dashboard_average_response_time",
|
13
|
-
card_classes: 'b-full',
|
9
|
+
<div class="grid-item">
|
10
|
+
<%= render 'rails_pulse/components/panel', {
|
14
11
|
title: 'Average Response Time',
|
12
|
+
card_classes: 'b-full',
|
15
13
|
help_heading: 'Average Response Time',
|
16
14
|
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
|
-
|
19
|
-
|
20
|
-
|
15
|
+
actions: [{ url: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }]
|
16
|
+
} do %>
|
17
|
+
<% if @average_response_time_chart_data.present? %>
|
18
|
+
<div class="chart-container chart-container--slim">
|
19
|
+
<%= bar_chart(
|
20
|
+
@average_response_time_chart_data,
|
21
|
+
code: false,
|
22
|
+
id: "dashboard_average_response_time_chart",
|
23
|
+
height: "100%",
|
24
|
+
options: bar_chart_options(
|
25
|
+
units: "ms"
|
26
|
+
)
|
27
|
+
) %>
|
28
|
+
</div>
|
29
|
+
<% end %>
|
30
|
+
<% end %>
|
21
31
|
</div>
|
22
32
|
|
23
33
|
<div class="grid-item">
|
24
|
-
<%=
|
25
|
-
component: "panel",
|
26
|
-
id: "dashboard_p95_response_time",
|
27
|
-
card_classes: 'b-full',
|
34
|
+
<%= render 'rails_pulse/components/panel', {
|
28
35
|
title: 'Query Performance',
|
36
|
+
card_classes: 'b-full',
|
29
37
|
help_heading: 'Query Performance',
|
30
38
|
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:
|
32
|
-
|
33
|
-
|
34
|
-
|
39
|
+
actions: [{ url: queries_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }]
|
40
|
+
} do %>
|
41
|
+
<% if @p95_response_time_chart_data.present? %>
|
42
|
+
<div class="chart-container chart-container--slim">
|
43
|
+
<%= bar_chart(
|
44
|
+
@p95_response_time_chart_data,
|
45
|
+
code: false,
|
46
|
+
id: "dashboard_p95_response_time_chart",
|
47
|
+
height: "100%",
|
48
|
+
options: bar_chart_options(
|
49
|
+
units: "ms"
|
50
|
+
)
|
51
|
+
) %>
|
52
|
+
</div>
|
53
|
+
<% end %>
|
54
|
+
<% end %>
|
35
55
|
</div>
|
36
56
|
</div>
|
37
57
|
|
38
58
|
<div class="row">
|
39
59
|
<div class="grid-item">
|
40
|
-
<%=
|
41
|
-
component: "panel",
|
42
|
-
id: "dashboard_slow_routes",
|
60
|
+
<%= render 'rails_pulse/components/panel', {
|
43
61
|
title: 'Slowest Routes This Week',
|
44
62
|
help_heading: 'Slowest Routes',
|
45
|
-
help_text: 'This panel shows the slowest routes in your application this week
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
63
|
+
help_text: 'This panel shows the slowest routes in your application this week, including average response time and total request count.',
|
64
|
+
actions: [{ url: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
|
65
|
+
card_classes: 'table-container'
|
66
|
+
} do %>
|
67
|
+
<%= render 'rails_pulse/components/table', table_data: @slow_routes_table_data %>
|
68
|
+
<% end %>
|
50
69
|
</div>
|
51
70
|
|
52
71
|
<div class="grid-item">
|
53
|
-
<%=
|
54
|
-
component: "panel",
|
55
|
-
id: "dashboard_slow_queries",
|
72
|
+
<%= render 'rails_pulse/components/panel', {
|
56
73
|
title: 'Slowest Queries This Week',
|
57
|
-
help_heading: 'Slowest Queries',
|
74
|
+
help_heading: 'Slowest Queries',
|
58
75
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
76
|
+
actions: [{ url: queries_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
|
77
|
+
card_classes: 'table-container'
|
78
|
+
} do %>
|
79
|
+
<%= render 'rails_pulse/components/table', table_data: @slow_queries_table_data %>
|
80
|
+
<% end %>
|
63
81
|
</div>
|
64
82
|
</div>
|
@@ -4,23 +4,25 @@
|
|
4
4
|
|
5
5
|
<div class="card">
|
6
6
|
<%= turbo_frame_tag "operation_#{@operation.id}_details" do %>
|
7
|
+
<% if @operation.operation_type == "sql" %>
|
8
|
+
<div class="mbe-4">
|
9
|
+
<%= render 'rails_pulse/components/code_panel', { value: @operation.label } %>
|
10
|
+
</div>
|
11
|
+
<% end %>
|
12
|
+
|
7
13
|
<dl class="descriptive-list">
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
<dd>
|
18
|
-
<% if @operation.operation_type == "sql" %>
|
19
|
-
<%= render 'rails_pulse/components/code_panel', { value: @operation.label } %>
|
20
|
-
<% else %>
|
14
|
+
<% if @operation.operation_type != "sql" %>
|
15
|
+
<dt>
|
16
|
+
<% if ["template", "partial", "layout", "collection"].include?(@operation.operation_type) %>
|
17
|
+
View Render
|
18
|
+
<% else %>
|
19
|
+
<%= html_escape(@operation.label) %>
|
20
|
+
<% end %>
|
21
|
+
</dt>
|
22
|
+
<dd>
|
21
23
|
<%= html_escape(@operation.label) %>
|
22
|
-
|
23
|
-
|
24
|
+
</dd>
|
25
|
+
<% end %>
|
24
26
|
|
25
27
|
<dt>Operation Type</dt>
|
26
28
|
<dd><pre><%= @operation.operation_type %></pre></dd>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<div class="panel panel--danger">
|
2
|
+
<div>
|
3
|
+
<h3 class="text-sm bold">Analysis Failed</h3>
|
4
|
+
<p class="text-sm mt-1">
|
5
|
+
<%= error_message %>
|
6
|
+
</p>
|
7
|
+
<div class="mt-3">
|
8
|
+
<%= button_to analyze_query_path(query), method: :post,
|
9
|
+
data: { turbo_frame: "query_analysis" },
|
10
|
+
class: "btn btn--sm" do %>
|
11
|
+
Try Again
|
12
|
+
<% end %>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
</div>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<div class="center py-4">
|
2
|
+
<h3 class="text-lg mb-2">Analyze Query Performance</h3>
|
3
|
+
<p class="mb-4">
|
4
|
+
Get detailed insights about this query's performance, potential issues, and optimization suggestions.
|
5
|
+
</p>
|
6
|
+
|
7
|
+
<% if query.has_recent_operations? %>
|
8
|
+
<%= button_to analyze_query_path(query), method: :post,
|
9
|
+
data: { turbo_frame: "query_analysis" },
|
10
|
+
class: "btn btn--primary" do %>
|
11
|
+
Analyze Query
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<p class="text-sm mt-2">
|
15
|
+
Analysis will examine recent executions and provide performance insights.
|
16
|
+
</p>
|
17
|
+
<% else %>
|
18
|
+
<div class="badge badge--warning p-3 mt-3">
|
19
|
+
<div>
|
20
|
+
<h4 class="text-sm bold">No Recent Data Available</h4>
|
21
|
+
<p class="text-sm mt-1">
|
22
|
+
This query hasn't been executed recently. Analysis requires recent execution data to provide meaningful insights.
|
23
|
+
</p>
|
24
|
+
</div>
|
25
|
+
</div>
|
26
|
+
<% end %>
|
27
|
+
</div>
|
@@ -0,0 +1,87 @@
|
|
1
|
+
<div class="row">
|
2
|
+
<div>
|
3
|
+
<!-- Query Characteristics -->
|
4
|
+
<% if query.query_stats.present? %>
|
5
|
+
<h5>Query Characteristics</h5>
|
6
|
+
<dl class="descriptive-list mbs-4">
|
7
|
+
<dt>Last Analyzed</dt>
|
8
|
+
<dd><%= time_ago_in_words(query.analyzed_at) %></dd>
|
9
|
+
<dt>Query Type</dt>
|
10
|
+
<dd><%= query.query_stats['query_type'] %></dd>
|
11
|
+
<dt>Tables</dt>
|
12
|
+
<dd><%= query.query_stats['table_count'] || 0 %></dd>
|
13
|
+
<dt>Joins</dt>
|
14
|
+
<dd><%= query.query_stats['join_count'] || 0 %></dd>
|
15
|
+
<dt>Complexity Score</dt>
|
16
|
+
<dd><%= query.query_stats['estimated_complexity'] || 0 %></dd>
|
17
|
+
<dt>Has LIMIT</dt>
|
18
|
+
<dd><%= query.query_stats['has_limit'] ? 'Yes' : 'No' %></dd>
|
19
|
+
<dt>Has ORDER BY</dt>
|
20
|
+
<dd><%= query.query_stats['has_order_by'] ? 'Yes' : 'No' %></dd>
|
21
|
+
<dt>Has GROUP BY</dt>
|
22
|
+
<dd><%= query.query_stats['has_group_by'] ? 'Yes' : 'No' %></dd>
|
23
|
+
<dt>Has Subqueries</dt>
|
24
|
+
<dd><%= query.query_stats['has_subqueries'] ? 'Yes' : 'No' %></dd>
|
25
|
+
</dl>
|
26
|
+
<% end %>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<div>
|
30
|
+
<!-- Execution Analysis -->
|
31
|
+
<% if query.backtrace_analysis.present? %>
|
32
|
+
<h5>Execution Analysis</h5>
|
33
|
+
<dl class="descriptive-list mbs-4">
|
34
|
+
<dt>Total Executions</dt>
|
35
|
+
<dd><%= query.backtrace_analysis['total_executions'] || 0 %></dd>
|
36
|
+
<dt>Unique Locations</dt>
|
37
|
+
<dd><%= query.backtrace_analysis['unique_locations'] || 0 %></dd>
|
38
|
+
<dt>Execution Frequency</dt>
|
39
|
+
<dd><%= query.backtrace_analysis['execution_frequency'] || 0 %>/hour</dd>
|
40
|
+
<dt>Most Common Location</dt>
|
41
|
+
<dd><code><%= query.backtrace_analysis['most_common_location']['location'] || 'N/A' %></code></dd>
|
42
|
+
<dt>N+1 Pattern Detected</dt>
|
43
|
+
<dd>
|
44
|
+
<% if query.backtrace_analysis['potential_n_plus_one'] %>
|
45
|
+
Yes
|
46
|
+
<% else %>
|
47
|
+
No
|
48
|
+
<% end %>
|
49
|
+
</dd>
|
50
|
+
</dl>
|
51
|
+
<% end %>
|
52
|
+
</div>
|
53
|
+
</div>
|
54
|
+
|
55
|
+
<!-- Issues Summary -->
|
56
|
+
<% if query.issues.present? && query.issues.any? %>
|
57
|
+
<div class="panel panel--danger mb-4">
|
58
|
+
<h3 class="text-lg bold mb-3">Issues Detected</h3>
|
59
|
+
<ul>
|
60
|
+
<% query.issues.each do |issue| %>
|
61
|
+
<li><%= issue['description'] %></li>
|
62
|
+
<% end %>
|
63
|
+
</ul>
|
64
|
+
</div>
|
65
|
+
<% end %>
|
66
|
+
|
67
|
+
<!-- Suggestions -->
|
68
|
+
<% if query.suggestions.present? && query.suggestions.any? %>
|
69
|
+
<div class="panel panel--info mb-4">
|
70
|
+
<h3 class="text-lg bold mb-3">Optimization Suggestions</h3>
|
71
|
+
<ul>
|
72
|
+
<% query.suggestions.each do |suggestion| %>
|
73
|
+
<li>
|
74
|
+
<%= suggestion['action'] %>.
|
75
|
+
<%= suggestion['benefit'] if suggestion['benefit'].present? %>
|
76
|
+
</li>
|
77
|
+
<% end %>
|
78
|
+
</ul>
|
79
|
+
</div>
|
80
|
+
<% end %>
|
81
|
+
|
82
|
+
<!-- EXPLAIN Plan -->
|
83
|
+
<% if query.explain_plan.present? %>
|
84
|
+
<%= render 'rails_pulse/components/panel', title: 'Execution Plan' do %>
|
85
|
+
<pre class="text-sm"><%= query.explain_plan %></pre>
|
86
|
+
<% end %>
|
87
|
+
<% end %>
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<%
|
2
|
+
analyze_actions = []
|
3
|
+
analyze_actions << {
|
4
|
+
url: analyze_query_path(query),
|
5
|
+
title: query.analyzed? ? "Re-analyze query" : "Analyze query performance",
|
6
|
+
icon: "refresh-cw",
|
7
|
+
data: {
|
8
|
+
turbo_method: "post",
|
9
|
+
turbo_frame: "query_analysis"
|
10
|
+
}
|
11
|
+
}
|
12
|
+
%>
|
13
|
+
|
14
|
+
<%= render 'rails_pulse/components/panel', {
|
15
|
+
title: 'Query Analysis',
|
16
|
+
actions: analyze_actions
|
17
|
+
} do %>
|
18
|
+
<% if local_assigns[:error_message] %>
|
19
|
+
<%= render 'rails_pulse/queries/analysis_error', error_message: error_message, query: query %>
|
20
|
+
<% elsif query.analyzed? %>
|
21
|
+
<%= render 'rails_pulse/queries/analysis_results', query: query %>
|
22
|
+
<% else %>
|
23
|
+
<div class="flex items-center justify-center pbs-12 pbe-12 pis-6 pie-6 mb-8">
|
24
|
+
<div class="flex items-center gap">
|
25
|
+
<div class="shrink-0">
|
26
|
+
<img src="<%= asset_path('search.svg') %>" class="w-48 h-48" alt="No data available" />
|
27
|
+
</div>
|
28
|
+
<div class="text-subtle pis-8">
|
29
|
+
<p class="text-lg font-semibold mbe-2">Analyze Query Performance</p>
|
30
|
+
<% if query.has_recent_operations? %>
|
31
|
+
<p class="text-sm mbe-2"> Get detailed insights about this query's performance, potential issues, and optimization suggestions. </p>
|
32
|
+
<% else %>
|
33
|
+
<p class="text-sm mbe-2">No recent data available for analysis</p>
|
34
|
+
<% end %>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
<% end %>
|
39
|
+
<% end %>
|
@@ -1,9 +1,9 @@
|
|
1
1
|
<% columns = [
|
2
|
-
{ field: :occurred_at, label: '
|
2
|
+
{ field: :occurred_at, label: 'Timestamp', class: 'w-auto' },
|
3
3
|
{ field: :duration, label: 'Duration', class: 'w-32'}
|
4
4
|
] %>
|
5
5
|
|
6
|
-
<table class="table mbs-4">
|
6
|
+
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
7
7
|
<%= render "rails_pulse/components/table_head", columns: columns %>
|
8
8
|
|
9
9
|
<tbody>
|
@@ -1,28 +1,26 @@
|
|
1
1
|
<% columns = [
|
2
2
|
{ field: :normalized_sql, label: 'Query', class: 'w-auto' },
|
3
|
-
{ field: :
|
4
|
-
{ field: :
|
5
|
-
{ field: :
|
6
|
-
{ field: :performance_status, label: 'Status', class: 'w-16' }
|
7
|
-
{ field: :occurred_at, label: 'Last Seen', class: 'w-32' }
|
3
|
+
{ field: :execution_count_sort, label: 'Executions', class: 'w-24' },
|
4
|
+
{ field: :avg_duration_sort, label: 'Avg Time', class: 'w-24' },
|
5
|
+
{ field: :total_time_consumed_sort, label: 'Total Time', class: 'w-28' },
|
6
|
+
{ field: :performance_status, label: 'Status', class: 'w-16', sortable: false }
|
8
7
|
] %>
|
9
8
|
|
10
|
-
<table class="table mbs-4">
|
9
|
+
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
11
10
|
<%= render "rails_pulse/components/table_head", columns: columns %>
|
12
11
|
|
13
12
|
<tbody>
|
14
|
-
<% @table_data.each do |
|
13
|
+
<% @table_data.each do |summary| %>
|
15
14
|
<tr>
|
16
15
|
<td class="whitespace-nowrap">
|
17
16
|
<div>
|
18
|
-
<%= link_to html_escape(truncate_sql(
|
17
|
+
<%= link_to html_escape(truncate_sql(summary.normalized_sql)), query_path(summary.query_id), data: { turbo_frame: '_top', } %>
|
19
18
|
</div>
|
20
19
|
</td>
|
21
|
-
<td class="whitespace-nowrap"><%= number_with_delimiter
|
22
|
-
<td class="whitespace-nowrap"><%=
|
23
|
-
<td class="whitespace-nowrap"><%= number_with_delimiter
|
24
|
-
<td class="whitespace-nowrap text-center"><%= query_status_indicator(
|
25
|
-
<td class="whitespace-nowrap"><%= human_readable_occurred_at(query.occurred_at) if query.occurred_at %></td>
|
20
|
+
<td class="whitespace-nowrap"><%= number_with_delimiter summary.execution_count %></td>
|
21
|
+
<td class="whitespace-nowrap"><%= summary.avg_duration.to_i %> ms</td>
|
22
|
+
<td class="whitespace-nowrap"><%= number_with_delimiter summary.total_time_consumed.to_i %> ms</td>
|
23
|
+
<td class="whitespace-nowrap text-center"><%= query_status_indicator(summary.avg_duration) %></td>
|
26
24
|
</tr>
|
27
25
|
<% end %>
|
28
26
|
</tbody>
|
@@ -1,37 +1,37 @@
|
|
1
1
|
<%= render 'rails_pulse/components/breadcrumbs' %>
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
</div>
|
3
|
+
<% unless turbo_frame_request? %>
|
4
|
+
<div class="row">
|
5
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
|
6
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_query_times_metric_card } %>
|
7
|
+
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @execution_rate_metric_card } %>
|
8
|
+
</div>
|
9
|
+
<% end %>
|
9
10
|
|
10
11
|
<div
|
11
|
-
class="row"
|
12
12
|
data-controller="rails-pulse--index"
|
13
13
|
data-rails-pulse--index-chart-id-value="average_query_times_chart"
|
14
14
|
>
|
15
|
-
|
16
|
-
<%=
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
<% end %>
|
15
|
+
<%= render 'rails_pulse/components/panel', { title: 'Average Query Time', } do %>
|
16
|
+
<%= search_form_for @ransack_query, url: queries_path, class: "flex items-center justify-between gap mb-4" do |form| %>
|
17
|
+
<div class="flex items-center grow gap">
|
18
|
+
<%= form.select :period_start_range,
|
19
|
+
RailsPulse::QueriesController::TIME_RANGE_OPTIONS,
|
20
|
+
{ selected: @selected_time_range },
|
21
|
+
{ class: "input" }
|
22
|
+
%>
|
23
|
+
<%= form.select :avg_duration,
|
24
|
+
duration_options(:query),
|
25
|
+
{ selected: @selected_response_range },
|
26
|
+
{ class: "input" }
|
27
|
+
%>
|
28
|
+
<%= link_to "Reset", queries_path, class: "btn btn--borderless show@md" if params.has_key?(:q) %>
|
29
|
+
<%= form.submit "Search", class: "btn show@sm" %>
|
30
|
+
</div>
|
31
|
+
<% end %>
|
33
32
|
|
34
|
-
|
33
|
+
<% if @has_data %>
|
34
|
+
<% if @chart_data && @chart_data.values.any? { |v| v > 0 } %>
|
35
35
|
<div
|
36
36
|
class="chart-container chart-container--slim"
|
37
37
|
data-rails-pulse--index-target="chart"
|
@@ -56,9 +56,13 @@
|
|
56
56
|
</div>
|
57
57
|
<% end %>
|
58
58
|
|
59
|
-
<%= turbo_frame_tag :
|
59
|
+
<%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
|
60
60
|
<%= render 'rails_pulse/queries/table' %>
|
61
61
|
<% end %>
|
62
|
+
<% else %>
|
63
|
+
<%= render 'rails_pulse/components/empty_state',
|
64
|
+
title: 'No query data found for the selected filters.',
|
65
|
+
description: 'Try adjusting your time range or filters to see results.' %>
|
62
66
|
<% end %>
|
63
|
-
|
67
|
+
<% end %>
|
64
68
|
</div>
|