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
@@ -7,39 +7,61 @@ module RailsPulse
7
7
  end
8
8
 
9
9
  def to_metric_card
10
- requests = if @route
11
- RailsPulse::Request.where(route: @route)
12
- else
13
- RailsPulse::Request.all
14
- end
10
+ last_7_days = 7.days.ago.beginning_of_day
11
+ previous_7_days = 14.days.ago.beginning_of_day
15
12
 
16
- requests = requests.where("occurred_at >= ?", 2.weeks.ago.beginning_of_day)
13
+ # Single query to get all aggregated metrics with conditional sums
14
+ base_query = RailsPulse::Summary.where(
15
+ summarizable_type: "RailsPulse::Route",
16
+ period_type: "day",
17
+ period_start: 2.weeks.ago.beginning_of_day..Time.current
18
+ )
19
+ base_query = base_query.where(summarizable_id: @route.id) if @route
17
20
 
18
- # Calculate overall average response time
19
- average_response_time = requests.average(:duration)&.round(0) || 0
21
+ metrics = base_query.select(
22
+ "SUM(avg_duration * count) AS total_weighted_duration",
23
+ "SUM(count) AS total_requests",
24
+ "SUM(CASE WHEN period_start >= '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN avg_duration * count ELSE 0 END) AS current_weighted_duration",
25
+ "SUM(CASE WHEN period_start >= '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN count ELSE 0 END) AS current_requests",
26
+ "SUM(CASE WHEN period_start >= '#{previous_7_days.strftime('%Y-%m-%d %H:%M:%S')}' AND period_start < '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN avg_duration * count ELSE 0 END) AS previous_weighted_duration",
27
+ "SUM(CASE WHEN period_start >= '#{previous_7_days.strftime('%Y-%m-%d %H:%M:%S')}' AND period_start < '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN count ELSE 0 END) AS previous_requests"
28
+ ).take
20
29
 
21
- # Calculate trend by comparing last 7 days vs previous 7 days
22
- last_7_days = 7.days.ago.beginning_of_day
23
- previous_7_days = 14.days.ago.beginning_of_day
24
- current_period_avg = requests.where("occurred_at >= ?", last_7_days).average(:duration) || 0
25
- previous_period_avg = requests.where("occurred_at >= ? AND occurred_at < ?", previous_7_days, last_7_days).average(:duration) || 0
30
+ # Calculate metrics from single query result
31
+ average_response_time = metrics.total_requests.to_i > 0 ? (metrics.total_weighted_duration / metrics.total_requests).round(0) : 0
32
+ current_period_avg = metrics.current_requests.to_i > 0 ? (metrics.current_weighted_duration / metrics.current_requests) : 0
33
+ previous_period_avg = metrics.previous_requests.to_i > 0 ? (metrics.previous_weighted_duration / metrics.previous_requests) : 0
26
34
 
27
- percentage = previous_period_avg.zero? ? 0 : ((previous_period_avg - current_period_avg) / previous_period_avg * 100).abs.round(1)
28
- trend_icon = percentage < 0.1 ? "move-right" : current_period_avg < previous_period_avg ? "trending-down" : "trending-up"
35
+ percentage = previous_period_avg.zero? ? 0 : ((previous_period_avg - current_period_avg) / previous_period_avg * 100).abs.round(1)
36
+ trend_icon = percentage < 0.1 ? "move-right" : current_period_avg < previous_period_avg ? "trending-down" : "trending-up"
29
37
  trend_amount = previous_period_avg.zero? ? "0%" : "#{percentage}%"
30
38
 
31
- sparkline_data = requests
32
- .group_by_week(:occurred_at, time_zone: "UTC")
33
- .average(:duration)
34
- .each_with_object({}) do |(date, avg), hash|
35
- formatted_date = date.strftime("%b %-d")
36
- value = avg&.round(0) || 0
37
- hash[formatted_date] = {
38
- value: value
39
+ # Separate query for sparkline data - manually calculate weighted averages by week
40
+ sparkline_data = {}
41
+ base_query.each do |summary|
42
+ week_start = summary.period_start.beginning_of_week
43
+ formatted_date = week_start.strftime("%b %-d")
44
+
45
+ if sparkline_data[formatted_date]
46
+ sparkline_data[formatted_date][:total_weighted] += (summary.avg_duration || 0) * (summary.count || 0)
47
+ sparkline_data[formatted_date][:total_count] += (summary.count || 0)
48
+ else
49
+ sparkline_data[formatted_date] = {
50
+ total_weighted: (summary.avg_duration || 0) * (summary.count || 0),
51
+ total_count: (summary.count || 0)
39
52
  }
40
53
  end
54
+ end
55
+
56
+ # Convert to final format
57
+ sparkline_data = sparkline_data.transform_values do |data|
58
+ weighted_avg = data[:total_count] > 0 ? (data[:total_weighted] / data[:total_count]).round(0) : 0
59
+ { value: weighted_avg }
60
+ end
41
61
 
42
62
  {
63
+ id: "average_response_times",
64
+ context: "routes",
43
65
  title: "Average Response Time",
44
66
  summary: "#{average_response_time} ms",
45
67
  line_chart_data: sparkline_data,
@@ -7,60 +7,53 @@ module RailsPulse
7
7
  end
8
8
 
9
9
  def to_metric_card
10
- # Calculate error rate for each route or a specific route
11
- routes = if @route
12
- RailsPulse::Route.where(id: @route)
13
- else
14
- RailsPulse::Route.all
15
- end
10
+ last_7_days = 7.days.ago.beginning_of_day
11
+ previous_7_days = 14.days.ago.beginning_of_day
16
12
 
17
- routes = routes.where("occurred_at >= ?", 2.weeks.ago.beginning_of_day)
13
+ # Single query to get all error metrics with conditional aggregation
14
+ base_query = RailsPulse::Summary.where(
15
+ summarizable_type: "RailsPulse::Route",
16
+ period_type: "day",
17
+ period_start: 2.weeks.ago.beginning_of_day..Time.current
18
+ )
19
+ base_query = base_query.where(summarizable_id: @route.id) if @route
18
20
 
19
- error_rates = routes.joins(:requests)
20
- .select("rails_pulse_routes.id, rails_pulse_routes.path, COUNT(rails_pulse_requests.id) as total_requests, SUM(CASE WHEN rails_pulse_requests.is_error = true THEN 1 ELSE 0 END) as error_count")
21
- .group("rails_pulse_routes.id, rails_pulse_routes.path")
22
- .map do |route|
23
- error_rate = route.error_count.to_f / route.total_requests * 100
24
- {
25
- path: route.path,
26
- error_rate: error_rate.round(2)
27
- }
28
- end
21
+ metrics = base_query.select(
22
+ "SUM(error_count) AS total_errors",
23
+ "SUM(count) AS total_requests",
24
+ "SUM(CASE WHEN period_start >= '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN error_count ELSE 0 END) AS current_errors",
25
+ "SUM(CASE WHEN period_start >= '#{previous_7_days.strftime('%Y-%m-%d %H:%M:%S')}' AND period_start < '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN error_count ELSE 0 END) AS previous_errors"
26
+ ).take
29
27
 
30
- # Calculate overall error rate summary as errors per day
31
- requests = @route ? RailsPulse::Request.where(route: @route) : RailsPulse::Request.all
32
- total_errors = requests.where(is_error: true).count
33
- min_time = requests.minimum(:occurred_at)
34
- max_time = requests.maximum(:occurred_at)
35
- total_days = min_time && max_time && min_time != max_time ? (max_time - min_time) / 1.day : 1
36
- errors_per_day = total_errors / total_days
37
- error_rate_summary = "#{errors_per_day.round(2)} / day"
28
+ # Calculate metrics from single query result
29
+ total_errors = metrics.total_errors || 0
30
+ total_requests = metrics.total_requests || 0
31
+ current_period_errors = metrics.current_errors || 0
32
+ previous_period_errors = metrics.previous_errors || 0
38
33
 
39
- # Generate sparkline data
40
- sparkline_data = requests
41
- .where(is_error: true)
42
- .group_by_week(:occurred_at, time_zone: "UTC")
43
- .count
44
- .each_with_object({}) do |(date, count), hash|
45
- formatted_date = date.strftime("%b %-d")
46
- hash[formatted_date] = {
47
- value: count
48
- }
49
- end
34
+ # Calculate overall error rate percentage
35
+ overall_error_rate = total_requests > 0 ? (total_errors.to_f / total_requests * 100).round(2) : 0
50
36
 
51
- # Determine trend direction and amount
52
- last_7_days = 7.days.ago.beginning_of_day
53
- previous_7_days = 14.days.ago.beginning_of_day
54
- current_period_errors = requests.where("occurred_at >= ? AND is_error = ?", last_7_days, true).count
55
- previous_period_errors = requests.where("occurred_at >= ? AND occurred_at < ? AND is_error = ?", previous_7_days, last_7_days, true).count
37
+ # Calculate trend
38
+ percentage = previous_period_errors.zero? ? 0 : ((previous_period_errors - current_period_errors) / previous_period_errors.to_f * 100).abs.round(1)
39
+ trend_icon = percentage < 0.1 ? "move-right" : current_period_errors < previous_period_errors ? "trending-down" : "trending-up"
40
+ trend_amount = previous_period_errors.zero? ? "0%" : "#{percentage}%"
56
41
 
57
- trend_amount = previous_period_errors.zero? ? "0%" : "#{((current_period_errors - previous_period_errors) / previous_period_errors.to_f * 100).round(1)}%"
58
- trend_icon = trend_amount.to_f < 0.1 ? "move-right" : current_period_errors < previous_period_errors ? "trending-down" : "trending-up"
42
+ # Separate query for sparkline data - group by week using Rails
43
+ sparkline_data = base_query
44
+ .group_by_week(:period_start, time_zone: "UTC")
45
+ .sum(:error_count)
46
+ .each_with_object({}) do |(week_start, total_errors), hash|
47
+ formatted_date = week_start.strftime("%b %-d")
48
+ value = total_errors || 0
49
+ hash[formatted_date] = { value: value }
50
+ end
59
51
 
60
52
  {
53
+ id: "error_rate_per_route",
54
+ context: "routes",
61
55
  title: "Error Rate Per Route",
62
- data: error_rates,
63
- summary: error_rate_summary,
56
+ summary: "#{overall_error_rate}%",
64
57
  line_chart_data: sparkline_data,
65
58
  trend_icon: trend_icon,
66
59
  trend_amount: trend_amount,
@@ -7,60 +7,47 @@ module RailsPulse
7
7
  end
8
8
 
9
9
  def to_metric_card
10
- requests = if @route
11
- RailsPulse::Request.where(route: @route)
12
- else
13
- RailsPulse::Request.all
14
- end
15
-
16
- requests = requests.where("occurred_at >= ?", 2.weeks.ago.beginning_of_day)
17
-
18
- # Calculate overall 95th percentile response time
19
- count = requests.count
20
- percentile_95th = if count > 0
21
- requests.select("duration").order("duration").limit(1).offset((count * 0.95).floor).pluck(:duration).first.round(0) || 0
22
- else
23
- 0
24
- end
25
-
26
- # Calculate trend by comparing last 7 days vs previous 7 days for 95th percentile
27
10
  last_7_days = 7.days.ago.beginning_of_day
28
11
  previous_7_days = 14.days.ago.beginning_of_day
29
12
 
30
- current_period = requests.where("occurred_at >= ?", last_7_days)
31
- current_count = current_period.count
32
- current_period_95th = if current_count > 0
33
- current_period.select("duration").order("duration").limit(1).offset((current_count * 0.95).floor).pluck(:duration).first || 0
34
- else
35
- 0
36
- end
37
-
38
- previous_period = requests.where("occurred_at >= ? AND occurred_at < ?", previous_7_days, last_7_days)
39
- previous_count = previous_period.count
40
- previous_period_95th = if previous_count > 0
41
- previous_period.select("duration").order("duration").limit(1).offset((previous_count * 0.95).floor).pluck(:duration).first || 0
42
- else
43
- 0
44
- end
45
-
46
- percentage = previous_period_95th.zero? ? 0 : ((previous_period_95th - current_period_95th) / previous_period_95th * 100).abs.round(1)
47
- trend_icon = percentage < 0.1 ? "move-right" : current_period_95th < previous_period_95th ? "trending-down" : "trending-up"
48
- trend_amount = previous_period_95th.zero? ? "0%" : "#{percentage}%"
49
-
50
- sparkline_data = requests
51
- .group_by_week(:occurred_at, time_zone: "UTC")
52
- .average(:duration)
53
- .each_with_object({}) do |(date, avg), hash|
54
- formatted_date = date.strftime("%b %-d")
55
- value = avg&.round(0) || 0
56
- hash[formatted_date] = {
57
- value: value
58
- }
13
+ # Single query to get all P95 metrics with conditional aggregation
14
+ base_query = RailsPulse::Summary.where(
15
+ summarizable_type: "RailsPulse::Route",
16
+ period_type: "day",
17
+ period_start: 2.weeks.ago.beginning_of_day..Time.current
18
+ )
19
+ base_query = base_query.where(summarizable_id: @route.id) if @route
20
+
21
+ metrics = base_query.select(
22
+ "AVG(p95_duration) AS overall_p95",
23
+ "AVG(CASE WHEN period_start >= '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN p95_duration ELSE NULL END) AS current_p95",
24
+ "AVG(CASE WHEN period_start >= '#{previous_7_days.strftime('%Y-%m-%d %H:%M:%S')}' AND period_start < '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN p95_duration ELSE NULL END) AS previous_p95"
25
+ ).take
26
+
27
+ # Calculate metrics from single query result
28
+ p95_response_time = (metrics.overall_p95 || 0).round(0)
29
+ current_period_p95 = metrics.current_p95 || 0
30
+ previous_period_p95 = metrics.previous_p95 || 0
31
+
32
+ percentage = previous_period_p95.zero? ? 0 : ((previous_period_p95 - current_period_p95) / previous_period_p95 * 100).abs.round(1)
33
+ trend_icon = percentage < 0.1 ? "move-right" : current_period_p95 < previous_period_p95 ? "trending-down" : "trending-up"
34
+ trend_amount = previous_period_p95.zero? ? "0%" : "#{percentage}%"
35
+
36
+ # Separate query for sparkline data - group by week using Rails
37
+ sparkline_data = base_query
38
+ .group_by_week(:period_start, time_zone: "UTC")
39
+ .average(:p95_duration)
40
+ .each_with_object({}) do |(week_start, avg_p95), hash|
41
+ formatted_date = week_start.strftime("%b %-d")
42
+ value = (avg_p95 || 0).round(0)
43
+ hash[formatted_date] = { value: value }
59
44
  end
60
45
 
61
46
  {
47
+ id: "percentile_response_times",
48
+ context: "routes",
62
49
  title: "95th Percentile Response Time",
63
- summary: "#{percentile_95th} ms",
50
+ summary: "#{p95_response_time} ms",
64
51
  line_chart_data: sparkline_data,
65
52
  trend_icon: trend_icon,
66
53
  trend_amount: trend_amount,
@@ -7,44 +7,49 @@ module RailsPulse
7
7
  end
8
8
 
9
9
  def to_metric_card
10
- requests = if @route
11
- RailsPulse::Request.where(route: @route)
12
- else
13
- RailsPulse::Request.all
14
- end
10
+ last_7_days = 7.days.ago.beginning_of_day
11
+ previous_7_days = 14.days.ago.beginning_of_day
15
12
 
16
- requests = requests.where("occurred_at >= ?", 2.weeks.ago.beginning_of_day)
13
+ # Single query to get all count metrics with conditional aggregation
14
+ base_query = RailsPulse::Summary.where(
15
+ summarizable_type: "RailsPulse::Route",
16
+ period_type: "day",
17
+ period_start: 2.weeks.ago.beginning_of_day..Time.current
18
+ )
19
+ base_query = base_query.where(summarizable_id: @route.id) if @route
17
20
 
18
- # Calculate total request count
19
- total_request_count = requests.count
21
+ metrics = base_query.select(
22
+ "SUM(count) AS total_count",
23
+ "SUM(CASE WHEN period_start >= '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN count ELSE 0 END) AS current_count",
24
+ "SUM(CASE WHEN period_start >= '#{previous_7_days.strftime('%Y-%m-%d %H:%M:%S')}' AND period_start < '#{last_7_days.strftime('%Y-%m-%d %H:%M:%S')}' THEN count ELSE 0 END) AS previous_count"
25
+ ).take
20
26
 
21
- # Calculate trend by comparing last 7 days vs previous 7 days
22
- last_7_days = 7.days.ago.beginning_of_day
23
- previous_7_days = 14.days.ago.beginning_of_day
24
- current_period_count = requests.where("occurred_at >= ?", last_7_days).count
25
- previous_period_count = requests.where("occurred_at >= ? AND occurred_at < ?", previous_7_days, last_7_days).count
27
+ # Calculate metrics from single query result
28
+ total_request_count = metrics.total_count || 0
29
+ current_period_count = metrics.current_count || 0
30
+ previous_period_count = metrics.previous_count || 0
26
31
 
27
32
  percentage = previous_period_count.zero? ? 0 : ((previous_period_count - current_period_count) / previous_period_count.to_f * 100).abs.round(1)
28
33
  trend_icon = percentage < 0.1 ? "move-right" : current_period_count < previous_period_count ? "trending-down" : "trending-up"
29
34
  trend_amount = previous_period_count.zero? ? "0%" : "#{percentage}%"
30
35
 
31
- sparkline_data = requests
32
- .group_by_week(:occurred_at, time_zone: "UTC")
33
- .count
34
- .each_with_object({}) do |(date, count), hash|
35
- formatted_date = date.strftime("%b %-d")
36
- hash[formatted_date] = {
37
- value: count
38
- }
36
+ # Separate query for sparkline data - group by week using Rails
37
+ sparkline_data = base_query
38
+ .group_by_week(:period_start, time_zone: "UTC")
39
+ .sum(:count)
40
+ .each_with_object({}) do |(week_start, total_count), hash|
41
+ formatted_date = week_start.strftime("%b %-d")
42
+ value = total_count || 0
43
+ hash[formatted_date] = { value: value }
39
44
  end
40
45
 
41
- # Calculate average requests per minute
42
- min_time = requests.minimum(:occurred_at)
43
- max_time = requests.maximum(:occurred_at)
44
- total_minutes = min_time && max_time && min_time != max_time ? (max_time - min_time) / 60.0 : 1
46
+ # Calculate average requests per minute over 2-week period
47
+ total_minutes = 2.weeks / 1.minute
45
48
  average_requests_per_minute = total_request_count / total_minutes
46
49
 
47
50
  {
51
+ id: "request_count_totals",
52
+ context: "routes",
48
53
  title: "Request Count Total",
49
54
  summary: "#{average_requests_per_minute.round(2)} / min",
50
55
  line_chart_data: sparkline_data,
@@ -2,112 +2,35 @@ module RailsPulse
2
2
  module Routes
3
3
  module Charts
4
4
  class AverageResponseTimes
5
- def initialize(ransack_query:, group_by: :group_by_day, route: nil)
5
+ def initialize(ransack_query:, period_type: nil, route: nil, start_time: nil, end_time: nil, start_duration: nil)
6
6
  @ransack_query = ransack_query
7
- @group_by = group_by
7
+ @period_type = period_type
8
8
  @route = route
9
+ @start_time = start_time
10
+ @end_time = end_time
11
+ @start_duration = start_duration
9
12
  end
10
13
 
11
14
  def to_rails_chart
12
- # Get actual data using existing logic
13
- actual_data = if @route
14
- # These are the requests for the specific route so it will just be a collection of Requests that we can
15
- # filter and sort using the attributes on each Request
16
- @ransack_query.result(distinct: false)
17
- .public_send(@group_by, "occurred_at", series: true, time_zone: "UTC")
18
- .average(:duration)
19
- else
20
- # Use the existing query structure with left_joins from ransack
21
- @ransack_query.result(distinct: false)
22
- .left_joins(:requests)
23
- .public_send(
24
- @group_by,
25
- "rails_pulse_requests.occurred_at",
26
- series: true,
27
- time_zone: "UTC"
28
- )
29
- .average("rails_pulse_requests.duration")
30
- end
31
-
32
- # Create full time range and fill in missing periods
33
- fill_missing_periods(actual_data)
34
- end
35
-
36
- private
37
-
38
- def fill_missing_periods(actual_data)
39
- # Extract actual time range from ransack query conditions
40
- start_time, end_time = extract_time_range_from_ransack
41
-
42
- # Create time range based on grouping type
43
- case @group_by
44
- when :group_by_hour
45
- time_range = generate_hour_range(start_time, end_time)
46
- else # :group_by_day
47
- time_range = generate_day_range(start_time, end_time)
48
- end
49
-
50
- # Fill in all periods with zero values for missing periods
51
- time_range.each_with_object({}) do |period, result|
52
- occurred_at = period.is_a?(String) ? Time.parse(period) : period
53
- occurred_at = (occurred_at.is_a?(Time) || occurred_at.is_a?(Date)) ? occurred_at : Time.current
54
-
55
- normalized_occurred_at =
56
- case @group_by
57
- when :group_by_hour
58
- occurred_at&.beginning_of_hour || occurred_at
59
- when :group_by_day
60
- occurred_at&.beginning_of_day || occurred_at
61
- else
62
- occurred_at
63
- end
64
-
65
- # Use actual data if available, otherwise default to 0
66
- average_duration = actual_data[period] || 0
67
- result[normalized_occurred_at.to_i] = {
68
- value: average_duration.to_f
69
- }
70
- end
71
- end
72
-
73
- def generate_day_range(start_time, end_time)
74
- (start_time.to_date..end_time.to_date).map(&:beginning_of_day)
75
- end
76
-
77
- def generate_hour_range(start_time, end_time)
78
- current = start_time
79
- hours = []
80
- while current <= end_time
81
- hours << current
82
- current += 1.hour
83
- end
84
- hours
85
- end
86
-
87
- def extract_time_range_from_ransack
88
- # Extract time range from ransack conditions
89
- conditions = @ransack_query.conditions
90
-
91
- if @route
92
- # For specific route queries, look for occurred_at conditions
93
- start_condition = conditions.find { |c| c.a.first == "occurred_at" && c.p == "gteq" }
94
- end_condition = conditions.find { |c| c.a.first == "occurred_at" && c.p == "lt" }
95
- else
96
- # For general route queries, look for requests_occurred_at conditions
97
- start_condition = conditions.find { |c| c.a.first == "requests_occurred_at" && c.p == "gteq" }
98
- end_condition = conditions.find { |c| c.a.first == "requests_occurred_at" && c.p == "lt" }
99
- end
100
-
101
- start_time = start_condition&.v || 2.weeks.ago
102
- end_time = end_condition&.v || Time.current
103
-
104
- # Normalize time boundaries based on grouping
105
- case @group_by
106
- when :group_by_hour
107
- [ start_time.beginning_of_hour, end_time.beginning_of_hour ]
108
- else
109
- [ start_time.beginning_of_day, end_time.beginning_of_day ]
15
+ summaries = @ransack_query.result(distinct: false).where(
16
+ summarizable_type: "RailsPulse::Route",
17
+ period_type: @period_type
18
+ )
19
+
20
+ summaries = summaries.where(summarizable_id: @route.id) if @route
21
+ summaries = summaries
22
+ .group(:period_start)
23
+ .having("AVG(avg_duration) > ?", @start_duration || 0)
24
+ .average(:avg_duration)
25
+ .transform_keys(&:to_i)
26
+
27
+ # Pad missing data points with zeros
28
+ step = @period_type == :hour ? 1.hour : 1.day
29
+ data = {}
30
+ (@start_time.to_i..@end_time.to_i).step(step) do |timestamp|
31
+ data[timestamp.to_i] = summaries[timestamp.to_i].to_f.round(2)
110
32
  end
33
+ data
111
34
  end
112
35
  end
113
36
  end
@@ -2,60 +2,77 @@ module RailsPulse
2
2
  module Routes
3
3
  module Tables
4
4
  class Index
5
- def initialize(ransack_query:, start_time:, params:)
5
+ def initialize(ransack_query:, period_type: nil, start_time:, params:)
6
6
  @ransack_query = ransack_query
7
+ @period_type = period_type
7
8
  @start_time = start_time
8
9
  @params = params
9
10
  end
10
11
 
11
12
  def to_table
12
- # Pre-calculate values to avoid SQL injection and improve readability
13
- minutes_elapsed = calculate_minutes_elapsed
13
+ # Check if we have explicit ransack sorts
14
+ has_sorts = @ransack_query.sorts.any?
14
15
 
15
- # Get thresholds with safe defaults to avoid nil access errors
16
- config = RailsPulse.configuration rescue nil
17
- thresholds = config&.route_thresholds || { slow: 500, very_slow: 1500, critical: 3000 }
18
-
19
- requests_per_minute_divisor = minutes_elapsed > 0 ? minutes_elapsed : 1
16
+ base_query = @ransack_query.result(distinct: false)
17
+ .joins("INNER JOIN rails_pulse_routes ON rails_pulse_routes.id = rails_pulse_summaries.summarizable_id")
18
+ .where(
19
+ summarizable_type: "RailsPulse::Route",
20
+ period_type: @period_type
21
+ )
20
22
 
21
- status_sql = build_status_sql(thresholds)
23
+ base_query = base_query.where(summarizable_id: @route.id) if @route
22
24
 
23
- @ransack_query.result(distinct: false)
24
- .left_joins(:requests)
25
- .group("rails_pulse_routes.id")
25
+ # Apply grouping and aggregation
26
+ grouped_query = base_query
27
+ .group(
28
+ "rails_pulse_summaries.summarizable_id",
29
+ "rails_pulse_summaries.summarizable_type",
30
+ "rails_pulse_routes.id",
31
+ "rails_pulse_routes.path",
32
+ "rails_pulse_routes.method"
33
+ )
26
34
  .select(
27
- "rails_pulse_routes.*",
28
- "COALESCE(AVG(rails_pulse_requests.duration), 0) AS average_response_time_ms",
29
- "COUNT(rails_pulse_requests.id) AS request_count",
30
- "COALESCE(COUNT(rails_pulse_requests.id) / #{requests_per_minute_divisor}, 0) AS requests_per_minute",
31
- "COALESCE(SUM(CASE WHEN rails_pulse_requests.is_error = true THEN 1 ELSE 0 END), 0) AS error_count",
32
- "CASE WHEN COUNT(rails_pulse_requests.id) > 0 THEN ROUND((COALESCE(SUM(CASE WHEN rails_pulse_requests.is_error = true THEN 1 ELSE 0 END), 0) * 100.0) / COUNT(rails_pulse_requests.id), 2) ELSE 0 END AS error_rate_percentage",
33
- "COALESCE(MAX(rails_pulse_requests.duration), 0) AS max_response_time_ms",
34
- "#{status_sql} AS status_indicator"
35
+ "rails_pulse_summaries.summarizable_id",
36
+ "rails_pulse_summaries.summarizable_type",
37
+ "rails_pulse_routes.id as route_id",
38
+ "rails_pulse_routes.path",
39
+ "rails_pulse_routes.method as route_method",
40
+ "AVG(rails_pulse_summaries.avg_duration) as avg_duration",
41
+ "MAX(rails_pulse_summaries.max_duration) as max_duration",
42
+ "SUM(rails_pulse_summaries.count) as count",
43
+ "SUM(rails_pulse_summaries.error_count) as error_count",
44
+ "SUM(rails_pulse_summaries.success_count) as success_count"
35
45
  )
36
- end
37
-
38
- private
39
46
 
40
- def calculate_minutes_elapsed
41
- start_timestamp = Time.at(@start_time.to_i).utc
42
- ((Time.current.utc - start_timestamp) / 60.0).round(2)
43
- end
47
+ # Apply sorting based on ransack sorts or use default
48
+ if has_sorts
49
+ # Apply custom sorting based on ransack parameters
50
+ sort = @ransack_query.sorts.first
51
+ direction = sort.dir == "desc" ? :desc : :asc
44
52
 
45
- def build_status_sql(thresholds)
46
- # Ensure all thresholds have default values
47
- slow = thresholds[:slow] || 500
48
- very_slow = thresholds[:very_slow] || 1500
49
- critical = thresholds[:critical] || 3000
53
+ case sort.name
54
+ when "avg_duration_sort"
55
+ grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").send(direction))
56
+ when "max_duration_sort"
57
+ grouped_query = grouped_query.order(Arel.sql("MAX(rails_pulse_summaries.max_duration)").send(direction))
58
+ when "count_sort"
59
+ grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count)").send(direction))
60
+ when "requests_per_minute"
61
+ grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count) / 60.0").send(direction))
62
+ when "error_rate_percentage"
63
+ grouped_query = grouped_query.order(Arel.sql("(SUM(rails_pulse_summaries.error_count) * 100.0) / SUM(rails_pulse_summaries.count)").send(direction))
64
+ when "route_path"
65
+ grouped_query = grouped_query.order(Arel.sql("rails_pulse_routes.path").send(direction))
66
+ else
67
+ # Unknown sort field, fallback to default
68
+ grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").desc)
69
+ end
70
+ else
71
+ # Apply default sort when no explicit sort is provided (matches controller default_table_sort)
72
+ grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").desc)
73
+ end
50
74
 
51
- <<-SQL.squish
52
- CASE
53
- WHEN COALESCE(AVG(rails_pulse_requests.duration), 0) >= #{critical} THEN 3
54
- WHEN COALESCE(AVG(rails_pulse_requests.duration), 0) >= #{very_slow} THEN 2
55
- WHEN COALESCE(AVG(rails_pulse_requests.duration), 0) >= #{slow} THEN 1
56
- ELSE 0
57
- END
58
- SQL
75
+ grouped_query
59
76
  end
60
77
  end
61
78
  end