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
@@ -2,42 +2,66 @@ module RailsPulse
2
2
  module Queries
3
3
  module Cards
4
4
  class AverageQueryTimes
5
- def initialize(query:)
5
+ def initialize(query: nil)
6
6
  @query = query
7
7
  end
8
8
 
9
9
  def to_metric_card
10
- operations = if @query
11
- RailsPulse::Operation.where(query: @query)
12
- else
13
- RailsPulse::Operation.all
14
- end
15
-
16
- # Calculate overall average response time
17
- average_query_time = operations.average(:duration)&.round(0) || 0
18
-
19
- # Calculate trend by comparing last 7 days vs previous 7 days
20
10
  last_7_days = 7.days.ago.beginning_of_day
21
11
  previous_7_days = 14.days.ago.beginning_of_day
22
- current_period_avg = operations.where("occurred_at >= ?", last_7_days).average(:duration) || 0
23
- previous_period_avg = operations.where("occurred_at >= ? AND occurred_at < ?", previous_7_days, last_7_days).average(:duration) || 0
24
12
 
25
- percentage = previous_period_avg.zero? ? 0 : ((previous_period_avg - current_period_avg) / previous_period_avg * 100).abs.round(1)
26
- trend_icon = percentage < 0.1 ? "move-right" : current_period_avg < previous_period_avg ? "trending-down" : "trending-up"
13
+ # Single query to get all aggregated metrics with conditional sums
14
+ base_query = RailsPulse::Summary.where(
15
+ summarizable_type: "RailsPulse::Query",
16
+ period_type: "day",
17
+ period_start: 2.weeks.ago.beginning_of_day..Time.current
18
+ )
19
+ base_query = base_query.where(summarizable_id: @query.id) if @query
20
+
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
29
+
30
+ # Calculate metrics from single query result
31
+ average_query_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
34
+
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"
27
37
  trend_amount = previous_period_avg.zero? ? "0%" : "#{percentage}%"
28
38
 
29
- sparkline_data = operations
30
- .group_by_week(:occurred_at, time_zone: "UTC")
31
- .average(:duration)
32
- .each_with_object({}) do |(date, avg), hash|
33
- formatted_date = date.strftime("%b %-d")
34
- value = avg&.round(0) || 0
35
- hash[formatted_date] = {
36
- 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)
37
52
  }
38
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
39
61
 
40
62
  {
63
+ id: "average_query_times",
64
+ context: "queries",
41
65
  title: "Average Query Time",
42
66
  summary: "#{average_query_time} ms",
43
67
  line_chart_data: sparkline_data,
@@ -7,44 +7,51 @@ module RailsPulse
7
7
  end
8
8
 
9
9
  def to_metric_card
10
- operations = if @query
11
- RailsPulse::Operation.where(query: @query)
12
- else
13
- RailsPulse::Operation.all
14
- end
15
-
16
- # Calculate total request count
17
- total_request_count = operations.count
18
-
19
- # Calculate trend by comparing last 7 days vs previous 7 days
20
10
  last_7_days = 7.days.ago.beginning_of_day
21
11
  previous_7_days = 14.days.ago.beginning_of_day
22
- current_period_count = operations.where("occurred_at >= ?", last_7_days).count
23
- previous_period_count = operations.where("occurred_at >= ? AND occurred_at < ?", previous_7_days, last_7_days).count
12
+
13
+ # Single query to get all count metrics with conditional aggregation
14
+ base_query = RailsPulse::Summary.where(
15
+ summarizable_type: "RailsPulse::Query",
16
+ period_type: "day",
17
+ period_start: 2.weeks.ago.beginning_of_day..Time.current
18
+ )
19
+ base_query = base_query.where(summarizable_id: @query.id) if @query
20
+
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
26
+
27
+ # Calculate metrics from single query result
28
+ total_execution_count = metrics.total_count || 0
29
+ current_period_count = metrics.current_count || 0
30
+ previous_period_count = metrics.previous_count || 0
24
31
 
25
32
  percentage = previous_period_count.zero? ? 0 : ((previous_period_count - current_period_count) / previous_period_count.to_f * 100).abs.round(1)
26
33
  trend_icon = percentage < 0.1 ? "move-right" : current_period_count < previous_period_count ? "trending-down" : "trending-up"
27
34
  trend_amount = previous_period_count.zero? ? "0%" : "#{percentage}%"
28
35
 
29
- sparkline_data = operations
30
- .group_by_week(:occurred_at, time_zone: "UTC")
31
- .count
32
- .each_with_object({}) do |(date, count), hash|
33
- formatted_date = date.strftime("%b %-d")
34
- hash[formatted_date] = {
35
- value: count
36
- }
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 }
37
44
  end
38
45
 
39
- # Calculate average operations per minute
40
- min_time = operations.minimum(:occurred_at)
41
- max_time = operations.maximum(:occurred_at)
42
- total_minutes = min_time && max_time && min_time != max_time ? (max_time - min_time) / 60.0 : 1
43
- average_operations_per_minute = total_request_count / total_minutes
46
+ # Calculate average executions per minute over 2-week period
47
+ total_minutes = 2.weeks / 1.minute
48
+ average_executions_per_minute = total_execution_count / total_minutes
44
49
 
45
50
  {
51
+ id: "execution_rate",
52
+ context: "queries",
46
53
  title: "Execution Rate",
47
- summary: "#{average_operations_per_minute.round(2)} / min",
54
+ summary: "#{average_executions_per_minute.round(2)} / min",
48
55
  line_chart_data: sparkline_data,
49
56
  trend_icon: trend_icon,
50
57
  trend_amount: trend_amount,
@@ -7,58 +7,47 @@ module RailsPulse
7
7
  end
8
8
 
9
9
  def to_metric_card
10
- operations = if @query
11
- RailsPulse::Operation.where(query: @query)
12
- else
13
- RailsPulse::Operation.all
14
- end
15
-
16
- # Calculate overall 95th percentile response time
17
- count = operations.count
18
- percentile_95th = if count > 0
19
- operations.select("duration").order("duration").limit(1).offset((count * 0.95).floor).pluck(:duration).first || 0
20
- else
21
- 0
22
- end
23
-
24
- # Calculate trend by comparing last 7 days vs previous 7 days for 95th percentile
25
10
  last_7_days = 7.days.ago.beginning_of_day
26
11
  previous_7_days = 14.days.ago.beginning_of_day
27
12
 
28
- current_period = operations.where("occurred_at >= ?", last_7_days)
29
- current_count = current_period.count
30
- current_period_95th = if current_count > 0
31
- current_period.select("duration").order("duration").limit(1).offset((current_count * 0.95).floor).pluck(:duration).first || 0
32
- else
33
- 0
34
- end
35
-
36
- previous_period = operations.where("occurred_at >= ? AND occurred_at < ?", previous_7_days, last_7_days)
37
- previous_count = previous_period.count
38
- previous_period_95th = if previous_count > 0
39
- previous_period.select("duration").order("duration").limit(1).offset((previous_count * 0.95).floor).pluck(:duration).first || 0
40
- else
41
- 0
42
- end
43
-
44
- percentage = previous_period_95th.zero? ? 0 : ((previous_period_95th - current_period_95th) / previous_period_95th * 100).abs.round(1)
45
- trend_icon = percentage < 0.1 ? "move-right" : current_period_95th < previous_period_95th ? "trending-down" : "trending-up"
46
- trend_amount = previous_period_95th.zero? ? "0%" : "#{percentage}%"
47
-
48
- sparkline_data = operations
49
- .group_by_week(:occurred_at, time_zone: "UTC")
50
- .average(:duration)
51
- .each_with_object({}) do |(date, avg), hash|
52
- formatted_date = date.strftime("%b %-d")
53
- value = avg&.round(0) || 0
54
- hash[formatted_date] = {
55
- value: value
56
- }
13
+ # Single query to get all P95 metrics with conditional aggregation
14
+ base_query = RailsPulse::Summary.where(
15
+ summarizable_type: "RailsPulse::Query",
16
+ period_type: "day",
17
+ period_start: 2.weeks.ago.beginning_of_day..Time.current
18
+ )
19
+ base_query = base_query.where(summarizable_id: @query.id) if @query
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_query_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 }
57
44
  end
58
45
 
59
46
  {
47
+ id: "percentile_query_times",
48
+ context: "queries",
60
49
  title: "95th Percentile Query Time",
61
- summary: "#{percentile_95th} ms",
50
+ summary: "#{p95_query_time} ms",
62
51
  line_chart_data: sparkline_data,
63
52
  trend_icon: trend_icon,
64
53
  trend_amount: trend_amount,
@@ -2,109 +2,35 @@ module RailsPulse
2
2
  module Queries
3
3
  module Charts
4
4
  class AverageQueryTimes
5
- def initialize(ransack_query:, group_by: :group_by_day, query: nil)
5
+ def initialize(ransack_query:, period_type: nil, query: 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
  @query = query
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 @query
14
- @ransack_query.result(distinct: false)
15
- .public_send(@group_by, "occurred_at", series: true, time_zone: "UTC")
16
- .average(:duration)
17
- else
18
- @ransack_query.result(distinct: false)
19
- .left_joins(:operations)
20
- .public_send(
21
- @group_by,
22
- "rails_pulse_operations.occurred_at",
23
- series: true,
24
- time_zone: "UTC"
25
- )
26
- .average("rails_pulse_operations.duration")
27
- end
28
-
29
- # Create full time range and fill in missing periods
30
- fill_missing_periods(actual_data)
31
- end
32
-
33
- private
34
-
35
- def fill_missing_periods(actual_data)
36
- # Extract actual time range from ransack query conditions
37
- start_time, end_time = extract_time_range_from_ransack
38
-
39
- # Create time range based on grouping type
40
- case @group_by
41
- when :group_by_hour
42
- time_range = generate_hour_range(start_time, end_time)
43
- else # :group_by_day
44
- time_range = generate_day_range(start_time, end_time)
45
- end
46
-
47
- # Fill in all periods with zero values for missing periods
48
- time_range.each_with_object({}) do |period, result|
49
- occurred_at = period.is_a?(String) ? Time.parse(period) : period
50
- occurred_at = (occurred_at.is_a?(Time) || occurred_at.is_a?(Date)) ? occurred_at : Time.current
51
-
52
- normalized_occurred_at =
53
- case @group_by
54
- when :group_by_hour
55
- occurred_at&.beginning_of_hour || occurred_at
56
- when :group_by_day
57
- occurred_at&.beginning_of_day || occurred_at
58
- else
59
- occurred_at
60
- end
61
-
62
- # Use actual data if available, otherwise default to 0
63
- average_duration = actual_data[period] || 0
64
- result[normalized_occurred_at.to_i] = {
65
- value: average_duration.to_f
66
- }
67
- end
68
- end
69
-
70
- def generate_day_range(start_time, end_time)
71
- (start_time.to_date..end_time.to_date).map(&:beginning_of_day)
72
- end
73
-
74
- def generate_hour_range(start_time, end_time)
75
- current = start_time
76
- hours = []
77
- while current <= end_time
78
- hours << current
79
- current += 1.hour
80
- end
81
- hours
82
- end
83
-
84
- def extract_time_range_from_ransack
85
- # Extract time range from ransack conditions
86
- conditions = @ransack_query.conditions
87
-
88
- if @query
89
- # For specific query, look for occurred_at conditions
90
- start_condition = conditions.find { |c| c.a.first == "occurred_at" && c.p == "gteq" }
91
- end_condition = conditions.find { |c| c.a.first == "occurred_at" && c.p == "lt" }
92
- else
93
- # For general operations, look for rails_pulse_operations_occurred_at conditions
94
- start_condition = conditions.find { |c| c.a.first == "rails_pulse_operations_occurred_at" && c.p == "gteq" }
95
- end_condition = conditions.find { |c| c.a.first == "rails_pulse_operations_occurred_at" && c.p == "lt" }
96
- end
97
-
98
- start_time = start_condition&.v || 2.weeks.ago
99
- end_time = end_condition&.v || Time.current
100
-
101
- # Normalize time boundaries based on grouping
102
- case @group_by
103
- when :group_by_hour
104
- [ start_time.beginning_of_hour, end_time.beginning_of_hour ]
105
- else
106
- [ start_time.beginning_of_day, end_time.beginning_of_day ]
15
+ summaries = @ransack_query.result(distinct: false).where(
16
+ summarizable_type: "RailsPulse::Query",
17
+ period_type: @period_type
18
+ )
19
+
20
+ summaries = summaries.where(summarizable_id: @query.id) if @query
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)
107
32
  end
33
+ data
108
34
  end
109
35
  end
110
36
  end
@@ -0,0 +1,74 @@
1
+ module RailsPulse
2
+ module Queries
3
+ module Tables
4
+ class Index
5
+ def initialize(ransack_query:, period_type: nil, start_time:, params:, query: nil)
6
+ @ransack_query = ransack_query
7
+ @period_type = period_type
8
+ @start_time = start_time
9
+ @params = params
10
+ @query = query
11
+ end
12
+
13
+ def to_table
14
+ # Check if we have explicit ransack sorts
15
+ has_sorts = @ransack_query.sorts.any?
16
+
17
+ base_query = @ransack_query.result(distinct: false)
18
+ .joins("INNER JOIN rails_pulse_queries ON rails_pulse_queries.id = rails_pulse_summaries.summarizable_id")
19
+ .where(
20
+ summarizable_type: "RailsPulse::Query",
21
+ period_type: @period_type
22
+ )
23
+
24
+ base_query = base_query.where(summarizable_id: @query.id) if @query
25
+
26
+ # Apply grouping and aggregation
27
+ grouped_query = base_query
28
+ .group(
29
+ "rails_pulse_summaries.summarizable_id",
30
+ "rails_pulse_summaries.summarizable_type",
31
+ "rails_pulse_queries.id",
32
+ "rails_pulse_queries.normalized_sql"
33
+ )
34
+ .select(
35
+ "rails_pulse_summaries.summarizable_id",
36
+ "rails_pulse_summaries.summarizable_type",
37
+ "rails_pulse_queries.id as query_id",
38
+ "rails_pulse_queries.normalized_sql",
39
+ "AVG(rails_pulse_summaries.avg_duration) as avg_duration",
40
+ "MAX(rails_pulse_summaries.max_duration) as max_duration",
41
+ "SUM(rails_pulse_summaries.count) as execution_count",
42
+ "SUM(rails_pulse_summaries.count * rails_pulse_summaries.avg_duration) as total_time_consumed"
43
+ )
44
+
45
+ # Apply sorting based on ransack sorts or use default
46
+ if has_sorts
47
+ # Apply custom sorting based on ransack parameters
48
+ sort = @ransack_query.sorts.first
49
+ direction = sort.dir == "desc" ? :desc : :asc
50
+
51
+ case sort.name
52
+ when "avg_duration_sort"
53
+ grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").send(direction))
54
+ when "execution_count_sort"
55
+ grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count)").send(direction))
56
+ when "total_time_consumed_sort"
57
+ grouped_query = grouped_query.order(Arel.sql("SUM(rails_pulse_summaries.count * rails_pulse_summaries.avg_duration)").send(direction))
58
+ when "normalized_sql"
59
+ grouped_query = grouped_query.order(Arel.sql("rails_pulse_queries.normalized_sql").send(direction))
60
+ else
61
+ # Unknown sort field, fallback to default
62
+ grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").desc)
63
+ end
64
+ else
65
+ # Apply default sort when no explicit sort is provided
66
+ grouped_query = grouped_query.order(Arel.sql("AVG(rails_pulse_summaries.avg_duration)").desc)
67
+ end
68
+
69
+ grouped_query
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -4,6 +4,7 @@ module RailsPulse
4
4
 
5
5
  # Associations
6
6
  has_many :operations, class_name: "RailsPulse::Operation", inverse_of: :query
7
+ has_many :summaries, as: :summarizable, class_name: "RailsPulse::Summary", dependent: :destroy
7
8
 
8
9
  # Validations
9
10
  validates :normalized_sql, presence: true, uniqueness: true
@@ -2,96 +2,35 @@ module RailsPulse
2
2
  module Requests
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 = @ransack_query.result(distinct: false)
14
- .public_send(
15
- @group_by,
16
- "rails_pulse_requests.occurred_at",
17
- series: true,
18
- time_zone: "UTC"
19
- )
20
- .average("rails_pulse_requests.duration")
21
-
22
- # Create full time range and fill in missing periods
23
- fill_missing_periods(actual_data)
24
- end
25
-
26
- private
27
-
28
- def fill_missing_periods(actual_data)
29
- # Extract actual time range from ransack query conditions
30
- start_time, end_time = extract_time_range_from_ransack
31
-
32
- # Create time range based on grouping type
33
- case @group_by
34
- when :group_by_hour
35
- time_range = generate_hour_range(start_time, end_time)
36
- else # :group_by_day
37
- time_range = generate_day_range(start_time, end_time)
38
- end
39
-
40
- # Fill in all periods with zero values for missing periods
41
- time_range.each_with_object({}) do |period, result|
42
- occurred_at = period.is_a?(String) ? Time.parse(period) : period
43
- occurred_at = (occurred_at.is_a?(Time) || occurred_at.is_a?(Date)) ? occurred_at : Time.current
44
-
45
- normalized_occurred_at =
46
- case @group_by
47
- when :group_by_hour
48
- occurred_at&.beginning_of_hour || occurred_at
49
- when :group_by_day
50
- occurred_at&.beginning_of_day || occurred_at
51
- else
52
- occurred_at
53
- end
54
-
55
- # Use actual data if available, otherwise default to 0
56
- average_duration = actual_data[period] || 0
57
- result[normalized_occurred_at.to_i] = {
58
- value: average_duration.to_f
59
- }
60
- end
61
- end
62
-
63
- def generate_day_range(start_time, end_time)
64
- (start_time.to_date..end_time.to_date).map(&:beginning_of_day)
65
- end
66
-
67
- def generate_hour_range(start_time, end_time)
68
- current = start_time
69
- hours = []
70
- while current <= end_time
71
- hours << current
72
- current += 1.hour
73
- end
74
- hours
75
- end
76
-
77
- def extract_time_range_from_ransack
78
- # Extract time range from ransack conditions
79
- conditions = @ransack_query.conditions
80
-
81
- # For requests, look for occurred_at conditions on rails_pulse_requests
82
- start_condition = conditions.find { |c| c.a.first == "rails_pulse_requests_occurred_at" && c.p == "gteq" }
83
- end_condition = conditions.find { |c| c.a.first == "rails_pulse_requests_occurred_at" && c.p == "lt" }
84
-
85
- start_time = start_condition&.v || 2.weeks.ago
86
- end_time = end_condition&.v || Time.current
87
-
88
- # Normalize time boundaries based on grouping
89
- case @group_by
90
- when :group_by_hour
91
- [ start_time.beginning_of_hour, end_time.beginning_of_hour ]
92
- else
93
- [ 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)
94
32
  end
33
+ data
95
34
  end
96
35
  end
97
36
  end
@@ -4,6 +4,7 @@ module RailsPulse
4
4
 
5
5
  # Associations
6
6
  has_many :requests, class_name: "RailsPulse::Request", foreign_key: "route_id", dependent: :restrict_with_exception
7
+ has_many :summaries, as: :summarizable, class_name: "RailsPulse::Summary", dependent: :destroy
7
8
 
8
9
  # Validations
9
10
  validates :method, presence: true
@@ -56,12 +57,6 @@ module RailsPulse
56
57
  Arel.sql("COUNT(rails_pulse_requests.id)")
57
58
  end
58
59
 
59
- # Remove the problematic ransacker that causes SQL syntax errors
60
- # The status_indicator will be handled differently or removed from filtering
61
- # ransacker :status_indicator do
62
- # # Removed to fix SQL generation issues in tests
63
- # end
64
-
65
60
  def to_breadcrumb
66
61
  path
67
62
  end