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.
- checksums.yaml +4 -4
- data/README.md +269 -12
- data/Rakefile +142 -8
- data/app/assets/stylesheets/rails_pulse/components/table.css +16 -1
- data/app/assets/stylesheets/rails_pulse/components/tags.css +7 -2
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +3 -0
- data/app/controllers/concerns/chart_table_concern.rb +2 -1
- data/app/controllers/rails_pulse/application_controller.rb +11 -1
- data/app/controllers/rails_pulse/assets_controller.rb +18 -2
- data/app/controllers/rails_pulse/job_runs_controller.rb +37 -0
- data/app/controllers/rails_pulse/jobs_controller.rb +80 -0
- data/app/controllers/rails_pulse/operations_controller.rb +43 -31
- data/app/controllers/rails_pulse/queries_controller.rb +1 -1
- data/app/controllers/rails_pulse/requests_controller.rb +3 -9
- data/app/controllers/rails_pulse/routes_controller.rb +1 -1
- data/app/controllers/rails_pulse/tags_controller.rb +31 -5
- data/app/helpers/rails_pulse/application_helper.rb +32 -1
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +15 -1
- data/app/helpers/rails_pulse/status_helper.rb +16 -0
- data/app/helpers/rails_pulse/tags_helper.rb +39 -1
- data/app/javascript/rails_pulse/controllers/chart_controller.js +112 -8
- data/app/models/concerns/rails_pulse/taggable.rb +25 -2
- data/app/models/rails_pulse/charts/operations_chart.rb +33 -0
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +1 -2
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
- data/app/models/rails_pulse/job.rb +85 -0
- data/app/models/rails_pulse/job_run.rb +76 -0
- data/app/models/rails_pulse/jobs/cards/average_duration.rb +85 -0
- data/app/models/rails_pulse/jobs/cards/base.rb +70 -0
- data/app/models/rails_pulse/jobs/cards/failure_rate.rb +85 -0
- data/app/models/rails_pulse/jobs/cards/total_jobs.rb +74 -0
- data/app/models/rails_pulse/jobs/cards/total_runs.rb +48 -0
- data/app/models/rails_pulse/operation.rb +16 -3
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +3 -3
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +1 -1
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/tables/index.rb +2 -1
- data/app/models/rails_pulse/query.rb +10 -1
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +3 -2
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +1 -1
- data/app/models/rails_pulse/routes/tables/index.rb +2 -1
- data/app/models/rails_pulse/summary.rb +10 -3
- data/app/services/rails_pulse/summary_service.rb +46 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
- data/app/views/layouts/rails_pulse/application.html.erb +23 -0
- data/app/views/rails_pulse/components/_active_filters.html.erb +7 -6
- data/app/views/rails_pulse/components/_page_header.html.erb +8 -7
- data/app/views/rails_pulse/components/_table.html.erb +7 -4
- data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
- data/app/views/rails_pulse/job_runs/_operations.html.erb +78 -0
- data/app/views/rails_pulse/job_runs/index.html.erb +3 -0
- data/app/views/rails_pulse/job_runs/show.html.erb +51 -0
- data/app/views/rails_pulse/jobs/_job_runs_table.html.erb +35 -0
- data/app/views/rails_pulse/jobs/_table.html.erb +43 -0
- data/app/views/rails_pulse/jobs/index.html.erb +34 -0
- data/app/views/rails_pulse/jobs/show.html.erb +49 -0
- data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +29 -27
- data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +11 -9
- data/app/views/rails_pulse/operations/show.html.erb +10 -8
- data/app/views/rails_pulse/queries/_table.html.erb +3 -3
- data/app/views/rails_pulse/requests/_table.html.erb +6 -6
- data/app/views/rails_pulse/routes/_table.html.erb +3 -3
- data/app/views/rails_pulse/routes/show.html.erb +1 -1
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +7 -14
- data/config/brakeman.ignore +213 -0
- data/config/brakeman.yml +68 -0
- data/config/initializers/rails_pulse.rb +52 -0
- data/config/routes.rb +6 -0
- data/db/rails_pulse_migrate/20250113000000_add_jobs_to_rails_pulse.rb +95 -0
- data/db/rails_pulse_migrate/20250122000000_add_query_fingerprinting.rb +150 -0
- data/db/rails_pulse_migrate/20250202000000_add_index_to_request_uuid.rb +14 -0
- data/db/rails_pulse_schema.rb +186 -103
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +186 -103
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +30 -1
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +31 -0
- data/lib/rails_pulse/active_job_extensions.rb +13 -0
- data/lib/rails_pulse/adapters/delayed_job_plugin.rb +25 -0
- data/lib/rails_pulse/adapters/sidekiq_middleware.rb +41 -0
- data/lib/rails_pulse/cleanup_service.rb +65 -0
- data/lib/rails_pulse/configuration.rb +80 -7
- data/lib/rails_pulse/engine.rb +34 -3
- data/lib/rails_pulse/extensions/active_record.rb +82 -0
- data/lib/rails_pulse/job_run_collector.rb +172 -0
- data/lib/rails_pulse/middleware/request_collector.rb +20 -43
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +11 -5
- data/lib/rails_pulse/tracker.rb +82 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +2 -0
- data/lib/rails_pulse_server.ru +107 -0
- data/lib/tasks/rails_pulse_benchmark.rake +382 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +3 -2
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +1 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +3 -3
- metadata +35 -7
- data/app/models/rails_pulse/requests/charts/operations_chart.rb +0 -35
- 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
|
data/lib/rails_pulse/version.rb
CHANGED
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
|
|
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
|
|
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
|
}
|