rails_pulse 0.1.1 → 0.1.3

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +79 -177
  3. data/Rakefile +77 -2
  4. data/app/assets/images/rails_pulse/dashboard.png +0 -0
  5. data/app/assets/images/rails_pulse/request.png +0 -0
  6. data/app/assets/stylesheets/rails_pulse/application.css +28 -17
  7. data/app/assets/stylesheets/rails_pulse/components/badge.css +13 -0
  8. data/app/assets/stylesheets/rails_pulse/components/base.css +12 -2
  9. data/app/assets/stylesheets/rails_pulse/components/collapsible.css +30 -0
  10. data/app/assets/stylesheets/rails_pulse/components/popover.css +0 -1
  11. data/app/assets/stylesheets/rails_pulse/components/row.css +55 -3
  12. data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +23 -0
  13. data/app/controllers/concerns/chart_table_concern.rb +21 -4
  14. data/app/controllers/concerns/response_range_concern.rb +6 -3
  15. data/app/controllers/concerns/time_range_concern.rb +5 -10
  16. data/app/controllers/concerns/zoom_range_concern.rb +32 -1
  17. data/app/controllers/rails_pulse/application_controller.rb +13 -5
  18. data/app/controllers/rails_pulse/dashboard_controller.rb +12 -0
  19. data/app/controllers/rails_pulse/queries_controller.rb +111 -51
  20. data/app/controllers/rails_pulse/requests_controller.rb +37 -12
  21. data/app/controllers/rails_pulse/routes_controller.rb +98 -24
  22. data/app/helpers/rails_pulse/application_helper.rb +0 -1
  23. data/app/helpers/rails_pulse/chart_formatters.rb +3 -3
  24. data/app/helpers/rails_pulse/chart_helper.rb +21 -9
  25. data/app/helpers/rails_pulse/status_helper.rb +10 -4
  26. data/app/javascript/rails_pulse/application.js +34 -3
  27. data/app/javascript/rails_pulse/controllers/collapsible_controller.js +32 -0
  28. data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +2 -1
  29. data/app/javascript/rails_pulse/controllers/expandable_rows_controller.js +58 -0
  30. data/app/javascript/rails_pulse/controllers/index_controller.js +353 -39
  31. data/app/javascript/rails_pulse/controllers/pagination_controller.js +17 -27
  32. data/app/javascript/rails_pulse/controllers/popover_controller.js +28 -4
  33. data/app/javascript/rails_pulse/controllers/table_sort_controller.js +14 -0
  34. data/app/jobs/rails_pulse/backfill_summaries_job.rb +41 -0
  35. data/app/jobs/rails_pulse/summary_job.rb +53 -0
  36. data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +28 -7
  37. data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +18 -22
  38. data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +18 -7
  39. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +34 -41
  40. data/app/models/rails_pulse/operation.rb +1 -1
  41. data/app/models/rails_pulse/queries/cards/average_query_times.rb +49 -25
  42. data/app/models/rails_pulse/queries/cards/execution_rate.rb +40 -28
  43. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +37 -43
  44. data/app/models/rails_pulse/queries/charts/average_query_times.rb +23 -97
  45. data/app/models/rails_pulse/queries/tables/index.rb +74 -0
  46. data/app/models/rails_pulse/query.rb +47 -0
  47. data/app/models/rails_pulse/requests/charts/average_response_times.rb +23 -84
  48. data/app/models/rails_pulse/route.rb +1 -6
  49. data/app/models/rails_pulse/routes/cards/average_response_times.rb +45 -25
  50. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +43 -45
  51. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +36 -44
  52. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +37 -27
  53. data/app/models/rails_pulse/routes/charts/average_response_times.rb +23 -100
  54. data/app/models/rails_pulse/routes/tables/index.rb +57 -40
  55. data/app/models/rails_pulse/summary.rb +143 -0
  56. data/app/services/rails_pulse/analysis/backtrace_analyzer.rb +256 -0
  57. data/app/services/rails_pulse/analysis/base_analyzer.rb +67 -0
  58. data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +206 -0
  59. data/app/services/rails_pulse/analysis/index_recommendation_engine.rb +326 -0
  60. data/app/services/rails_pulse/analysis/n_plus_one_detector.rb +241 -0
  61. data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +146 -0
  62. data/app/services/rails_pulse/analysis/suggestion_generator.rb +217 -0
  63. data/app/services/rails_pulse/query_analysis_service.rb +125 -0
  64. data/app/services/rails_pulse/summary_service.rb +199 -0
  65. data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +0 -1
  66. data/app/views/layouts/rails_pulse/application.html.erb +4 -6
  67. data/app/views/rails_pulse/components/_breadcrumbs.html.erb +1 -1
  68. data/app/views/rails_pulse/components/_code_panel.html.erb +17 -3
  69. data/app/views/rails_pulse/components/_empty_state.html.erb +11 -0
  70. data/app/views/rails_pulse/components/_metric_card.html.erb +37 -28
  71. data/app/views/rails_pulse/components/_panel.html.erb +1 -1
  72. data/app/views/rails_pulse/components/_sparkline_stats.html.erb +5 -7
  73. data/app/views/rails_pulse/components/_table_head.html.erb +6 -1
  74. data/app/views/rails_pulse/dashboard/index.html.erb +55 -37
  75. data/app/views/rails_pulse/operations/show.html.erb +17 -15
  76. data/app/views/rails_pulse/queries/_analysis_error.html.erb +15 -0
  77. data/app/views/rails_pulse/queries/_analysis_prompt.html.erb +27 -0
  78. data/app/views/rails_pulse/queries/_analysis_results.html.erb +87 -0
  79. data/app/views/rails_pulse/queries/_analysis_section.html.erb +39 -0
  80. data/app/views/rails_pulse/queries/_show_table.html.erb +2 -2
  81. data/app/views/rails_pulse/queries/_table.html.erb +11 -13
  82. data/app/views/rails_pulse/queries/index.html.erb +32 -28
  83. data/app/views/rails_pulse/queries/show.html.erb +45 -34
  84. data/app/views/rails_pulse/requests/_operations.html.erb +38 -45
  85. data/app/views/rails_pulse/requests/_table.html.erb +3 -3
  86. data/app/views/rails_pulse/requests/index.html.erb +33 -28
  87. data/app/views/rails_pulse/routes/_table.html.erb +14 -14
  88. data/app/views/rails_pulse/routes/index.html.erb +34 -29
  89. data/app/views/rails_pulse/routes/show.html.erb +43 -36
  90. data/config/initializers/rails_pulse.rb +0 -12
  91. data/config/routes.rb +5 -1
  92. data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +54 -0
  93. data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +13 -0
  94. data/db/rails_pulse_schema.rb +130 -0
  95. data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +65 -0
  96. data/lib/generators/rails_pulse/install_generator.rb +94 -4
  97. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +60 -0
  98. data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +22 -0
  99. data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +19 -0
  100. data/lib/generators/rails_pulse/templates/rails_pulse.rb +0 -12
  101. data/lib/generators/rails_pulse/upgrade_generator.rb +225 -0
  102. data/lib/rails_pulse/configuration.rb +0 -11
  103. data/lib/rails_pulse/engine.rb +0 -1
  104. data/lib/rails_pulse/version.rb +1 -1
  105. data/lib/tasks/rails_pulse.rake +77 -0
  106. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  107. data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
  108. data/public/rails-pulse-assets/rails-pulse.js +53 -53
  109. data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
  110. data/public/rails-pulse-assets/search.svg +43 -0
  111. metadata +48 -14
  112. data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
  113. data/app/assets/images/rails_pulse/routes.png +0 -0
  114. data/app/controllers/rails_pulse/caches_controller.rb +0 -115
  115. data/app/helpers/rails_pulse/cached_component_helper.rb +0 -73
  116. data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +0 -67
  117. data/app/models/rails_pulse/component_cache_key.rb +0 -33
  118. data/app/views/rails_pulse/caches/show.html.erb +0 -9
  119. data/db/migrate/20250227235904_create_routes.rb +0 -12
  120. data/db/migrate/20250227235915_create_requests.rb +0 -19
  121. data/db/migrate/20250228000000_create_queries.rb +0 -14
  122. data/db/migrate/20250228000056_create_operations.rb +0 -24
  123. data/lib/rails_pulse/migration.rb +0 -29
@@ -1,41 +1,37 @@
1
1
  <%= render 'rails_pulse/components/breadcrumbs' %>
2
2
 
3
- <div class="row">
4
- <%= cached_component(component: "metric_card", id: "average_query_times", context: "query_#{@query.id}", class: "grid-item block") %>
5
- <%= cached_component(component: "metric_card", id: "request_count_totals", context: "query_#{@query.id}", class: "grid-item block") %>
6
- <%= cached_component(component: "metric_card", id: "execution_rate", context: "query_#{@query.id}", class: "grid-item block") %>
7
- </div>
8
-
9
- <div class="mb-4">
10
- <%= render 'rails_pulse/components/code_panel', { title: 'Normalized SQL', value: @query.normalized_sql } %>
11
- </div>
3
+ <% unless turbo_frame_request? %>
4
+ <div class="row">
5
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
6
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_query_times_metric_card } %>
7
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @execution_rate_metric_card } %>
8
+ </div>
9
+ <% end %>
12
10
 
13
11
  <div
14
- class="row"
15
12
  data-controller="rails-pulse--index"
16
13
  data-rails-pulse--index-chart-id-value="query_responses_chart"
17
- data-rails-pulse--index-occurred-at-param-value="occurred_at"
18
14
  >
19
- <div class="grid-item">
20
- <%= render 'rails_pulse/components/panel', { title: 'Query Responses' } do %>
21
- <%= search_form_for @ransack_query, url: query_path(@query), class: "flex items-center justify-between gap mb-4" do |form| %>
22
- <div class="flex items-center grow gap">
23
- <%= form.select :occurred_at_range,
24
- RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
25
- { selected: @selected_time_range },
26
- { class: "input" }
27
- %>
28
- <%= form.select :duration,
29
- duration_options(:query),
30
- { selected: @selected_response_range },
31
- { class: "input" }
32
- %>
33
- <%= link_to "Reset", query_path(@query), class: "btn btn--borderless show@md" if params.has_key?(:q) %>
34
- <%= form.submit "Search", class: "btn show@sm" %>
35
- </div>
36
- <% end %>
15
+ <%= render 'rails_pulse/components/panel', { title: 'Query Responses' } do %>
16
+ <%= search_form_for @ransack_query, url: query_path(@query), class: "flex items-center justify-between gap mb-4" do |form| %>
17
+ <div class="flex items-center grow gap">
18
+ <%= form.select :period_start_range,
19
+ RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
20
+ { selected: @selected_time_range },
21
+ { class: "input" }
22
+ %>
23
+ <%= form.select :duration,
24
+ duration_options(:query),
25
+ { selected: @selected_response_range },
26
+ { class: "input" }
27
+ %>
28
+ <%= link_to "Reset", query_path(@query), class: "btn btn--borderless show@md" if params.has_key?(:q) %>
29
+ <%= form.submit "Search", class: "btn show@sm" %>
30
+ </div>
31
+ <% end %>
37
32
 
38
- <% if @chart_data.present? %>
33
+ <% if @has_data %>
34
+ <% if @chart_data && @chart_data.values.any? { |v| v > 0 } %>
39
35
  <div
40
36
  class="chart-container chart-container--slim"
41
37
  data-rails-pulse--index-target="chart"
@@ -51,19 +47,34 @@
51
47
  chart_start: 0,
52
48
  chart_end: @chart_data.length - 1,
53
49
  xaxis_formatter: @xaxis_formatter,
54
- tooltip_formatter: @tooltip_formatter
50
+ tooltip_formatter: @tooltip_formatter,
51
+ zoom_start: @zoom_start,
52
+ zoom_end: @zoom_end,
53
+ chart_data: @chart_data
55
54
  )
56
55
  ) %>
57
56
  </div>
58
57
  <% end %>
59
58
 
60
- <%= turbo_frame_tag :index_table do %>
59
+ <%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
61
60
  <%= render 'rails_pulse/queries/show_table' %>
62
61
  <% end %>
62
+ <% else %>
63
+ <%= render 'rails_pulse/components/empty_state',
64
+ title: 'No query responses found for the selected filters.',
65
+ description: 'Try adjusting your time range or filters to see results.' %>
63
66
  <% end %>
64
- </div>
67
+ <% end %>
65
68
  </div>
66
69
 
70
+ <div class="mb-4">
71
+ <%= render 'rails_pulse/components/code_panel', { title: 'Normalized Query', value: @query.normalized_sql } %>
72
+ </div>
73
+
74
+ <%= turbo_frame_tag "query_analysis", class: "mb-4" do %>
75
+ <%= render 'rails_pulse/queries/analysis_section', query: @query %>
76
+ <% end %>
77
+
67
78
  <%= render 'rails_pulse/components/panel', { title: 'Query Locations' } do %>
68
79
  <table class="table">
69
80
  <thead>
@@ -76,7 +87,7 @@
76
87
  <tbody>
77
88
  <% @query.operations.group_by(&:codebase_location).each do |codebase_location, operations| %>
78
89
  <tr>
79
- <td><%= link_to codebase_location, '#' %></td>
90
+ <td><%= codebase_location %></td>
80
91
  <td><%= (operations.sum(&:duration) / operations.count).round(2) %></td>
81
92
  <td><%= operations.count %></td>
82
93
  </tr>
@@ -1,62 +1,54 @@
1
- <div class="row">
2
- <div class="grid-item">
3
- <%= render 'rails_pulse/components/panel', { title: 'Event Sequence ' } do %>
4
- <table class="table operations-table">
5
- <thead>
6
- <tr>
7
- <th class="w-8"></th>
8
- <th class="operations-label-cell">Operation</th>
9
- <th class="operations-duration-cell">Duration</th>
10
- <th class="w-20">Impact</th>
11
- <th class="w-16"></th>
12
- <th class="operations-event-cell">Timeline</th>
13
- <th class="w-16">Actions</th>
14
- </tr>
15
- </thead>
16
- <tbody>
17
- <% total_request_duration = @request.duration.to_f %>
18
- <% @operation_timeline.bars.each_with_index do |bar, index| %>
19
- <tbody data-controller="rails-pulse--expandable-row">
20
- <!-- Main operation row -->
21
- <tr
22
- class="<%= cycle('bg-shade', 'bg-white') %> operation-row"
23
- data-rails-pulse--expandable-row-target="trigger"
24
- data-operation-id="<%= bar.operation.id %>"
25
- >
1
+ <%= render 'rails_pulse/components/panel', { title: 'Event Sequence ' } do %>
2
+ <% if operations.any? %>
3
+ <table class="table operations-table">
4
+ <thead>
5
+ <tr>
6
+ <th class="w-8"></th>
7
+ <th class="operations-label-cell">Operation</th>
8
+ <th class="operations-duration-cell">Duration</th>
9
+ <th class="w-20">Impact</th>
10
+ <th class="w-16"></th>
11
+ <th class="operations-event-cell">Timeline</th>
12
+ <th class="w-16">Actions</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody data-controller="rails-pulse--expandable-rows" data-action="click->rails-pulse--expandable-rows#toggle">
16
+ <% total_request_duration = @request.duration.to_f %>
17
+ <% @operation_timeline.bars.each_with_index do |bar, index| %>
18
+ <tr data-operation-id="<%= bar.operation.id %>">
26
19
  <!-- Toggle chevron column -->
27
- <td class="text-center cursor-pointer"
28
- data-action="click->rails-pulse--expandable-row#toggle">
29
- <div data-rails-pulse--expandable-row-target="chevron">
20
+ <td class="text-center cursor-pointer">
21
+ <div class="chevron mbs-2">
30
22
  <%= rails_pulse_icon('chevron-right', width: '16', class: 'transition-transform duration-200') %>
31
23
  </div>
32
24
  </td>
33
-
25
+
34
26
  <td class="operations-label-cell">
35
27
  <span class="text-link" style="color: <%= event_color(bar.operation.operation_type) %>;">
36
28
  <%= html_escape(bar.operation.label) %>
37
29
  </span>
38
30
  </td>
39
-
31
+
40
32
  <td class="whitespace-nowrap">
41
33
  <%= bar.duration.round(2) %> ms
42
34
  </td>
43
-
35
+
44
36
  <td class="whitespace-nowrap">
45
37
  <% impact_percentage = total_request_duration > 0 ? (bar.operation.duration / total_request_duration * 100).round(1) : 0 %>
46
38
  <%= impact_percentage %>%
47
39
  </td>
48
-
40
+
49
41
  <td class="whitespace-nowrap text-center">
50
42
  <%= operation_status_indicator(bar.operation) %>
51
43
  </td>
52
-
44
+
53
45
  <td class="operations-event-cell">
54
46
  <div
55
47
  class="operations-event"
56
48
  style="left: <%= bar.left_pct %>%; width: <%= bar.width_pct %>%; background-color: <%= event_color(bar.operation.operation_type) %>;">
57
49
  </div>
58
50
  </td>
59
-
51
+
60
52
  <td>
61
53
  <%= link_to rails_pulse_icon('eye', width: '16', class: 'inline-block mbi-2'), operation_path(bar.operation), title: 'View details', data: { turbo_frame: "_top" } %>
62
54
  <% if bar.operation.operation_type == "sql" && bar.operation.query.present? %>
@@ -64,22 +56,23 @@
64
56
  <% end %>
65
57
  </td>
66
58
  </tr>
67
-
59
+
68
60
  <!-- Expandable details row -->
69
- <tr
61
+ <tr
70
62
  class="operation-details-row hidden"
71
- data-rails-pulse--expandable-row-target="details"
72
63
  data-operation-id="<%= bar.operation.id %>"
73
64
  >
74
- <td colspan="7" class="p-4">
65
+ <td colspan="7" class="pi-8 pb-4">
75
66
  <%= turbo_frame_tag "operation_#{bar.operation.id}_details", data: { "operation-url": operation_path(bar.operation) } do %>
76
67
  <% end %>
77
68
  </td>
78
69
  </tr>
79
- </tbody>
80
- <% end %>
81
- </tbody>
82
- </table>
83
- <% end %>
84
- </div>
85
- </div>
70
+ <% end %>
71
+ </tbody>
72
+ </table>
73
+ <% else %>
74
+ <%= render 'rails_pulse/components/empty_state',
75
+ title: 'No operations found for this request.',
76
+ description: 'This request may not have had any tracked operations.' %>
77
+ <% end %>
78
+ <% end %>
@@ -3,14 +3,14 @@
3
3
  columns << { field: :route_path, label: 'Route', class: 'w-auto' } if @route.blank?
4
4
 
5
5
  columns += [
6
- { field: :occurred_at, label: 'Timestamp', class: (@route.present? ? 'w-auto' : 'w-40') },
7
- { field: :duration, label: 'Duration', class: 'w-24' },
6
+ { field: :occurred_at, label: 'Timestamp', class: 'w-36' },
7
+ { field: :duration, label: 'Response Time', class: 'w-24' },
8
8
  { field: :status, label: 'HTTP Status', class: 'w-20' },
9
9
  { field: :status_indicator, label: 'Status', class: 'w-16' }
10
10
  ]
11
11
  %>
12
12
 
13
- <table class="table mbs-4">
13
+ <table class="table mbs-4" data-controller="rails-pulse--table-sort">
14
14
  <%= render "rails_pulse/components/table_head", columns: columns %>
15
15
 
16
16
  <tbody>
@@ -1,37 +1,38 @@
1
1
  <%= render 'rails_pulse/components/breadcrumbs' %>
2
2
 
3
- <div class="row">
4
- <%= cached_component(component: "metric_card", id: "average_response_times", context: "requests", class: "grid-item block") %>
5
- <%= cached_component(component: "metric_card", id: "percentile_response_times", context: "requests", class: "grid-item block") %>
6
- <%= cached_component(component: "metric_card", id: "request_count_totals", context: "requests", class: "grid-item block") %>
7
- <%= cached_component(component: "metric_card", id: "error_rate_per_route", context: "requests", class: "grid-item block") %>
8
- </div>
3
+ <% unless turbo_frame_request? %>
4
+ <div class="row">
5
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_response_times_metric_card } %>
6
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_response_times_metric_card } %>
7
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @request_count_totals_metric_card } %>
8
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @error_rate_per_route_metric_card } %>
9
+ </div>
10
+ <% end %>
9
11
 
10
12
  <div
11
- class="row"
12
13
  data-controller="rails-pulse--index"
13
14
  data-rails-pulse--index-chart-id-value="average_response_times_chart"
14
15
  >
15
- <div class="grid-item">
16
- <%= render 'rails_pulse/components/panel', { title: 'Average Response Time', } do %>
17
- <%= search_form_for @ransack_query, url: requests_path, class: "flex items-center justify-between gap mb-4" do |form| %>
18
- <div class="flex items-center grow gap">
19
- <%= form.select :occurred_at_range,
20
- RailsPulse::RequestsController::TIME_RANGE_OPTIONS,
21
- { selected: @selected_time_range },
22
- { class: "input" }
23
- %>
24
- <%= form.select :duration,
25
- duration_options(:request),
26
- { selected: @selected_response_range },
27
- { class: "input" }
28
- %>
29
- <%= link_to "Reset", requests_path, class: "btn btn--borderless show@md" if params.has_key?(:q) %>
30
- <%= form.submit "Search", class: "btn show@sm" %>
31
- </div>
32
- <% end %>
16
+ <%= render 'rails_pulse/components/panel', { title: 'Average Response Time', } do %>
17
+ <%= search_form_for @ransack_query, url: requests_path, class: "flex items-center justify-between gap mb-4" do |form| %>
18
+ <div class="flex items-center grow gap">
19
+ <%= form.select :period_start_range,
20
+ RailsPulse::RequestsController::TIME_RANGE_OPTIONS,
21
+ { selected: @selected_time_range },
22
+ { class: "input" }
23
+ %>
24
+ <%= form.select :avg_duration,
25
+ duration_options(:request),
26
+ { selected: @selected_response_range },
27
+ { class: "input" }
28
+ %>
29
+ <%= link_to "Reset", requests_path, class: "btn btn--borderless show@md" if params.has_key?(:q) %>
30
+ <%= form.submit "Search", class: "btn show@sm" %>
31
+ </div>
32
+ <% end %>
33
33
 
34
- <% if @chart_data.present? %>
34
+ <% if @has_data %>
35
+ <% if @chart_data && @chart_data.values.any? { |v| v > 0 } %>
35
36
  <div
36
37
  class="chart-container chart-container--slim"
37
38
  data-rails-pulse--index-target="chart"
@@ -56,9 +57,13 @@
56
57
  </div>
57
58
  <% end %>
58
59
 
59
- <%= turbo_frame_tag :requests_index_table, data: { rails_pulse__index_target: "indexTable" } do %>
60
+ <%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
60
61
  <%= render 'rails_pulse/requests/table' %>
61
62
  <% end %>
63
+ <% else %>
64
+ <%= render 'rails_pulse/components/empty_state',
65
+ title: 'No request data found for the selected filters.',
66
+ description: 'Try adjusting your time range or filters to see results.' %>
62
67
  <% end %>
63
- </div>
68
+ <% end %>
64
69
  </div>
@@ -1,26 +1,26 @@
1
1
  <% columns = [
2
- { field: :path, label: 'Route', class: 'w-auto' },
3
- { field: :average_response_time_ms, label: 'Average Response Time', class: 'w-36' },
4
- { field: :max_response_time_ms, label: 'Max Response Time', class: 'w-32' },
5
- { field: :request_count, label: 'Requests', class: 'w-24' },
2
+ { field: :route_path, label: 'Route', class: 'w-auto' },
3
+ { field: :avg_duration_sort, label: 'Average Response Time', class: 'w-36' },
4
+ { field: :max_duration_sort, label: 'Max Response Time', class: 'w-32' },
5
+ { field: :count_sort, label: 'Requests', class: 'w-24' },
6
6
  { field: :requests_per_minute, label: 'Requests Per Minute', class: 'w-28' },
7
7
  { field: :error_rate_percentage, label: 'Error Rate (%)', class: 'w-20' },
8
- { field: :status_indicator, label: 'Status', class: 'w-16' }
8
+ { field: :status_indicator, label: 'Status', class: 'w-16', sortable: false }
9
9
  ] %>
10
10
 
11
- <table class="table mbs-4">
11
+ <table class="table mbs-4" data-controller="rails-pulse--table-sort">
12
12
  <%= render "rails_pulse/components/table_head", columns: columns %>
13
13
 
14
14
  <tbody>
15
- <% @table_data.each do |route| %>
15
+ <% @table_data.each do |summary| %>
16
16
  <tr>
17
- <td class="whitespace-nowrap"><%= link_to route.path_and_method, route, data: { turbo_frame: '_top' } %></td>
18
- <td class="whitespace-nowrap"><%= route.average_response_time_ms.to_i %> ms</td>
19
- <td class="whitespace-nowrap"><%= route.max_response_time_ms.to_i %> ms</td>
20
- <td class="whitespace-nowrap"><%= number_with_delimiter route.request_count %></td>
21
- <td class="whitespace-nowrap"><%= route.requests_per_minute < 1 ? '< 1' : route.requests_per_minute.round(2) %></td>
22
- <td class="whitespace-nowrap"><%= route.error_rate_percentage %>%</td>
23
- <td class="whitespace-nowrap text-center"><%= route_status_indicator(route.status_indicator) %></td>
17
+ <td class="whitespace-nowrap"><%= link_to "#{summary.path} #{summary.route_method}", route_path(summary.route_id), data: { turbo_frame: '_top' } %></td>
18
+ <td class="whitespace-nowrap"><%= summary.avg_duration.to_i %> ms</td>
19
+ <td class="whitespace-nowrap"><%= summary.max_duration.to_i %> ms</td>
20
+ <td class="whitespace-nowrap"><%= number_with_delimiter summary.count %></td>
21
+ <td class="whitespace-nowrap"><%= summary.count < 1 ? '< 1' : (summary.count / 60.0).round(2) %></td>
22
+ <td class="whitespace-nowrap"><%= ((summary.error_count.to_f / summary.count) * 100).round(2) %>%</td>
23
+ <td class="whitespace-nowrap text-center"><%= route_status_indicator(summary.avg_duration >= 500 ? 1 : 0) %></td>
24
24
  </tr>
25
25
  <% end %>
26
26
  </tbody>
@@ -1,38 +1,39 @@
1
1
  <%= render 'rails_pulse/components/breadcrumbs' %>
2
2
 
3
- <div class="row">
4
- <%= cached_component(component: "metric_card", id: "average_response_times", context: "routes", class: "grid-item block") %>
5
- <%= cached_component(component: "metric_card", id: "percentile_response_times", context: "routes", class: "grid-item block") %>
6
- <%= cached_component(component: "metric_card", id: "request_count_totals", context: "routes", class: "grid-item block") %>
7
- <%= cached_component(component: "metric_card", id: "error_rate_per_route", context: "routes", class: "grid-item block") %>
8
- </div>
3
+ <% unless turbo_frame_request? %>
4
+ <div class="row">
5
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
6
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_response_times_metric_card } %>
7
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @request_count_totals_metric_card } %>
8
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @error_rate_per_route_metric_card } %>
9
+ </div>
10
+ <% end %>
9
11
 
10
12
  <div
11
- class="row"
12
13
  data-controller="rails-pulse--index"
13
14
  data-rails-pulse--index-chart-id-value="average_response_times_chart"
14
15
  >
15
- <div class="grid-item">
16
- <%= render 'rails_pulse/components/panel', { title: 'Average Response Time', } do %>
17
- <%= search_form_for @ransack_query, url: routes_path, class: "flex items-center justify-between gap mb-4" do |form| %>
18
- <div class="flex items-center grow gap">
19
- <%= form.search_field :path_cont, placeholder: "Filter by route", autocomplete: "off", class: "input", style: "max-inline-size: 250px" %>
20
- <%= form.select :occurred_at_range,
21
- RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
22
- { selected: @selected_time_range },
23
- { class: "input" }
24
- %>
25
- <%= form.select :duration,
26
- duration_options(:route),
27
- { selected: @selected_response_range },
28
- { class: "input" }
29
- %>
30
- <%= link_to "Reset", routes_path, class: "btn btn--borderless show@md" if params.has_key?(:q) %>
31
- <%= form.submit "Search", class: "btn show@sm" %>
32
- </div>
33
- <% end %>
16
+ <%= render 'rails_pulse/components/panel', { title: 'Average Response Time', card_classes: 'b-full' } do %>
17
+ <%= search_form_for @ransack_query, url: routes_path, class: "flex items-center justify-between gap mb-4" do |form| %>
18
+ <div class="flex items-center grow gap">
19
+ <%= form.search_field :route_path_cont, placeholder: "Filter by route", autocomplete: "off", class: "input", style: "max-inline-size: 250px" %>
20
+ <%= form.select :period_start_range,
21
+ RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
22
+ { selected: @selected_time_range },
23
+ { class: "input" }
24
+ %>
25
+ <%= form.select :avg_duration,
26
+ duration_options(:route),
27
+ { selected: @selected_response_range },
28
+ { class: "input" }
29
+ %>
30
+ <%= link_to "Reset", routes_path, class: "btn btn--borderless show@md" if params.has_key?(:q) %>
31
+ <%= form.submit "Search", class: "btn show@sm" %>
32
+ </div>
33
+ <% end %>
34
34
 
35
- <% if @chart_data.present? %>
35
+ <% if @has_data %>
36
+ <% if @chart_data && @chart_data.values.any? { |v| v > 0 } %>
36
37
  <div
37
38
  class="chart-container chart-container--slim"
38
39
  data-rails-pulse--index-target="chart"
@@ -57,9 +58,13 @@
57
58
  </div>
58
59
  <% end %>
59
60
 
60
- <%= turbo_frame_tag :routes_index_table, data: { rails_pulse__index_target: "indexTable" } do %>
61
+ <%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
61
62
  <%= render 'rails_pulse/routes/table' %>
62
63
  <% end %>
64
+ <% else %>
65
+ <%= render 'rails_pulse/components/empty_state',
66
+ title: 'No route data found for the selected filters.',
67
+ description: 'Try adjusting your time range or filters to see results.' %>
63
68
  <% end %>
64
- </div>
69
+ <% end %>
65
70
  </div>
@@ -1,30 +1,31 @@
1
1
  <%= render 'rails_pulse/components/breadcrumbs' %>
2
2
 
3
- <h1 class="text-2xl mbe-2"><%= @route.path_and_method %></h1>
3
+ <h1 class="text-2xl mis-2"><%= @route.path_and_method %></h1>
4
4
 
5
- <div class="row">
6
- <%= cached_component(component: "metric_card", id: "average_response_times", context: "routes_#{@route.id}", class: "grid-item block") %>
7
- <%= cached_component(component: "metric_card", id: "percentile_response_times", context: "routes_#{@route.id}", class: "grid-item block") %>
8
- <%= cached_component(component: "metric_card", id: "request_count_totals", context: "routes_#{@route.id}", class: "grid-item block") %>
9
- <%= cached_component(component: "metric_card", id: "error_rate_per_route", context: "routes_#{@route.id}", class: "grid-item block") %>
10
- </div>
5
+ <% unless turbo_frame_request? %>
6
+ <div class="row">
7
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
8
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_response_times_metric_card } %>
9
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @request_count_totals_metric_card } %>
10
+ <%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @error_rate_per_route_metric_card } %>
11
+ </div>
12
+ <% end %>
11
13
 
12
14
  <div
13
15
  class="row"
14
16
  data-controller="rails-pulse--index"
15
17
  data-rails-pulse--index-chart-id-value="route_repsonses_chart"
16
- data-rails-pulse--index-occurred-at-param-value="occurred_at"
17
18
  >
18
19
  <div class="grid-item">
19
20
  <%= render 'rails_pulse/components/panel', { title: 'Route Reqeusts', } do %>
20
21
  <%= search_form_for @ransack_query, url: route_path(@route), class: "flex items-center justify-between gap mb-4" do |form| %>
21
22
  <div class="flex items-center grow gap">
22
- <%= form.select :occurred_at_range,
23
+ <%= form.select :period_start_range,
23
24
  RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
24
25
  { selected: @selected_time_range },
25
26
  { class: "input" }
26
27
  %>
27
- <%= form.select :duration,
28
+ <%= form.select :avg_duration,
28
29
  duration_options(:route),
29
30
  { selected: @selected_response_range },
30
31
  { class: "input" }
@@ -34,33 +35,39 @@
34
35
  </div>
35
36
  <% end %>
36
37
 
37
- <% if @chart_data.present? %>
38
- <div
39
- class="chart-container chart-container--slim"
40
- data-rails-pulse--index-target="chart"
41
- >
42
- <%= bar_chart(
43
- @chart_data,
44
- code: false,
45
- id: "route_repsonses_chart",
46
- height: "100%",
47
- options: bar_chart_options(
48
- units: "ms",
49
- zoom: true,
50
- chart_start: 0,
51
- chart_end: @chart_data.length - 1,
52
- xaxis_formatter: @xaxis_formatter,
53
- tooltip_formatter: @tooltip_formatter,
54
- zoom_start: @zoom_start,
55
- zoom_end: @zoom_end,
56
- chart_data: @chart_data
57
- )
58
- ) %>
59
- </div>
60
- <% end %>
38
+ <% if @has_data %>
39
+ <% if @chart_data && @chart_data.values.any? { |v| v > 0 } %>
40
+ <div
41
+ class="chart-container chart-container--slim"
42
+ data-rails-pulse--index-target="chart"
43
+ >
44
+ <%= bar_chart(
45
+ @chart_data,
46
+ code: false,
47
+ id: "route_repsonses_chart",
48
+ height: "100%",
49
+ options: bar_chart_options(
50
+ units: "ms",
51
+ zoom: true,
52
+ chart_start: 0,
53
+ chart_end: @chart_data.length - 1,
54
+ xaxis_formatter: @xaxis_formatter,
55
+ tooltip_formatter: @tooltip_formatter,
56
+ zoom_start: @zoom_start,
57
+ zoom_end: @zoom_end,
58
+ chart_data: @chart_data
59
+ )
60
+ ) %>
61
+ </div>
62
+ <% end %>
61
63
 
62
- <%= turbo_frame_tag :index_table do %>
63
- <%= render 'rails_pulse/requests/table' %>
64
+ <%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
65
+ <%= render 'rails_pulse/requests/table' %>
66
+ <% end %>
67
+ <% else %>
68
+ <%= render 'rails_pulse/components/empty_state',
69
+ title: 'No route requests found for the selected filters.',
70
+ description: 'Try adjusting your time range or filters to see results.' %>
64
71
  <% end %>
65
72
  <% end %>
66
73
  </div>
@@ -71,18 +71,6 @@ RailsPulse.configure do |config|
71
71
  config.ignored_requests = []
72
72
  config.ignored_queries = []
73
73
 
74
- # ====================================================================================================
75
- # CACHING
76
- # ====================================================================================================
77
- # Configure metric card caching to improve performance of the Rails Pulse dashboard.
78
- # Caching reduces database load for expensive metric calculations.
79
-
80
- # Enable/disable metric card caching
81
- config.component_cache_enabled = true
82
-
83
- # How long to cache metric card results
84
- config.component_cache_duration = 1.hour
85
-
86
74
  # ====================================================================================================
87
75
  # DATABASE CONFIGURATION
88
76
  # ====================================================================================================
data/config/routes.rb CHANGED
@@ -3,7 +3,11 @@ RailsPulse::Engine.routes.draw do
3
3
 
4
4
  resources :routes, only: %i[index show]
5
5
  resources :requests, only: %i[index show]
6
- resources :queries, only: %i[index show]
6
+ resources :queries, only: %i[index show] do
7
+ member do
8
+ post :analyze
9
+ end
10
+ end
7
11
  resources :operations, only: %i[show]
8
12
  resources :caches, only: %i[show], as: :cache
9
13
  patch "pagination/limit", to: "application#set_pagination_limit"