rails_pulse 0.1.1 → 0.1.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -176
  3. data/Rakefile +77 -2
  4. data/app/assets/stylesheets/rails_pulse/application.css +0 -12
  5. data/app/controllers/concerns/chart_table_concern.rb +21 -4
  6. data/app/controllers/concerns/response_range_concern.rb +6 -3
  7. data/app/controllers/concerns/time_range_concern.rb +5 -10
  8. data/app/controllers/concerns/zoom_range_concern.rb +1 -1
  9. data/app/controllers/rails_pulse/application_controller.rb +8 -4
  10. data/app/controllers/rails_pulse/dashboard_controller.rb +12 -0
  11. data/app/controllers/rails_pulse/queries_controller.rb +65 -50
  12. data/app/controllers/rails_pulse/requests_controller.rb +24 -12
  13. data/app/controllers/rails_pulse/routes_controller.rb +59 -24
  14. data/app/helpers/rails_pulse/application_helper.rb +0 -1
  15. data/app/helpers/rails_pulse/chart_formatters.rb +3 -3
  16. data/app/helpers/rails_pulse/chart_helper.rb +6 -2
  17. data/app/helpers/rails_pulse/status_helper.rb +10 -4
  18. data/app/javascript/rails_pulse/controllers/index_controller.js +117 -33
  19. data/app/javascript/rails_pulse/controllers/pagination_controller.js +17 -27
  20. data/app/jobs/rails_pulse/backfill_summaries_job.rb +41 -0
  21. data/app/jobs/rails_pulse/summary_job.rb +53 -0
  22. data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +28 -7
  23. data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +18 -22
  24. data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +18 -7
  25. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +34 -41
  26. data/app/models/rails_pulse/operation.rb +1 -1
  27. data/app/models/rails_pulse/queries/cards/average_query_times.rb +47 -23
  28. data/app/models/rails_pulse/queries/cards/execution_rate.rb +33 -26
  29. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +34 -45
  30. data/app/models/rails_pulse/queries/charts/average_query_times.rb +23 -97
  31. data/app/models/rails_pulse/queries/tables/index.rb +74 -0
  32. data/app/models/rails_pulse/query.rb +1 -0
  33. data/app/models/rails_pulse/requests/charts/average_response_times.rb +23 -84
  34. data/app/models/rails_pulse/route.rb +1 -6
  35. data/app/models/rails_pulse/routes/cards/average_response_times.rb +45 -23
  36. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +38 -45
  37. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +34 -47
  38. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +30 -25
  39. data/app/models/rails_pulse/routes/charts/average_response_times.rb +23 -100
  40. data/app/models/rails_pulse/routes/tables/index.rb +57 -40
  41. data/app/models/rails_pulse/summary.rb +143 -0
  42. data/app/services/rails_pulse/summary_service.rb +199 -0
  43. data/app/views/layouts/rails_pulse/application.html.erb +4 -4
  44. data/app/views/rails_pulse/components/_empty_state.html.erb +11 -0
  45. data/app/views/rails_pulse/components/_metric_card.html.erb +10 -24
  46. data/app/views/rails_pulse/dashboard/index.html.erb +54 -36
  47. data/app/views/rails_pulse/queries/_show_table.html.erb +1 -1
  48. data/app/views/rails_pulse/queries/_table.html.erb +10 -12
  49. data/app/views/rails_pulse/queries/index.html.erb +41 -34
  50. data/app/views/rails_pulse/queries/show.html.erb +38 -31
  51. data/app/views/rails_pulse/requests/_operations.html.erb +32 -26
  52. data/app/views/rails_pulse/requests/_table.html.erb +1 -3
  53. data/app/views/rails_pulse/requests/index.html.erb +42 -34
  54. data/app/views/rails_pulse/routes/_table.html.erb +13 -13
  55. data/app/views/rails_pulse/routes/index.html.erb +43 -35
  56. data/app/views/rails_pulse/routes/show.html.erb +42 -35
  57. data/config/initializers/rails_pulse.rb +0 -12
  58. data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +54 -0
  59. data/db/rails_pulse_schema.rb +121 -0
  60. data/lib/generators/rails_pulse/install_generator.rb +41 -4
  61. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +60 -0
  62. data/lib/generators/rails_pulse/templates/rails_pulse.rb +0 -12
  63. data/lib/rails_pulse/configuration.rb +0 -11
  64. data/lib/rails_pulse/engine.rb +0 -1
  65. data/lib/rails_pulse/version.rb +1 -1
  66. data/lib/tasks/rails_pulse.rake +58 -0
  67. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  68. data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
  69. data/public/rails-pulse-assets/rails-pulse.js +1 -1
  70. data/public/rails-pulse-assets/rails-pulse.js.map +3 -3
  71. data/public/rails-pulse-assets/search.svg +43 -0
  72. metadata +27 -11
  73. data/app/controllers/rails_pulse/caches_controller.rb +0 -115
  74. data/app/helpers/rails_pulse/cached_component_helper.rb +0 -73
  75. data/app/models/rails_pulse/component_cache_key.rb +0 -33
  76. data/app/views/rails_pulse/caches/show.html.erb +0 -9
  77. data/db/migrate/20250227235904_create_routes.rb +0 -12
  78. data/db/migrate/20250227235915_create_requests.rb +0 -19
  79. data/db/migrate/20250228000000_create_queries.rb +0 -14
  80. data/db/migrate/20250228000056_create_operations.rb +0 -24
  81. data/lib/rails_pulse/migration.rb +0 -29
@@ -0,0 +1,143 @@
1
+ module RailsPulse
2
+ class Summary < RailsPulse::ApplicationRecord
3
+ self.table_name = "rails_pulse_summaries"
4
+
5
+ PERIOD_TYPES = %w[hour day week month].freeze
6
+
7
+ # Polymorphic association
8
+ belongs_to :summarizable, polymorphic: true, optional: true # Optional for Request summaries
9
+
10
+ # Convenience associations for easier querying
11
+ belongs_to :route, -> { where(rails_pulse_summaries: { summarizable_type: "RailsPulse::Route" }) },
12
+ foreign_key: "summarizable_id", class_name: "RailsPulse::Route", optional: true
13
+ belongs_to :query, -> { where(rails_pulse_summaries: { summarizable_type: "RailsPulse::Query" }) },
14
+ foreign_key: "summarizable_id", class_name: "RailsPulse::Query", optional: true
15
+
16
+ # Validations
17
+ validates :period_type, inclusion: { in: PERIOD_TYPES }
18
+ validates :period_start, presence: true
19
+ validates :period_end, presence: true
20
+
21
+ # Scopes
22
+ scope :for_period_type, ->(type) { where(period_type: type) }
23
+ scope :for_date_range, ->(start_date, end_date) {
24
+ where(period_start: start_date..end_date)
25
+ }
26
+ scope :for_requests, -> { where(summarizable_type: "RailsPulse::Request") }
27
+ scope :for_routes, -> { where(summarizable_type: "RailsPulse::Route") }
28
+ scope :for_queries, -> { where(summarizable_type: "RailsPulse::Query") }
29
+ scope :recent, -> { order(period_start: :desc) }
30
+
31
+ # Special scope for overall request summaries
32
+ scope :overall_requests, -> {
33
+ where(summarizable_type: "RailsPulse::Request", summarizable_id: 0)
34
+ }
35
+
36
+ # Ransack configuration
37
+ def self.ransackable_attributes(auth_object = nil)
38
+ %w[
39
+ period_start period_end avg_duration max_duration count error_count
40
+ requests_per_minute error_rate_percentage route_path_cont
41
+ execution_count total_time_consumed normalized_sql
42
+ ]
43
+ end
44
+
45
+ def self.ransackable_associations(auth_object = nil)
46
+ %w[route query]
47
+ end
48
+
49
+ # Custom ransackers for calculated fields (designed to work with GROUP BY queries)
50
+ ransacker :count do
51
+ Arel.sql("SUM(rails_pulse_summaries.count)") # Use SUM for proper grouping
52
+ end
53
+
54
+ ransacker :requests_per_minute do
55
+ Arel.sql("SUM(rails_pulse_summaries.count) / 60.0") # Use SUM for consistency
56
+ end
57
+
58
+ ransacker :error_rate_percentage do
59
+ Arel.sql("(SUM(rails_pulse_summaries.error_count) * 100.0) / SUM(rails_pulse_summaries.count)") # Use SUM for both
60
+ end
61
+
62
+
63
+ # Ransacker for route path sorting (when joined with routes table)
64
+ ransacker :route_path do
65
+ Arel.sql("rails_pulse_routes.path")
66
+ end
67
+
68
+ # Ransacker for route path filtering using subquery (works without JOIN)
69
+ ransacker :route_path_cont do |parent|
70
+ Arel.sql(<<-SQL)
71
+ rails_pulse_summaries.summarizable_id IN (
72
+ SELECT id FROM rails_pulse_routes
73
+ WHERE rails_pulse_routes.path LIKE '%' || ? || '%'
74
+ )
75
+ SQL
76
+ end
77
+
78
+ # Sorting-specific ransackers for GROUP BY compatibility (used only in ORDER BY)
79
+ # These use different names to avoid conflicts with filtering
80
+ ransacker :avg_duration_sort do
81
+ Arel.sql("AVG(rails_pulse_summaries.avg_duration)")
82
+ end
83
+
84
+ ransacker :max_duration_sort do
85
+ Arel.sql("MAX(rails_pulse_summaries.max_duration)")
86
+ end
87
+
88
+ ransacker :count_sort do
89
+ Arel.sql("SUM(rails_pulse_summaries.count)")
90
+ end
91
+
92
+ ransacker :error_count_sort do
93
+ Arel.sql("SUM(rails_pulse_summaries.error_count)")
94
+ end
95
+
96
+ ransacker :success_count_sort do
97
+ Arel.sql("SUM(rails_pulse_summaries.success_count)")
98
+ end
99
+
100
+ ransacker :total_time_consumed_sort do
101
+ Arel.sql("SUM(rails_pulse_summaries.count * rails_pulse_summaries.avg_duration)")
102
+ end
103
+
104
+ # Alias execution_count_sort to count_sort for queries table compatibility
105
+ ransacker :execution_count_sort do
106
+ Arel.sql("SUM(rails_pulse_summaries.count)")
107
+ end
108
+
109
+ # Ransackers for queries table calculated fields
110
+ ransacker :execution_count do
111
+ Arel.sql("SUM(rails_pulse_summaries.count)") # Total executions
112
+ end
113
+
114
+ ransacker :total_time_consumed do
115
+ Arel.sql("SUM(rails_pulse_summaries.count * rails_pulse_summaries.avg_duration)") # Total time consumed
116
+ end
117
+
118
+ # Ransacker for query SQL sorting (when joined with queries table)
119
+ ransacker :normalized_sql do
120
+ Arel.sql("rails_pulse_queries.normalized_sql")
121
+ end
122
+
123
+ class << self
124
+ def calculate_period_end(period_type, start_time)
125
+ case period_type
126
+ when "hour" then start_time.end_of_hour
127
+ when "day" then start_time.end_of_day
128
+ when "week" then start_time.end_of_week
129
+ when "month" then start_time.end_of_month
130
+ end
131
+ end
132
+
133
+ def normalize_period_start(period_type, time)
134
+ case period_type
135
+ when "hour" then time.beginning_of_hour
136
+ when "day" then time.beginning_of_day
137
+ when "week" then time.beginning_of_week
138
+ when "month" then time.beginning_of_month
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,199 @@
1
+
2
+ module RailsPulse
3
+ class SummaryService
4
+ attr_reader :period_type, :start_time, :end_time
5
+
6
+ def initialize(period_type, start_time)
7
+ @period_type = period_type
8
+ @start_time = Summary.normalize_period_start(period_type, start_time)
9
+ @end_time = Summary.calculate_period_end(period_type, @start_time)
10
+ end
11
+
12
+ def perform
13
+ Rails.logger.info "[RailsPulse] Starting #{period_type} summary for #{start_time}"
14
+
15
+ ActiveRecord::Base.transaction do
16
+ aggregate_requests # Overall system metrics
17
+ aggregate_routes # Per-route metrics
18
+ aggregate_queries # Per-query metrics
19
+ end
20
+
21
+ Rails.logger.info "[RailsPulse] Completed #{period_type} summary"
22
+ rescue => e
23
+ Rails.logger.error "[RailsPulse] Summary failed: #{e.message}"
24
+ raise
25
+ end
26
+
27
+ private
28
+
29
+ def aggregate_requests
30
+ # Create a single summary for ALL requests in this period
31
+ requests = Request.where(occurred_at: start_time...end_time)
32
+
33
+ return if requests.empty?
34
+
35
+ # Get all durations and statuses for percentile calculations
36
+ request_data = requests.pluck(:duration, :status)
37
+ durations = request_data.map(&:first).compact.sort
38
+ statuses = request_data.map(&:second)
39
+
40
+ # Find or create the overall request summary
41
+ summary = Summary.find_or_initialize_by(
42
+ summarizable_type: "RailsPulse::Request",
43
+ summarizable_id: 0, # Use 0 as a special ID for overall summaries
44
+ period_type: period_type,
45
+ period_start: start_time
46
+ )
47
+
48
+ summary.assign_attributes(
49
+ period_end: end_time,
50
+ count: durations.size,
51
+ avg_duration: durations.any? ? durations.sum.to_f / durations.size : 0,
52
+ min_duration: durations.min,
53
+ max_duration: durations.max,
54
+ total_duration: durations.sum,
55
+ p50_duration: calculate_percentile(durations, 0.5),
56
+ p95_duration: calculate_percentile(durations, 0.95),
57
+ p99_duration: calculate_percentile(durations, 0.99),
58
+ stddev_duration: calculate_stddev(durations, durations.sum.to_f / durations.size),
59
+ error_count: statuses.count { |s| s >= 400 },
60
+ success_count: statuses.count { |s| s < 400 },
61
+ status_2xx: statuses.count { |s| s.between?(200, 299) },
62
+ status_3xx: statuses.count { |s| s.between?(300, 399) },
63
+ status_4xx: statuses.count { |s| s.between?(400, 499) },
64
+ status_5xx: statuses.count { |s| s >= 500 }
65
+ )
66
+
67
+ summary.save!
68
+ end
69
+
70
+ private
71
+
72
+ def aggregate_routes
73
+ # Use ActiveRecord for cross-database compatibility
74
+ route_groups = Request
75
+ .where(occurred_at: start_time...end_time)
76
+ .where.not(route_id: nil)
77
+ .group(:route_id)
78
+
79
+ # Calculate basic aggregates
80
+ basic_stats = route_groups.pluck(
81
+ :route_id,
82
+ Arel.sql("COUNT(*) as request_count"),
83
+ Arel.sql("AVG(duration) as avg_duration"),
84
+ Arel.sql("MIN(duration) as min_duration"),
85
+ Arel.sql("MAX(duration) as max_duration"),
86
+ Arel.sql("SUM(duration) as total_duration")
87
+ )
88
+
89
+ basic_stats.each do |stats|
90
+ route_id = stats[0]
91
+
92
+ # Calculate percentiles and status counts separately for cross-DB compatibility
93
+ durations = Request
94
+ .where(occurred_at: start_time...end_time)
95
+ .where(route_id: route_id)
96
+ .pluck(:duration, :status)
97
+
98
+ sorted_durations = durations.map(&:first).compact.sort
99
+ statuses = durations.map(&:last)
100
+
101
+ summary = Summary.find_or_initialize_by(
102
+ summarizable_type: "RailsPulse::Route",
103
+ summarizable_id: route_id,
104
+ period_type: period_type,
105
+ period_start: start_time
106
+ )
107
+
108
+ summary.assign_attributes(
109
+ period_end: end_time,
110
+ count: stats[1],
111
+ avg_duration: stats[2],
112
+ min_duration: stats[3],
113
+ max_duration: stats[4],
114
+ total_duration: stats[5],
115
+ p50_duration: calculate_percentile(sorted_durations, 0.5),
116
+ p95_duration: calculate_percentile(sorted_durations, 0.95),
117
+ p99_duration: calculate_percentile(sorted_durations, 0.99),
118
+ stddev_duration: calculate_stddev(sorted_durations, stats[2]),
119
+ error_count: statuses.count { |s| s >= 400 },
120
+ success_count: statuses.count { |s| s < 400 },
121
+ status_2xx: statuses.count { |s| s.between?(200, 299) },
122
+ status_3xx: statuses.count { |s| s.between?(300, 399) },
123
+ status_4xx: statuses.count { |s| s.between?(400, 499) },
124
+ status_5xx: statuses.count { |s| s >= 500 }
125
+ )
126
+
127
+ summary.save!
128
+ end
129
+ end
130
+
131
+ def aggregate_queries
132
+ query_groups = Operation
133
+ .where(occurred_at: start_time...end_time)
134
+ .where.not(query_id: nil)
135
+ .group(:query_id)
136
+
137
+ basic_stats = query_groups.pluck(
138
+ :query_id,
139
+ Arel.sql("COUNT(*) as execution_count"),
140
+ Arel.sql("AVG(duration) as avg_duration"),
141
+ Arel.sql("MIN(duration) as min_duration"),
142
+ Arel.sql("MAX(duration) as max_duration"),
143
+ Arel.sql("SUM(duration) as total_duration")
144
+ )
145
+
146
+ basic_stats.each do |stats|
147
+ query_id = stats[0]
148
+
149
+ # Calculate percentiles separately
150
+ durations = Operation
151
+ .where(occurred_at: start_time...end_time)
152
+ .where(query_id: query_id)
153
+ .pluck(:duration)
154
+ .compact
155
+ .sort
156
+
157
+ summary = Summary.find_or_initialize_by(
158
+ summarizable_type: "RailsPulse::Query",
159
+ summarizable_id: query_id,
160
+ period_type: period_type,
161
+ period_start: start_time
162
+ )
163
+
164
+ summary.assign_attributes(
165
+ period_end: end_time,
166
+ count: stats[1],
167
+ avg_duration: stats[2],
168
+ min_duration: stats[3],
169
+ max_duration: stats[4],
170
+ total_duration: stats[5],
171
+ p50_duration: calculate_percentile(durations, 0.5),
172
+ p95_duration: calculate_percentile(durations, 0.95),
173
+ p99_duration: calculate_percentile(durations, 0.99),
174
+ stddev_duration: calculate_stddev(durations, stats[2])
175
+ )
176
+
177
+ summary.save!
178
+ end
179
+ end
180
+
181
+ def calculate_percentile(sorted_array, percentile)
182
+ return nil if sorted_array.empty?
183
+
184
+ k = (percentile * (sorted_array.length - 1)).floor
185
+ f = (percentile * (sorted_array.length - 1)) - k
186
+
187
+ return sorted_array[k] if f == 0 || k + 1 >= sorted_array.length
188
+
189
+ sorted_array[k] + (sorted_array[k + 1] - sorted_array[k]) * f
190
+ end
191
+
192
+ def calculate_stddev(values, mean)
193
+ return nil if values.empty? || values.size == 1
194
+
195
+ sum_of_squares = values.sum { |v| (v - mean) ** 2 }
196
+ Math.sqrt(sum_of_squares / (values.size - 1))
197
+ end
198
+ end
199
+ end
@@ -28,12 +28,12 @@
28
28
  <dialog class="sheet sheet--left" style="--sheet-size: 288px;" data-rails-pulse--dialog-target="menu" data-action="click->rails-pulse--dialog#closeOnClickOutside">
29
29
  <div class="sheet__content p-2">
30
30
  <div class="sidebar-menu">
31
- <a class="btn sidebar-menu__button" href="#">
31
+ <%= link_to rails_pulse.root_path, class: "btn sidebar-menu__button" do %>
32
32
  <div class="flex flex-col text-start leading-tight overflow-hidden">
33
33
  <span class="overflow-ellipsis font-semibold">Rails Pulse</span>
34
34
  <span class="overflow-ellipsis text-xs">Open Source</span>
35
35
  </div>
36
- </a>
36
+ <% end %>
37
37
  <div class="sidebar-menu__content">
38
38
  <div class="sidebar-menu__group">
39
39
  <nav class="sidebar-menu__items">
@@ -47,9 +47,9 @@
47
47
  </div>
48
48
 
49
49
  <div class="flex items-center gap">
50
- <a class="flex items-center gap mie-2" href="#">
50
+ <%= link_to rails_pulse.root_path, class: "flex items-center gap mie-2" do %>
51
51
  <span class="font-bold">Rails Pulse</span>
52
- </a>
52
+ <% end %>
53
53
  <nav class="flex items-center gap text-sm text-subtle show@md" style="--column-gap: 1rem">
54
54
  <%= render 'layouts/rails_pulse/menu_items' %>
55
55
  </nav>
@@ -0,0 +1,11 @@
1
+ <div class="flex items-center justify-center pbs-12 pbe-12 pis-6 pie-6 mb-8">
2
+ <div class="flex items-center gap">
3
+ <div class="shrink-0">
4
+ <img src="<%= asset_path('search.svg') %>" class="w-32 h-24 opacity-50" alt="No data available" />
5
+ </div>
6
+ <div class="text-subtle pis-8">
7
+ <p class="text-lg font-semibold mbe-2"><%= title %></p>
8
+ <p class="text-sm"><%= description %></p>
9
+ </div>
10
+ </div>
11
+ </div>
@@ -1,15 +1,15 @@
1
1
  <%
2
- @component_data ||= {}
3
- id ||= @component_data[:id]
4
- context ||= @component_data[:context]
5
- title ||= @component_data[:title]
6
- summary ||= @component_data[:summary]
7
- line_chart_data ||= @component_data[:line_chart_data]
8
- trend_icon ||= @component_data[:trend_icon]
9
- trend_amount ||= @component_data[:trend_amount]
10
- trend_text ||= @component_data[: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
14
  <div class="row mbs-2" style="height: 50px; margin-bottom: 0;">
15
15
  <div class="grid-item">
@@ -35,20 +35,6 @@
35
35
  <%= trend_amount %>
36
36
  <p class="mis-2 text-subtle text-xs"><%= trend_text %></p>
37
37
  </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
38
  </div>
53
39
  </div>
54
40
  <% end %>
@@ -1,64 +1,82 @@
1
1
  <div class="row">
2
- <%= cached_component(component: "metric_card", id: "average_response_times", class: "grid-item block") %>
3
- <%= cached_component(component: "metric_card", id: "percentile_response_times", class: "grid-item block") %>
4
- <%= cached_component(component: "metric_card", id: "request_count_totals", class: "grid-item block") %>
5
- <%= cached_component(component: "metric_card", id: "error_rate_per_route", class: "grid-item block") %>
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
9
  <div class="grid-item" style="height:300px">
10
- <%= cached_component(
11
- component: "panel",
12
- id: "dashboard_average_response_time",
13
- card_classes: 'b-full',
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
- refresh_action: true,
19
- content_partial: 'rails_pulse/dashboard/charts/bar_chart')
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
- <%= cached_component(
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: routes_path, icon: 'external-link', title: 'View details', data: { turbo_frame: '_top' } }],
32
- refresh_action: true,
33
- content_partial: 'rails_pulse/dashboard/charts/bar_chart')
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
- <%= cached_component(
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 compared to last week, including average response time, week-over-week change, and total request count.',
46
- refresh_action: true,
47
- content_partial: 'rails_pulse/dashboard/tables/standard_table',
48
- class: "table-container")
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
- <%= cached_component(
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
- refresh_action: true,
60
- content_partial: 'rails_pulse/dashboard/tables/standard_table',
61
- class: "table-container")
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>
@@ -1,5 +1,5 @@
1
1
  <% columns = [
2
- { field: :occurred_at, label: 'Occurred At', class: 'w-auto' },
2
+ { field: :occurred_at, label: 'Timestamp', class: 'w-auto' },
3
3
  { field: :duration, label: 'Duration', class: 'w-32'}
4
4
  ] %>
5
5
 
@@ -1,28 +1,26 @@
1
1
  <% columns = [
2
2
  { field: :normalized_sql, label: 'Query', class: 'w-auto' },
3
- { field: :execution_count, label: 'Executions', class: 'w-24' },
4
- { field: :average_query_time_ms, label: 'Avg Time', class: 'w-24' },
5
- { field: :total_time_consumed, label: 'Total Time', class: 'w-28' },
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
9
  <table class="table mbs-4">
11
10
  <%= render "rails_pulse/components/table_head", columns: columns %>
12
11
 
13
12
  <tbody>
14
- <% @table_data.each do |query| %>
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(query.normalized_sql)), query_path(query), data: { turbo_frame: '_top', } %>
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 query.execution_count %></td>
22
- <td class="whitespace-nowrap"><%= query.average_query_time_ms.to_i %> ms</td>
23
- <td class="whitespace-nowrap"><%= number_with_delimiter query.total_time_consumed.to_i %> ms</td>
24
- <td class="whitespace-nowrap text-center"><%= query_status_indicator(query.average_query_time_ms) %></td>
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>