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,29 +2,30 @@
2
2
 
3
3
  <h1 class="text-2xl mbe-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 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
  # ====================================================================================================
@@ -0,0 +1,54 @@
1
+ class CreateRailsPulseSummaries < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :rails_pulse_summaries do |t|
4
+ # Time fields
5
+ t.datetime :period_start, null: false
6
+ t.datetime :period_end, null: false
7
+ t.string :period_type, null: false # 'hour', 'day', 'week', 'month'
8
+
9
+ # Polymorphic association to handle both routes and queries
10
+ t.references :summarizable, polymorphic: true, null: false, index: true
11
+ # This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
12
+ # and summarizable_id (route_id or query_id)
13
+
14
+ # Universal metrics
15
+ t.integer :count, default: 0, null: false
16
+ t.float :avg_duration
17
+ t.float :min_duration
18
+ t.float :max_duration
19
+ t.float :p50_duration
20
+ t.float :p95_duration
21
+ t.float :p99_duration
22
+ t.float :total_duration
23
+ t.float :stddev_duration
24
+
25
+ # Request/Route specific metrics
26
+ t.integer :error_count, default: 0
27
+ t.integer :success_count, default: 0
28
+ t.integer :status_2xx, default: 0
29
+ t.integer :status_3xx, default: 0
30
+ t.integer :status_4xx, default: 0
31
+ t.integer :status_5xx, default: 0
32
+
33
+ t.timestamps
34
+
35
+ # Unique constraint and indexes
36
+ t.index [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
37
+ unique: true,
38
+ name: 'idx_pulse_summaries_unique'
39
+ t.index [ :period_type, :period_start ]
40
+ t.index :created_at
41
+ end
42
+
43
+ # Add indexes to existing tables for efficient aggregation
44
+ add_index :rails_pulse_requests, [ :created_at, :route_id ],
45
+ name: 'idx_requests_for_aggregation'
46
+ add_index :rails_pulse_requests, :created_at,
47
+ name: 'idx_requests_created_at'
48
+
49
+ add_index :rails_pulse_operations, [ :created_at, :query_id ],
50
+ name: 'idx_operations_for_aggregation'
51
+ add_index :rails_pulse_operations, :created_at,
52
+ name: 'idx_operations_created_at'
53
+ end
54
+ end
@@ -0,0 +1,121 @@
1
+ # Rails Pulse Database Schema
2
+ # This file contains the complete schema for Rails Pulse tables
3
+ # Load with: rails db:schema:load:rails_pulse or db:prepare
4
+
5
+ RailsPulse::Schema = lambda do |connection|
6
+ # Skip if all tables already exist to prevent conflicts
7
+ required_tables = [:rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_summaries]
8
+
9
+ if ENV["CI"] == "true"
10
+ existing_tables = required_tables.select { |table| connection.table_exists?(table) }
11
+ missing_tables = required_tables - existing_tables
12
+ puts "[RailsPulse::Schema] Existing tables: #{existing_tables.join(', ')}" if existing_tables.any?
13
+ puts "[RailsPulse::Schema] Missing tables: #{missing_tables.join(', ')}" if missing_tables.any?
14
+ end
15
+
16
+ return if required_tables.all? { |table| connection.table_exists?(table) }
17
+
18
+ connection.create_table :rails_pulse_routes do |t|
19
+ t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
20
+ t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
21
+ t.timestamps
22
+ end
23
+
24
+ connection.add_index :rails_pulse_routes, [ :method, :path ], unique: true, name: "index_rails_pulse_routes_on_method_and_path"
25
+
26
+ connection.create_table :rails_pulse_queries do |t|
27
+ t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
28
+ t.timestamps
29
+ end
30
+
31
+ connection.add_index :rails_pulse_queries, :normalized_sql, unique: true, name: "index_rails_pulse_queries_on_normalized_sql", length: 191
32
+
33
+ connection.create_table :rails_pulse_requests do |t|
34
+ t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
35
+ t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
36
+ t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
37
+ t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
38
+ t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
39
+ t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
40
+ t.timestamp :occurred_at, null: false, comment: "When the request started"
41
+ t.timestamps
42
+ end
43
+
44
+ connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
45
+ connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
46
+ connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
47
+
48
+ connection.create_table :rails_pulse_operations do |t|
49
+ t.references :request, null: false, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
50
+ t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
51
+ t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
52
+ t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
53
+ t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
54
+ t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
55
+ t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
56
+ t.timestamp :occurred_at, null: false, comment: "When the request started"
57
+ t.timestamps
58
+ end
59
+
60
+ connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
61
+ connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
62
+ connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
63
+ connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
64
+ connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
65
+
66
+ connection.create_table :rails_pulse_summaries do |t|
67
+ # Time fields
68
+ t.datetime :period_start, null: false, comment: "Start of the aggregation period"
69
+ t.datetime :period_end, null: false, comment: "End of the aggregation period"
70
+ t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
71
+
72
+ # Polymorphic association to handle both routes and queries
73
+ t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
74
+ # This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
75
+ # and summarizable_id (route_id or query_id)
76
+
77
+ # Universal metrics
78
+ t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
79
+ t.float :avg_duration, comment: "Average duration in milliseconds"
80
+ t.float :min_duration, comment: "Minimum duration in milliseconds"
81
+ t.float :max_duration, comment: "Maximum duration in milliseconds"
82
+ t.float :p50_duration, comment: "50th percentile duration"
83
+ t.float :p95_duration, comment: "95th percentile duration"
84
+ t.float :p99_duration, comment: "99th percentile duration"
85
+ t.float :total_duration, comment: "Total duration in milliseconds"
86
+ t.float :stddev_duration, comment: "Standard deviation of duration"
87
+
88
+ # Request/Route specific metrics
89
+ t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
90
+ t.integer :success_count, default: 0, comment: "Number of successful responses"
91
+ t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
92
+ t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
93
+ t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
94
+ t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
95
+
96
+ t.timestamps
97
+ end
98
+
99
+ # Unique constraint and indexes for summaries
100
+ connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
101
+ unique: true,
102
+ name: "idx_pulse_summaries_unique"
103
+ connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
104
+ connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
105
+
106
+ # Add indexes to existing tables for efficient aggregation
107
+ connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
108
+ connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
109
+
110
+ connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
111
+ connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
112
+
113
+ if ENV["CI"] == "true"
114
+ created_tables = required_tables.select { |table| connection.table_exists?(table) }
115
+ puts "[RailsPulse::Schema] Successfully created tables: #{created_tables.join(', ')}"
116
+ end
117
+ end
118
+
119
+ if defined?(RailsPulse::ApplicationRecord)
120
+ RailsPulse::Schema.call(RailsPulse::ApplicationRecord.connection)
121
+ end
@@ -3,15 +3,52 @@ module RailsPulse
3
3
  class InstallGenerator < Rails::Generators::Base
4
4
  source_root File.expand_path("templates", __dir__)
5
5
 
6
- desc "Copies Rails Pulse migrations to the application."
7
- def copy_migrations
8
- rake "rails_pulse:install:migrations"
6
+ desc "Install Rails Pulse with schema-based setup"
7
+
8
+ def copy_schema
9
+ copy_file "db/rails_pulse_schema.rb", "db/rails_pulse_schema.rb"
9
10
  end
10
11
 
11
- desc "Copies Rails Pulse example configuration file to the application."
12
12
  def copy_initializer
13
13
  copy_file "rails_pulse.rb", "config/initializers/rails_pulse.rb"
14
14
  end
15
+
16
+ def create_database_migration_paths
17
+ if separate_database?
18
+ create_file "db/rails_pulse_migrate/.keep"
19
+ end
20
+ end
21
+
22
+ def display_post_install_message
23
+ say <<~MESSAGE
24
+
25
+ Rails Pulse installation complete!
26
+
27
+ Next steps:
28
+ 1. Configure your database in config/database.yml (see README for examples)
29
+ 2. Run: rails db:prepare (creates database and loads schema)
30
+ 3. Restart your Rails server
31
+
32
+ For separate database setup, add to config/database.yml:
33
+ #{environment}:
34
+ rails_pulse:
35
+ <<: *default
36
+ database: storage/#{environment}_rails_pulse.sqlite3
37
+ migrations_paths: db/rails_pulse_migrate
38
+
39
+ MESSAGE
40
+ end
41
+
42
+ private
43
+
44
+ def separate_database?
45
+ # Could make this configurable via options
46
+ false
47
+ end
48
+
49
+ def environment
50
+ Rails.env.production? ? "production" : "development"
51
+ end
15
52
  end
16
53
  end
17
54
  end
@@ -0,0 +1,60 @@
1
+ # Rails Pulse Database Schema
2
+ # This file contains the complete schema for Rails Pulse tables
3
+ # Load with: rails db:schema:load:rails_pulse or db:prepare
4
+
5
+ RailsPulse::Schema = lambda do |connection|
6
+ # Skip if tables already exist to prevent conflicts
7
+ return if connection.table_exists?(:rails_pulse_routes)
8
+
9
+ connection.create_table :rails_pulse_routes do |t|
10
+ t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
11
+ t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
12
+ t.timestamps
13
+ end
14
+
15
+ connection.add_index :rails_pulse_routes, [ :method, :path ], unique: true, name: "index_rails_pulse_routes_on_method_and_path"
16
+
17
+ connection.create_table :rails_pulse_queries do |t|
18
+ t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
19
+ t.timestamps
20
+ end
21
+
22
+ connection.add_index :rails_pulse_queries, :normalized_sql, unique: true, name: "index_rails_pulse_queries_on_normalized_sql", length: 191
23
+
24
+ connection.create_table :rails_pulse_requests do |t|
25
+ t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
26
+ t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
27
+ t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
28
+ t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
29
+ t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
30
+ t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
31
+ t.timestamp :occurred_at, null: false, comment: "When the request started"
32
+ t.timestamps
33
+ end
34
+
35
+ connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
36
+ connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
37
+ connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
38
+
39
+ connection.create_table :rails_pulse_operations do |t|
40
+ t.references :request, null: false, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
41
+ t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
42
+ t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
43
+ t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
44
+ t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
45
+ t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
46
+ t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
47
+ t.timestamp :occurred_at, null: false, comment: "When the request started"
48
+ t.timestamps
49
+ end
50
+
51
+ connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
52
+ connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
53
+ connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
54
+ connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
55
+ connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
56
+ end
57
+
58
+ if defined?(RailsPulse::ApplicationRecord)
59
+ RailsPulse::Schema.call(RailsPulse::ApplicationRecord.connection)
60
+ end
@@ -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
  # ====================================================================================================
@@ -13,8 +13,6 @@ module RailsPulse
13
13
  :full_retention_period,
14
14
  :archiving_enabled,
15
15
  :max_table_records,
16
- :component_cache_enabled,
17
- :component_cache_duration,
18
16
  :connects_to,
19
17
  :authentication_enabled,
20
18
  :authentication_method,
@@ -39,8 +37,6 @@ module RailsPulse
39
37
  rails_pulse_routes: 1000,
40
38
  rails_pulse_queries: 500
41
39
  }
42
- @component_cache_enabled = true
43
- @component_cache_duration = 1.hour
44
40
  @connects_to = nil
45
41
  @authentication_enabled = Rails.env.production?
46
42
  @authentication_method = nil
@@ -66,7 +62,6 @@ module RailsPulse
66
62
  validate_thresholds!
67
63
  validate_retention_settings!
68
64
  validate_patterns!
69
- validate_cache_settings!
70
65
  validate_database_settings!
71
66
  validate_authentication_settings!
72
67
  end
@@ -119,12 +114,6 @@ module RailsPulse
119
114
  end
120
115
  end
121
116
 
122
- def validate_cache_settings!
123
- unless @component_cache_duration.respond_to?(:seconds)
124
- raise ArgumentError, "component_cache_duration must be a time duration (e.g., 1.hour), got #{@component_cache_duration}"
125
- end
126
- end
127
-
128
117
  def validate_database_settings!
129
118
  if @connects_to && !@connects_to.is_a?(Hash)
130
119
  raise ArgumentError, "connects_to must be a hash with database connection configuration"
@@ -1,5 +1,4 @@
1
1
  require "rails_pulse/version"
2
- require "rails_pulse/migration"
3
2
  require "rails_pulse/middleware/request_collector"
4
3
  require "rails_pulse/middleware/asset_server"
5
4
  require "rails_pulse/subscribers/operation_subscriber"
@@ -1,3 +1,3 @@
1
1
  module RailsPulse
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -0,0 +1,58 @@
1
+ namespace :db do
2
+ namespace :schema do
3
+ desc "Load Rails Pulse schema"
4
+ task load_rails_pulse: :environment do
5
+ schema_file = Rails.root.join("db/rails_pulse_schema.rb")
6
+ if schema_file.exist?
7
+ load schema_file
8
+ puts "Rails Pulse schema loaded successfully"
9
+ else
10
+ puts "Rails Pulse schema file not found. Run: rails generate rails_pulse:install"
11
+ end
12
+ end
13
+ end
14
+
15
+ # Hook into common database tasks to load schema
16
+ task prepare: "schema:load_rails_pulse"
17
+ task setup: "schema:load_rails_pulse"
18
+ end
19
+
20
+ namespace :rails_pulse do
21
+ desc "Backfill summary data from existing requests and operations"
22
+ task backfill_summaries: :environment do
23
+ puts "Starting Rails Pulse summary backfill..."
24
+
25
+ # Find earliest data
26
+ earliest_request = RailsPulse::Request.minimum(:occurred_at)
27
+ earliest_operation = RailsPulse::Operation.minimum(:occurred_at)
28
+
29
+ historical_start_time = if earliest_request && earliest_operation
30
+ [ earliest_request, earliest_operation ].min.beginning_of_day
31
+ elsif earliest_request
32
+ earliest_request.beginning_of_day
33
+ elsif earliest_operation
34
+ earliest_operation.beginning_of_day
35
+ else
36
+ puts "No Rails Pulse data found - skipping summary generation"
37
+ return
38
+ end
39
+
40
+ historical_end_time = Time.current
41
+
42
+ # Generate daily summaries from beginning of data
43
+ puts "\nCreating daily summaries from #{historical_start_time.strftime('%B %d, %Y')} to #{historical_end_time.strftime('%B %d, %Y')}"
44
+ RailsPulse::BackfillSummariesJob.perform_now(historical_start_time, historical_end_time, [ "day" ])
45
+
46
+ # Generate hourly summaries for past 26 hours
47
+ puts "\nCreating hourly summaries for the past 26 hours..."
48
+ hourly_start_time = 26.hours.ago
49
+ hourly_end_time = Time.current
50
+
51
+ puts "From #{hourly_start_time.strftime('%B %d at %I:%M %p')} to #{hourly_end_time.strftime('%B %d at %I:%M %p')}"
52
+ RailsPulse::BackfillSummariesJob.perform_now(hourly_start_time, hourly_end_time, [ "hour" ])
53
+
54
+ puts "\nSummary backfill completed!"
55
+ puts "Total summaries: #{RailsPulse::Summary.count}"
56
+ puts "\nTo keep summaries up to date, schedule RailsPulse::SummaryJob to run hourly"
57
+ end
58
+ end