rails_pulse 0.3.3 → 0.4.0.pre.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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -0
  3. data/README.md +44 -3
  4. data/app/assets/stylesheets/rails_pulse/application.css +21 -32
  5. data/app/assets/stylesheets/rails_pulse/components/backtrace.css +238 -0
  6. data/app/assets/stylesheets/rails_pulse/components/badge.css +32 -0
  7. data/app/assets/stylesheets/rails_pulse/components/button.css +2 -36
  8. data/app/assets/stylesheets/rails_pulse/components/card.css +8 -11
  9. data/app/assets/stylesheets/rails_pulse/components/chart.css +16 -3
  10. data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +5 -39
  11. data/app/assets/stylesheets/rails_pulse/components/dashboard.css +0 -52
  12. data/app/assets/stylesheets/rails_pulse/components/deployments.css +39 -0
  13. data/app/assets/stylesheets/rails_pulse/components/dialog.css +0 -5
  14. data/app/assets/stylesheets/rails_pulse/components/input.css +0 -14
  15. data/app/assets/stylesheets/rails_pulse/components/layouts.css +0 -20
  16. data/app/assets/stylesheets/rails_pulse/components/menu.css +0 -16
  17. data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +0 -16
  18. data/app/assets/stylesheets/rails_pulse/components/storage.css +187 -0
  19. data/app/assets/stylesheets/rails_pulse/components/tags.css +0 -14
  20. data/app/assets/stylesheets/rails_pulse/components/time_range_selector.css +5 -5
  21. data/app/assets/stylesheets/rails_pulse/components/utilities.css +0 -17
  22. data/app/controllers/concerns/chart_table_concern.rb +15 -3
  23. data/app/controllers/concerns/ransack_params_concern.rb +18 -0
  24. data/app/controllers/concerns/response_range_concern.rb +1 -1
  25. data/app/controllers/concerns/time_range_concern.rb +28 -14
  26. data/app/controllers/rails_pulse/application_controller.rb +104 -31
  27. data/app/controllers/rails_pulse/dashboard_controller.rb +9 -0
  28. data/app/controllers/rails_pulse/deployments_controller.rb +26 -5
  29. data/app/controllers/rails_pulse/exception_occurrences_controller.rb +19 -0
  30. data/app/controllers/rails_pulse/exceptions_controller.rb +119 -0
  31. data/app/controllers/rails_pulse/job_runs_controller.rb +1 -1
  32. data/app/controllers/rails_pulse/requests_controller.rb +1 -1
  33. data/app/controllers/rails_pulse/routes_controller.rb +7 -1
  34. data/app/controllers/rails_pulse/storage_controller.rb +7 -0
  35. data/app/controllers/rails_pulse/tags_controller.rb +2 -2
  36. data/app/helpers/rails_pulse/application_helper.rb +1 -0
  37. data/app/helpers/rails_pulse/backtrace_helper.rb +85 -0
  38. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +4 -1
  39. data/app/helpers/rails_pulse/chart_helper.rb +0 -3
  40. data/app/helpers/rails_pulse/route_helper.rb +8 -16
  41. data/app/javascript/rails_pulse/application.js +29 -8
  42. data/app/javascript/rails_pulse/controllers/clipboard_controller.js +58 -0
  43. data/app/javascript/rails_pulse/controllers/index_controller.js +16 -10
  44. data/app/javascript/rails_pulse/controllers/pagination_controller.js +6 -2
  45. data/app/javascript/rails_pulse/controllers/popover_controller.js +4 -0
  46. data/app/models/concerns/rails_pulse/taggable.rb +11 -6
  47. data/app/models/rails_pulse/cards/base.rb +53 -0
  48. data/app/models/rails_pulse/dashboard/health_summary.rb +52 -0
  49. data/app/models/rails_pulse/dashboard/needs_attention.rb +73 -1
  50. data/app/models/rails_pulse/dashboard/storage_status.rb +490 -0
  51. data/app/models/rails_pulse/deployment.rb +47 -1
  52. data/app/models/rails_pulse/exception_group.rb +37 -0
  53. data/app/models/rails_pulse/exception_occurrence.rb +27 -0
  54. data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +116 -0
  55. data/app/models/rails_pulse/exceptions/cards/open_groups.rb +49 -0
  56. data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +62 -0
  57. data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +56 -0
  58. data/app/models/rails_pulse/exceptions/raw_data.rb +216 -0
  59. data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -2
  60. data/app/models/rails_pulse/operation.rb +13 -4
  61. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +8 -3
  62. data/app/models/rails_pulse/request.rb +5 -1
  63. data/app/models/rails_pulse/route.rb +71 -21
  64. data/app/models/rails_pulse/routes/cards/error_rates.rb +6 -3
  65. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +8 -3
  66. data/app/models/rails_pulse/routes/tables/index.rb +8 -6
  67. data/app/models/rails_pulse/summary.rb +21 -10
  68. data/app/models/rails_pulse/tables/base.rb +4 -2
  69. data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +47 -12
  70. data/app/services/rails_pulse/exception_capture_service.rb +228 -0
  71. data/app/services/rails_pulse/exception_message_sanitizer.rb +152 -0
  72. data/app/services/rails_pulse/operations/change_point.rb +144 -0
  73. data/app/services/rails_pulse/operations/compare.rb +92 -0
  74. data/app/services/rails_pulse/operations/comparison.rb +126 -0
  75. data/app/services/rails_pulse/operations/metric.rb +63 -0
  76. data/app/services/rails_pulse/operations/series.rb +84 -0
  77. data/app/services/rails_pulse/operations/subject.rb +77 -0
  78. data/app/services/rails_pulse/route_controller_action_backfiller.rb +110 -0
  79. data/app/services/rails_pulse/route_merger.rb +99 -0
  80. data/app/services/rails_pulse/route_migrator.rb +139 -0
  81. data/app/services/rails_pulse/route_path_normalizer.rb +79 -0
  82. data/app/services/rails_pulse/route_recognizer.rb +47 -0
  83. data/app/services/rails_pulse/summary_service.rb +45 -0
  84. data/app/services/rails_pulse/tag_filter_service.rb +5 -3
  85. data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
  86. data/app/views/layouts/rails_pulse/_onboarding_banner.html.erb +8 -0
  87. data/app/views/layouts/rails_pulse/_setup_screen.html.erb +5 -5
  88. data/app/views/layouts/rails_pulse/application.html.erb +4 -4
  89. data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
  90. data/app/views/rails_pulse/components/_panel.html.erb +16 -6
  91. data/app/views/rails_pulse/components/_raw_data.html.erb +34 -0
  92. data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -8
  93. data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +64 -0
  94. data/app/views/rails_pulse/dashboard/_health_badge.html.erb +21 -10
  95. data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +67 -0
  96. data/app/views/rails_pulse/dashboard/index.html.erb +22 -1
  97. data/app/views/rails_pulse/deployments/_status_badge.html.erb +5 -0
  98. data/app/views/rails_pulse/deployments/_table.html.erb +29 -0
  99. data/app/views/rails_pulse/deployments/index.html.erb +15 -0
  100. data/app/views/rails_pulse/deployments/show.html.erb +62 -0
  101. data/app/views/rails_pulse/exception_occurrences/show.html.erb +45 -0
  102. data/app/views/rails_pulse/exceptions/_backtrace.html.erb +33 -0
  103. data/app/views/rails_pulse/exceptions/_metric_cards.html.erb +6 -0
  104. data/app/views/rails_pulse/exceptions/_occurrences_table.html.erb +23 -0
  105. data/app/views/rails_pulse/exceptions/_status_badge.html.erb +6 -0
  106. data/app/views/rails_pulse/exceptions/_table.html.erb +33 -0
  107. data/app/views/rails_pulse/exceptions/index.html.erb +66 -0
  108. data/app/views/rails_pulse/exceptions/show.html.erb +52 -0
  109. data/app/views/rails_pulse/queries/_raw_data.html.erb +1 -23
  110. data/app/views/rails_pulse/requests/_table.html.erb +1 -1
  111. data/app/views/rails_pulse/routes/_requests_table.html.erb +5 -0
  112. data/app/views/rails_pulse/routes/_table.html.erb +22 -8
  113. data/app/views/rails_pulse/routes/index.html.erb +5 -1
  114. data/app/views/rails_pulse/routes/show.html.erb +5 -0
  115. data/app/views/rails_pulse/storage/show.html.erb +123 -0
  116. data/config/routes.rb +13 -4
  117. data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +50 -0
  118. data/db/rails_pulse_migrate/20260507000001_add_actual_query_to_operations.rb +10 -5
  119. data/db/rails_pulse_migrate/20260610000001_add_controller_action_to_rails_pulse_routes.rb +8 -0
  120. data/db/rails_pulse_migrate/20260610000002_change_rails_pulse_routes_to_multi_verb_model.rb +185 -0
  121. data/db/rails_pulse_migrate/20260610000003_add_null_action_unique_index_to_routes.rb +32 -0
  122. data/db/rails_pulse_migrate/20260823000001_add_location_to_exception_groups.rb +12 -0
  123. data/db/rails_pulse_schema.rb +54 -6
  124. data/lib/generators/rails_pulse/base_methods.rb +2 -0
  125. data/lib/generators/rails_pulse/install_generator.rb +5 -0
  126. data/lib/generators/rails_pulse/schema_parser.rb +22 -2
  127. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +54 -6
  128. data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +1 -1
  129. data/lib/generators/rails_pulse/templates/rails_pulse.rb +157 -16
  130. data/lib/generators/rails_pulse/upgrade_generator.rb +166 -38
  131. data/lib/rails_pulse/cleanup_service.rb +110 -15
  132. data/lib/rails_pulse/configuration.rb +125 -6
  133. data/lib/rails_pulse/engine.rb +32 -17
  134. data/lib/rails_pulse/installers/config_updater.rb +202 -0
  135. data/lib/rails_pulse/job_run_collector.rb +43 -16
  136. data/lib/rails_pulse/middleware/asset_server.rb +11 -0
  137. data/lib/rails_pulse/middleware/request_collector.rb +9 -4
  138. data/lib/rails_pulse/packaged_assets.rb +147 -0
  139. data/lib/rails_pulse/route_indexes.rb +66 -0
  140. data/lib/rails_pulse/subscribers/exception_subscriber.rb +36 -0
  141. data/lib/rails_pulse/tracker.rb +29 -10
  142. data/lib/rails_pulse/version.rb +1 -1
  143. data/lib/rails_pulse.rb +1 -0
  144. data/lib/rails_pulse_server.ru +16 -47
  145. data/lib/tasks/rails_pulse.rake +83 -4
  146. data/lib/tasks/rails_pulse_assets.rake +15 -0
  147. data/lib/tasks/rails_pulse_tasks.rake +29 -0
  148. data/public/rails-pulse-assets/rails-pulse-icons.js +6 -2
  149. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  150. data/public/rails-pulse-assets/rails-pulse.js +18 -117
  151. metadata +78 -37
  152. data/Rakefile +0 -554
  153. data/app/javascript/rails_pulse/controllers/form_controller.js +0 -39
  154. data/app/javascript/rails_pulse/controllers/period_selector_controller.js +0 -19
  155. data/app/javascript/rails_pulse/controllers/timezone_controller.js +0 -89
  156. data/app/javascript/rails_pulse/theme.js +0 -416
  157. data/config/brakeman.ignore +0 -190
  158. data/config/brakeman.yml +0 -68
  159. data/config/initializers/rails_pulse.rb +0 -287
  160. data/public/rails-pulse-assets/csp-test.js +0 -120
  161. data/public/rails-pulse-assets/rails-pulse-icons.js.map +0 -13
  162. data/public/rails-pulse-assets/rails-pulse.css.map +0 -1
  163. data/public/rails-pulse-assets/rails-pulse.js.map +0 -7
  164. data/vendor/assets/javascripts/rails-pulse-icons.js +0 -93
  165. data/vendor/assets/javascripts/rails-pulse-icons.js.map +0 -13
  166. data/vendor/assets/javascripts/rails-pulse.js +0 -157
  167. data/vendor/assets/javascripts/rails-pulse.js.map +0 -7
  168. data/vendor/assets/stylesheets/rails-pulse.css +0 -1
  169. data/vendor/assets/stylesheets/rails-pulse.css.map +0 -1
data/Rakefile DELETED
@@ -1,554 +0,0 @@
1
- require "bundler/setup"
2
- require "bundler/gem_tasks"
3
-
4
- # Load environment variables from .env file
5
- require "dotenv/load" if File.exist?(".env")
6
-
7
- APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
8
- load "rails/tasks/engine.rake"
9
-
10
- desc "Verify dummy app migrations are in sync with gem migrations"
11
- task :verify_dummy_migrations do
12
- # Check if db/rails_pulse_migrate directory exists (separate database setup)
13
- if Dir.exist?("db/rails_pulse_migrate")
14
- gem_migrations = Dir["db/rails_pulse_migrate/*.rb"].map { |f| File.basename(f) }.sort
15
-
16
- # Get all migrations from dummy app
17
- dummy_migrations = Dir["test/dummy/db/migrate/*.rb"].map { |f| File.basename(f) }
18
-
19
- missing = gem_migrations - dummy_migrations
20
-
21
- if missing.any?
22
- puts "\n❌ Dummy app is missing Rails Pulse migrations:"
23
- missing.each { |m| puts " • #{m}" }
24
- puts "\nTo fix this, run:"
25
- puts " cd test/dummy"
26
- puts " rails generate rails_pulse:upgrade"
27
- puts " rails db:migrate RAILS_ENV=test"
28
- puts "\nThen commit the new migration files."
29
- exit 1
30
- else
31
- puts "✅ Dummy app migrations are in sync with gem migrations"
32
- end
33
- else
34
- puts "✅ Dummy app migrations check skipped (single database setup)"
35
- end
36
- end
37
-
38
- desc "Sync Rails Pulse schema to test/dummy app"
39
- task :sync_test_schema do
40
- require "fileutils"
41
-
42
- source = "db/rails_pulse_schema.rb"
43
- dest = "test/dummy/db/rails_pulse_schema.rb"
44
-
45
- if File.exist?(source)
46
- FileUtils.cp(source, dest)
47
- puts "✅ Synced schema: #{source} → #{dest}"
48
- else
49
- puts "⚠️ Source schema not found: #{source}"
50
- end
51
- end
52
-
53
- desc "Setup database for testing"
54
- task :test_setup do
55
- database = ENV['DB'] || 'sqlite3'
56
-
57
- puts "\n" + "=" * 50
58
- puts "🛠️ Rails Pulse Test Setup"
59
- puts "=" * 50
60
- puts "Database: #{database.upcase}"
61
- puts "=" * 50
62
- puts
63
-
64
- begin
65
- # Sync schema file from gem to test/dummy
66
- Rake::Task[:sync_test_schema].invoke
67
-
68
- # Remove schema.rb to ensure clean migration
69
- schema_file = "test/dummy/db/schema.rb"
70
- if File.exist?(schema_file)
71
- puts "🧹 Removing existing schema.rb file..."
72
- File.delete(schema_file)
73
- end
74
-
75
- case database.downcase
76
- when 'sqlite3', 'sqlite'
77
- puts "📦 Setting up SQLite database..."
78
- sh "RAILS_ENV=test bin/rails db:drop db:create db:migrate"
79
-
80
- when 'mysql2', 'mysql'
81
- puts "🐬 Setting up MySQL database..."
82
- mysql_password = ENV.fetch("MYSQL_PASSWORD", "")
83
- sh "DB=mysql2 MYSQL_PASSWORD=#{mysql_password} RAILS_ENV=test rails db:drop db:create db:migrate"
84
-
85
- when 'postgresql', 'postgres'
86
- puts "🐘 Setting up PostgreSQL database..."
87
- sh "DB=postgresql RAILS_ENV=test rails db:drop db:create db:migrate"
88
-
89
- else
90
- puts "⚠️ Unknown database: #{database}"
91
- puts "Supported databases: sqlite3, mysql2, postgresql"
92
- exit 1
93
- end
94
-
95
- puts "\n✅ Database setup complete!"
96
- puts "Ready to run: rake test"
97
-
98
- rescue => e
99
- puts "\n❌ Database setup failed!"
100
- puts "Error: #{e.message}"
101
- puts "\nTroubleshooting:"
102
- puts "• Ensure #{database} is installed and running"
103
- puts "• Check database credentials in test/dummy/config/database.yml"
104
- puts "• Verify RAILS_ENV=test environment is configured"
105
- exit 1
106
- end
107
- end
108
-
109
- desc "Run migration regression tests in an isolated process"
110
- task :test_migrations do
111
- database = ENV["DB"] || "sqlite3"
112
-
113
- puts "\n" + "=" * 50
114
- puts "🔄 Rails Pulse Migration Regression Tests"
115
- puts "=" * 50
116
- puts "Database: #{database.upcase}"
117
- puts "=" * 50
118
- puts
119
-
120
- sh "rails test test/migrations"
121
- end
122
-
123
- desc "Run test suite"
124
- task :test do
125
- database = ENV['DB'] || 'sqlite3'
126
-
127
- # Get Rails version from Gemfile.lock or fallback
128
- rails_version = begin
129
- require 'rails'
130
- Rails.version
131
- rescue LoadError
132
- # Try to get from Gemfile.lock
133
- gemfile_lock = File.read('Gemfile.lock') rescue nil
134
- if gemfile_lock && gemfile_lock.match(/rails \(([^)]+)\)/)
135
- $1
136
- else
137
- 'unknown'
138
- end
139
- end
140
-
141
- puts "\n" + "=" * 50
142
- puts "💛 Rails Pulse Test Suite"
143
- puts "=" * 50
144
- puts "Database: #{database.upcase}"
145
- puts "Rails: #{rails_version}"
146
- puts "=" * 50
147
- puts
148
-
149
- sh "rails test test/controllers test/generators test/helpers test/instrumentation test/jobs test/lib test/middleware test/models test/services test/rails_pulse_test.rb test/tracker_test.rb"
150
- end
151
-
152
- desc "Run test suite with code coverage"
153
- task :test_coverage do
154
- ENV["COVERAGE"] = "true"
155
- Rake::Task[:test].invoke
156
-
157
- puts "\n" + "=" * 50
158
- puts "📊 Coverage Report"
159
- puts "=" * 50
160
- puts "Open coverage/index.html to view detailed results"
161
- puts "=" * 50
162
- end
163
-
164
- desc "Setup database for specific Rails version and database"
165
- task :test_setup_for_version, [ :database, :rails_version ] do |t, args|
166
- database = args[:database] || ENV['DB'] || 'sqlite3'
167
- rails_version = args[:rails_version] || 'rails-8-0'
168
-
169
- puts "\n" + "=" * 50
170
- puts "🛠️ Rails Pulse Test Setup"
171
- puts "=" * 50
172
- puts "Database: #{database.upcase}"
173
- puts "Rails: #{rails_version.upcase.gsub('-', ' ')}"
174
- puts "=" * 50
175
- puts
176
-
177
- begin
178
- # Sync schema file from gem to test/dummy
179
- Rake::Task[:sync_test_schema].reenable
180
- Rake::Task[:sync_test_schema].invoke
181
-
182
- # Remove schema.rb to ensure clean migration
183
- schema_file = "test/dummy/db/schema.rb"
184
- if File.exist?(schema_file)
185
- puts "🧹 Removing existing schema.rb file..."
186
- File.delete(schema_file)
187
- end
188
-
189
- puts "📦 Setting up #{database.upcase} database with #{rails_version.upcase.gsub('-', ' ')}..."
190
- sh "DB=#{database} bundle exec appraisal #{rails_version} rails db:drop db:create db:migrate RAILS_ENV=test"
191
-
192
- puts "\n✅ Database setup complete for #{database.upcase} + #{rails_version.upcase.gsub('-', ' ')}!"
193
-
194
- rescue => e
195
- puts "\n❌ Database setup failed!"
196
- puts "Error: #{e.message}"
197
- exit 1
198
- end
199
- end
200
-
201
- desc "Test all database and Rails version combinations"
202
- task :test_matrix do
203
- databases = %w[mysql2 postgresql sqlite3]
204
- rails_versions = %w[rails-7-2 rails-8-0 rails-8-1]
205
-
206
- failed_combinations = []
207
- total_combinations = databases.size * rails_versions.size
208
- current = 0
209
- base_test_paths = "test/controllers test/generators test/helpers test/instrumentation test/jobs test/lib test/middleware test/models test/services test/rails_pulse_test.rb test/tracker_test.rb test/system"
210
-
211
- puts "\n" + "=" * 60
212
- puts "🚀 Rails Pulse Full Test Matrix"
213
- puts "=" * 60
214
- puts "Testing #{total_combinations} combinations..."
215
- puts "=" * 60
216
-
217
- databases.each do |database|
218
- rails_versions.each do |rails_version|
219
- current += 1
220
-
221
- puts "\n[#{current}/#{total_combinations}] Testing: #{database.upcase} + #{rails_version.upcase.gsub('-', ' ')}"
222
- puts "-" * 50
223
-
224
- test_paths = base_test_paths
225
- begin
226
- # First setup the database for this specific combination
227
- Rake::Task[:test_setup_for_version].reenable
228
- Rake::Task[:test_setup_for_version].invoke(database, rails_version)
229
-
230
- # Then run the tests
231
- sh "DB=#{database} MYSQL_PASSWORD=#{ENV.fetch('MYSQL_PASSWORD', '')} bundle exec appraisal #{rails_version} rails test #{test_paths}"
232
-
233
- puts "✅ PASSED: #{database} + #{rails_version}"
234
-
235
- rescue => e
236
- puts "❌ FAILED: #{database} + #{rails_version}"
237
- puts " Error: #{e.message}"
238
- failed_combinations << "#{database} + #{rails_version}"
239
- end
240
- end
241
- end
242
-
243
- puts "\n" + "=" * 60
244
- puts "🏁 Test Matrix Results"
245
- puts "=" * 60
246
-
247
- if failed_combinations.empty?
248
- puts "🎉 All #{total_combinations} combinations passed!"
249
- else
250
- puts "✅ Passed: #{total_combinations - failed_combinations.size}/#{total_combinations}"
251
- puts "❌ Failed combinations:"
252
- failed_combinations.each { |combo| puts " • #{combo}" }
253
- exit 1
254
- end
255
- end
256
-
257
- desc "Pre-release testing with comprehensive checks"
258
- task :test_release do
259
- puts "\n" + "=" * 70
260
- puts "🚀 Rails Pulse Pre-Release Validation"
261
- puts "=" * 70
262
- puts
263
-
264
- failed_tasks = []
265
- current_step = 0
266
- total_steps = 14
267
-
268
- # Step 1: Update appraisal gemfiles
269
- current_step += 1
270
- begin
271
- puts "\n[#{current_step}/#{total_steps}] Updating appraisal gemfiles..."
272
- puts "-" * 70
273
- sh "bundle exec appraisal install"
274
- puts "✅ Appraisal gemfiles updated!"
275
- rescue => e
276
- puts "❌ Appraisal update failed!"
277
- puts " Error: #{e.message}"
278
- failed_tasks << "appraisal_install"
279
- end
280
-
281
- # Step 2: Sync test schema
282
- current_step += 1
283
- begin
284
- puts "\n[#{current_step}/#{total_steps}] Syncing test schema..."
285
- puts "-" * 70
286
- Rake::Task[:sync_test_schema].invoke
287
- rescue => e
288
- puts "❌ Schema sync failed!"
289
- puts " Error: #{e.message}"
290
- failed_tasks << "sync_test_schema"
291
- end
292
-
293
- # Step 3: Verify dummy migrations
294
- current_step += 1
295
- begin
296
- puts "\n[#{current_step}/#{total_steps}] Verifying dummy app migrations..."
297
- puts "-" * 70
298
- Rake::Task[:verify_dummy_migrations].invoke
299
- rescue => e
300
- puts "❌ Dummy app migration verification failed!"
301
- puts " Error: #{e.message}"
302
- failed_tasks << "verify_dummy_migrations"
303
- end
304
-
305
- # Step 4: Git status check
306
- current_step += 1
307
- begin
308
- puts "\n[#{current_step}/#{total_steps}] Checking git status..."
309
- puts "-" * 70
310
-
311
- git_status = `git status --porcelain`.strip
312
- if !git_status.empty?
313
- puts "❌ Git working directory is not clean!"
314
- puts "\nUncommitted changes:"
315
- puts git_status
316
- puts "\nPlease commit or stash your changes before running pre-release tests."
317
- failed_tasks << "git_status_check"
318
- else
319
- puts "✅ Git working directory is clean"
320
- end
321
- rescue => e
322
- puts "⚠️ Warning: Could not check git status (#{e.message})"
323
- end
324
-
325
- # Step 5: RuboCop linting
326
- current_step += 1
327
- begin
328
- puts "\n[#{current_step}/#{total_steps}] Running RuboCop linting..."
329
- puts "-" * 70
330
- sh "bundle exec rubocop"
331
- puts "✅ Code style checks passed!"
332
- rescue => e
333
- puts "❌ RuboCop linting failed!"
334
- puts " Error: #{e.message}"
335
- failed_tasks << "rubocop"
336
- end
337
-
338
- # Step 6: Brakeman security scan
339
- current_step += 1
340
- begin
341
- puts "\n[#{current_step}/#{total_steps}] Running Brakeman security scanner..."
342
- puts "-" * 70
343
- Rake::Task[:brakeman].invoke
344
- rescue => e
345
- puts "❌ Brakeman security scan failed!"
346
- puts " Error: #{e.message}"
347
- failed_tasks << "brakeman"
348
- end
349
-
350
- # Step 7: Install Node dependencies
351
- current_step += 1
352
- begin
353
- puts "\n[#{current_step}/#{total_steps}] Installing Node dependencies..."
354
- puts "-" * 70
355
- sh "npm install"
356
- puts "✅ Node dependencies installed!"
357
- rescue => e
358
- puts "❌ npm install failed!"
359
- puts " Error: #{e.message}"
360
- failed_tasks << "npm_install"
361
- end
362
-
363
- # Step 8: ESLint JS linting
364
- current_step += 1
365
- begin
366
- puts "\n[#{current_step}/#{total_steps}] Running ESLint JS linting..."
367
- puts "-" * 70
368
- sh "npm run lint:js"
369
- puts "✅ JS linting passed!"
370
- rescue => e
371
- puts "❌ ESLint linting failed!"
372
- puts " Error: #{e.message}"
373
- failed_tasks << "eslint"
374
- end
375
-
376
- # Step 9: JavaScript unit tests
377
- current_step += 1
378
- begin
379
- puts "\n[#{current_step}/#{total_steps}] Running JavaScript unit tests..."
380
- puts "-" * 70
381
- sh "npm run test:js"
382
- puts "✅ JavaScript tests passed!"
383
- rescue => e
384
- puts "❌ JavaScript tests failed!"
385
- puts " Error: #{e.message}"
386
- failed_tasks << "js_tests"
387
- end
388
-
389
- # Step 10: Build and verify assets
390
- current_step += 1
391
- begin
392
- puts "\n[#{current_step}/#{total_steps}] Building production assets..."
393
- puts "-" * 70
394
- sh "npm run build"
395
-
396
- # Verify assets were built in both locations
397
- public_assets = "public/rails-pulse-assets"
398
- vendor_css = "vendor/assets/stylesheets"
399
- vendor_js = "vendor/assets/javascripts"
400
-
401
- all_dirs_valid = true
402
-
403
- if Dir.exist?(public_assets) && !Dir.empty?(public_assets)
404
- puts "✅ Public assets built: #{public_assets}"
405
- else
406
- puts "❌ Public assets directory is missing or empty!"
407
- failed_tasks << "public_assets_verification"
408
- all_dirs_valid = false
409
- end
410
-
411
- if Dir.exist?(vendor_css) && !Dir.empty?(vendor_css)
412
- puts "✅ Vendor CSS built: #{vendor_css}"
413
- else
414
- puts "❌ Vendor CSS directory is missing or empty!"
415
- failed_tasks << "vendor_css_verification"
416
- all_dirs_valid = false
417
- end
418
-
419
- if Dir.exist?(vendor_js) && !Dir.empty?(vendor_js)
420
- puts "✅ Vendor JS built: #{vendor_js}"
421
- else
422
- puts "❌ Vendor JS directory is missing or empty!"
423
- failed_tasks << "vendor_js_verification"
424
- all_dirs_valid = false
425
- end
426
-
427
- if all_dirs_valid
428
- puts "✅ All assets built successfully!"
429
- end
430
- rescue => e
431
- puts "❌ Asset building failed!"
432
- puts " Error: #{e.message}"
433
- failed_tasks << "npm_build"
434
- end
435
-
436
- # Step 11: Verify gem builds
437
- current_step += 1
438
- begin
439
- puts "\n[#{current_step}/#{total_steps}] Verifying gem builds correctly..."
440
- puts "-" * 70
441
- sh "gem build rails_pulse.gemspec"
442
-
443
- # Clean up the built gem
444
- built_gems = Dir.glob("rails_pulse-*.gem")
445
- built_gems.each { |gem_file| File.delete(gem_file) }
446
-
447
- puts "✅ Gem builds successfully!"
448
- rescue => e
449
- puts "❌ Gem build failed!"
450
- puts " Error: #{e.message}"
451
- failed_tasks << "gem_build"
452
- end
453
-
454
- # Step 12: Run generator tests
455
- current_step += 1
456
- begin
457
- puts "\n[#{current_step}/#{total_steps}] Running generator tests..."
458
- puts "-" * 70
459
- sh "./bin/test_generators"
460
- puts "✅ Generator tests passed!"
461
- rescue => e
462
- puts "❌ Generator tests failed!"
463
- puts " Error: #{e.message}"
464
- failed_tasks << "test_generators"
465
- end
466
-
467
- # Step 13: Run migration regression tests
468
- current_step += 1
469
- begin
470
- puts "\n[#{current_step}/#{total_steps}] Running migration regression tests..."
471
- puts "-" * 70
472
- sh "rake test_migrations"
473
- puts "✅ Migration regression tests passed!"
474
- rescue => e
475
- puts "❌ Migration regression tests failed!"
476
- puts " Error: #{e.message}"
477
- failed_tasks << "test_migrations"
478
- end
479
-
480
- # Step 14: Run full test matrix
481
- current_step += 1
482
- begin
483
- puts "\n[#{current_step}/#{total_steps}] Running full test matrix..."
484
- puts "-" * 70
485
- sh "rake test_matrix"
486
- puts "✅ Test matrix passed!"
487
- rescue => e
488
- puts "❌ Test matrix failed!"
489
- puts " Error: #{e.message}"
490
- failed_tasks << "test_matrix"
491
- end
492
-
493
- # Print final results
494
- puts "\n" + "=" * 70
495
- puts "🏁 Pre-Release Validation Results"
496
- puts "=" * 70
497
-
498
- if failed_tasks.empty?
499
- puts "🎉 All pre-release checks passed!"
500
- puts "\n✅ Ready for release!"
501
- puts "\nNext steps:"
502
- puts " 1. Update version in lib/rails_pulse/version.rb"
503
- puts " 2. Update Gemfile.lock files for all Rails versions"
504
- puts " 3. Follow the release process in docs/releasing.md"
505
- else
506
- puts "❌ Failed checks (#{failed_tasks.size}/#{total_steps}):"
507
- failed_tasks.each { |task| puts " • #{task}" }
508
- puts "\n⚠️ Fix these issues before releasing."
509
- exit 1
510
- end
511
- end
512
-
513
- desc "Run Brakeman security scanner"
514
- task :brakeman do
515
- require "brakeman"
516
-
517
- puts "\n" + "=" * 50
518
- puts "🔒 Running Brakeman Security Scanner"
519
- puts "=" * 50
520
- puts
521
-
522
- begin
523
- # Run Brakeman with the configuration file
524
- result = Brakeman.run(
525
- app_path: ".",
526
- config_file: "config/brakeman.yml",
527
- print_report: true,
528
- pager: false
529
- )
530
-
531
- # Check if any unignored warnings were found
532
- # result.filtered_warnings only includes warnings that aren't ignored
533
- unignored_warnings = result.filtered_warnings
534
- total_warnings = result.warnings.count
535
- ignored_count = total_warnings - unignored_warnings.count
536
-
537
- if unignored_warnings.any? || result.errors.any?
538
- puts "\n❌ Security issues found!"
539
- puts " Warnings: #{unignored_warnings.count}"
540
- puts " Ignored: #{ignored_count}" if ignored_count > 0
541
- puts " Errors: #{result.errors.count}"
542
- exit 1
543
- else
544
- puts "\n✅ No security issues found!"
545
- puts " (#{ignored_count} warnings reviewed and ignored)" if ignored_count > 0
546
- end
547
- rescue => e
548
- puts "\n❌ Brakeman scan failed!"
549
- puts " Error: #{e.message}"
550
- exit 1
551
- end
552
- end
553
-
554
- task default: :test
@@ -1,39 +0,0 @@
1
- import { Controller } from "@hotwired/stimulus"
2
-
3
- export default class extends Controller {
4
- static targets = [ "cancel" ]
5
-
6
- initialize() {
7
- // Simple debounce implementation for asset independence
8
- this.search = this.debounce(this.search.bind(this), 500)
9
- }
10
-
11
- // Simple debounce implementation (replaces lodash dependency)
12
- debounce(func, wait) {
13
- let timeout
14
- return function executedFunction(...args) {
15
- const later = () => {
16
- clearTimeout(timeout)
17
- func(...args)
18
- }
19
- clearTimeout(timeout)
20
- timeout = setTimeout(later, wait)
21
- }
22
- }
23
-
24
- submit() {
25
- this.element.requestSubmit()
26
- }
27
-
28
- search() {
29
- this.element.requestSubmit()
30
- }
31
-
32
- cancel() {
33
- this.cancelTarget?.click()
34
- }
35
-
36
- preventAttachment(event) {
37
- event.preventDefault()
38
- }
39
- }
@@ -1,19 +0,0 @@
1
- import { Controller } from "@hotwired/stimulus"
2
-
3
- // Handles the 7d/14d/30d period toggle on the dashboard.
4
- // Optimistically updates button active state on click.
5
- export default class extends Controller {
6
- static targets = ["button"]
7
-
8
- select(event) {
9
- const clickedEl = event.currentTarget
10
-
11
- // Optimistic UI: immediately reflect the new active state
12
- this.buttonTargets.forEach(btn => {
13
- const isActive = btn === clickedEl
14
- btn.dataset.active = isActive ? "true" : "false"
15
- btn.style.background = isActive ? "var(--color-primary)" : "transparent"
16
- btn.style.color = isActive ? "var(--color-text-reversed)" : "var(--color-text-subtle)"
17
- })
18
- }
19
- }
@@ -1,89 +0,0 @@
1
- import { Controller } from "@hotwired/stimulus"
2
-
3
- export default class extends Controller {
4
- static values = { cachedAt: String, targetFrame: String }
5
-
6
- connect() {
7
- this.updateTimestamp()
8
- this.setupObserver()
9
- }
10
-
11
- disconnect() {
12
- if (this.observer) {
13
- this.observer.disconnect()
14
- }
15
- if (this.frameObserver) {
16
- this.frameObserver.disconnect()
17
- }
18
- if (this.documentObserver) {
19
- this.documentObserver.disconnect()
20
- }
21
- }
22
-
23
- setupObserver() {
24
- if (this.targetFrameValue) {
25
- // Try immediately
26
- this.updateTimestamp()
27
-
28
- // Also observe for when the frame appears or changes
29
- const observer = new MutationObserver(() => {
30
- const targetFrame = document.getElementById(this.targetFrameValue)
31
- if (targetFrame) {
32
- this.updateTimestamp()
33
- // Watch for attribute changes on the frame
34
- if (this.frameObserver) {
35
- this.frameObserver.disconnect()
36
- }
37
- this.frameObserver = new MutationObserver(() => {
38
- this.updateTimestamp()
39
- })
40
- this.frameObserver.observe(targetFrame, {
41
- attributes: true,
42
- attributeFilter: ['data-cached-at'],
43
- childList: true,
44
- subtree: true
45
- })
46
- }
47
- })
48
-
49
- // Watch the whole document for the frame to appear
50
- observer.observe(document.body, { childList: true, subtree: true })
51
- this.documentObserver = observer
52
- }
53
- }
54
-
55
- cachedAtValueChanged() {
56
- this.updateTimestamp()
57
- }
58
-
59
- updateTimestamp() {
60
- let cachedAtValue = this.cachedAtValue
61
-
62
- // If no direct cached value but we have a target frame, try to get it from there
63
- if (!cachedAtValue && this.targetFrameValue) {
64
- const targetFrame = document.getElementById(this.targetFrameValue)
65
- if (targetFrame) {
66
- cachedAtValue = targetFrame.dataset.cachedAt
67
- }
68
- }
69
-
70
- if (cachedAtValue) {
71
- try {
72
- const date = new Date(cachedAtValue)
73
- const localTimeString = date.toLocaleString('en-US', {
74
- year: 'numeric',
75
- month: 'long',
76
- day: 'numeric',
77
- hour: 'numeric',
78
- minute: '2-digit',
79
- hour12: true
80
- })
81
- this.element.title = `Last updated: ${localTimeString}`
82
- } catch {
83
- this.element.title = 'Cache time unavailable'
84
- }
85
- } else {
86
- this.element.title = 'Cache time unavailable'
87
- }
88
- }
89
- }