rails_pulse 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +56 -16
- data/Rakefile +169 -86
- data/app/controllers/rails_pulse/queries_controller.rb +14 -20
- data/app/controllers/rails_pulse/requests_controller.rb +43 -30
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +1 -1
- data/app/helpers/rails_pulse/chart_helper.rb +1 -1
- data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
- data/app/javascript/rails_pulse/controllers/index_controller.js +11 -3
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +1 -1
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +56 -17
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +3 -7
- data/app/models/rails_pulse/request.rb +1 -1
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +2 -2
- data/app/models/rails_pulse/requests/tables/index.rb +77 -0
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +16 -5
- data/app/models/rails_pulse/routes/tables/index.rb +4 -2
- data/app/models/rails_pulse/summary.rb +7 -7
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +11 -3
- data/app/views/rails_pulse/components/_metric_card.html.erb +2 -2
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/_analysis_results.html.erb +53 -23
- data/app/views/rails_pulse/queries/_show_table.html.erb +33 -5
- data/app/views/rails_pulse/queries/_table.html.erb +3 -7
- data/app/views/rails_pulse/requests/_table.html.erb +30 -19
- data/app/views/rails_pulse/requests/index.html.erb +8 -0
- data/app/views/rails_pulse/requests/show.html.erb +0 -2
- data/app/views/rails_pulse/routes/_requests_table.html.erb +39 -0
- data/app/views/rails_pulse/routes/_table.html.erb +3 -9
- data/app/views/rails_pulse/routes/show.html.erb +3 -5
- data/config/initializers/rails_charts_csp_patch.rb +32 -40
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
- data/db/rails_pulse_schema.rb +1 -1
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +25 -9
- data/lib/generators/rails_pulse/install_generator.rb +9 -5
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +72 -2
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +3 -2
- data/lib/generators/rails_pulse/upgrade_generator.rb +2 -1
- data/lib/rails_pulse/engine.rb +21 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +1 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +2 -2
- metadata +5 -4
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +0 -54
- data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +0 -13
@@ -192,6 +192,8 @@ export default class extends Controller {
|
|
192
192
|
updatePaginationLimit() {
|
193
193
|
// Update or set the limit param in the browser so if the user refreshes the page,
|
194
194
|
// the limit will be preserved.
|
195
|
+
if (!this.hasPaginationLimitTarget) return;
|
196
|
+
|
195
197
|
const url = new URL(window.location.href);
|
196
198
|
const currentParams = new URLSearchParams(url.search);
|
197
199
|
const limit = this.paginationLimitTarget.value;
|
@@ -244,7 +246,9 @@ export default class extends Controller {
|
|
244
246
|
currentParams.set('zoom_end_time', endTimestamp);
|
245
247
|
|
246
248
|
// Set the limit param based on the value in the pagination selector
|
247
|
-
|
249
|
+
if (this.hasPaginationLimitTarget) {
|
250
|
+
currentParams.set('limit', this.paginationLimitTarget.value);
|
251
|
+
}
|
248
252
|
|
249
253
|
// Update the URL's search parameters
|
250
254
|
url.search = currentParams.toString();
|
@@ -443,7 +447,9 @@ export default class extends Controller {
|
|
443
447
|
currentParams.set('selected_column_time', selectedTimestamp);
|
444
448
|
|
445
449
|
// Preserve pagination limit
|
446
|
-
|
450
|
+
if (this.hasPaginationLimitTarget) {
|
451
|
+
currentParams.set('limit', this.paginationLimitTarget.value);
|
452
|
+
}
|
447
453
|
|
448
454
|
url.search = currentParams.toString();
|
449
455
|
|
@@ -463,7 +469,9 @@ export default class extends Controller {
|
|
463
469
|
currentParams.delete('selected_column_time');
|
464
470
|
|
465
471
|
// Preserve pagination limit
|
466
|
-
|
472
|
+
if (this.hasPaginationLimitTarget) {
|
473
|
+
currentParams.set('limit', this.paginationLimitTarget.value);
|
474
|
+
}
|
467
475
|
|
468
476
|
url.search = currentParams.toString();
|
469
477
|
|
@@ -32,7 +32,7 @@ module RailsPulse
|
|
32
32
|
{
|
33
33
|
query_text: truncate_query(record.normalized_sql),
|
34
34
|
query_id: record.query_id,
|
35
|
-
query_link:
|
35
|
+
query_link: RailsPulse::Engine.routes.url_helpers.query_path(record.query_id),
|
36
36
|
average_time: record.avg_duration.to_f.round(0),
|
37
37
|
request_count: record.request_count,
|
38
38
|
last_request: time_ago_in_words(record.last_seen)
|
@@ -33,7 +33,7 @@ module RailsPulse
|
|
33
33
|
{
|
34
34
|
route_path: record.path,
|
35
35
|
route_id: record.route_id,
|
36
|
-
route_link:
|
36
|
+
route_link: RailsPulse::Engine.routes.url_helpers.route_path(record.route_id),
|
37
37
|
average_time: record.avg_duration.to_f.round(0),
|
38
38
|
request_count: record.request_count,
|
39
39
|
last_request: time_ago_in_words(record.last_seen)
|
@@ -64,7 +64,7 @@ module RailsPulse
|
|
64
64
|
context: "queries",
|
65
65
|
title: "Average Query Time",
|
66
66
|
summary: "#{average_query_time} ms",
|
67
|
-
|
67
|
+
chart_data: sparkline_data,
|
68
68
|
trend_icon: trend_icon,
|
69
69
|
trend_amount: trend_amount,
|
70
70
|
trend_text: "Compared to last week"
|
@@ -10,10 +10,20 @@ module RailsPulse
|
|
10
10
|
last_7_days = 7.days.ago.beginning_of_day
|
11
11
|
previous_7_days = 14.days.ago.beginning_of_day
|
12
12
|
|
13
|
+
# Get the most common period type for this query, or fall back to "day"
|
14
|
+
period_type = if @query
|
15
|
+
RailsPulse::Summary.where(
|
16
|
+
summarizable_type: "RailsPulse::Query",
|
17
|
+
summarizable_id: @query.id
|
18
|
+
).group(:period_type).count.max_by(&:last)&.first || "day"
|
19
|
+
else
|
20
|
+
"day"
|
21
|
+
end
|
22
|
+
|
13
23
|
# Single query to get all count metrics with conditional aggregation
|
14
24
|
base_query = RailsPulse::Summary.where(
|
15
25
|
summarizable_type: "RailsPulse::Query",
|
16
|
-
period_type:
|
26
|
+
period_type: period_type,
|
17
27
|
period_start: 2.weeks.ago.beginning_of_day..Time.current
|
18
28
|
)
|
19
29
|
base_query = base_query.where(summarizable_id: @query.id) if @query
|
@@ -33,31 +43,60 @@ module RailsPulse
|
|
33
43
|
trend_icon = percentage < 0.1 ? "move-right" : current_period_count < previous_period_count ? "trending-down" : "trending-up"
|
34
44
|
trend_amount = previous_period_count.zero? ? "0%" : "#{percentage}%"
|
35
45
|
|
36
|
-
# Sparkline data
|
37
|
-
|
38
|
-
|
39
|
-
|
46
|
+
# Sparkline data with zero-filled periods over the last 14 days
|
47
|
+
if period_type == "day"
|
48
|
+
grouped_data = base_query
|
49
|
+
.group_by_day(:period_start, time_zone: "UTC")
|
50
|
+
.sum(:count)
|
51
|
+
|
52
|
+
start_period = 2.weeks.ago.beginning_of_day.to_date
|
53
|
+
end_period = Time.current.to_date
|
40
54
|
|
41
|
-
|
42
|
-
|
55
|
+
sparkline_data = {}
|
56
|
+
(start_period..end_period).each do |day|
|
57
|
+
total = grouped_data[day] || 0
|
58
|
+
label = day.strftime("%b %-d")
|
59
|
+
sparkline_data[label] = { value: total }
|
60
|
+
end
|
61
|
+
else
|
62
|
+
# For hourly data, group by day for sparkline display
|
63
|
+
grouped_data = base_query
|
64
|
+
.group("DATE(period_start)")
|
65
|
+
.sum(:count)
|
43
66
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
67
|
+
start_period = 2.weeks.ago.beginning_of_day.to_date
|
68
|
+
end_period = Time.current.to_date
|
69
|
+
|
70
|
+
sparkline_data = {}
|
71
|
+
(start_period..end_period).each do |day|
|
72
|
+
date_key = day.strftime("%Y-%m-%d")
|
73
|
+
total = grouped_data[date_key] || 0
|
74
|
+
label = day.strftime("%b %-d")
|
75
|
+
sparkline_data[label] = { value: total }
|
76
|
+
end
|
49
77
|
end
|
50
78
|
|
51
|
-
# Calculate
|
52
|
-
total_minutes = 2.weeks / 1.minute
|
53
|
-
|
79
|
+
# Calculate appropriate rate display based on frequency
|
80
|
+
total_minutes = 2.weeks / 1.minute.to_f
|
81
|
+
executions_per_minute = total_execution_count.to_f / total_minutes
|
82
|
+
|
83
|
+
# Choose appropriate time unit for display
|
84
|
+
if executions_per_minute >= 1
|
85
|
+
summary = "#{executions_per_minute.round(2)} / min"
|
86
|
+
elsif executions_per_minute * 60 >= 1
|
87
|
+
executions_per_hour = executions_per_minute * 60
|
88
|
+
summary = "#{executions_per_hour.round(2)} / hour"
|
89
|
+
else
|
90
|
+
executions_per_day = executions_per_minute * 60 * 24
|
91
|
+
summary = "#{executions_per_day.round(2)} / day"
|
92
|
+
end
|
54
93
|
|
55
94
|
{
|
56
95
|
id: "execution_rate",
|
57
96
|
context: "queries",
|
58
97
|
title: "Execution Rate",
|
59
|
-
summary:
|
60
|
-
|
98
|
+
summary: summary,
|
99
|
+
chart_data: sparkline_data,
|
61
100
|
trend_icon: trend_icon,
|
62
101
|
trend_amount: trend_amount,
|
63
102
|
trend_text: "Compared to last week"
|
@@ -53,7 +53,7 @@ module RailsPulse
|
|
53
53
|
context: "queries",
|
54
54
|
title: "95th Percentile Query Time",
|
55
55
|
summary: "#{p95_query_time} ms",
|
56
|
-
|
56
|
+
chart_data: sparkline_data,
|
57
57
|
trend_icon: trend_icon,
|
58
58
|
trend_amount: trend_amount,
|
59
59
|
trend_text: "Compared to last week"
|
@@ -12,13 +12,9 @@ module RailsPulse
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def to_rails_chart
|
15
|
-
|
16
|
-
|
17
|
-
period_type: @period_type
|
18
|
-
)
|
19
|
-
|
20
|
-
summaries = summaries.where(summarizable_id: @query.id) if @query
|
21
|
-
summaries = summaries
|
15
|
+
# The ransack query already contains the correct filters, just add period_type
|
16
|
+
summaries = @ransack_query.result(distinct: false)
|
17
|
+
.where(period_type: @period_type)
|
22
18
|
.group(:period_start)
|
23
19
|
.having("AVG(avg_duration) > ?", @start_duration || 0)
|
24
20
|
.average(:avg_duration)
|
@@ -13,11 +13,11 @@ module RailsPulse
|
|
13
13
|
|
14
14
|
def to_rails_chart
|
15
15
|
summaries = @ransack_query.result(distinct: false).where(
|
16
|
-
summarizable_type: "RailsPulse::
|
16
|
+
summarizable_type: "RailsPulse::Request",
|
17
|
+
summarizable_id: 0, # Overall request summaries
|
17
18
|
period_type: @period_type
|
18
19
|
)
|
19
20
|
|
20
|
-
summaries = summaries.where(summarizable_id: @route.id) if @route
|
21
21
|
summaries = summaries
|
22
22
|
.group(:period_start)
|
23
23
|
.having("AVG(avg_duration) > ?", @start_duration || 0)
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module Requests
|
3
|
+
module Tables
|
4
|
+
class Index
|
5
|
+
def initialize(ransack_query:, period_type: nil, start_time:, params:)
|
6
|
+
@ransack_query = ransack_query
|
7
|
+
@period_type = period_type
|
8
|
+
@start_time = start_time
|
9
|
+
@params = params
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_table
|
13
|
+
# Check if we have explicit ransack sorts
|
14
|
+
has_sorts = @ransack_query.sorts.any?
|
15
|
+
|
16
|
+
base_query = @ransack_query.result(distinct: false)
|
17
|
+
.where(
|
18
|
+
summarizable_type: "RailsPulse::Request",
|
19
|
+
summarizable_id: 0, # Overall request summaries
|
20
|
+
period_type: @period_type
|
21
|
+
)
|
22
|
+
|
23
|
+
# Apply grouping and aggregation for time periods
|
24
|
+
grouped_query = base_query
|
25
|
+
.group(
|
26
|
+
"rails_pulse_summaries.period_start",
|
27
|
+
"rails_pulse_summaries.period_end",
|
28
|
+
"rails_pulse_summaries.period_type"
|
29
|
+
)
|
30
|
+
.select(
|
31
|
+
"rails_pulse_summaries.period_start",
|
32
|
+
"rails_pulse_summaries.period_end",
|
33
|
+
"rails_pulse_summaries.period_type",
|
34
|
+
"AVG(rails_pulse_summaries.avg_duration) as avg_duration",
|
35
|
+
"MAX(rails_pulse_summaries.max_duration) as max_duration",
|
36
|
+
"MIN(rails_pulse_summaries.min_duration) as min_duration",
|
37
|
+
"SUM(rails_pulse_summaries.count) as count",
|
38
|
+
"SUM(rails_pulse_summaries.error_count) as error_count",
|
39
|
+
"SUM(rails_pulse_summaries.success_count) as success_count"
|
40
|
+
)
|
41
|
+
|
42
|
+
# Apply sorting based on ransack sorts or use default
|
43
|
+
if has_sorts
|
44
|
+
# Apply custom sorting based on ransack parameters
|
45
|
+
sort = @ransack_query.sorts.first
|
46
|
+
direction = sort.dir == "desc" ? :desc : :asc
|
47
|
+
|
48
|
+
case sort.name
|
49
|
+
when "avg_duration"
|
50
|
+
grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").send(direction))
|
51
|
+
when "max_duration"
|
52
|
+
grouped_query = grouped_query.order(Arel.sql("MAX(rails_pulse_summaries.max_duration)").send(direction))
|
53
|
+
when "min_duration"
|
54
|
+
grouped_query = grouped_query.order(Arel.sql("MIN(rails_pulse_summaries.min_duration)").send(direction))
|
55
|
+
when "count"
|
56
|
+
grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count)").send(direction))
|
57
|
+
when "requests_per_minute"
|
58
|
+
grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count) / 60.0").send(direction))
|
59
|
+
when "error_rate_percentage"
|
60
|
+
grouped_query = grouped_query.order(Arel.sql("(SUM(rails_pulse_summaries.error_count) * 100.0) / SUM(rails_pulse_summaries.count)").send(direction))
|
61
|
+
when "period_start"
|
62
|
+
grouped_query = grouped_query.order(period_start: direction)
|
63
|
+
else
|
64
|
+
# Unknown sort field, fallback to default
|
65
|
+
grouped_query = grouped_query.order(period_start: :desc)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
# Apply default sort when no explicit sort is provided (matches controller default_table_sort)
|
69
|
+
grouped_query = grouped_query.order(period_start: :desc)
|
70
|
+
end
|
71
|
+
|
72
|
+
grouped_query
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -62,7 +62,7 @@ module RailsPulse
|
|
62
62
|
context: "routes",
|
63
63
|
title: "Average Response Time",
|
64
64
|
summary: "#{average_response_time} ms",
|
65
|
-
|
65
|
+
chart_data: sparkline_data,
|
66
66
|
trend_icon: trend_icon,
|
67
67
|
trend_amount: trend_amount,
|
68
68
|
trend_text: "Compared to last week"
|
@@ -59,7 +59,7 @@ module RailsPulse
|
|
59
59
|
context: "routes",
|
60
60
|
title: "Error Rate Per Route",
|
61
61
|
summary: "#{overall_error_rate}%",
|
62
|
-
|
62
|
+
chart_data: sparkline_data,
|
63
63
|
trend_icon: trend_icon,
|
64
64
|
trend_amount: trend_amount,
|
65
65
|
trend_text: "Compared to last week"
|
@@ -53,7 +53,7 @@ module RailsPulse
|
|
53
53
|
context: "routes",
|
54
54
|
title: "95th Percentile Response Time",
|
55
55
|
summary: "#{p95_response_time} ms",
|
56
|
-
|
56
|
+
chart_data: sparkline_data,
|
57
57
|
trend_icon: trend_icon,
|
58
58
|
trend_amount: trend_amount,
|
59
59
|
trend_text: "Compared to last week"
|
@@ -48,16 +48,27 @@ module RailsPulse
|
|
48
48
|
sparkline_data[label] = { value: total }
|
49
49
|
end
|
50
50
|
|
51
|
-
# Calculate
|
52
|
-
total_minutes = 2.weeks / 1.minute
|
53
|
-
|
51
|
+
# Calculate appropriate rate display based on frequency
|
52
|
+
total_minutes = 2.weeks / 1.minute.to_f
|
53
|
+
requests_per_minute = total_request_count.to_f / total_minutes
|
54
|
+
|
55
|
+
# Choose appropriate time unit for display
|
56
|
+
if requests_per_minute >= 1
|
57
|
+
summary = "#{requests_per_minute.round(2)} / min"
|
58
|
+
elsif requests_per_minute * 60 >= 1
|
59
|
+
requests_per_hour = requests_per_minute * 60
|
60
|
+
summary = "#{requests_per_hour.round(2)} / hour"
|
61
|
+
else
|
62
|
+
requests_per_day = requests_per_minute * 60 * 24
|
63
|
+
summary = "#{requests_per_day.round(2)} / day"
|
64
|
+
end
|
54
65
|
|
55
66
|
{
|
56
67
|
id: "request_count_totals",
|
57
68
|
context: "routes",
|
58
69
|
title: "Request Count Total",
|
59
|
-
summary:
|
60
|
-
|
70
|
+
summary: summary,
|
71
|
+
chart_data: sparkline_data,
|
61
72
|
trend_icon: trend_icon,
|
62
73
|
trend_amount: trend_amount,
|
63
74
|
trend_text: "Compared to last week"
|
@@ -13,7 +13,9 @@ module RailsPulse
|
|
13
13
|
# Check if we have explicit ransack sorts
|
14
14
|
has_sorts = @ransack_query.sorts.any?
|
15
15
|
|
16
|
-
|
16
|
+
# Store sorts for later and get result without ordering
|
17
|
+
# This prevents PostgreSQL GROUP BY issues with ORDER BY columns
|
18
|
+
base_query = @ransack_query.result(distinct: false).reorder(nil)
|
17
19
|
.joins("INNER JOIN rails_pulse_routes ON rails_pulse_routes.id = rails_pulse_summaries.summarizable_id")
|
18
20
|
.where(
|
19
21
|
summarizable_type: "RailsPulse::Route",
|
@@ -55,7 +57,7 @@ module RailsPulse
|
|
55
57
|
grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").send(direction))
|
56
58
|
when "max_duration_sort"
|
57
59
|
grouped_query = grouped_query.order(Arel.sql("MAX(rails_pulse_summaries.max_duration)").send(direction))
|
58
|
-
when "count_sort"
|
60
|
+
when "count_sort", "request_count_sort"
|
59
61
|
grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count)").send(direction))
|
60
62
|
when "requests_per_minute"
|
61
63
|
grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count) / 60.0").send(direction))
|
@@ -36,9 +36,10 @@ module RailsPulse
|
|
36
36
|
# Ransack configuration
|
37
37
|
def self.ransackable_attributes(auth_object = nil)
|
38
38
|
%w[
|
39
|
-
period_start period_end avg_duration max_duration count error_count
|
39
|
+
period_start period_end avg_duration min_duration max_duration count error_count
|
40
40
|
requests_per_minute error_rate_percentage route_path_cont
|
41
41
|
execution_count total_time_consumed normalized_sql
|
42
|
+
summarizable_id summarizable_type
|
42
43
|
]
|
43
44
|
end
|
44
45
|
|
@@ -46,17 +47,16 @@ module RailsPulse
|
|
46
47
|
%w[route query]
|
47
48
|
end
|
48
49
|
|
49
|
-
#
|
50
|
-
|
51
|
-
Arel.sql("SUM(rails_pulse_summaries.count)") # Use SUM for proper grouping
|
52
|
-
end
|
50
|
+
# Note: Basic fields like count, avg_duration, min_duration, max_duration
|
51
|
+
# are handled automatically by Ransack using actual database columns
|
53
52
|
|
53
|
+
# Custom ransackers for calculated fields only
|
54
54
|
ransacker :requests_per_minute do
|
55
|
-
Arel.sql("
|
55
|
+
Arel.sql("rails_pulse_summaries.count / 60.0")
|
56
56
|
end
|
57
57
|
|
58
58
|
ransacker :error_rate_percentage do
|
59
|
-
Arel.sql("(
|
59
|
+
Arel.sql("(rails_pulse_summaries.error_count * 100.0) / rails_pulse_summaries.count")
|
60
60
|
end
|
61
61
|
|
62
62
|
|
@@ -38,9 +38,17 @@ module RailsPulse
|
|
38
38
|
|
39
39
|
def count_tables
|
40
40
|
tables = []
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
|
42
|
+
# Match FROM clause with various table name formats
|
43
|
+
# Handles: table_name, schema.table, "quoted_table", `backtick_table`
|
44
|
+
tables.concat(sql.scan(/FROM\s+(?:`([^`]+)`|"([^"]+)"|'([^']+)'|(\w+(?:\.\w+)?))/i).flatten.compact)
|
45
|
+
|
46
|
+
# Match JOIN clauses (INNER JOIN, LEFT JOIN, etc.)
|
47
|
+
tables.concat(sql.scan(/(?:INNER\s+|LEFT\s+|RIGHT\s+|FULL\s+|CROSS\s+)?JOIN\s+(?:`([^`]+)`|"([^"]+)"|'([^']+)'|(\w+(?:\.\w+)?))/i).flatten.compact)
|
48
|
+
|
49
|
+
# Remove schema prefixes for uniqueness check (schema.table -> table)
|
50
|
+
normalized_tables = tables.map { |table| table.split(".").last }
|
51
|
+
normalized_tables.uniq.length
|
44
52
|
end
|
45
53
|
|
46
54
|
def count_joins
|
@@ -4,7 +4,7 @@
|
|
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]
|
@@ -38,7 +38,7 @@
|
|
38
38
|
}
|
39
39
|
)
|
40
40
|
%>
|
41
|
-
<%= bar_chart
|
41
|
+
<%= bar_chart chart_data, height: "100%", options: chart_options %>
|
42
42
|
</div>
|
43
43
|
</div>
|
44
44
|
</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 %>
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<h4 class="text-xl mbs-1 font-bold"><%= summary %></h4>
|
3
3
|
</div>
|
4
4
|
<div class="chart-container chart-container--slim">
|
5
|
-
<%= bar_chart
|
5
|
+
<%= bar_chart chart_data, height: "100%", options: sparkline_chart_options %>
|
6
6
|
</div>
|
7
7
|
<div>
|
8
8
|
<span class="badge badge--<%= trend_direction == "down" ? "positive" : "negative" %>-inverse p-0">
|
@@ -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'
|
@@ -12,7 +12,37 @@
|
|
12
12
|
<dd><%= query.query_stats['table_count'] || 0 %></dd>
|
13
13
|
<dt>Joins</dt>
|
14
14
|
<dd><%= query.query_stats['join_count'] || 0 %></dd>
|
15
|
-
<dt>
|
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>
|
16
46
|
<dd><%= query.query_stats['estimated_complexity'] || 0 %></dd>
|
17
47
|
<dt>Has LIMIT</dt>
|
18
48
|
<dd><%= query.query_stats['has_limit'] ? 'Yes' : 'No' %></dd>
|
@@ -52,36 +82,36 @@
|
|
52
82
|
</div>
|
53
83
|
</div>
|
54
84
|
|
85
|
+
|
55
86
|
<!-- Issues Summary -->
|
56
87
|
<% if query.issues.present? && query.issues.any? %>
|
57
|
-
<
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
</div>
|
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>
|
65
95
|
<% end %>
|
66
96
|
|
97
|
+
|
67
98
|
<!-- Suggestions -->
|
68
99
|
<% if query.suggestions.present? && query.suggestions.any? %>
|
69
|
-
<
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
</div>
|
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>
|
80
110
|
<% end %>
|
81
111
|
|
82
112
|
<!-- EXPLAIN Plan -->
|
83
113
|
<% if query.explain_plan.present? %>
|
84
|
-
|
85
|
-
|
86
|
-
|
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>
|
87
117
|
<% 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
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>
|