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
|
@@ -2,24 +2,32 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Tables
|
|
4
4
|
class Index
|
|
5
|
-
def initialize(ransack_query:, period_type: nil, start_time:, params:)
|
|
5
|
+
def initialize(ransack_query:, period_type: nil, start_time:, params:, disabled_tags: [])
|
|
6
6
|
@ransack_query = ransack_query
|
|
7
7
|
@period_type = period_type
|
|
8
8
|
@start_time = start_time
|
|
9
9
|
@params = params
|
|
10
|
+
@disabled_tags = disabled_tags
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def to_table
|
|
13
14
|
# Check if we have explicit ransack sorts
|
|
14
15
|
has_sorts = @ransack_query.sorts.any?
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
# Store sorts for later and get result without ordering
|
|
18
|
+
# This prevents PostgreSQL GROUP BY issues with ORDER BY columns
|
|
19
|
+
base_query = @ransack_query.result(distinct: false).reorder(nil)
|
|
17
20
|
.joins("INNER JOIN rails_pulse_routes ON rails_pulse_routes.id = rails_pulse_summaries.summarizable_id")
|
|
18
21
|
.where(
|
|
19
22
|
summarizable_type: "RailsPulse::Route",
|
|
20
23
|
period_type: @period_type
|
|
21
24
|
)
|
|
22
25
|
|
|
26
|
+
# Apply tag filters by excluding routes with disabled tags
|
|
27
|
+
@disabled_tags.each do |tag|
|
|
28
|
+
base_query = base_query.where.not("rails_pulse_routes.tags LIKE ?", "%#{tag}%")
|
|
29
|
+
end
|
|
30
|
+
|
|
23
31
|
base_query = base_query.where(summarizable_id: @route.id) if @route
|
|
24
32
|
|
|
25
33
|
# Apply grouping and aggregation
|
|
@@ -29,7 +37,8 @@ module RailsPulse
|
|
|
29
37
|
"rails_pulse_summaries.summarizable_type",
|
|
30
38
|
"rails_pulse_routes.id",
|
|
31
39
|
"rails_pulse_routes.path",
|
|
32
|
-
"rails_pulse_routes.method"
|
|
40
|
+
"rails_pulse_routes.method",
|
|
41
|
+
"rails_pulse_routes.tags"
|
|
33
42
|
)
|
|
34
43
|
.select(
|
|
35
44
|
"rails_pulse_summaries.summarizable_id",
|
|
@@ -37,6 +46,7 @@ module RailsPulse
|
|
|
37
46
|
"rails_pulse_routes.id as route_id",
|
|
38
47
|
"rails_pulse_routes.path",
|
|
39
48
|
"rails_pulse_routes.method as route_method",
|
|
49
|
+
"rails_pulse_routes.tags",
|
|
40
50
|
"AVG(rails_pulse_summaries.avg_duration) as avg_duration",
|
|
41
51
|
"MAX(rails_pulse_summaries.max_duration) as max_duration",
|
|
42
52
|
"SUM(rails_pulse_summaries.count) as count",
|
|
@@ -55,7 +65,7 @@ module RailsPulse
|
|
|
55
65
|
grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").send(direction))
|
|
56
66
|
when "max_duration_sort"
|
|
57
67
|
grouped_query = grouped_query.order(Arel.sql("MAX(rails_pulse_summaries.max_duration)").send(direction))
|
|
58
|
-
when "count_sort"
|
|
68
|
+
when "count_sort", "request_count_sort"
|
|
59
69
|
grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count)").send(direction))
|
|
60
70
|
when "requests_per_minute"
|
|
61
71
|
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
|
|
@@ -74,6 +74,7 @@ module RailsPulse
|
|
|
74
74
|
route_groups = Request
|
|
75
75
|
.where(occurred_at: start_time...end_time)
|
|
76
76
|
.where.not(route_id: nil)
|
|
77
|
+
.joins(:route)
|
|
77
78
|
.group(:route_id)
|
|
78
79
|
|
|
79
80
|
# Calculate basic aggregates
|
|
@@ -132,6 +133,7 @@ module RailsPulse
|
|
|
132
133
|
query_groups = Operation
|
|
133
134
|
.where(occurred_at: start_time...end_time)
|
|
134
135
|
.where.not(query_id: nil)
|
|
136
|
+
.joins(:query)
|
|
135
137
|
.group(:query_id)
|
|
136
138
|
|
|
137
139
|
basic_stats = query_groups.pluck(
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<% global_filters = session_global_filters %>
|
|
2
|
+
<% has_global_filters = global_filters['start_time'].present? && global_filters['end_time'].present? %>
|
|
3
|
+
<% has_performance_filter = global_filters['performance_threshold'].present? %>
|
|
4
|
+
<% disabled_tags = global_filters['disabled_tags'] || [] %>
|
|
5
|
+
<% has_tag_filters = disabled_tags.any? %>
|
|
6
|
+
<% has_any_filters = has_global_filters || has_performance_filter %>
|
|
7
|
+
<% current_date_range = has_global_filters ? "#{global_filters['start_time']} to #{global_filters['end_time']}" : "" %>
|
|
8
|
+
<% current_threshold = global_filters['performance_threshold'] %>
|
|
9
|
+
|
|
10
|
+
<div data-controller="rails-pulse--global-filters" data-rails-pulse--global-filters-active-value="<%= has_any_filters %>">
|
|
11
|
+
<%= link_to '#', "aria-label": "Global filters", role: "button", data: { action: "rails-pulse--global-filters#open", "rails-pulse--global-filters-target": "indicator" } do %>
|
|
12
|
+
<%= rails_pulse_icon has_any_filters ? 'list-filter-plus' : 'list-filter', width: '20' %>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<div data-rails-pulse--global-filters-target="wrapper" data-action="click->rails-pulse--global-filters#closeOnClickOutside" style="display: none; position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.8); z-index: 1000; align-items: center; justify-content: center;">
|
|
16
|
+
<div class="dialog" data-rails-pulse--global-filters-target="dialog" style="position: relative; opacity: 1; transform: scale(1);">
|
|
17
|
+
<div class="dialog__content">
|
|
18
|
+
<h2 class="text-lg font-semibold mb-4">Global Filters</h2>
|
|
19
|
+
<p class="text-sm text-subtle mb-4">Set default time filters that persist across all pages. These can be overridden by page-specific filters.</p>
|
|
20
|
+
|
|
21
|
+
<%= form_with url: settings_global_filters_path, method: :patch, local: true, data: { action: "submit->rails-pulse--global-filters#submit" } do |form| %>
|
|
22
|
+
<div class="flex flex-col gap mb-4">
|
|
23
|
+
<div class="flex flex-col gap" style="--row-gap: 0.5rem">
|
|
24
|
+
<label for="global_date_range" class="text-sm font-medium">Date Range</label>
|
|
25
|
+
<input
|
|
26
|
+
type="text"
|
|
27
|
+
name="date_range"
|
|
28
|
+
id="global_date_range"
|
|
29
|
+
value="<%= current_date_range %>"
|
|
30
|
+
placeholder="Pick date range"
|
|
31
|
+
class="input"
|
|
32
|
+
data-controller="rails-pulse--datepicker"
|
|
33
|
+
data-rails-pulse--datepicker-mode-value="range"
|
|
34
|
+
data-rails-pulse--datepicker-show-months-value="2"
|
|
35
|
+
data-rails-pulse--datepicker-type-value="datetime"
|
|
36
|
+
data-rails-pulse--global-filters-target="dateRange"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="flex flex-col gap" style="--row-gap: 0.5rem">
|
|
41
|
+
<label for="global_performance_threshold" class="text-sm font-medium">Performance Threshold</label>
|
|
42
|
+
<select name="performance_threshold" id="global_performance_threshold" class="input">
|
|
43
|
+
<option value="">All Requests</option>
|
|
44
|
+
<option value="slow" <%= 'selected' if current_threshold == 'slow' %>>Slow and Above</option>
|
|
45
|
+
<option value="very_slow" <%= 'selected' if current_threshold == 'very_slow' %>>Very Slow and Above</option>
|
|
46
|
+
<option value="critical" <%= 'selected' if current_threshold == 'critical' %>>Critical Only</option>
|
|
47
|
+
</select>
|
|
48
|
+
<p class="text-xs text-subtle">Filter requests/queries by performance threshold</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="flex flex-col gap" style="--row-gap: 0.5rem">
|
|
52
|
+
<label class="text-sm font-medium">Tag Visibility</label>
|
|
53
|
+
<p class="text-xs text-subtle">Toggle visibility of tagged data. Unchecked tags will be hidden from all views.</p>
|
|
54
|
+
<div class="flex flex-col gap" style="--row-gap: 0.75rem">
|
|
55
|
+
<% tags = RailsPulse.configuration.tags + ["non_tagged"] %>
|
|
56
|
+
<% tags.each do |tag| %>
|
|
57
|
+
<div class="flex items-center gap">
|
|
58
|
+
<input
|
|
59
|
+
type="checkbox"
|
|
60
|
+
name="enabled_tags[]"
|
|
61
|
+
id="tag_<%= tag %>"
|
|
62
|
+
value="<%= tag %>"
|
|
63
|
+
<%= 'checked="checked"' if tag == "non_tagged" ? session[:show_non_tagged] != false : !disabled_tags.include?(tag) %>
|
|
64
|
+
class="switch"
|
|
65
|
+
role="switch"
|
|
66
|
+
/>
|
|
67
|
+
<label class="text-sm font-medium" for="tag_<%= tag %>">
|
|
68
|
+
<%= tag.humanize %>
|
|
69
|
+
</label>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="flex items-center justify-end gap">
|
|
77
|
+
<%= form.button "Clear", type: "submit", name: "clear", value: "true", class: "btn btn--borderless", formnovalidate: true %>
|
|
78
|
+
<%= form.submit "Apply Filters", class: "btn" %>
|
|
79
|
+
</div>
|
|
80
|
+
<% end %>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
<span class="overflow-ellipsis">Routes</span>
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
11
|
-
<%= link_to requests_path, class: 'btn sidebar-menu__button' do %>
|
|
12
|
-
<%= rails_pulse_icon 'audio-lines', width: '16' %>
|
|
13
|
-
<span class="overflow-ellipsis">Requests</span>
|
|
14
|
-
<% end %>
|
|
15
|
-
|
|
16
11
|
<%= link_to queries_path, class: 'btn sidebar-menu__button' do %>
|
|
17
12
|
<%= rails_pulse_icon 'database', width: '16' %>
|
|
18
13
|
<span class="overflow-ellipsis">Queries</span>
|
|
19
14
|
<% end %>
|
|
15
|
+
|
|
16
|
+
<%= link_to requests_path, class: 'btn sidebar-menu__button' do %>
|
|
17
|
+
<%= rails_pulse_icon 'audio-lines', width: '16' %>
|
|
18
|
+
<span class="overflow-ellipsis">Requests</span>
|
|
19
|
+
<% end %>
|
|
@@ -53,10 +53,14 @@
|
|
|
53
53
|
</nav>
|
|
54
54
|
</div>
|
|
55
55
|
|
|
56
|
-
<div class="flex items-center"
|
|
57
|
-
<%=
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
<div class="flex items-center gap" style="--column-gap: 0.5rem">
|
|
57
|
+
<%= render 'layouts/rails_pulse/global_filters' %>
|
|
58
|
+
|
|
59
|
+
<div data-controller="rails-pulse--color-scheme">
|
|
60
|
+
<%= link_to '#', "aria-label": "Toggle color scheme", role: "button", data: { action: "rails-pulse--color-scheme#toggle" } do %>
|
|
61
|
+
<%= rails_pulse_icon 'sun', width: '48' %>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
60
64
|
</div>
|
|
61
65
|
</header>
|
|
62
66
|
|
|
@@ -65,6 +69,5 @@
|
|
|
65
69
|
<%= yield %>
|
|
66
70
|
</div>
|
|
67
71
|
</main>
|
|
68
|
-
|
|
69
72
|
</body>
|
|
70
73
|
</html>
|
|
@@ -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 %>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<div class="breadcrumb-container">
|
|
2
|
+
<nav class="breadcrumb mis-2 mbs-2" aria-label="Breadcrumb">
|
|
3
|
+
<% breadcrumbs.each_with_index do |crumb, index| %>
|
|
4
|
+
<% if crumb[:current] %>
|
|
5
|
+
<span class="text-primary" aria-disabled="true" aria-current="page" role="link"><%= crumb[:title] %></span>
|
|
6
|
+
<% else %>
|
|
7
|
+
<%= link_to crumb[:title], crumb[:path] %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% unless index == breadcrumbs.length - 1 %>
|
|
10
|
+
<%= rails_pulse_icon 'chevron-right', width: 14, height: 14, class: "breadcrumb-separator mbe-1" %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</nav>
|
|
14
|
+
|
|
15
|
+
<% if defined?(taggable) && taggable.present? %>
|
|
16
|
+
<div class="breadcrumb-tags">
|
|
17
|
+
<%= render 'rails_pulse/tags/tag_manager', taggable: taggable %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
@@ -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>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<% columns = [
|
|
2
2
|
{ field: :normalized_sql, label: 'Query', class: 'w-auto' },
|
|
3
|
+
{ field: :avg_duration_sort, label: 'Average Query Time', class: 'w-44' },
|
|
3
4
|
{ field: :execution_count_sort, label: 'Executions', class: 'w-24' },
|
|
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 }
|
|
5
|
+
{ field: nil, label: 'Tags', class: 'w-32' }
|
|
7
6
|
] %>
|
|
8
7
|
|
|
9
8
|
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
|
@@ -17,10 +16,9 @@
|
|
|
17
16
|
<%= link_to html_escape(truncate_sql(summary.normalized_sql)), query_path(summary.query_id), data: { turbo_frame: '_top', } %>
|
|
18
17
|
</div>
|
|
19
18
|
</td>
|
|
20
|
-
<td class="whitespace-nowrap"><%= number_with_delimiter summary.execution_count %></td>
|
|
21
19
|
<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
|
|
20
|
+
<td class="whitespace-nowrap"><%= number_with_delimiter summary.execution_count %></td>
|
|
21
|
+
<td class="whitespace-nowrap"><%= display_tag_badges(summary.tags) %></td>
|
|
24
22
|
</tr>
|
|
25
23
|
<% end %>
|
|
26
24
|
</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">
|
|
@@ -13,13 +13,9 @@
|
|
|
13
13
|
data-rails-pulse--index-chart-id-value="average_query_times_chart"
|
|
14
14
|
>
|
|
15
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| %>
|
|
16
|
+
<%= search_form_for @ransack_query, url: queries_path, class: "flex items-center justify-between gap mb-4", data: { controller: "rails-pulse--custom-range" } do |form| %>
|
|
17
17
|
<div class="flex items-center grow gap">
|
|
18
|
-
<%= form
|
|
19
|
-
RailsPulse::QueriesController::TIME_RANGE_OPTIONS,
|
|
20
|
-
{ selected: @selected_time_range },
|
|
21
|
-
{ class: "input" }
|
|
22
|
-
%>
|
|
18
|
+
<%= time_range_selector(form, time_range_options: RailsPulse::QueriesController::TIME_RANGE_OPTIONS, selected_time_range: @selected_time_range) %>
|
|
23
19
|
<%= form.select :avg_duration,
|
|
24
20
|
duration_options(:query),
|
|
25
21
|
{ selected: @selected_response_range },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%= render 'rails_pulse/components/
|
|
1
|
+
<%= render 'rails_pulse/components/page_header', taggable: @query %>
|
|
2
2
|
|
|
3
3
|
<% unless turbo_frame_request? %>
|
|
4
4
|
<div class="row">
|
|
@@ -13,13 +13,9 @@
|
|
|
13
13
|
data-rails-pulse--index-chart-id-value="query_responses_chart"
|
|
14
14
|
>
|
|
15
15
|
<%= render 'rails_pulse/components/panel', { title: 'Query Responses' } do %>
|
|
16
|
-
<%= search_form_for @ransack_query, url: query_path(@query), class: "flex items-center justify-between gap mb-4" do |form| %>
|
|
16
|
+
<%= search_form_for @ransack_query, url: query_path(@query), class: "flex items-center justify-between gap mb-4", data: { controller: "rails-pulse--custom-range" } do |form| %>
|
|
17
17
|
<div class="flex items-center grow gap">
|
|
18
|
-
<%= form
|
|
19
|
-
RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
|
|
20
|
-
{ selected: @selected_time_range },
|
|
21
|
-
{ class: "input" }
|
|
22
|
-
%>
|
|
18
|
+
<%= time_range_selector(form, time_range_options: RailsPulse::RoutesController::TIME_RANGE_OPTIONS, selected_time_range: @selected_time_range) %>
|
|
23
19
|
<%= form.select :duration,
|
|
24
20
|
duration_options(:query),
|
|
25
21
|
{ selected: @selected_response_range },
|
|
@@ -1,28 +1,41 @@
|
|
|
1
|
-
<%
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{ field: :status, label: 'HTTP Status', class: 'w-20' },
|
|
9
|
-
{ field: :status_indicator, label: 'Status', class: 'w-16' }
|
|
10
|
-
]
|
|
11
|
-
%>
|
|
1
|
+
<% columns = [
|
|
2
|
+
{ field: :occurred_at, label: 'Timestamp', class: 'w-36' },
|
|
3
|
+
{ field: :route_path, label: 'Route', class: 'w-auto' },
|
|
4
|
+
{ field: :duration, label: 'Response Time', class: 'w-36' },
|
|
5
|
+
{ field: :status, label: 'Status', class: 'w-20' },
|
|
6
|
+
{ field: nil, label: 'Tags', class: 'w-32' }
|
|
7
|
+
] %>
|
|
12
8
|
|
|
13
9
|
<table class="table mbs-4" data-controller="rails-pulse--table-sort">
|
|
14
10
|
<%= render "rails_pulse/components/table_head", columns: columns %>
|
|
15
11
|
|
|
16
12
|
<tbody>
|
|
17
|
-
<% @table_data.each do |
|
|
13
|
+
<% @table_data.each do |request| %>
|
|
14
|
+
<%
|
|
15
|
+
# Determine performance class based on request duration
|
|
16
|
+
performance_class = case request.duration
|
|
17
|
+
when 0..100 then "text-green-600"
|
|
18
|
+
when 100..300 then "text-yellow-600"
|
|
19
|
+
when 300..1000 then "text-orange-600"
|
|
20
|
+
else "text-red-600"
|
|
21
|
+
end
|
|
22
|
+
%>
|
|
18
23
|
<tr>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<td class="whitespace-nowrap"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<td class="whitespace-nowrap
|
|
24
|
+
<td class="whitespace-nowrap">
|
|
25
|
+
<%= link_to human_readable_occurred_at(request.occurred_at), request_path(request), data: { turbo_frame: '_top' } %>
|
|
26
|
+
</td>
|
|
27
|
+
<td class="whitespace-nowrap">
|
|
28
|
+
<%= link_to "#{request.route.path} #{request.route.method}", route_path(request.route), data: { turbo_frame: '_top' } %>
|
|
29
|
+
</td>
|
|
30
|
+
<td class="whitespace-nowrap">
|
|
31
|
+
<span class="<%= performance_class %> font-medium">
|
|
32
|
+
<%= request.duration.round(2) %> ms
|
|
33
|
+
</span>
|
|
34
|
+
</td>
|
|
35
|
+
<td class="whitespace-nowrap">
|
|
36
|
+
<span class="text-green-600"><%= request.status %></span>
|
|
37
|
+
</td>
|
|
38
|
+
<td class="whitespace-nowrap"><%= display_tag_badges(request) %></td>
|
|
26
39
|
</tr>
|
|
27
40
|
<% end %>
|
|
28
41
|
</tbody>
|