rails_pulse 0.2.4 → 0.2.5.pre.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +269 -12
  3. data/Rakefile +142 -8
  4. data/app/assets/stylesheets/rails_pulse/components/table.css +16 -1
  5. data/app/assets/stylesheets/rails_pulse/components/tags.css +7 -2
  6. data/app/assets/stylesheets/rails_pulse/components/utilities.css +3 -0
  7. data/app/controllers/concerns/chart_table_concern.rb +2 -1
  8. data/app/controllers/rails_pulse/application_controller.rb +11 -1
  9. data/app/controllers/rails_pulse/assets_controller.rb +18 -2
  10. data/app/controllers/rails_pulse/job_runs_controller.rb +37 -0
  11. data/app/controllers/rails_pulse/jobs_controller.rb +80 -0
  12. data/app/controllers/rails_pulse/operations_controller.rb +43 -31
  13. data/app/controllers/rails_pulse/queries_controller.rb +1 -1
  14. data/app/controllers/rails_pulse/requests_controller.rb +3 -9
  15. data/app/controllers/rails_pulse/routes_controller.rb +1 -1
  16. data/app/controllers/rails_pulse/tags_controller.rb +31 -5
  17. data/app/helpers/rails_pulse/application_helper.rb +32 -1
  18. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +15 -1
  19. data/app/helpers/rails_pulse/status_helper.rb +16 -0
  20. data/app/helpers/rails_pulse/tags_helper.rb +39 -1
  21. data/app/javascript/rails_pulse/controllers/chart_controller.js +112 -8
  22. data/app/models/concerns/rails_pulse/taggable.rb +25 -2
  23. data/app/models/rails_pulse/charts/operations_chart.rb +33 -0
  24. data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +1 -2
  25. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
  26. data/app/models/rails_pulse/job.rb +85 -0
  27. data/app/models/rails_pulse/job_run.rb +76 -0
  28. data/app/models/rails_pulse/jobs/cards/average_duration.rb +85 -0
  29. data/app/models/rails_pulse/jobs/cards/base.rb +70 -0
  30. data/app/models/rails_pulse/jobs/cards/failure_rate.rb +85 -0
  31. data/app/models/rails_pulse/jobs/cards/total_jobs.rb +74 -0
  32. data/app/models/rails_pulse/jobs/cards/total_runs.rb +48 -0
  33. data/app/models/rails_pulse/operation.rb +16 -3
  34. data/app/models/rails_pulse/queries/cards/average_query_times.rb +3 -3
  35. data/app/models/rails_pulse/queries/cards/execution_rate.rb +1 -1
  36. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
  37. data/app/models/rails_pulse/queries/tables/index.rb +2 -1
  38. data/app/models/rails_pulse/query.rb +10 -1
  39. data/app/models/rails_pulse/routes/cards/average_response_times.rb +3 -2
  40. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
  41. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
  42. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +1 -1
  43. data/app/models/rails_pulse/routes/tables/index.rb +2 -1
  44. data/app/models/rails_pulse/summary.rb +10 -3
  45. data/app/services/rails_pulse/summary_service.rb +46 -0
  46. data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
  47. data/app/views/layouts/rails_pulse/application.html.erb +23 -0
  48. data/app/views/rails_pulse/components/_active_filters.html.erb +7 -6
  49. data/app/views/rails_pulse/components/_page_header.html.erb +8 -7
  50. data/app/views/rails_pulse/components/_table.html.erb +7 -4
  51. data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
  52. data/app/views/rails_pulse/job_runs/_operations.html.erb +78 -0
  53. data/app/views/rails_pulse/job_runs/index.html.erb +3 -0
  54. data/app/views/rails_pulse/job_runs/show.html.erb +51 -0
  55. data/app/views/rails_pulse/jobs/_job_runs_table.html.erb +35 -0
  56. data/app/views/rails_pulse/jobs/_table.html.erb +43 -0
  57. data/app/views/rails_pulse/jobs/index.html.erb +34 -0
  58. data/app/views/rails_pulse/jobs/show.html.erb +49 -0
  59. data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +29 -27
  60. data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +11 -9
  61. data/app/views/rails_pulse/operations/show.html.erb +10 -8
  62. data/app/views/rails_pulse/queries/_table.html.erb +3 -3
  63. data/app/views/rails_pulse/requests/_table.html.erb +6 -6
  64. data/app/views/rails_pulse/routes/_table.html.erb +3 -3
  65. data/app/views/rails_pulse/routes/show.html.erb +1 -1
  66. data/app/views/rails_pulse/tags/_tag_manager.html.erb +7 -14
  67. data/config/brakeman.ignore +213 -0
  68. data/config/brakeman.yml +68 -0
  69. data/config/initializers/rails_pulse.rb +52 -0
  70. data/config/routes.rb +6 -0
  71. data/db/rails_pulse_migrate/20250113000000_add_jobs_to_rails_pulse.rb +95 -0
  72. data/db/rails_pulse_migrate/20250122000000_add_query_fingerprinting.rb +150 -0
  73. data/db/rails_pulse_migrate/20250202000000_add_index_to_request_uuid.rb +14 -0
  74. data/db/rails_pulse_schema.rb +186 -103
  75. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +186 -103
  76. data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +30 -1
  77. data/lib/generators/rails_pulse/templates/rails_pulse.rb +31 -0
  78. data/lib/rails_pulse/active_job_extensions.rb +13 -0
  79. data/lib/rails_pulse/adapters/delayed_job_plugin.rb +25 -0
  80. data/lib/rails_pulse/adapters/sidekiq_middleware.rb +41 -0
  81. data/lib/rails_pulse/cleanup_service.rb +65 -0
  82. data/lib/rails_pulse/configuration.rb +80 -7
  83. data/lib/rails_pulse/engine.rb +34 -3
  84. data/lib/rails_pulse/extensions/active_record.rb +82 -0
  85. data/lib/rails_pulse/job_run_collector.rb +172 -0
  86. data/lib/rails_pulse/middleware/request_collector.rb +20 -43
  87. data/lib/rails_pulse/subscribers/operation_subscriber.rb +11 -5
  88. data/lib/rails_pulse/tracker.rb +82 -0
  89. data/lib/rails_pulse/version.rb +1 -1
  90. data/lib/rails_pulse.rb +2 -0
  91. data/lib/rails_pulse_server.ru +107 -0
  92. data/lib/tasks/rails_pulse_benchmark.rake +382 -0
  93. data/public/rails-pulse-assets/rails-pulse-icons.js +3 -2
  94. data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
  95. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  96. data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
  97. data/public/rails-pulse-assets/rails-pulse.js +1 -1
  98. data/public/rails-pulse-assets/rails-pulse.js.map +3 -3
  99. metadata +35 -7
  100. data/app/models/rails_pulse/requests/charts/operations_chart.rb +0 -35
  101. data/db/migrate/20250930105043_install_rails_pulse_tables.rb +0 -23
@@ -0,0 +1,82 @@
1
+ require "async"
2
+
3
+ module RailsPulse
4
+ module Tracker
5
+ class << self
6
+ def track_request(data)
7
+ return if RequestStore.store[:skip_recording_rails_pulse_activity]
8
+
9
+ if RailsPulse.configuration.async
10
+ Async { perform_tracking(data) }
11
+ else
12
+ perform_tracking(data)
13
+ end
14
+ end
15
+
16
+ def healthy?
17
+ RailsPulse::ApplicationRecord.connection.execute("SELECT 1")
18
+ true
19
+ rescue
20
+ false
21
+ end
22
+
23
+ private
24
+
25
+ def perform_tracking(data, retry_count = 0)
26
+ RailsPulse::ApplicationRecord.connection_pool.with_connection do
27
+ # Set recursion prevention flag
28
+ RequestStore.store[:skip_recording_rails_pulse_activity] = true
29
+
30
+ begin
31
+ # Find or create route
32
+ route = RailsPulse::Route.find_or_create_by(
33
+ method: data[:method],
34
+ path: data[:path]
35
+ )
36
+
37
+ # Create request record
38
+ request = RailsPulse::Request.create!(
39
+ route: route,
40
+ duration: data[:duration],
41
+ status: data[:status],
42
+ is_error: data[:is_error],
43
+ request_uuid: data[:request_uuid],
44
+ controller_action: data[:controller_action],
45
+ occurred_at: data[:occurred_at]
46
+ )
47
+
48
+ # Create operation records
49
+ (data[:operations] || []).each do |op_data|
50
+ RailsPulse::Operation.create!(op_data.merge(request_id: request.id))
51
+ end
52
+
53
+ request
54
+ rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::StatementInvalid => e
55
+ # Retry transient database errors with exponential backoff
56
+ if retry_count < 2
57
+ sleep(0.1 * (2**retry_count)) # 0.1s, 0.2s
58
+ perform_tracking(data, retry_count + 1)
59
+ else
60
+ log_error(e, retry_count)
61
+ nil
62
+ end
63
+ rescue => e
64
+ log_error(e, retry_count)
65
+ nil # Don't raise - never fail main request
66
+ ensure
67
+ RequestStore.store[:skip_recording_rails_pulse_activity] = false
68
+ end
69
+ end
70
+ end
71
+
72
+ def log_error(error, retry_count = 0)
73
+ logger = RailsPulse.configuration.logger
74
+ return unless logger
75
+
76
+ retry_info = retry_count > 0 ? " (after #{retry_count} retries)" : ""
77
+ logger.error("[RailsPulse] Failed to persist tracking data#{retry_info}: #{error.message}")
78
+ logger.error(error.backtrace.join("\n")) if logger.debug?
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsPulse
2
- VERSION = "0.2.4"
2
+ VERSION = "0.2.5-pre.2"
3
3
  end
data/lib/rails_pulse.rb CHANGED
@@ -2,6 +2,7 @@ require "rails_pulse/version"
2
2
  require "rails_pulse/engine"
3
3
  require "rails_pulse/configuration"
4
4
  require "rails_pulse/cleanup_service"
5
+ require "rails_pulse/tracker"
5
6
 
6
7
  module RailsPulse
7
8
  class << self
@@ -10,6 +11,7 @@ module RailsPulse
10
11
  def configure
11
12
  self.configuration ||= Configuration.new
12
13
  yield(configuration)
14
+ configuration.validate_configuration!
13
15
  end
14
16
 
15
17
  def clear_metric_cache!
@@ -0,0 +1,107 @@
1
+ # Load Rails environment - try multiple locations
2
+ if File.exist?("test/dummy/config/environment.rb")
3
+ # Running from Rails Pulse gem root (for development/testing)
4
+ require_relative "../test/dummy/config/environment"
5
+ elsif File.exist?("config/environment.rb")
6
+ # Running from a Rails app that has Rails Pulse installed
7
+ require File.expand_path("config/environment", Dir.pwd)
8
+ else
9
+ # Standalone mode - load minimal dependencies
10
+ puts "=" * 80
11
+ puts "RailsPulse Dashboard (Standalone Mode)"
12
+ puts "=" * 80
13
+
14
+ require "bundler/setup"
15
+ require "active_support/all"
16
+ require "active_record"
17
+
18
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
19
+ require "rails_pulse"
20
+
21
+ # Load database configuration from environment
22
+ db_config = if ENV["DATABASE_URL"]
23
+ { url: ENV["DATABASE_URL"] }
24
+ elsif File.exist?("config/database.yml")
25
+ require "yaml"
26
+ db_yml = YAML.load_file("config/database.yml", aliases: true)
27
+ rails_env = ENV.fetch("RAILS_ENV", "production")
28
+
29
+ rails_pulse_config = db_yml.dig(rails_env, "rails_pulse")
30
+ if rails_pulse_config
31
+ rails_pulse_config
32
+ else
33
+ puts "WARNING: No 'rails_pulse' database found in config/database.yml, using primary connection"
34
+ db_yml[rails_env]
35
+ end
36
+ else
37
+ raise "Database configuration not found. Set DATABASE_URL or provide config/database.yml"
38
+ end
39
+
40
+ puts "Connecting to database: #{db_config[:database] || db_config[:url]&.split('@')&.last}"
41
+
42
+ # Configure RailsPulse for dashboard-only mode
43
+ RailsPulse.configure do |config|
44
+ # CRITICAL: Disable tracking in dashboard process
45
+ config.enabled = false
46
+
47
+ # Configure database connection
48
+ config.connects_to = { database: db_config }
49
+ end
50
+
51
+ # Establish database connection
52
+ RailsPulse::ApplicationRecord.establish_connection(db_config)
53
+
54
+ puts "Dashboard ready on port #{ENV.fetch('PORT', 3001)}"
55
+ puts "=" * 80
56
+ end
57
+
58
+ # Disable output buffering so logs appear immediately
59
+ $stdout.sync = true
60
+ $stderr.sync = true
61
+
62
+ # Build the Rack app with session support
63
+ require "rack/session/cookie"
64
+ require "securerandom"
65
+
66
+ # Simple Rack app that just serves the dashboard
67
+ class DashboardApp
68
+ def initialize
69
+ @dashboard = RailsPulse::Engine
70
+ end
71
+
72
+ def call(env)
73
+ # Health check endpoint
74
+ if env["PATH_INFO"] == "/health"
75
+ healthy = RailsPulse::Tracker.healthy? rescue false
76
+ status_code = healthy ? 200 : 503
77
+
78
+ return [
79
+ status_code,
80
+ { "content-type" => "application/json" },
81
+ [ {
82
+ status: healthy ? "ok" : "unhealthy",
83
+ mode: "dashboard",
84
+ database: healthy ? "connected" : "disconnected",
85
+ timestamp: Time.now.iso8601
86
+ }.to_json ]
87
+ ]
88
+ end
89
+
90
+ # All other requests go to RailsPulse Engine (dashboard)
91
+ @dashboard.call(env)
92
+ end
93
+ end
94
+
95
+ # Add session middleware for the dashboard
96
+ # Require SECRET_KEY_BASE for security
97
+ secret_key = ENV.fetch("SECRET_KEY_BASE") do
98
+ raise "SECRET_KEY_BASE environment variable must be set for standalone dashboard"
99
+ end
100
+
101
+ use Rack::Session::Cookie,
102
+ key: "rails_pulse_session",
103
+ secret: secret_key,
104
+ same_site: :lax,
105
+ max_age: 86400 # 1 day
106
+
107
+ run DashboardApp.new
@@ -0,0 +1,382 @@
1
+ begin
2
+ require "benchmark"
3
+ require "benchmark/ips"
4
+ require "memory_profiler"
5
+ rescue LoadError => e
6
+ # Benchmark gems not available - tasks will show error if run
7
+ end
8
+
9
+ namespace :rails_pulse do
10
+ namespace :benchmark do
11
+ desc "Run comprehensive performance benchmarks for Rails Pulse"
12
+ task all: :environment do
13
+ unless defined?(Benchmark::IPS) && defined?(MemoryProfiler)
14
+ puts "❌ Benchmark gems not installed. Add to your Gemfile:"
15
+ puts " gem 'benchmark-ips'"
16
+ puts " gem 'memory_profiler'"
17
+ puts "\nThen run: bundle install"
18
+ exit 1
19
+ end
20
+
21
+ puts "\n" + "=" * 80
22
+ puts "Rails Pulse Performance Benchmark Suite"
23
+ puts "=" * 80
24
+ puts "\nEnvironment:"
25
+ puts " Ruby: #{RUBY_VERSION}"
26
+ puts " Rails: #{Rails.version}"
27
+ puts " Database: #{ActiveRecord::Base.connection.adapter_name}"
28
+ puts " Rails Pulse: #{RailsPulse::VERSION}"
29
+ puts "\n"
30
+
31
+ # Run all benchmarks
32
+ Rake::Task["rails_pulse:benchmark:memory"].invoke
33
+ Rake::Task["rails_pulse:benchmark:request_overhead"].invoke
34
+ Rake::Task["rails_pulse:benchmark:middleware"].invoke
35
+ Rake::Task["rails_pulse:benchmark:job_tracking"].invoke
36
+ Rake::Task["rails_pulse:benchmark:database_queries"].invoke
37
+
38
+ puts "\n" + "=" * 80
39
+ puts "Benchmark suite completed!"
40
+ puts "=" * 80
41
+ end
42
+
43
+ desc "Benchmark memory usage with and without Rails Pulse"
44
+ task memory: :environment do
45
+ puts "\n" + "-" * 80
46
+ puts "Memory Usage Benchmark"
47
+ puts "-" * 80
48
+
49
+ # Ensure Rails Pulse is enabled
50
+ original_enabled = RailsPulse.configuration.enabled
51
+ RailsPulse.configuration.enabled = true
52
+
53
+ # Baseline memory (Rails Pulse disabled)
54
+ RailsPulse.configuration.enabled = false
55
+ GC.start
56
+ baseline_memory = GC.stat(:total_allocated_objects)
57
+
58
+ # Create some test data
59
+ route = RailsPulse::Route.find_or_create_by!(
60
+ method: "GET",
61
+ path: "/benchmark/test"
62
+ )
63
+
64
+ # Memory with Rails Pulse enabled
65
+ RailsPulse.configuration.enabled = true
66
+ GC.start
67
+ enabled_memory = GC.stat(:total_allocated_objects)
68
+
69
+ # Profile memory for creating a request
70
+ report = MemoryProfiler.report do
71
+ 10.times do
72
+ RailsPulse::Request.create!(
73
+ route: route,
74
+ occurred_at: Time.current,
75
+ duration: rand(50..500),
76
+ status: 200,
77
+ request_uuid: SecureRandom.uuid
78
+ )
79
+ end
80
+ end
81
+
82
+ puts "\nMemory Allocation Summary:"
83
+ puts " Total allocated: #{report.total_allocated_memsize / 1024.0} KB"
84
+ puts " Total retained: #{report.total_retained_memsize / 1024.0} KB"
85
+ puts " Allocated objects: #{report.total_allocated}"
86
+ puts " Retained objects: #{report.total_retained}"
87
+
88
+ puts "\nPer-Request Memory Overhead:"
89
+ puts " ~#{(report.total_allocated_memsize / 10.0 / 1024.0).round(2)} KB per request"
90
+
91
+ # Restore original state
92
+ RailsPulse.configuration.enabled = original_enabled
93
+ end
94
+
95
+ desc "Benchmark request processing overhead"
96
+ task request_overhead: :environment do
97
+ puts "\n" + "-" * 80
98
+ puts "Request Processing Overhead Benchmark"
99
+ puts "-" * 80
100
+
101
+ # Setup test data
102
+ route = RailsPulse::Route.find_or_create_by!(
103
+ method: "GET",
104
+ path: "/benchmark/test"
105
+ )
106
+
107
+ query = RailsPulse::Query.find_or_create_by!(
108
+ normalized_sql: "SELECT * FROM users WHERE id = ?"
109
+ )
110
+
111
+ puts "\nIterations per second (higher is better):\n"
112
+
113
+ Benchmark.ips do |x|
114
+ x.config(time: 5, warmup: 2)
115
+
116
+ x.report("Request creation (baseline)") do
117
+ RailsPulse::Request.new(
118
+ route: route,
119
+ occurred_at: Time.current,
120
+ duration: 100,
121
+ status: 200,
122
+ request_uuid: SecureRandom.uuid
123
+ )
124
+ end
125
+
126
+ x.report("Request creation + save") do
127
+ req = RailsPulse::Request.create!(
128
+ route: route,
129
+ occurred_at: Time.current,
130
+ duration: 100,
131
+ status: 200,
132
+ request_uuid: SecureRandom.uuid
133
+ )
134
+ req.destroy
135
+ end
136
+
137
+ x.report("Request + Operation") do
138
+ req = RailsPulse::Request.create!(
139
+ route: route,
140
+ occurred_at: Time.current,
141
+ duration: 100,
142
+ status: 200,
143
+ request_uuid: SecureRandom.uuid
144
+ )
145
+ RailsPulse::Operation.create!(
146
+ request: req,
147
+ query: query,
148
+ operation_type: "sql",
149
+ label: "User Load",
150
+ occurred_at: Time.current,
151
+ duration: 10
152
+ )
153
+ req.destroy
154
+ end
155
+
156
+ x.compare!
157
+ end
158
+
159
+ puts "\nAbsolute timing comparison:\n"
160
+ result = Benchmark.measure do
161
+ 1000.times do
162
+ req = RailsPulse::Request.create!(
163
+ route: route,
164
+ occurred_at: Time.current,
165
+ duration: 100,
166
+ status: 200,
167
+ request_uuid: SecureRandom.uuid
168
+ )
169
+ req.destroy
170
+ end
171
+ end
172
+
173
+ puts " 1000 requests: #{(result.real * 1000).round(2)}ms total"
174
+ puts " Average per request: #{result.real.round(5)}ms"
175
+ end
176
+
177
+ desc "Benchmark middleware overhead"
178
+ task middleware: :environment do
179
+ puts "\n" + "-" * 80
180
+ puts "Middleware Overhead Benchmark"
181
+ puts "-" * 80
182
+
183
+ # Create mock request environment
184
+ env = {
185
+ "REQUEST_METHOD" => "GET",
186
+ "PATH_INFO" => "/test",
187
+ "QUERY_STRING" => "",
188
+ "rack.input" => StringIO.new,
189
+ "rack.errors" => $stderr,
190
+ "action_dispatch.request_id" => SecureRandom.uuid
191
+ }
192
+
193
+ app = ->(env) { [ 200, { "Content-Type" => "text/plain" }, [ "OK" ] ] }
194
+ middleware = RailsPulse::Middleware::RequestCollector.new(app)
195
+
196
+ puts "\nMiddleware performance:\n"
197
+
198
+ # Benchmark with Rails Pulse enabled
199
+ RailsPulse.configuration.enabled = true
200
+ enabled_time = Benchmark.measure do
201
+ 1000.times do
202
+ test_env = env.dup
203
+ test_env["action_dispatch.request_id"] = SecureRandom.uuid
204
+ middleware.call(test_env)
205
+ end
206
+ end
207
+
208
+ # Benchmark with Rails Pulse disabled
209
+ RailsPulse.configuration.enabled = false
210
+ disabled_time = Benchmark.measure do
211
+ 1000.times do
212
+ test_env = env.dup
213
+ test_env["action_dispatch.request_id"] = SecureRandom.uuid
214
+ middleware.call(test_env)
215
+ end
216
+ end
217
+
218
+ # Benchmark without middleware
219
+ baseline_time = Benchmark.measure do
220
+ 1000.times { app.call(env.dup) }
221
+ end
222
+
223
+ overhead_enabled = (enabled_time.real - baseline_time.real) * 1000 / 1000
224
+ overhead_disabled = (disabled_time.real - baseline_time.real) * 1000 / 1000
225
+
226
+ puts " Baseline (no middleware): #{(baseline_time.real * 1000).round(2)}ms (1000 requests)"
227
+ puts " With Rails Pulse enabled: #{(enabled_time.real * 1000).round(2)}ms (1000 requests)"
228
+ puts " With Rails Pulse disabled: #{(disabled_time.real * 1000).round(2)}ms (1000 requests)"
229
+ puts "\n Overhead per request (enabled): #{overhead_enabled.round(3)}ms"
230
+ puts " Overhead per request (disabled): #{overhead_disabled.round(3)}ms"
231
+
232
+ # Restore
233
+ RailsPulse.configuration.enabled = true
234
+ end
235
+
236
+ desc "Benchmark job tracking overhead"
237
+ task job_tracking: :environment do
238
+ puts "\n" + "-" * 80
239
+ puts "Background Job Tracking Overhead Benchmark"
240
+ puts "-" * 80
241
+
242
+ # Skip if job tracking is disabled
243
+ unless RailsPulse.configuration.track_jobs
244
+ puts "\n ⚠️ Job tracking is disabled - skipping benchmark"
245
+ next
246
+ end
247
+
248
+ # Create a simple test job
249
+ test_job_class = Class.new(ApplicationJob) do
250
+ def perform(value)
251
+ # Simulate some work
252
+ sleep(0.001)
253
+ value * 2
254
+ end
255
+ end
256
+
257
+ puts "\nJob execution overhead:\n"
258
+
259
+ # Benchmark with job tracking enabled
260
+ RailsPulse.configuration.track_jobs = true
261
+ enabled_time = Benchmark.measure do
262
+ 100.times do |i|
263
+ test_job_class.new.perform(i)
264
+ end
265
+ end
266
+
267
+ # Benchmark with job tracking disabled
268
+ RailsPulse.configuration.track_jobs = false
269
+ disabled_time = Benchmark.measure do
270
+ 100.times do |i|
271
+ test_job_class.new.perform(i)
272
+ end
273
+ end
274
+
275
+ overhead = (enabled_time.real - disabled_time.real) * 1000 / 100
276
+
277
+ puts " With tracking enabled: #{(enabled_time.real * 1000).round(2)}ms (100 jobs)"
278
+ puts " With tracking disabled: #{(disabled_time.real * 1000).round(2)}ms (100 jobs)"
279
+ puts "\n Overhead per job: #{overhead.round(3)}ms"
280
+
281
+ # Restore
282
+ RailsPulse.configuration.track_jobs = true
283
+ end
284
+
285
+ desc "Benchmark database query overhead"
286
+ task database_queries: :environment do
287
+ puts "\n" + "-" * 80
288
+ puts "Database Query Overhead Benchmark"
289
+ puts "-" * 80
290
+
291
+ # Create test route for queries
292
+ route = RailsPulse::Route.find_or_create_by!(
293
+ method: "GET",
294
+ path: "/benchmark/queries"
295
+ )
296
+
297
+ puts "\nQuery performance comparison:\n"
298
+
299
+ # Test 1: Simple aggregation query
300
+ puts " 1. Average request duration calculation:"
301
+ time_enabled = Benchmark.measure do
302
+ 100.times { RailsPulse::Request.average(:duration) }
303
+ end
304
+
305
+ RailsPulse.configuration.enabled = false
306
+ time_disabled = Benchmark.measure do
307
+ 100.times { RailsPulse::Request.average(:duration) }
308
+ end
309
+ RailsPulse.configuration.enabled = true
310
+
311
+ puts " Enabled: #{(time_enabled.real * 1000).round(2)}ms (100 queries)"
312
+ puts " Disabled: #{(time_disabled.real * 1000).round(2)}ms (100 queries)"
313
+ puts " Overhead: #{((time_enabled.real - time_disabled.real) * 10).round(3)}ms per query"
314
+
315
+ # Test 2: Complex joins and grouping
316
+ puts "\n 2. Requests grouped by hour with joins:"
317
+ time_complex = Benchmark.measure do
318
+ 10.times do
319
+ RailsPulse::Request
320
+ .joins(:route)
321
+ .group("DATE_TRUNC('hour', occurred_at)")
322
+ .average(:duration)
323
+ end
324
+ end
325
+
326
+ puts " Time: #{(time_complex.real * 1000).round(2)}ms (10 queries)"
327
+ puts " Average: #{(time_complex.real * 100).round(3)}ms per query"
328
+
329
+ # Test 3: Summary aggregation
330
+ puts "\n 3. Summary data aggregation:"
331
+ time_summary = Benchmark.measure do
332
+ 10.times do
333
+ RailsPulse::Summary
334
+ .where("period_start > ?", 24.hours.ago)
335
+ .group(:period_type)
336
+ .average(:avg_duration)
337
+ end
338
+ end
339
+
340
+ puts " Time: #{(time_summary.real * 1000).round(2)}ms (10 queries)"
341
+ puts " Average: #{(time_summary.real * 100).round(3)}ms per query"
342
+ end
343
+
344
+ desc "Generate benchmark report and save to docs"
345
+ task report: :environment do
346
+ require "fileutils"
347
+
348
+ puts "\n" + "=" * 80
349
+ puts "Generating Performance Benchmark Report"
350
+ puts "=" * 80
351
+
352
+ output_file = Rails.root.join("../../docs/benchmark_results.md")
353
+ FileUtils.mkdir_p(File.dirname(output_file))
354
+
355
+ File.open(output_file, "w") do |f|
356
+ f.puts "# Rails Pulse Performance Benchmark Results"
357
+ f.puts ""
358
+ f.puts "**Generated:** #{Time.current.strftime('%Y-%m-%d %H:%M:%S %Z')}"
359
+ f.puts ""
360
+ f.puts "## Environment"
361
+ f.puts ""
362
+ f.puts "- **Ruby:** #{RUBY_VERSION}"
363
+ f.puts "- **Rails:** #{Rails.version}"
364
+ f.puts "- **Database:** #{ActiveRecord::Base.connection.adapter_name}"
365
+ f.puts "- **Rails Pulse:** #{RailsPulse::VERSION}"
366
+ f.puts ""
367
+ f.puts "## Summary"
368
+ f.puts ""
369
+ f.puts "This report contains automated performance benchmarks measuring Rails Pulse's overhead."
370
+ f.puts ""
371
+ f.puts "---"
372
+ f.puts ""
373
+ f.puts "*For full benchmark output, run:* `rails rails_pulse:benchmark:all`"
374
+ end
375
+
376
+ puts "\n✅ Report saved to: #{output_file}"
377
+
378
+ # Run full benchmark suite
379
+ Rake::Task["rails_pulse:benchmark:all"].invoke
380
+ end
381
+ end
382
+ end
@@ -1,5 +1,5 @@
1
1
  // Rails Pulse Icons Bundle - Auto-generated
2
- // Contains 34 SVG icons for Rails Pulse
2
+ // Contains 35 SVG icons for Rails Pulse
3
3
 
4
4
  (function() {
5
5
  'use strict';
@@ -39,7 +39,8 @@
39
39
  "trending-up": "<path d=\"M16 7h6v6\" /><path d=\"m22 7-8.5 8.5-5-5L2 17\" />",
40
40
  "trending-down": "<path d=\"M16 17h6v-6\" /><path d=\"m22 17-8.5-8.5-5 5L2 7\" />",
41
41
  "move-right": "<path d=\"M18 8L22 12L18 16\" /><path d=\"M2 12H22\" />",
42
- "eye": "<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />"
42
+ "eye": "<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />",
43
+ "zap": "<path d=\"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\" />"
43
44
  };
44
45
 
45
46
  // Global icon registry
@@ -8,6 +8,6 @@
8
8
  "names": [],
9
9
  "mappings": "",
10
10
  "sourcesContent": [
11
- "// Rails Pulse Icons Bundle - Auto-generated\n// Contains 34 SVG icons for Rails Pulse\n\n(function() {\n 'use strict';\n\n // Icon data\n const icons = {\n \"menu\": \"<path d=\\\"M4 5h16\\\" /><path d=\\\"M4 12h16\\\" /><path d=\\\"M4 19h16\\\" />\",\n \"sun\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"4\\\" /><path d=\\\"M12 2v2\\\" /><path d=\\\"M12 20v2\\\" /><path d=\\\"m4.93 4.93 1.41 1.41\\\" /><path d=\\\"m17.66 17.66 1.41 1.41\\\" /><path d=\\\"M2 12h2\\\" /><path d=\\\"M20 12h2\\\" /><path d=\\\"m6.34 17.66-1.41 1.41\\\" /><path d=\\\"m19.07 4.93-1.41 1.41\\\" />\",\n \"moon\": \"<path d=\\\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\\\" />\",\n \"chevron-right\": \"<path d=\\\"m9 18 6-6-6-6\\\" />\",\n \"chevron-left\": \"<path d=\\\"m15 18-6-6 6-6\\\" />\",\n \"chevron-down\": \"<path d=\\\"m6 9 6 6 6-6\\\" />\",\n \"chevron-up\": \"<path d=\\\"m18 15-6-6-6 6\\\" />\",\n \"chevrons-left\": \"<path d=\\\"m11 17-5-5 5-5\\\" /><path d=\\\"m18 17-5-5 5-5\\\" />\",\n \"chevrons-right\": \"<path d=\\\"m6 17 5-5-5-5\\\" /><path d=\\\"m13 17 5-5-5-5\\\" />\",\n \"loader-circle\": \"<path d=\\\"M12 2v4\\\" /><path d=\\\"m16.2 7.8 2.9-2.9\\\" /><path d=\\\"M18 12h4\\\" /><path d=\\\"m16.2 16.2 2.9 2.9\\\" /><path d=\\\"M12 18v4\\\" /><path d=\\\"m4.9 19.1 2.9-2.9\\\" /><path d=\\\"M2 12h4\\\" /><path d=\\\"m4.9 4.9 2.9 2.9\\\" />\",\n \"search\": \"<path d=\\\"m21 21-4.34-4.34\\\" /><circle cx=\\\"11\\\" cy=\\\"11\\\" r=\\\"8\\\" />\",\n \"list-filter\": \"<path d=\\\"M2 5h20\\\" /><path d=\\\"M6 12h12\\\" /><path d=\\\"M9 19h6\\\" />\",\n \"list-filter-plus\": \"<path d=\\\"M12 5H2\\\" /><path d=\\\"M6 12h12\\\" /><path d=\\\"M9 19h6\\\" /><path d=\\\"M16 5h6\\\" /><path d=\\\"M19 8V2\\\" />\",\n \"x\": \"<path d=\\\"M18 6 6 18\\\" /><path d=\\\"m6 6 12 12\\\" />\",\n \"x-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"m15 9-6 6\\\" /><path d=\\\"m9 9 6 6\\\" />\",\n \"check\": \"<path d=\\\"M20 6 9 17l-5-5\\\" />\",\n \"alert-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><line x1=\\\"12\\\" x2=\\\"12\\\" y1=\\\"8\\\" y2=\\\"12\\\" /><line x1=\\\"12\\\" x2=\\\"12.01\\\" y1=\\\"16\\\" y2=\\\"16\\\" />\",\n \"alert-triangle\": \"<path d=\\\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\\\" /><path d=\\\"M12 9v4\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"info\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"M12 16v-4\\\" /><path d=\\\"M12 8h.01\\\" />\",\n \"external-link\": \"<path d=\\\"M15 3h6v6\\\" /><path d=\\\"M10 14 21 3\\\" /><path d=\\\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\\\" />\",\n \"download\": \"<path d=\\\"M12 15V3\\\" /><path d=\\\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\\\" /><path d=\\\"m7 10 5 5 5-5\\\" />\",\n \"refresh-cw\": \"<path d=\\\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\\\" /><path d=\\\"M21 3v5h-5\\\" /><path d=\\\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\\\" /><path d=\\\"M8 16H3v5\\\" />\",\n \"clock\": \"<path d=\\\"M12 6v6l4 2\\\" /><circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" />\",\n \"database\": \"<ellipse cx=\\\"12\\\" cy=\\\"5\\\" rx=\\\"9\\\" ry=\\\"3\\\" /><path d=\\\"M3 5V19A9 3 0 0 0 21 19V5\\\" /><path d=\\\"M3 12A9 3 0 0 0 21 12\\\" />\",\n \"server\": \"<rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"2\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"14\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"6\\\" y2=\\\"6\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"18\\\" y2=\\\"18\\\" />\",\n \"activity\": \"<path d=\\\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\\\" />\",\n \"layout-dashboard\": \"<rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"3\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"14\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"14\\\" y=\\\"12\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"3\\\" y=\\\"16\\\" rx=\\\"1\\\" />\",\n \"audio-lines\": \"<path d=\\\"M2 10v3\\\" /><path d=\\\"M6 6v11\\\" /><path d=\\\"M10 3v18\\\" /><path d=\\\"M14 8v7\\\" /><path d=\\\"M18 5v13\\\" /><path d=\\\"M22 10v3\\\" />\",\n \"message-circle-question\": \"<path d=\\\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\\\" /><path d=\\\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"route\": \"<circle cx=\\\"6\\\" cy=\\\"19\\\" r=\\\"3\\\" /><path d=\\\"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\\\" /><circle cx=\\\"18\\\" cy=\\\"5\\\" r=\\\"3\\\" />\",\n \"trending-up\": \"<path d=\\\"M16 7h6v6\\\" /><path d=\\\"m22 7-8.5 8.5-5-5L2 17\\\" />\",\n \"trending-down\": \"<path d=\\\"M16 17h6v-6\\\" /><path d=\\\"m22 17-8.5-8.5-5 5L2 7\\\" />\",\n \"move-right\": \"<path d=\\\"M18 8L22 12L18 16\\\" /><path d=\\\"M2 12H22\\\" />\",\n \"eye\": \"<path d=\\\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\\\" /><circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"3\\\" />\"\n};\n\n // Global icon registry\n window.RailsPulseIcons = {\n icons: icons,\n\n // Get icon SVG content\n get: function(name) {\n return icons[name] || null;\n },\n\n // Check if icon exists\n has: function(name) {\n return name in icons;\n },\n\n // Get all icon names\n list: function() {\n return Object.keys(icons);\n },\n\n // Render icon to element (CSP-safe)\n render: function(name, element, options = {}) {\n const svgContent = this.get(name);\n if (!svgContent || !element) return false;\n\n // Create SVG element\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', options.width || '24');\n svg.setAttribute('height', options.height || '24');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('fill', 'none');\n svg.setAttribute('stroke', 'currentColor');\n svg.setAttribute('stroke-width', '2');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n\n // Add icon content\n svg.innerHTML = svgContent;\n\n // Replace element content\n element.innerHTML = '';\n element.appendChild(svg);\n\n return true;\n }\n };\n})();\n"
11
+ "// Rails Pulse Icons Bundle - Auto-generated\n// Contains 35 SVG icons for Rails Pulse\n\n(function() {\n 'use strict';\n\n // Icon data\n const icons = {\n \"menu\": \"<path d=\\\"M4 5h16\\\" /><path d=\\\"M4 12h16\\\" /><path d=\\\"M4 19h16\\\" />\",\n \"sun\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"4\\\" /><path d=\\\"M12 2v2\\\" /><path d=\\\"M12 20v2\\\" /><path d=\\\"m4.93 4.93 1.41 1.41\\\" /><path d=\\\"m17.66 17.66 1.41 1.41\\\" /><path d=\\\"M2 12h2\\\" /><path d=\\\"M20 12h2\\\" /><path d=\\\"m6.34 17.66-1.41 1.41\\\" /><path d=\\\"m19.07 4.93-1.41 1.41\\\" />\",\n \"moon\": \"<path d=\\\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\\\" />\",\n \"chevron-right\": \"<path d=\\\"m9 18 6-6-6-6\\\" />\",\n \"chevron-left\": \"<path d=\\\"m15 18-6-6 6-6\\\" />\",\n \"chevron-down\": \"<path d=\\\"m6 9 6 6 6-6\\\" />\",\n \"chevron-up\": \"<path d=\\\"m18 15-6-6-6 6\\\" />\",\n \"chevrons-left\": \"<path d=\\\"m11 17-5-5 5-5\\\" /><path d=\\\"m18 17-5-5 5-5\\\" />\",\n \"chevrons-right\": \"<path d=\\\"m6 17 5-5-5-5\\\" /><path d=\\\"m13 17 5-5-5-5\\\" />\",\n \"loader-circle\": \"<path d=\\\"M12 2v4\\\" /><path d=\\\"m16.2 7.8 2.9-2.9\\\" /><path d=\\\"M18 12h4\\\" /><path d=\\\"m16.2 16.2 2.9 2.9\\\" /><path d=\\\"M12 18v4\\\" /><path d=\\\"m4.9 19.1 2.9-2.9\\\" /><path d=\\\"M2 12h4\\\" /><path d=\\\"m4.9 4.9 2.9 2.9\\\" />\",\n \"search\": \"<path d=\\\"m21 21-4.34-4.34\\\" /><circle cx=\\\"11\\\" cy=\\\"11\\\" r=\\\"8\\\" />\",\n \"list-filter\": \"<path d=\\\"M2 5h20\\\" /><path d=\\\"M6 12h12\\\" /><path d=\\\"M9 19h6\\\" />\",\n \"list-filter-plus\": \"<path d=\\\"M12 5H2\\\" /><path d=\\\"M6 12h12\\\" /><path d=\\\"M9 19h6\\\" /><path d=\\\"M16 5h6\\\" /><path d=\\\"M19 8V2\\\" />\",\n \"x\": \"<path d=\\\"M18 6 6 18\\\" /><path d=\\\"m6 6 12 12\\\" />\",\n \"x-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"m15 9-6 6\\\" /><path d=\\\"m9 9 6 6\\\" />\",\n \"check\": \"<path d=\\\"M20 6 9 17l-5-5\\\" />\",\n \"alert-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><line x1=\\\"12\\\" x2=\\\"12\\\" y1=\\\"8\\\" y2=\\\"12\\\" /><line x1=\\\"12\\\" x2=\\\"12.01\\\" y1=\\\"16\\\" y2=\\\"16\\\" />\",\n \"alert-triangle\": \"<path d=\\\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\\\" /><path d=\\\"M12 9v4\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"info\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"M12 16v-4\\\" /><path d=\\\"M12 8h.01\\\" />\",\n \"external-link\": \"<path d=\\\"M15 3h6v6\\\" /><path d=\\\"M10 14 21 3\\\" /><path d=\\\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\\\" />\",\n \"download\": \"<path d=\\\"M12 15V3\\\" /><path d=\\\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\\\" /><path d=\\\"m7 10 5 5 5-5\\\" />\",\n \"refresh-cw\": \"<path d=\\\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\\\" /><path d=\\\"M21 3v5h-5\\\" /><path d=\\\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\\\" /><path d=\\\"M8 16H3v5\\\" />\",\n \"clock\": \"<path d=\\\"M12 6v6l4 2\\\" /><circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" />\",\n \"database\": \"<ellipse cx=\\\"12\\\" cy=\\\"5\\\" rx=\\\"9\\\" ry=\\\"3\\\" /><path d=\\\"M3 5V19A9 3 0 0 0 21 19V5\\\" /><path d=\\\"M3 12A9 3 0 0 0 21 12\\\" />\",\n \"server\": \"<rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"2\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"14\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"6\\\" y2=\\\"6\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"18\\\" y2=\\\"18\\\" />\",\n \"activity\": \"<path d=\\\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\\\" />\",\n \"layout-dashboard\": \"<rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"3\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"14\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"14\\\" y=\\\"12\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"3\\\" y=\\\"16\\\" rx=\\\"1\\\" />\",\n \"audio-lines\": \"<path d=\\\"M2 10v3\\\" /><path d=\\\"M6 6v11\\\" /><path d=\\\"M10 3v18\\\" /><path d=\\\"M14 8v7\\\" /><path d=\\\"M18 5v13\\\" /><path d=\\\"M22 10v3\\\" />\",\n \"message-circle-question\": \"<path d=\\\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\\\" /><path d=\\\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"route\": \"<circle cx=\\\"6\\\" cy=\\\"19\\\" r=\\\"3\\\" /><path d=\\\"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\\\" /><circle cx=\\\"18\\\" cy=\\\"5\\\" r=\\\"3\\\" />\",\n \"trending-up\": \"<path d=\\\"M16 7h6v6\\\" /><path d=\\\"m22 7-8.5 8.5-5-5L2 17\\\" />\",\n \"trending-down\": \"<path d=\\\"M16 17h6v-6\\\" /><path d=\\\"m22 17-8.5-8.5-5 5L2 7\\\" />\",\n \"move-right\": \"<path d=\\\"M18 8L22 12L18 16\\\" /><path d=\\\"M2 12H22\\\" />\",\n \"eye\": \"<path d=\\\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\\\" /><circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"3\\\" />\",\n \"zap\": \"<path d=\\\"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\\\" />\"\n};\n\n // Global icon registry\n window.RailsPulseIcons = {\n icons: icons,\n\n // Get icon SVG content\n get: function(name) {\n return icons[name] || null;\n },\n\n // Check if icon exists\n has: function(name) {\n return name in icons;\n },\n\n // Get all icon names\n list: function() {\n return Object.keys(icons);\n },\n\n // Render icon to element (CSP-safe)\n render: function(name, element, options = {}) {\n const svgContent = this.get(name);\n if (!svgContent || !element) return false;\n\n // Create SVG element\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', options.width || '24');\n svg.setAttribute('height', options.height || '24');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('fill', 'none');\n svg.setAttribute('stroke', 'currentColor');\n svg.setAttribute('stroke-width', '2');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n\n // Add icon content\n svg.innerHTML = svgContent;\n\n // Replace element content\n element.innerHTML = '';\n element.appendChild(svg);\n\n return true;\n }\n };\n})();\n"
12
12
  ]
13
13
  }