rails_pulse 0.1.0 → 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 +74 -178
  3. data/Rakefile +75 -173
  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 +20 -9
  25. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +19 -7
  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 +6 -12
  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 +28 -12
  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
data/Rakefile CHANGED
@@ -1,207 +1,109 @@
1
1
  require "bundler/setup"
2
2
 
3
+ # Load environment variables from .env file
4
+ require "dotenv/load" if File.exist?(".env")
5
+
3
6
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
7
  load "rails/tasks/engine.rake"
5
-
6
8
  load "rails/tasks/statistics.rake"
7
9
 
8
10
  require "bundler/gem_tasks"
9
11
 
10
12
  # Test tasks
11
13
  namespace :test do
12
- desc "Run unit tests (fastest - uses in-memory database)"
14
+ desc "Run unit tests (models, helpers, services, instrumentation)"
13
15
  task :unit do
14
- ENV["TEST_TYPE"] = "unit"
15
- ENV["MEMORY_DATABASE"] = "true"
16
- Rake::Task["test:run_unit"].invoke
16
+ sh "rails test test/models test/helpers test/services test/support test/instrumentation"
17
17
  end
18
18
 
19
- desc "Run functional tests"
19
+ desc "Run functional tests (controllers)"
20
20
  task :functional do
21
- ENV["TEST_TYPE"] = "functional"
22
- ENV["MEMORY_DATABASE"] = "true"
23
- Rake::Task["test:run_functional"].invoke
21
+ sh "rails test test/controllers"
24
22
  end
25
23
 
26
24
  desc "Run integration tests"
27
25
  task :integration do
28
- ENV["TEST_TYPE"] = "integration"
29
- ENV["MEMORY_DATABASE"] = "false"
30
- Rake::Task["test:run_integration"].invoke
26
+ sh "rails test test/integration test/system"
31
27
  end
32
28
 
33
- desc "Run all tests (unit, functional, integration)"
29
+ desc "Run all tests"
34
30
  task :all do
35
- %w[unit functional integration].each do |test_type|
36
- puts "\n=== Running #{test_type} tests ==="
37
- Rake::Task["test:#{test_type}"].invoke
38
- end
39
- end
40
-
41
- desc "Run unit tests (alias for test:unit)"
42
- task units: :unit
43
-
44
- desc "Run functional tests (alias for test:functional)"
45
- task functionals: :functional
46
-
47
- desc "Run integration tests (alias for test:integration)"
48
- task integrations: :integration
49
-
50
- desc "Run tests with speed optimizations"
51
- task fast: :unit
52
-
53
-
54
- # Internal tasks
55
- task :run_unit do
56
- sh "rails test test/models test/middleware test/lib test/support"
57
- end
58
-
59
- task :run_functional do
60
- sh "rails test test/controllers test/helpers"
61
- end
62
-
63
- task :run_integration do
64
- sh "rails test test/integration"
65
- end
66
- end
67
-
68
- # Speed-optimized test task (unit tests only)
69
- desc "Run fast unit tests only"
70
- task test_fast: "test:unit"
71
-
72
- # Override default test task to run all tests
73
- desc "Run all tests"
74
- task test: "test:all"
75
-
76
- # Simplified database testing tasks
77
- namespace :test do
78
- desc "Run tests with SQLite (default)"
79
- task :sqlite do
80
- puts "🗂️ Testing with SQLite..."
81
- Rake::Task["test:all"].invoke
82
- end
83
-
84
- desc "Run tests with PostgreSQL"
85
- task :postgresql do
86
- puts "🐘 Testing with PostgreSQL..."
87
- env = ENV.to_h.merge(
88
- "DATABASE_ADAPTER" => "postgresql",
89
- "FORCE_DB_CONFIG" => "true"
90
- )
91
- system(env, "rails test:all") || raise("PostgreSQL tests failed")
92
- end
93
-
94
- desc "Run tests with MySQL"
95
- task :mysql do
96
- puts "🐬 Testing with MySQL..."
97
- env = ENV.to_h.merge(
98
- "DATABASE_ADAPTER" => "mysql2",
99
- "FORCE_DB_CONFIG" => "true",
100
- "PARALLEL_WORKERS" => "1"
101
- )
102
- system(env, "rails test:all") || raise("MySQL tests failed")
31
+ sh "rails test"
103
32
  end
104
33
 
105
- desc "Run test matrix (SQLite + PostgreSQL + MySQL)"
34
+ desc "Run tests across all database and Rails version combinations (local only - CI uses sqlite3 + postgresql)"
106
35
  task :matrix do
107
- puts "\n🧪 Running test matrix...\n"
108
-
109
- databases = [
110
- { name: "SQLite", env: {}, emoji: "🗂️" },
111
- { name: "PostgreSQL", env: { "DATABASE_ADAPTER" => "postgresql", "FORCE_DB_CONFIG" => "true" }, emoji: "🐘" },
112
- { name: "MySQL", env: { "DATABASE_ADAPTER" => "mysql2", "FORCE_DB_CONFIG" => "true", "PARALLEL_WORKERS" => "1" }, emoji: "🐬" }
113
- ]
114
-
115
- results = {}
116
-
117
- databases.each do |db|
118
- puts "\n" + "="*60
119
- puts "#{db[:emoji]} Testing with #{db[:name]}..."
120
- puts "="*60
121
-
122
- begin
123
- env = ENV.to_h.merge(db[:env])
124
- success = system(env, "rails test:all")
125
-
126
- results[db[:name]] = success ? "✅ PASSED" : "❌ FAILED"
127
- rescue => e
128
- results[db[:name]] = "❌ FAILED"
129
- puts "Error: #{e.message}"
36
+ databases = [ "sqlite3", "postgresql", "mysql2" ]
37
+ rails_versions = [ "rails-7-2", "rails-8-0" ]
38
+
39
+ failed_combinations = []
40
+
41
+ databases.each do |database|
42
+ rails_versions.each do |rails_version|
43
+ puts "\n" + "=" * 80
44
+ puts "🧪 Local Testing: #{database.upcase} + #{rails_version.upcase}"
45
+ puts "(CI only tests SQLite3 + PostgreSQL for reliability)"
46
+ puts "=" * 80
47
+
48
+ begin
49
+ gemfile = "gemfiles/#{rails_version.gsub('-', '_')}.gemfile"
50
+
51
+ # Set environment variables
52
+ env_vars = {
53
+ "DB" => database,
54
+ "BUNDLE_GEMFILE" => gemfile,
55
+ "FORCE_DB_CONFIG" => "true"
56
+ }
57
+
58
+ # Add database-specific environment variables
59
+ case database
60
+ when "postgresql"
61
+ env_vars.merge!({
62
+ "POSTGRES_USERNAME" => ENV.fetch("POSTGRES_USERNAME", "postgres"),
63
+ "POSTGRES_PASSWORD" => ENV.fetch("POSTGRES_PASSWORD", ""),
64
+ "POSTGRES_HOST" => ENV.fetch("POSTGRES_HOST", "localhost"),
65
+ "POSTGRES_PORT" => ENV.fetch("POSTGRES_PORT", "5432")
66
+ })
67
+ when "mysql2"
68
+ env_vars.merge!({
69
+ "MYSQL_USERNAME" => ENV.fetch("MYSQL_USERNAME", "root"),
70
+ "MYSQL_PASSWORD" => ENV.fetch("MYSQL_PASSWORD", "password"),
71
+ "MYSQL_HOST" => ENV.fetch("MYSQL_HOST", "localhost"),
72
+ "MYSQL_PORT" => ENV.fetch("MYSQL_PORT", "3306")
73
+ })
74
+ end
75
+
76
+ # Build environment string
77
+ env_string = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ")
78
+
79
+ # Run the test command
80
+ sh "#{env_string} bundle exec rails test:all"
81
+
82
+ puts "✅ PASSED: #{database} + #{rails_version}"
83
+
84
+ rescue => e
85
+ puts "❌ FAILED: #{database} + #{rails_version}"
86
+ puts "Error: #{e.message}"
87
+ failed_combinations << "#{database} + #{rails_version}"
88
+ end
130
89
  end
131
90
  end
132
91
 
133
- # Print summary
134
- puts "\n" + "="*60
135
- puts "🏁 TEST MATRIX SUMMARY"
136
- puts "="*60
137
- results.each do |db, status|
138
- puts "#{status} #{db}"
139
- end
140
- puts "="*60
92
+ puts "\n" + "=" * 80
93
+ puts "🏁 Local Test Matrix Results"
94
+ puts "(CI automatically tests SQLite3 + PostgreSQL only)"
95
+ puts "=" * 80
141
96
 
142
- # Fail if any tests failed
143
- failed_count = results.values.count { |status| status.include?("FAILED") }
144
- if failed_count > 0
145
- puts "\n❌ #{failed_count} database(s) failed tests"
146
- exit 1
97
+ if failed_combinations.empty?
98
+ puts "✅ All combinations passed!"
147
99
  else
148
- puts "\n🎉 All databases passed!"
149
- end
150
- end
151
-
152
- desc "Run full test matrix (SQLite + PostgreSQL + MySQL)"
153
- task :matrix_full do
154
- puts "\n🧪 Running full test matrix...\n"
155
-
156
- databases = [
157
- { name: "SQLite", env: {}, emoji: "🗂️" },
158
- { name: "PostgreSQL", env: { "DATABASE_ADAPTER" => "postgresql", "FORCE_DB_CONFIG" => "true" }, emoji: "🐘" },
159
- { name: "MySQL", env: { "DATABASE_ADAPTER" => "mysql2", "FORCE_DB_CONFIG" => "true", "PARALLEL_WORKERS" => "1" }, emoji: "🐬" }
160
- ]
161
-
162
- results = {}
163
-
164
- databases.each do |db|
165
- puts "\n" + "="*60
166
- puts "#{db[:emoji]} Testing with #{db[:name]}..."
167
- puts "="*60
168
-
169
- begin
170
- env = ENV.to_h.merge(db[:env])
171
- success = system(env, "rails test:all")
172
-
173
- results[db[:name]] = success ? "✅ PASSED" : "❌ FAILED"
174
- rescue => e
175
- results[db[:name]] = "❌ FAILED"
176
- puts "Error: #{e.message}"
177
- end
178
- end
179
-
180
- # Print summary
181
- puts "\n" + "="*60
182
- puts "🏁 FULL TEST MATRIX SUMMARY"
183
- puts "="*60
184
- results.each do |db, status|
185
- puts "#{status} #{db}"
186
- end
187
- puts "="*60
188
-
189
- # Fail if any tests failed
190
- failed_count = results.values.count { |status| status.include?("FAILED") }
191
- if failed_count > 0
192
- puts "\n❌ #{failed_count} database(s) failed tests"
100
+ puts " Failed combinations:"
101
+ failed_combinations.each { |combo| puts " - #{combo}" }
193
102
  exit 1
194
- else
195
- puts "\n🎉 All databases passed!"
196
103
  end
197
104
  end
198
105
  end
199
106
 
200
- # Helper methods
201
- def mysql_available?
202
- system("mysql --version > /dev/null 2>&1")
203
- end
204
-
205
- def postgresql_available?
206
- system("psql --version > /dev/null 2>&1")
207
- end
107
+ # Override default test task
108
+ desc "Run all tests"
109
+ task test: "test:all"
@@ -80,18 +80,6 @@ a:hover {
80
80
  position:absolute;
81
81
  top:0
82
82
  }
83
- .bar.db {
84
- background-color:#92c282
85
- }
86
- .bar.app {
87
- background-color:#00adc4
88
- }
89
- .bar.gc {
90
- background-color:#323333
91
- }
92
- .bar.view {
93
- background-color:#b48da3
94
- }
95
83
  .bar:first-child {
96
84
  border-bottom-left-radius:1px;
97
85
  border-top-left-radius:1px
@@ -16,14 +16,17 @@ module ChartTableConcern
16
16
  def setup_chart_and_table_data
17
17
  ransack_params = params[:q] || {}
18
18
 
19
- # Setup chart data first using original time range (no sorting from table)
20
19
  unless turbo_frame_request?
21
- setup_chart_formatters
20
+ # Setup chart data first using original time range (no sorting from table)
22
21
  setup_chart_data(ransack_params)
22
+ setup_chart_formatters
23
23
  end
24
24
 
25
25
  # Setup table data using zoom parameters if present, otherwise use chart parameters
26
26
  setup_table_data(ransack_params)
27
+
28
+ # Set flag to determine if we have meaningful data to display
29
+ @has_data = has_meaningful_data?
27
30
  end
28
31
 
29
32
  def setup_chart_data(ransack_params)
@@ -31,7 +34,10 @@ module ChartTableConcern
31
34
  chart_ransack_query = chart_model.ransack(chart_ransack_params)
32
35
  @chart_data = chart_class.new(
33
36
  ransack_query: chart_ransack_query,
34
- group_by: group_by,
37
+ period_type: period_type,
38
+ start_time: @start_time,
39
+ end_time: @end_time,
40
+ start_duration: @start_duration,
35
41
  **chart_options
36
42
  ).to_rails_chart
37
43
  end
@@ -43,6 +49,7 @@ module ChartTableConcern
43
49
 
44
50
  table_results = build_table_results
45
51
  handle_pagination
52
+
46
53
  @pagy, @table_data = pagy(table_results, limit: session_pagination_limit)
47
54
  end
48
55
 
@@ -56,14 +63,24 @@ module ChartTableConcern
56
63
  end
57
64
 
58
65
  def setup_chart_formatters
59
- @xaxis_formatter = RailsPulse::ChartFormatters.occurred_at_as_time_or_date(@time_diff_hours)
66
+ @xaxis_formatter = RailsPulse::ChartFormatters.period_as_time_or_date(@time_diff_hours)
60
67
  @tooltip_formatter = RailsPulse::ChartFormatters.tooltip_as_time_or_date_with_marker(@time_diff_hours)
61
68
  end
62
69
 
70
+ def period_type
71
+ @time_diff_hours <= 25 ? :hour : :day
72
+ end
73
+
63
74
  def group_by
64
75
  @time_diff_hours <= 25 ? :group_by_hour : :group_by_day
65
76
  end
66
77
 
78
+ def has_meaningful_data?
79
+ has_chart_data = @chart_data && @chart_data.values.any? { |v| v > 0 }
80
+ has_table_data = @table_data && @table_data.any?
81
+ has_chart_data || has_table_data
82
+ end
83
+
67
84
  def handle_pagination
68
85
  method = pagination_method
69
86
  send(method, params[:limit]) if params[:limit].present?
@@ -5,10 +5,13 @@ module ResponseRangeConcern
5
5
  ransack_params = params[:q] || {}
6
6
  thresholds = RailsPulse.configuration.public_send("#{type}_thresholds")
7
7
 
8
- if ransack_params[:duration].present?
9
- selected_range = ransack_params[:duration]
8
+ # Check both avg_duration (for Summary) and duration (for Request/Operation)
9
+ duration_param = ransack_params[:avg_duration] || ransack_params[:duration]
10
+
11
+ if duration_param.present?
12
+ selected_range = duration_param
10
13
  start_duration =
11
- case ransack_params[:duration].to_sym
14
+ case duration_param.to_sym
12
15
  when :slow then thresholds[:slow]
13
16
  when :very_slow then thresholds[:very_slow]
14
17
  when :critical then thresholds[:critical]
@@ -6,8 +6,7 @@ module TimeRangeConcern
6
6
  const_set(:TIME_RANGE_OPTIONS, [
7
7
  [ "Last 24 hours", :last_day ],
8
8
  [ "Last Week", :last_week ],
9
- [ "Last Month", :last_month ],
10
- [ "All Time", :all_time ]
9
+ [ "Last Month", :last_month ]
11
10
  ].freeze)
12
11
  end
13
12
 
@@ -18,23 +17,19 @@ module TimeRangeConcern
18
17
 
19
18
  ransack_params = params[:q] || {}
20
19
 
21
- if ransack_params[:requests_occurred_at_gteq].present?
22
- # Custom time range from routes index chart zoom which filters requests through an association
23
- start_time = parse_time_param(ransack_params[:requests_occurred_at_gteq])
24
- end_time = parse_time_param(ransack_params[:requests_occurred_at_lt])
25
- elsif ransack_params[:occurred_at_gteq].present?
20
+ if ransack_params[:occurred_at_gteq].present?
26
21
  # Custom time range from chart zoom where there is no association
27
22
  start_time = parse_time_param(ransack_params[:occurred_at_gteq])
28
23
  end_time = parse_time_param(ransack_params[:occurred_at_lt])
29
- elsif ransack_params[:occurred_at_range]
24
+ elsif ransack_params[:period_start_range]
30
25
  # Predefined time range from dropdown
31
- selected_time_range = ransack_params[:occurred_at_range]
26
+ selected_time_range = ransack_params[:period_start_range]
32
27
  start_time =
33
28
  case selected_time_range.to_sym
34
29
  when :last_day then 1.day.ago
35
30
  when :last_week then 1.week.ago
36
31
  when :last_month then 1.month.ago
37
- when :all_time then 100.years.ago
32
+ else 1.day.ago # Default fallback
38
33
  end
39
34
  end
40
35
 
@@ -35,6 +35,6 @@ module ZoomRangeConcern
35
35
  end_time = end_time_obj&.end_of_day || end_time_obj
36
36
  end
37
37
 
38
- [ start_time, end_time ]
38
+ [ start_time.to_i, end_time.to_i ]
39
39
  end
40
40
  end
@@ -2,8 +2,9 @@ module RailsPulse
2
2
  class ApplicationController < ActionController::Base
3
3
  before_action :authenticate_rails_pulse_user!
4
4
 
5
- def set_pagination_limit
6
- session[:pagination_limit] = params[:limit].to_i if params[:limit].present?
5
+ def set_pagination_limit(limit = nil)
6
+ limit = limit || params[:limit]
7
+ session[:pagination_limit] = limit.to_i if limit.present?
7
8
  render json: { status: "ok" }
8
9
  end
9
10
 
@@ -54,8 +55,11 @@ module RailsPulse
54
55
  end
55
56
 
56
57
  def session_pagination_limit
57
- # Keep default small for optimal performance
58
- session[:pagination_limit] || 10
58
+ # Use URL param if present, otherwise session, otherwise default
59
+ limit = params[:limit].presence || session[:pagination_limit] || 10
60
+ # Update session if URL param was used
61
+ session[:pagination_limit] = limit.to_i if params[:limit].present?
62
+ limit.to_i
59
63
  end
60
64
 
61
65
  def store_pagination_limit(limit)
@@ -1,6 +1,18 @@
1
1
  module RailsPulse
2
2
  class DashboardController < ApplicationController
3
3
  def index
4
+ @average_query_times_metric_card = RailsPulse::Routes::Cards::AverageResponseTimes.new(route: nil).to_metric_card
5
+ @percentile_response_times_metric_card = RailsPulse::Routes::Cards::PercentileResponseTimes.new(route: nil).to_metric_card
6
+ @request_count_totals_metric_card = RailsPulse::Routes::Cards::RequestCountTotals.new(route: nil).to_metric_card
7
+ @error_rate_per_route_metric_card = RailsPulse::Routes::Cards::ErrorRatePerRoute.new(route: nil).to_metric_card
8
+
9
+ # Generate chart data for inline rendering
10
+ @average_response_time_chart_data = RailsPulse::Dashboard::Charts::AverageResponseTime.new.to_chart_data
11
+ @p95_response_time_chart_data = RailsPulse::Dashboard::Charts::P95ResponseTime.new.to_chart_data
12
+
13
+ # Generate table data for inline rendering
14
+ @slow_routes_table_data = RailsPulse::Dashboard::Tables::SlowRoutes.new.to_table_data
15
+ @slow_queries_table_data = RailsPulse::Dashboard::Tables::SlowQueries.new.to_table_data
4
16
  end
5
17
  end
6
18
  end
@@ -5,21 +5,23 @@ module RailsPulse
5
5
  before_action :set_query, only: :show
6
6
 
7
7
  def index
8
+ setup_metric_cards
8
9
  setup_chart_and_table_data
9
10
  end
10
11
 
11
12
  def show
13
+ setup_metric_cards
12
14
  setup_chart_and_table_data
13
15
  end
14
16
 
15
17
  private
16
18
 
17
19
  def chart_model
18
- show_action? ? Operation : Query
20
+ Summary
19
21
  end
20
22
 
21
23
  def table_model
22
- show_action? ? Operation : Query
24
+ show_action? ? Operation : Summary
23
25
  end
24
26
 
25
27
  def chart_class
@@ -31,73 +33,67 @@ module RailsPulse
31
33
  end
32
34
 
33
35
  def build_chart_ransack_params(ransack_params)
34
- base_params = ransack_params.except(:s)
36
+ base_params = ransack_params.except(:s).merge(
37
+ period_start_gteq: Time.at(@start_time),
38
+ period_start_lt: Time.at(@end_time)
39
+ )
40
+
41
+ # Only add duration filter if we have a meaningful threshold
42
+ base_params[:avg_duration_gteq] = @start_duration if @start_duration && @start_duration > 0
35
43
 
36
44
  if show_action?
37
- base_params.merge(
38
- query_id_eq: @query.id,
39
- occurred_at_gteq: @start_time,
40
- occurred_at_lt: @end_time,
41
- duration_gteq: @start_duration
42
- )
45
+ base_params.merge(summarizable_id_eq: @query.id)
43
46
  else
44
- base_params.merge(
45
- operations_occurred_at_gteq: @start_time,
46
- operations_occurred_at_lt: @end_time,
47
- operations_duration_gteq: @start_duration
48
- )
47
+ base_params
49
48
  end
50
49
  end
51
50
 
52
51
  def build_table_ransack_params(ransack_params)
53
52
  if show_action?
54
- ransack_params.merge(
55
- query_id_eq: @query.id,
56
- occurred_at_gteq: @table_start_time,
57
- occurred_at_lt: @table_end_time,
58
- duration_gteq: @start_duration
53
+ # For Operation model on show page
54
+ params = ransack_params.merge(
55
+ occurred_at_gteq: Time.at(@table_start_time),
56
+ occurred_at_lt: Time.at(@table_end_time),
57
+ query_id_eq: @query.id
59
58
  )
59
+ params[:duration_gteq] = @start_duration if @start_duration && @start_duration > 0
60
+ params
60
61
  else
61
- ransack_params.merge(
62
- operations_occurred_at_gteq: @table_start_time,
63
- operations_occurred_at_lt: @table_end_time,
64
- operations_duration_gteq: @start_duration
62
+ # For Summary model on index page
63
+ params = ransack_params.merge(
64
+ period_start_gteq: Time.at(@table_start_time),
65
+ period_start_lt: Time.at(@table_end_time)
65
66
  )
67
+ params[:avg_duration_gteq] = @start_duration if @start_duration && @start_duration > 0
68
+ params
66
69
  end
67
70
  end
68
71
 
69
72
  def default_table_sort
70
- "occurred_at desc"
73
+ show_action? ? "occurred_at desc" : "period_start desc"
71
74
  end
72
75
 
73
76
  def build_table_results
74
77
  if show_action?
75
- @ransack_query.result.select("id", "occurred_at", "duration")
78
+ @ransack_query.result
76
79
  else
77
- # Optimized query: Use INNER JOIN since we only want queries with operations in time range
78
- # This dramatically reduces the dataset before aggregation
79
- @ransack_query.result(distinct: false)
80
- .joins("INNER JOIN rails_pulse_operations ON rails_pulse_operations.query_id = rails_pulse_queries.id")
81
- .where("rails_pulse_operations.occurred_at >= ? AND rails_pulse_operations.occurred_at < ?",
82
- @table_start_time, @table_end_time)
83
- .group("rails_pulse_queries.id, rails_pulse_queries.normalized_sql, rails_pulse_queries.created_at, rails_pulse_queries.updated_at")
84
- .select(
85
- "rails_pulse_queries.*",
86
- optimized_aggregations_sql
87
- )
80
+ Queries::Tables::Index.new(
81
+ ransack_query: @ransack_query,
82
+ period_type: period_type,
83
+ start_time: @start_time,
84
+ params: params
85
+ ).to_table
88
86
  end
89
87
  end
90
88
 
91
89
  private
92
90
 
93
- def optimized_aggregations_sql
94
- # Efficient aggregations that work with our composite indexes
95
- [
96
- "COALESCE(AVG(rails_pulse_operations.duration), 0) AS average_query_time_ms",
97
- "COUNT(rails_pulse_operations.id) AS execution_count",
98
- "COALESCE(SUM(rails_pulse_operations.duration), 0) AS total_time_consumed",
99
- "MAX(rails_pulse_operations.occurred_at) AS occurred_at"
100
- ].join(", ")
91
+ def setup_metric_cards
92
+ return if turbo_frame_request?
93
+
94
+ @average_query_times_metric_card = RailsPulse::Queries::Cards::AverageQueryTimes.new(query: @query).to_metric_card
95
+ @percentile_query_times_metric_card = RailsPulse::Queries::Cards::PercentileQueryTimes.new(query: @query).to_metric_card
96
+ @execution_rate_metric_card = RailsPulse::Queries::Cards::ExecutionRate.new(query: @query).to_metric_card
101
97
  end
102
98
 
103
99
  def show_action?
@@ -108,14 +104,33 @@ module RailsPulse
108
104
  show_action? ? :set_pagination_limit : :store_pagination_limit
109
105
  end
110
106
 
111
- def set_query
112
- @query = Query.find(params[:id])
107
+ def setup_table_data(ransack_params)
108
+ table_ransack_params = build_table_ransack_params(ransack_params)
109
+ @ransack_query = table_model.ransack(table_ransack_params)
110
+
111
+ # Only apply default sort if not using Queries::Tables::Index (which handles its own sorting)
112
+ if show_action?
113
+ @ransack_query.sorts = default_table_sort if @ransack_query.sorts.empty?
114
+ end
115
+
116
+ table_results = build_table_results
117
+ handle_pagination
118
+
119
+ @pagy, @table_data = pagy(table_results, limit: session_pagination_limit)
120
+ end
121
+
122
+ def handle_pagination
123
+ method = pagination_method
124
+ send(method, params[:limit]) if params[:limit].present?
113
125
  end
114
126
 
115
- def setup_metic_cards
116
- @average_query_times_card = Queries::Cards::AverageQueryTimes.new(query: @query).to_metric_card
117
- @percentile_response_times_card = Queries::Cards::PercentileQueryTimes.new(query: @query).to_metric_card
118
- @execution_rate_card = Queries::Cards::ExecutionRate.new(query: @query).to_metric_card
127
+ def setup_time_and_response_ranges
128
+ @start_time, @end_time, @selected_time_range, @time_diff_hours = setup_time_range
129
+ @start_duration, @selected_response_range = setup_duration_range(:query)
130
+ end
131
+
132
+ def set_query
133
+ @query = Query.find(params[:id])
119
134
  end
120
135
  end
121
136
  end