rails_pulse 0.1.2 → 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 +66 -20
- data/Rakefile +169 -86
- 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 -5
- 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/zoom_range_concern.rb +31 -0
- data/app/controllers/rails_pulse/application_controller.rb +5 -1
- data/app/controllers/rails_pulse/queries_controller.rb +49 -10
- data/app/controllers/rails_pulse/requests_controller.rb +46 -20
- data/app/controllers/rails_pulse/routes_controller.rb +40 -1
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +1 -1
- data/app/helpers/rails_pulse/chart_helper.rb +16 -8
- data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
- 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 +249 -11
- 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/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 +20 -20
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +58 -14
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +14 -9
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +3 -7
- data/app/models/rails_pulse/query.rb +46 -0
- 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 +18 -20
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +14 -9
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +14 -9
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +29 -13
- 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/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 +154 -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/views/layouts/rails_pulse/_sidebar_menu.html.erb +0 -1
- data/app/views/layouts/rails_pulse/application.html.erb +0 -2
- 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 +1 -1
- data/app/views/rails_pulse/components/_metric_card.html.erb +28 -5
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
- 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 +2 -2
- 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 +117 -0
- data/app/views/rails_pulse/queries/_analysis_section.html.erb +39 -0
- data/app/views/rails_pulse/queries/_show_table.html.erb +34 -6
- data/app/views/rails_pulse/queries/_table.html.erb +4 -8
- data/app/views/rails_pulse/queries/index.html.erb +48 -51
- data/app/views/rails_pulse/queries/show.html.erb +56 -52
- data/app/views/rails_pulse/requests/_operations.html.erb +30 -43
- data/app/views/rails_pulse/requests/_table.html.erb +31 -18
- data/app/views/rails_pulse/requests/index.html.erb +55 -50
- 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 +4 -10
- data/app/views/rails_pulse/routes/index.html.erb +49 -52
- data/app/views/rails_pulse/routes/show.html.erb +6 -8
- data/config/initializers/rails_charts_csp_patch.rb +32 -40
- data/config/routes.rb +5 -1
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
- data/db/rails_pulse_schema.rb +10 -1
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +81 -0
- data/lib/generators/rails_pulse/install_generator.rb +75 -18
- 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 +23 -0
- data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +19 -0
- data/lib/generators/rails_pulse/upgrade_generator.rb +226 -0
- data/lib/rails_pulse/engine.rb +21 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/tasks/rails_pulse.rake +27 -8
- 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
- metadata +25 -6
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/images/rails_pulse/routes.png +0 -0
- data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +0 -67
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +0 -54
@@ -0,0 +1,125 @@
|
|
1
|
+
# Orchestrates comprehensive query analysis using modular analyzers.
|
2
|
+
# Coordinates multiple specialized analyzers and consolidates results into actionable insights.
|
3
|
+
module RailsPulse
|
4
|
+
class QueryAnalysisService
|
5
|
+
def self.analyze_query(query_id)
|
6
|
+
query = RailsPulse::Query.find(query_id)
|
7
|
+
new(query).analyze
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(query)
|
11
|
+
@query = query
|
12
|
+
@operations = fetch_recent_operations
|
13
|
+
end
|
14
|
+
|
15
|
+
def analyze
|
16
|
+
# Run all analyzers
|
17
|
+
results = {
|
18
|
+
analyzed_at: Time.current,
|
19
|
+
query_characteristics: analyze_query_characteristics,
|
20
|
+
index_recommendations: analyze_index_recommendations,
|
21
|
+
n_plus_one_analysis: analyze_n_plus_one,
|
22
|
+
explain_plan: analyze_explain_plan,
|
23
|
+
backtrace_analysis: analyze_backtraces
|
24
|
+
}
|
25
|
+
|
26
|
+
# Generate consolidated suggestions
|
27
|
+
results[:suggestions] = generate_suggestions(results)
|
28
|
+
|
29
|
+
# Build compatible format for query model
|
30
|
+
compatible_results = build_compatible_results(results)
|
31
|
+
|
32
|
+
# Save results to query
|
33
|
+
save_results_to_query(compatible_results)
|
34
|
+
|
35
|
+
results
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def fetch_recent_operations
|
41
|
+
@query.operations
|
42
|
+
.where("occurred_at > ?", 48.hours.ago)
|
43
|
+
.order(occurred_at: :desc)
|
44
|
+
.limit(50)
|
45
|
+
end
|
46
|
+
|
47
|
+
def analyze_query_characteristics
|
48
|
+
Analysis::QueryCharacteristicsAnalyzer.new(@query, @operations).analyze
|
49
|
+
end
|
50
|
+
|
51
|
+
def analyze_index_recommendations
|
52
|
+
Analysis::IndexRecommendationEngine.new(@query, @operations).analyze
|
53
|
+
end
|
54
|
+
|
55
|
+
def analyze_n_plus_one
|
56
|
+
Analysis::NPlusOneDetector.new(@query, @operations).analyze
|
57
|
+
end
|
58
|
+
|
59
|
+
def analyze_explain_plan
|
60
|
+
return { explain_plan: nil, issues: [] } if @operations.empty?
|
61
|
+
Analysis::ExplainPlanAnalyzer.new(@query, @operations).analyze
|
62
|
+
end
|
63
|
+
|
64
|
+
def analyze_backtraces
|
65
|
+
return {} if @operations.empty?
|
66
|
+
Analysis::BacktraceAnalyzer.new(@query, @operations).analyze
|
67
|
+
end
|
68
|
+
|
69
|
+
def generate_suggestions(analysis_results)
|
70
|
+
Analysis::SuggestionGenerator.new(analysis_results).generate
|
71
|
+
end
|
72
|
+
|
73
|
+
# Build compatible format for query model storage
|
74
|
+
def build_compatible_results(results)
|
75
|
+
characteristics = results[:query_characteristics]
|
76
|
+
explain_result = results[:explain_plan]
|
77
|
+
|
78
|
+
{
|
79
|
+
analyzed_at: results[:analyzed_at],
|
80
|
+
explain_plan: explain_result[:explain_plan],
|
81
|
+
issues: extract_all_issues(characteristics, explain_result),
|
82
|
+
metadata: build_metadata(results),
|
83
|
+
query_stats: extract_query_stats(characteristics),
|
84
|
+
backtrace_analysis: results[:backtrace_analysis],
|
85
|
+
index_recommendations: results[:index_recommendations],
|
86
|
+
n_plus_one_analysis: results[:n_plus_one_analysis],
|
87
|
+
suggestions: results[:suggestions]
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def extract_all_issues(characteristics, explain_result)
|
92
|
+
issues = []
|
93
|
+
issues.concat(characteristics[:pattern_issues] || [])
|
94
|
+
issues.concat(explain_result[:issues] || [])
|
95
|
+
issues
|
96
|
+
end
|
97
|
+
|
98
|
+
def extract_query_stats(characteristics)
|
99
|
+
characteristics.except(:pattern_issues)
|
100
|
+
end
|
101
|
+
|
102
|
+
def build_metadata(results)
|
103
|
+
{
|
104
|
+
analyzers_used: results.keys.reject { |k| k.in?([ :analyzed_at, :suggestions ]) },
|
105
|
+
analysis_version: "2.0",
|
106
|
+
total_recommendations: results[:index_recommendations]&.count || 0,
|
107
|
+
n_plus_one_detected: results.dig(:n_plus_one_analysis, :is_likely_n_plus_one) || false
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def save_results_to_query(results)
|
112
|
+
@query.update!(
|
113
|
+
analyzed_at: results[:analyzed_at],
|
114
|
+
explain_plan: results[:explain_plan],
|
115
|
+
issues: results[:issues],
|
116
|
+
metadata: results[:metadata],
|
117
|
+
query_stats: results[:query_stats],
|
118
|
+
backtrace_analysis: results[:backtrace_analysis],
|
119
|
+
index_recommendations: results[:index_recommendations],
|
120
|
+
n_plus_one_analysis: results[:n_plus_one_analysis],
|
121
|
+
suggestions: results[:suggestions]
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -22,7 +22,6 @@
|
|
22
22
|
<div class="hide@md" data-controller="rails-pulse--dialog">
|
23
23
|
<button type="button" class="btn btn--icon" data-action="rails-pulse--dialog#showModal">
|
24
24
|
<%= rails_pulse_icon 'menu', width: '20' %>
|
25
|
-
<span class="sr-only">Open menu</span>
|
26
25
|
</button>
|
27
26
|
|
28
27
|
<dialog class="sheet sheet--left" style="--sheet-size: 288px;" data-rails-pulse--dialog-target="menu" data-action="click->rails-pulse--dialog#closeOnClickOutside">
|
@@ -31,7 +30,6 @@
|
|
31
30
|
<%= link_to rails_pulse.root_path, class: "btn sidebar-menu__button" do %>
|
32
31
|
<div class="flex flex-col text-start leading-tight overflow-hidden">
|
33
32
|
<span class="overflow-ellipsis font-semibold">Rails Pulse</span>
|
34
|
-
<span class="overflow-ellipsis text-xs">Open Source</span>
|
35
33
|
</div>
|
36
34
|
<% end %>
|
37
35
|
<div class="sidebar-menu__content">
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<nav class="breadcrumb" aria-label="Breadcrumb">
|
1
|
+
<nav class="breadcrumb mis-2" aria-label="Breadcrumb">
|
2
2
|
<% breadcrumbs.each_with_index do |crumb, index| %>
|
3
3
|
<% if crumb[:current] %>
|
4
4
|
<span class="text-primary" aria-disabled="true" aria-current="page" role="link"><%= crumb[:title] %></span>
|
@@ -2,11 +2,25 @@
|
|
2
2
|
title ||= nil
|
3
3
|
%>
|
4
4
|
|
5
|
-
<div
|
5
|
+
<div
|
6
|
+
data-controller="rails-pulse--collapsible"
|
7
|
+
data-rails-pulse--collapsible-collapsed-class="collapsed"
|
8
|
+
class="collapsible-code"
|
9
|
+
>
|
6
10
|
<% if title %>
|
7
|
-
<h2 class="grow font-semibold leading-none mbe-1 uppercase text-xs"
|
11
|
+
<h2 class="grow font-semibold leading-none mbe-1 uppercase text-xs">
|
12
|
+
<%= title %>
|
13
|
+
<button
|
14
|
+
type="button"
|
15
|
+
class="collapsible-toggle"
|
16
|
+
data-rails-pulse--collapsible-target="toggle"
|
17
|
+
data-action="click->rails-pulse--collapsible#toggle"
|
18
|
+
>
|
19
|
+
show more
|
20
|
+
</button>
|
21
|
+
</h2>
|
8
22
|
<% end %>
|
9
|
-
<div class="prose max-i-none">
|
23
|
+
<div class="prose max-i-none" data-rails-pulse--collapsible-target="content">
|
10
24
|
<pre class="mbs-0" style="white-space: normal; margin-block-end: 0; margin-block-start: 0;"><code><%= html_escape(value) %></code></pre>
|
11
25
|
</div>
|
12
26
|
</div>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<div class="flex items-center justify-center pbs-12 pbe-12 pis-6 pie-6 mb-8">
|
2
2
|
<div class="flex items-center gap">
|
3
3
|
<div class="shrink-0">
|
4
|
-
<img src="<%= asset_path('search.svg') %>" class="w-
|
4
|
+
<img src="<%= asset_path('search.svg') %>" class="w-48 h-48" alt="No data available" />
|
5
5
|
</div>
|
6
6
|
<div class="text-subtle pis-8">
|
7
7
|
<p class="text-lg font-semibold mbe-2"><%= title %></p>
|
@@ -4,19 +4,42 @@
|
|
4
4
|
context = data[:context]
|
5
5
|
title = data[:title]
|
6
6
|
summary = data[:summary]
|
7
|
-
|
7
|
+
chart_data = data[:chart_data]
|
8
8
|
trend_icon = data[:trend_icon]
|
9
9
|
trend_amount = data[:trend_amount]
|
10
10
|
trend_text = data[:trend_text]
|
11
11
|
%>
|
12
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 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,9 +53,9 @@
|
|
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
61
|
</div>
|
@@ -26,7 +26,7 @@
|
|
26
26
|
<div>
|
27
27
|
<h4 class="text-xs font-medium text-subtle uppercase">Occurred At</h4>
|
28
28
|
<div class="text-sm">
|
29
|
-
<%= operation.occurred_at.strftime("%H:%M:%S.%L") %>
|
29
|
+
<%= operation.occurred_at.getlocal.strftime("%H:%M:%S.%L") %>
|
30
30
|
</div>
|
31
31
|
</div>
|
32
32
|
<% 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 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>
|
@@ -6,7 +6,7 @@
|
|
6
6
|
</div>
|
7
7
|
|
8
8
|
<div class="row">
|
9
|
-
<div class="grid-item"
|
9
|
+
<div class="grid-item">
|
10
10
|
<%= render 'rails_pulse/components/panel', {
|
11
11
|
title: 'Average Response Time',
|
12
12
|
card_classes: 'b-full',
|
@@ -71,7 +71,7 @@
|
|
71
71
|
<div class="grid-item">
|
72
72
|
<%= render 'rails_pulse/components/panel', {
|
73
73
|
title: 'Slowest Queries This Week',
|
74
|
-
help_heading: 'Slowest Queries',
|
74
|
+
help_heading: 'Slowest Queries',
|
75
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.',
|
76
76
|
actions: [{ url: queries_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
|
77
77
|
card_classes: 'table-container'
|
@@ -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,117 @@
|
|
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>
|
16
|
+
Complexity Score
|
17
|
+
<div data-controller="rails-pulse--popover" data-rails-pulse--popover-placement-value="bottom-start" style="display: inline-block; margin-left: 4px;">
|
18
|
+
<a href="#"
|
19
|
+
data-rails-pulse--popover-target="button"
|
20
|
+
data-action="rails-pulse--popover#toggle"
|
21
|
+
data-popovertarget="complexity-score-popover"
|
22
|
+
style="color: var(--gray-500); vertical-align: top;">
|
23
|
+
<%= rails_pulse_icon 'info', height: "14px" %>
|
24
|
+
</a>
|
25
|
+
|
26
|
+
<div popover class="popover card" data-rails-pulse--popover-target="menu" style="max-width: 22rem">
|
27
|
+
<div class="flex flex-col">
|
28
|
+
<h3 class="font-semibold leading-none mbe-2 uppercase text-sm">Complexity Score</h3>
|
29
|
+
<p class="text-sm text-subtle mbe-3">
|
30
|
+
A calculated score representing query complexity based on multiple factors:
|
31
|
+
</p>
|
32
|
+
<ul class="text-sm text-subtle" style="list-style-type: disc; padding-left: 16px; margin: 0;">
|
33
|
+
<li>Tables: +2 points per table</li>
|
34
|
+
<li>Joins: +3 points per join</li>
|
35
|
+
<li>WHERE conditions: +1 point per condition, +2 per function</li>
|
36
|
+
<li>UNIONs: +4 points each</li>
|
37
|
+
<li>Subqueries: +5 points each</li>
|
38
|
+
</ul>
|
39
|
+
<p class="text-sm text-subtle mbs-3">
|
40
|
+
Higher scores indicate more complex queries that may need optimization.
|
41
|
+
</p>
|
42
|
+
</div>
|
43
|
+
</div>
|
44
|
+
</div>
|
45
|
+
</dt>
|
46
|
+
<dd><%= query.query_stats['estimated_complexity'] || 0 %></dd>
|
47
|
+
<dt>Has LIMIT</dt>
|
48
|
+
<dd><%= query.query_stats['has_limit'] ? 'Yes' : 'No' %></dd>
|
49
|
+
<dt>Has ORDER BY</dt>
|
50
|
+
<dd><%= query.query_stats['has_order_by'] ? 'Yes' : 'No' %></dd>
|
51
|
+
<dt>Has GROUP BY</dt>
|
52
|
+
<dd><%= query.query_stats['has_group_by'] ? 'Yes' : 'No' %></dd>
|
53
|
+
<dt>Has Subqueries</dt>
|
54
|
+
<dd><%= query.query_stats['has_subqueries'] ? 'Yes' : 'No' %></dd>
|
55
|
+
</dl>
|
56
|
+
<% end %>
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<div>
|
60
|
+
<!-- Execution Analysis -->
|
61
|
+
<% if query.backtrace_analysis.present? %>
|
62
|
+
<h5>Execution Analysis</h5>
|
63
|
+
<dl class="descriptive-list mbs-4">
|
64
|
+
<dt>Total Executions</dt>
|
65
|
+
<dd><%= query.backtrace_analysis['total_executions'] || 0 %></dd>
|
66
|
+
<dt>Unique Locations</dt>
|
67
|
+
<dd><%= query.backtrace_analysis['unique_locations'] || 0 %></dd>
|
68
|
+
<dt>Execution Frequency</dt>
|
69
|
+
<dd><%= query.backtrace_analysis['execution_frequency'] || 0 %>/hour</dd>
|
70
|
+
<dt>Most Common Location</dt>
|
71
|
+
<dd><code><%= query.backtrace_analysis['most_common_location']['location'] || 'N/A' %></code></dd>
|
72
|
+
<dt>N+1 Pattern Detected</dt>
|
73
|
+
<dd>
|
74
|
+
<% if query.backtrace_analysis['potential_n_plus_one'] %>
|
75
|
+
Yes
|
76
|
+
<% else %>
|
77
|
+
No
|
78
|
+
<% end %>
|
79
|
+
</dd>
|
80
|
+
</dl>
|
81
|
+
<% end %>
|
82
|
+
</div>
|
83
|
+
</div>
|
84
|
+
|
85
|
+
|
86
|
+
<!-- Issues Summary -->
|
87
|
+
<% if query.issues.present? && query.issues.any? %>
|
88
|
+
<hr class="mb-4" />
|
89
|
+
<h3 class="text-lg bold">Issues Detected</h3>
|
90
|
+
<ul style="list-style-type: disc; padding-left: 20px;">
|
91
|
+
<% query.issues.each do |issue| %>
|
92
|
+
<li><%= issue['description'] %></li>
|
93
|
+
<% end %>
|
94
|
+
</ul>
|
95
|
+
<% end %>
|
96
|
+
|
97
|
+
|
98
|
+
<!-- Suggestions -->
|
99
|
+
<% if query.suggestions.present? && query.suggestions.any? %>
|
100
|
+
<hr class="mb-4" />
|
101
|
+
<h3 class="text-lg bold">Optimization Suggestions</h3>
|
102
|
+
<ul style="list-style-type: disc; padding-left: 20px;">
|
103
|
+
<% query.suggestions.each do |suggestion| %>
|
104
|
+
<li>
|
105
|
+
<%= suggestion['action'] %>.
|
106
|
+
<%= suggestion['benefit'] if suggestion['benefit'].present? %>
|
107
|
+
</li>
|
108
|
+
<% end %>
|
109
|
+
</ul>
|
110
|
+
<% end %>
|
111
|
+
|
112
|
+
<!-- EXPLAIN Plan -->
|
113
|
+
<% if query.explain_plan.present? %>
|
114
|
+
<hr class="mb-4" />
|
115
|
+
<h3 class="text-lg bold">Execution Plan</h3>
|
116
|
+
<pre class="text-sm" style="overflow: scroll"><%= query.explain_plan %></pre>
|
117
|
+
<% 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,16 +1,44 @@
|
|
1
1
|
<% columns = [
|
2
|
-
{ field: :
|
3
|
-
{ field: :
|
2
|
+
{ field: :period_start, label: 'Time Period', class: 'w-auto' },
|
3
|
+
{ field: :count, label: 'Executions', class: 'w-32'},
|
4
|
+
{ field: :avg_duration, label: 'Avg Duration', class: 'w-32'},
|
5
|
+
{ field: :min_duration, label: 'Min Duration', class: 'w-32'},
|
6
|
+
{ field: :max_duration, label: 'Max Duration', class: 'w-32'}
|
4
7
|
] %>
|
5
8
|
|
6
|
-
<table class="table mbs-4">
|
9
|
+
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
7
10
|
<%= render "rails_pulse/components/table_head", columns: columns %>
|
8
11
|
|
9
12
|
<tbody>
|
10
|
-
<% @table_data.each do |
|
13
|
+
<% @table_data.each do |summary| %>
|
14
|
+
<%
|
15
|
+
# Determine performance class based on average duration
|
16
|
+
avg_duration_ms = summary.avg_duration&.round(2) || 0
|
17
|
+
performance_class = case avg_duration_ms
|
18
|
+
when 0..10 then "text-green-600"
|
19
|
+
when 10..50 then "text-yellow-600"
|
20
|
+
when 50..100 then "text-orange-600"
|
21
|
+
else "text-red-600"
|
22
|
+
end
|
23
|
+
%>
|
11
24
|
<tr>
|
12
|
-
<td class="whitespace-nowrap"
|
13
|
-
|
25
|
+
<td class="whitespace-nowrap">
|
26
|
+
<%= human_readable_summary_period(summary) %>
|
27
|
+
</td>
|
28
|
+
<td class="whitespace-nowrap text-center">
|
29
|
+
<span class="font-medium"><%= summary.count %></span>
|
30
|
+
</td>
|
31
|
+
<td class="whitespace-nowrap">
|
32
|
+
<span class="<%= performance_class %> font-medium">
|
33
|
+
<%= avg_duration_ms %> ms
|
34
|
+
</span>
|
35
|
+
</td>
|
36
|
+
<td class="whitespace-nowrap text-center">
|
37
|
+
<%= summary.min_duration&.round(2) || 0 %> ms
|
38
|
+
</td>
|
39
|
+
<td class="whitespace-nowrap text-center">
|
40
|
+
<%= summary.max_duration&.round(2) || 0 %> ms
|
41
|
+
</td>
|
14
42
|
</tr>
|
15
43
|
<% end %>
|
16
44
|
</tbody>
|
@@ -1,12 +1,10 @@
|
|
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
|
-
<table class="table mbs-4">
|
7
|
+
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
10
8
|
<%= render "rails_pulse/components/table_head", columns: columns %>
|
11
9
|
|
12
10
|
<tbody>
|
@@ -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>
|