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
@@ -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,13 @@
1
+ class AddAnalysisToRailsPulseQueries < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :rails_pulse_queries, :analyzed_at, :datetime, comment: "When query analysis was last performed"
4
+ add_column :rails_pulse_queries, :explain_plan, :text, comment: "EXPLAIN output from actual SQL execution"
5
+ add_column :rails_pulse_queries, :issues, :text, comment: "JSON array of detected performance issues"
6
+ add_column :rails_pulse_queries, :metadata, :text, comment: "JSON object containing query complexity metrics"
7
+ add_column :rails_pulse_queries, :query_stats, :text, comment: "JSON object with query characteristics analysis"
8
+ add_column :rails_pulse_queries, :backtrace_analysis, :text, comment: "JSON object with call chain and N+1 detection"
9
+ add_column :rails_pulse_queries, :suggestions, :text, comment: "JSON array of optimization recommendations"
10
+ add_column :rails_pulse_queries, :index_recommendations, :text, comment: "JSON array of database index recommendations"
11
+ add_column :rails_pulse_queries, :n_plus_one_analysis, :text, comment: "JSON object with enhanced N+1 query detection results"
12
+ end
13
+ end
@@ -0,0 +1,130 @@
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.datetime :analyzed_at, comment: "When query analysis was last performed"
29
+ t.text :explain_plan, comment: "EXPLAIN output from actual SQL execution"
30
+ t.text :issues, comment: "JSON array of detected performance issues"
31
+ t.text :metadata, comment: "JSON object containing query complexity metrics"
32
+ t.text :query_stats, comment: "JSON object with query characteristics analysis"
33
+ t.text :backtrace_analysis, comment: "JSON object with call chain and N+1 detection"
34
+ t.text :index_recommendations, comment: "JSON array of database index recommendations"
35
+ t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
36
+ t.text :suggestions, comment: "JSON array of optimization recommendations"
37
+ t.timestamps
38
+ end
39
+
40
+ connection.add_index :rails_pulse_queries, :normalized_sql, unique: true, name: "index_rails_pulse_queries_on_normalized_sql", length: 191
41
+
42
+ connection.create_table :rails_pulse_requests do |t|
43
+ t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
44
+ t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
45
+ t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
46
+ t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
47
+ t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
48
+ t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
49
+ t.timestamp :occurred_at, null: false, comment: "When the request started"
50
+ t.timestamps
51
+ end
52
+
53
+ connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
54
+ connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
55
+ connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
56
+
57
+ connection.create_table :rails_pulse_operations do |t|
58
+ t.references :request, null: false, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
59
+ t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
60
+ t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
61
+ t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
62
+ t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
63
+ t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
64
+ t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
65
+ t.timestamp :occurred_at, null: false, comment: "When the request started"
66
+ t.timestamps
67
+ end
68
+
69
+ connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
70
+ connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
71
+ connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
72
+ connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
73
+ connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
74
+
75
+ connection.create_table :rails_pulse_summaries do |t|
76
+ # Time fields
77
+ t.datetime :period_start, null: false, comment: "Start of the aggregation period"
78
+ t.datetime :period_end, null: false, comment: "End of the aggregation period"
79
+ t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
80
+
81
+ # Polymorphic association to handle both routes and queries
82
+ t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
83
+ # This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
84
+ # and summarizable_id (route_id or query_id)
85
+
86
+ # Universal metrics
87
+ t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
88
+ t.float :avg_duration, comment: "Average duration in milliseconds"
89
+ t.float :min_duration, comment: "Minimum duration in milliseconds"
90
+ t.float :max_duration, comment: "Maximum duration in milliseconds"
91
+ t.float :p50_duration, comment: "50th percentile duration"
92
+ t.float :p95_duration, comment: "95th percentile duration"
93
+ t.float :p99_duration, comment: "99th percentile duration"
94
+ t.float :total_duration, comment: "Total duration in milliseconds"
95
+ t.float :stddev_duration, comment: "Standard deviation of duration"
96
+
97
+ # Request/Route specific metrics
98
+ t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
99
+ t.integer :success_count, default: 0, comment: "Number of successful responses"
100
+ t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
101
+ t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
102
+ t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
103
+ t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
104
+
105
+ t.timestamps
106
+ end
107
+
108
+ # Unique constraint and indexes for summaries
109
+ connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
110
+ unique: true,
111
+ name: "idx_pulse_summaries_unique"
112
+ connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
113
+ connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
114
+
115
+ # Add indexes to existing tables for efficient aggregation
116
+ connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
117
+ connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
118
+
119
+ connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
120
+ connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
121
+
122
+ if ENV["CI"] == "true"
123
+ created_tables = required_tables.select { |table| connection.table_exists?(table) }
124
+ puts "[RailsPulse::Schema] Successfully created tables: #{created_tables.join(', ')}"
125
+ end
126
+ end
127
+
128
+ if defined?(RailsPulse::ApplicationRecord)
129
+ RailsPulse::Schema.call(RailsPulse::ApplicationRecord.connection)
130
+ end
@@ -0,0 +1,65 @@
1
+ module RailsPulse
2
+ module Generators
3
+ class ConvertToMigrationsGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+ source_root File.expand_path("templates", __dir__)
6
+
7
+ desc "Convert Rails Pulse schema file to migrations for single database setup"
8
+
9
+ def self.next_migration_number(path)
10
+ next_migration_number = current_migration_number(path) + 1
11
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
12
+ end
13
+
14
+ def check_schema_file
15
+ unless File.exist?("db/rails_pulse_schema.rb")
16
+ say "No db/rails_pulse_schema.rb file found. Run 'rails generate rails_pulse:install' first.", :red
17
+ exit 1
18
+ end
19
+
20
+ if rails_pulse_tables_exist?
21
+ say "Rails Pulse tables already exist. No conversion needed.", :yellow
22
+ say "Use 'rails generate rails_pulse:upgrade' to update existing installation.", :blue
23
+ exit 0
24
+ end
25
+ end
26
+
27
+ def create_conversion_migration
28
+ say "Converting db/rails_pulse_schema.rb to migration...", :green
29
+
30
+ migration_template(
31
+ "migrations/install_rails_pulse_tables.rb",
32
+ "db/migrate/install_rails_pulse_tables.rb"
33
+ )
34
+ end
35
+
36
+ def display_completion_message
37
+ say <<~MESSAGE
38
+
39
+ Conversion complete!
40
+
41
+ Next steps:
42
+ 1. Run: rails db:migrate
43
+ 2. Delete: db/rails_pulse_schema.rb (no longer needed)
44
+ 3. Remove db/rails_pulse_migrate/ directory if it exists
45
+ 4. Restart your Rails server
46
+
47
+ Future Rails Pulse updates will come as regular migrations.
48
+
49
+ MESSAGE
50
+ end
51
+
52
+ private
53
+
54
+ def rails_pulse_tables_exist?
55
+ return false unless defined?(ActiveRecord::Base)
56
+
57
+ connection = ActiveRecord::Base.connection
58
+ %w[rails_pulse_routes rails_pulse_queries rails_pulse_requests rails_pulse_operations rails_pulse_summaries]
59
+ .all? { |table| connection.table_exists?(table) }
60
+ rescue
61
+ false
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,17 +1,107 @@
1
1
  module RailsPulse
2
2
  module Generators
3
3
  class InstallGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
4
5
  source_root File.expand_path("templates", __dir__)
5
6
 
6
- desc "Copies Rails Pulse migrations to the application."
7
- def copy_migrations
8
- rake "rails_pulse:install:migrations"
7
+ desc "Install Rails Pulse with flexible database setup options"
8
+
9
+ class_option :database, type: :string, default: "single",
10
+ desc: "Database setup: 'single' (default) or 'separate'"
11
+
12
+ def self.next_migration_number(path)
13
+ next_migration_number = current_migration_number(path) + 1
14
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
15
+ end
16
+
17
+ def copy_schema
18
+ copy_file "db/rails_pulse_schema.rb", "db/rails_pulse_schema.rb"
9
19
  end
10
20
 
11
- desc "Copies Rails Pulse example configuration file to the application."
12
21
  def copy_initializer
13
22
  copy_file "rails_pulse.rb", "config/initializers/rails_pulse.rb"
14
23
  end
24
+
25
+ def setup_database_configuration
26
+ if separate_database?
27
+ create_separate_database_setup
28
+ else
29
+ create_single_database_setup
30
+ end
31
+ end
32
+
33
+ def display_post_install_message
34
+ if separate_database?
35
+ display_separate_database_message
36
+ else
37
+ display_single_database_message
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def separate_database?
44
+ options[:database] == "separate"
45
+ end
46
+
47
+ def create_separate_database_setup
48
+ create_file "db/rails_pulse_migrate/.keep"
49
+
50
+ say "Setting up separate database configuration...", :green
51
+
52
+ # Could add database.yml configuration here if needed
53
+ # For now, users will configure manually
54
+ end
55
+
56
+ def create_single_database_setup
57
+ say "Setting up single database configuration...", :green
58
+
59
+ # Create a migration that loads the schema
60
+ migration_template(
61
+ "migrations/install_rails_pulse_tables.rb",
62
+ "db/migrate/install_rails_pulse_tables.rb"
63
+ )
64
+ end
65
+
66
+ def display_separate_database_message
67
+ say <<~MESSAGE
68
+
69
+ Rails Pulse installation complete! (Separate Database Setup)
70
+
71
+ Next steps:
72
+ 1. Add Rails Pulse database configuration to config/database.yml:
73
+
74
+ #{Rails.env}:
75
+ rails_pulse:
76
+ <<: *default
77
+ database: storage/#{Rails.env}_rails_pulse.sqlite3
78
+ migrations_paths: db/rails_pulse_migrate
79
+
80
+ 2. Run: rails db:prepare (creates database and loads schema)
81
+ 3. Restart your Rails server
82
+
83
+ Future schema changes will come as regular migrations in db/rails_pulse_migrate/
84
+
85
+ MESSAGE
86
+ end
87
+
88
+ def display_single_database_message
89
+ say <<~MESSAGE
90
+
91
+ Rails Pulse installation complete! (Single Database Setup)
92
+
93
+ Next steps:
94
+ 1. Run: rails db:migrate (creates Rails Pulse tables in your main database)
95
+ 2. Delete: db/rails_pulse_schema.rb (no longer needed)
96
+ 3. Restart your Rails server
97
+
98
+ Future schema changes will come as regular migrations in db/migrate/
99
+
100
+ Note: The installation migration was created from db/rails_pulse_schema.rb
101
+ and includes all current Rails Pulse tables and columns.
102
+
103
+ MESSAGE
104
+ end
15
105
  end
16
106
  end
17
107
  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
@@ -0,0 +1,22 @@
1
+ # Generated from Rails Pulse schema - automatically loads current schema definition
2
+ class InstallRailsPulseTables < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
3
+ def change
4
+ # Load and execute the Rails Pulse schema directly
5
+ # This ensures the migration is always in sync with the schema file
6
+ schema_file = File.join(::Rails.root.to_s, "db/rails_pulse_schema.rb")
7
+
8
+ if File.exist?(schema_file)
9
+ say "Loading Rails Pulse schema from db/rails_pulse_schema.rb"
10
+
11
+ # Load the schema file to define RailsPulse::Schema
12
+ load schema_file
13
+
14
+ # Execute the schema in the context of this migration
15
+ RailsPulse::Schema.call(connection)
16
+
17
+ say "Rails Pulse tables created successfully"
18
+ else
19
+ raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # Upgrade Rails Pulse tables with new features
2
+ class UpgradeRailsPulseTables < ActiveRecord::Migration[<%= @migration_version %>]
3
+ def change
4
+ <% @missing_columns.each do |table_name, columns| %>
5
+ <% columns.each do |column_name, definition| %>
6
+ # Add <%= column_name %> column to <%= table_name %>
7
+ add_column :<%= table_name %>, :<%= column_name %>, :<%= definition[:type] %><% if definition[:comment] %>, comment: "<%= definition[:comment] %>"<% end %>
8
+ <% end %>
9
+ <% end %>
10
+ end
11
+
12
+ def down
13
+ <% @missing_columns.each do |table_name, columns| %>
14
+ <% columns.each do |column_name, _definition| %>
15
+ remove_column :<%= table_name %>, :<%= column_name %>
16
+ <% end %>
17
+ <% end %>
18
+ end
19
+ 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
  # ====================================================================================================