capydash 0.2.0 → 0.2.1
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 +70 -171
- data/capydash.gemspec +7 -13
- data/lib/capydash/rspec.rb +415 -0
- data/lib/capydash/templates/report.html.erb +4 -26
- data/lib/capydash/version.rb +2 -2
- data/lib/capydash.rb +3 -60
- metadata +13 -84
- data/lib/capydash/auth.rb +0 -103
- data/lib/capydash/configuration.rb +0 -186
- data/lib/capydash/dashboard_server.rb +0 -167
- data/lib/capydash/engine.rb +0 -52
- data/lib/capydash/error_handler.rb +0 -101
- data/lib/capydash/event_emitter.rb +0 -29
- data/lib/capydash/forwarder.rb +0 -78
- data/lib/capydash/instrumentation.rb +0 -153
- data/lib/capydash/logger.rb +0 -99
- data/lib/capydash/persistence.rb +0 -134
- data/lib/capydash/report_generator.rb +0 -1007
- data/lib/capydash/rspec_integration.rb +0 -285
- data/lib/capydash/test_data_aggregator.rb +0 -221
- data/lib/capydash/test_data_collector.rb +0 -58
- data/lib/generators/capydash/install_generator.rb +0 -124
- data/lib/tasks/capydash.rake +0 -67
data/lib/capydash/auth.rb
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
require 'securerandom'
|
|
2
|
-
require 'digest'
|
|
3
|
-
|
|
4
|
-
module CapyDash
|
|
5
|
-
class Auth
|
|
6
|
-
class << self
|
|
7
|
-
def authenticate(username, password)
|
|
8
|
-
return false unless auth_enabled?
|
|
9
|
-
|
|
10
|
-
# Simple hardcoded credentials for MVP
|
|
11
|
-
# In production, this would connect to a proper user database
|
|
12
|
-
valid_credentials = {
|
|
13
|
-
'admin' => 'capydash123',
|
|
14
|
-
'developer' => 'test123',
|
|
15
|
-
'viewer' => 'readonly123'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if valid_credentials[username] == password
|
|
19
|
-
token = generate_token(username)
|
|
20
|
-
Logger.info("User authenticated", {
|
|
21
|
-
username: username,
|
|
22
|
-
token: token[0..8] + "..."
|
|
23
|
-
})
|
|
24
|
-
token
|
|
25
|
-
else
|
|
26
|
-
Logger.warn("Authentication failed", {
|
|
27
|
-
username: username,
|
|
28
|
-
ip: current_ip
|
|
29
|
-
})
|
|
30
|
-
nil
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def validate_token(token)
|
|
35
|
-
return false unless auth_enabled?
|
|
36
|
-
return false unless token && token.length > 10
|
|
37
|
-
|
|
38
|
-
# Simple token validation for MVP
|
|
39
|
-
# In production, this would use JWT or similar
|
|
40
|
-
begin
|
|
41
|
-
decoded = Base64.decode64(token)
|
|
42
|
-
parts = decoded.split(':')
|
|
43
|
-
return false unless parts.length == 3
|
|
44
|
-
|
|
45
|
-
username, timestamp, signature = parts
|
|
46
|
-
expected_signature = generate_signature(username, timestamp)
|
|
47
|
-
|
|
48
|
-
if signature == expected_signature
|
|
49
|
-
# Check if token is not expired (24 hours)
|
|
50
|
-
token_time = Time.at(timestamp.to_i)
|
|
51
|
-
if Time.now - token_time < 24 * 60 * 60
|
|
52
|
-
Logger.debug("Token validated", { username: username })
|
|
53
|
-
username
|
|
54
|
-
else
|
|
55
|
-
Logger.warn("Token expired", { username: username })
|
|
56
|
-
false
|
|
57
|
-
end
|
|
58
|
-
else
|
|
59
|
-
Logger.warn("Invalid token signature", { token: token[0..8] + "..." })
|
|
60
|
-
false
|
|
61
|
-
end
|
|
62
|
-
rescue => e
|
|
63
|
-
ErrorHandler.handle_error(e, {
|
|
64
|
-
error_type: 'authentication',
|
|
65
|
-
operation: 'validate_token'
|
|
66
|
-
})
|
|
67
|
-
false
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def auth_enabled?
|
|
72
|
-
CapyDash.config.auth_enabled?
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def require_auth!
|
|
76
|
-
return true unless auth_enabled?
|
|
77
|
-
# This would be called by middleware or controllers
|
|
78
|
-
# For now, just return true
|
|
79
|
-
true
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
def generate_token(username)
|
|
85
|
-
timestamp = Time.now.to_i.to_s
|
|
86
|
-
signature = generate_signature(username, timestamp)
|
|
87
|
-
token_data = "#{username}:#{timestamp}:#{signature}"
|
|
88
|
-
Base64.encode64(token_data).strip
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def generate_signature(username, timestamp)
|
|
92
|
-
secret = CapyDash.config.secret_key
|
|
93
|
-
data = "#{username}:#{timestamp}:#{secret}"
|
|
94
|
-
Digest::SHA256.hexdigest(data)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def current_ip
|
|
98
|
-
# In a real app, this would get the actual IP
|
|
99
|
-
"127.0.0.1"
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
module CapyDash
|
|
2
|
-
class Configuration
|
|
3
|
-
attr_accessor :server, :dashboard, :tests, :logging, :security, :performance
|
|
4
|
-
|
|
5
|
-
def initialize
|
|
6
|
-
@server = {
|
|
7
|
-
host: "localhost",
|
|
8
|
-
port: 4000,
|
|
9
|
-
websocket_path: "/websocket",
|
|
10
|
-
max_connections: 100,
|
|
11
|
-
message_history_limit: 1000
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
@dashboard = {
|
|
15
|
-
title: "CapyDash Test Monitor",
|
|
16
|
-
refresh_interval: 1000,
|
|
17
|
-
auto_scroll: true,
|
|
18
|
-
show_timestamps: true,
|
|
19
|
-
screenshot_quality: 0.8,
|
|
20
|
-
max_screenshot_width: 1200
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
@tests = {
|
|
24
|
-
default_directory: "test",
|
|
25
|
-
system_tests_dir: "test/system",
|
|
26
|
-
feature_tests_dir: "test/features",
|
|
27
|
-
controller_tests_dir: "test/controllers",
|
|
28
|
-
model_tests_dir: "test/models",
|
|
29
|
-
screenshot_dir: "tmp/capydash_screenshots",
|
|
30
|
-
timeout: 300
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@logging = {
|
|
34
|
-
level: "info",
|
|
35
|
-
file: "log/capydash.log",
|
|
36
|
-
max_file_size: "10MB",
|
|
37
|
-
max_files: 5
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@security = {
|
|
41
|
-
enable_auth: false,
|
|
42
|
-
secret_key: "your-secret-key-here",
|
|
43
|
-
session_timeout: 3600
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@performance = {
|
|
47
|
-
enable_compression: true,
|
|
48
|
-
cleanup_interval: 300,
|
|
49
|
-
max_memory_usage: "512MB"
|
|
50
|
-
}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def self.load_from_file(config_path = nil)
|
|
54
|
-
config_path ||= File.join(Dir.pwd, "config", "capydash.yml")
|
|
55
|
-
|
|
56
|
-
if File.exist?(config_path)
|
|
57
|
-
require 'yaml'
|
|
58
|
-
yaml_config = YAML.load_file(config_path)
|
|
59
|
-
|
|
60
|
-
config = new
|
|
61
|
-
config.load_from_hash(yaml_config)
|
|
62
|
-
config
|
|
63
|
-
else
|
|
64
|
-
# Return default configuration if file doesn't exist
|
|
65
|
-
new
|
|
66
|
-
end
|
|
67
|
-
rescue => e
|
|
68
|
-
puts "Warning: Could not load configuration from #{config_path}: #{e.message}"
|
|
69
|
-
puts "Using default configuration."
|
|
70
|
-
new
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def load_from_hash(hash)
|
|
74
|
-
@server.merge!(hash['server']) if hash['server']
|
|
75
|
-
@dashboard.merge!(hash['dashboard']) if hash['dashboard']
|
|
76
|
-
@tests.merge!(hash['tests']) if hash['tests']
|
|
77
|
-
@logging.merge!(hash['logging']) if hash['logging']
|
|
78
|
-
@security.merge!(hash['security']) if hash['security']
|
|
79
|
-
@performance.merge!(hash['performance']) if hash['performance']
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def server_host
|
|
83
|
-
@server[:host]
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def server_port
|
|
87
|
-
@server[:port]
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def websocket_path
|
|
91
|
-
@server[:websocket_path]
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def max_connections
|
|
95
|
-
@server[:max_connections]
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def message_history_limit
|
|
99
|
-
@server[:message_history_limit]
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def dashboard_title
|
|
103
|
-
@dashboard[:title]
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def auto_scroll?
|
|
107
|
-
@dashboard[:auto_scroll]
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def show_timestamps?
|
|
111
|
-
@dashboard[:show_timestamps]
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def screenshot_quality
|
|
115
|
-
@dashboard[:screenshot_quality]
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def max_screenshot_width
|
|
119
|
-
@dashboard[:max_screenshot_width]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def system_tests_dir
|
|
123
|
-
@tests[:system_tests_dir]
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def feature_tests_dir
|
|
127
|
-
@tests[:feature_tests_dir]
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def controller_tests_dir
|
|
131
|
-
@tests[:controller_tests_dir]
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def model_tests_dir
|
|
135
|
-
@tests[:model_tests_dir]
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def screenshot_dir
|
|
139
|
-
@tests[:screenshot_dir]
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def test_timeout
|
|
143
|
-
@tests[:timeout]
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def log_level
|
|
147
|
-
@logging[:level]
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def log_file
|
|
151
|
-
@logging[:file]
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def max_files
|
|
155
|
-
@logging[:max_files]
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def max_file_size
|
|
159
|
-
@logging[:max_file_size]
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def auth_enabled?
|
|
163
|
-
@security[:enable_auth]
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def secret_key
|
|
167
|
-
@security[:secret_key]
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def session_timeout
|
|
171
|
-
@security[:session_timeout]
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def compression_enabled?
|
|
175
|
-
@performance[:enable_compression]
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def cleanup_interval
|
|
179
|
-
@performance[:cleanup_interval]
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def max_memory_usage
|
|
183
|
-
@performance[:max_memory_usage]
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
require 'eventmachine'
|
|
2
|
-
require 'em-websocket'
|
|
3
|
-
|
|
4
|
-
module CapyDash
|
|
5
|
-
class DashboardServer
|
|
6
|
-
attr_reader :port, :clients
|
|
7
|
-
|
|
8
|
-
# Provide a single shared instance for the whole process
|
|
9
|
-
def self.instance(port: nil)
|
|
10
|
-
configured_port = port || CapyDash.config.server_port
|
|
11
|
-
@instance ||= new(port: configured_port)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def initialize(port: nil)
|
|
15
|
-
@port = port || CapyDash.config.server_port
|
|
16
|
-
@clients = []
|
|
17
|
-
@history = [] # recent events to replay on reconnect
|
|
18
|
-
@history_limit = CapyDash.config.message_history_limit
|
|
19
|
-
@max_connections = CapyDash.config.max_connections
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Start the WebSocket server in a background thread
|
|
23
|
-
def start
|
|
24
|
-
return if defined?(@started) && @started
|
|
25
|
-
@started = true
|
|
26
|
-
Thread.new do
|
|
27
|
-
EM.run do
|
|
28
|
-
EM::WebSocket.run(host: "0.0.0.0", port: @port) do |ws|
|
|
29
|
-
|
|
30
|
-
ws.onopen do
|
|
31
|
-
begin
|
|
32
|
-
# Check connection limit
|
|
33
|
-
if @clients.length >= @max_connections
|
|
34
|
-
CapyDash::Logger.warn("Connection limit reached", {
|
|
35
|
-
current_connections: @clients.length,
|
|
36
|
-
max_connections: @max_connections
|
|
37
|
-
})
|
|
38
|
-
ws.close
|
|
39
|
-
return
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
@clients << ws
|
|
43
|
-
CapyDash::Logger.info("Client connected", {
|
|
44
|
-
total_clients: @clients.length,
|
|
45
|
-
client_id: ws.object_id
|
|
46
|
-
})
|
|
47
|
-
puts "Client connected"
|
|
48
|
-
|
|
49
|
-
# Replay recent history so refreshed clients see last events
|
|
50
|
-
@history.each do |msg|
|
|
51
|
-
begin
|
|
52
|
-
ws.send(msg)
|
|
53
|
-
rescue => e
|
|
54
|
-
CapyDash::ErrorHandler.handle_websocket_error(e, ws)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
rescue => e
|
|
58
|
-
CapyDash::ErrorHandler.handle_websocket_error(e, ws)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
ws.onclose do
|
|
63
|
-
begin
|
|
64
|
-
@clients.delete(ws)
|
|
65
|
-
CapyDash::Logger.info("Client disconnected", {
|
|
66
|
-
remaining_clients: @clients.length,
|
|
67
|
-
client_id: ws.object_id
|
|
68
|
-
})
|
|
69
|
-
puts "Client disconnected"
|
|
70
|
-
rescue => e
|
|
71
|
-
CapyDash::ErrorHandler.handle_websocket_error(e, ws)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
ws.onmessage do |msg|
|
|
76
|
-
begin
|
|
77
|
-
CapyDash::Logger.debug("Received WebSocket message", {
|
|
78
|
-
message_length: msg.length,
|
|
79
|
-
client_id: ws.object_id
|
|
80
|
-
})
|
|
81
|
-
puts "Received message: #{msg}"
|
|
82
|
-
|
|
83
|
-
data = JSON.parse(msg) rescue nil
|
|
84
|
-
if data && data["command"] == "run_tests"
|
|
85
|
-
args = data["args"] || ["bundle", "exec", "rails", "test", "test/system"]
|
|
86
|
-
CapyDash::Logger.info("Executing test command", {
|
|
87
|
-
command: args.join(' '),
|
|
88
|
-
client_id: ws.object_id
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
Thread.new do
|
|
92
|
-
begin
|
|
93
|
-
# Change to the dummy app directory and run tests there
|
|
94
|
-
current_dir = File.dirname(__FILE__)
|
|
95
|
-
gem_root = File.expand_path(File.join(current_dir, "..", ".."))
|
|
96
|
-
dummy_app_path = File.join(gem_root, "spec", "dummy_app")
|
|
97
|
-
|
|
98
|
-
CapyDash::Logger.info("Running tests in directory", {
|
|
99
|
-
directory: dummy_app_path,
|
|
100
|
-
exists: Dir.exist?(dummy_app_path)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
Dir.chdir(dummy_app_path) do
|
|
104
|
-
# Set Rails environment and ensure CapyDash is loaded
|
|
105
|
-
ENV["RAILS_ENV"] = "test"
|
|
106
|
-
ENV["CAPYDASH_EXTERNAL_WS"] = "1" # Use external WebSocket mode
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
# Run the command and capture both stdout and stderr
|
|
110
|
-
IO.popen(args, err: [:child, :out]) do |io|
|
|
111
|
-
io.each_line do |line|
|
|
112
|
-
event = { type: "runner", line: line.strip, status: "running", ts: Time.now.to_i }
|
|
113
|
-
broadcast(event.to_json)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
rescue => e
|
|
118
|
-
CapyDash::ErrorHandler.handle_test_execution_error(e, args.join(' '))
|
|
119
|
-
broadcast({ type: "runner", line: "Error: #{e.message}", status: "failed", ts: Time.now.to_i }.to_json)
|
|
120
|
-
end
|
|
121
|
-
broadcast({ type: "runner", line: "Finished", status: "passed", ts: Time.now.to_i }.to_json)
|
|
122
|
-
end
|
|
123
|
-
else
|
|
124
|
-
# Optionally broadcast to all clients
|
|
125
|
-
broadcast(msg)
|
|
126
|
-
end
|
|
127
|
-
rescue => e
|
|
128
|
-
CapyDash::ErrorHandler.handle_websocket_error(e, ws)
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Send a message to all connected clients
|
|
138
|
-
def broadcast(message)
|
|
139
|
-
begin
|
|
140
|
-
# store in history buffer
|
|
141
|
-
@history << message
|
|
142
|
-
@history.shift if @history.length > @history_limit
|
|
143
|
-
|
|
144
|
-
# fan out to all connected clients
|
|
145
|
-
@clients.each do |client|
|
|
146
|
-
begin
|
|
147
|
-
client.send(message)
|
|
148
|
-
rescue => e
|
|
149
|
-
CapyDash::ErrorHandler.handle_websocket_error(e, client)
|
|
150
|
-
# Remove dead connections
|
|
151
|
-
@clients.delete(client)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
rescue => e
|
|
155
|
-
CapyDash::ErrorHandler.handle_error(e, {
|
|
156
|
-
error_type: 'broadcast',
|
|
157
|
-
message_length: message&.length
|
|
158
|
-
})
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Stop the WebSocket server gracefully
|
|
163
|
-
def stop
|
|
164
|
-
EM.stop_event_loop if EM.reactor_running?
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
data/lib/capydash/engine.rb
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
module CapyDash
|
|
2
|
-
class Engine < ::Rails::Engine
|
|
3
|
-
isolate_namespace CapyDash
|
|
4
|
-
|
|
5
|
-
rake_tasks do
|
|
6
|
-
load File.expand_path("../tasks/capydash.rake", __dir__)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
initializer "capydash.configuration", before: :load_config_initializers do
|
|
10
|
-
# Load configuration
|
|
11
|
-
CapyDash.config = CapyDash::Configuration.load_from_file
|
|
12
|
-
CapyDash::Logger.setup(CapyDash.config)
|
|
13
|
-
|
|
14
|
-
CapyDash::Logger.info("CapyDash configuration loaded", {
|
|
15
|
-
server_port: CapyDash.config.server_port,
|
|
16
|
-
log_level: CapyDash.config.log_level,
|
|
17
|
-
auth_enabled: CapyDash.config.auth_enabled?
|
|
18
|
-
})
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
initializer "capydash.instrumentation", after: :load_config_initializers do
|
|
22
|
-
begin
|
|
23
|
-
# Hook into Capybara sessions automatically
|
|
24
|
-
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
|
25
|
-
Capybara::Session.include(CapyDash::Instrumentation)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Start WebSocket server automatically in dev/test unless using external WS
|
|
29
|
-
if (Rails.env.development? || Rails.env.test?) && ENV["CAPYDASH_EXTERNAL_WS"] != "1"
|
|
30
|
-
port = CapyDash.config.server_port
|
|
31
|
-
CapyDash::DashboardServer.instance(port: port).start
|
|
32
|
-
|
|
33
|
-
CapyDash::Logger.info("WebSocket server started", {
|
|
34
|
-
port: port,
|
|
35
|
-
environment: Rails.env
|
|
36
|
-
})
|
|
37
|
-
else
|
|
38
|
-
CapyDash::Logger.info("Skipping in-process WebSocket server", {
|
|
39
|
-
reason: ENV["CAPYDASH_EXTERNAL_WS"] == "1" ? "external_mode" : "production_environment",
|
|
40
|
-
environment: Rails.env
|
|
41
|
-
})
|
|
42
|
-
end
|
|
43
|
-
rescue => e
|
|
44
|
-
CapyDash::ErrorHandler.handle_error(e, {
|
|
45
|
-
error_type: 'initialization',
|
|
46
|
-
component: 'engine'
|
|
47
|
-
})
|
|
48
|
-
raise
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
module CapyDash
|
|
2
|
-
class ErrorHandler
|
|
3
|
-
class << self
|
|
4
|
-
def handle_error(error, context = {})
|
|
5
|
-
log_error(error, context)
|
|
6
|
-
notify_error(error, context) if should_notify?(error)
|
|
7
|
-
recover_from_error(error, context)
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def handle_websocket_error(error, connection = nil)
|
|
11
|
-
context = {
|
|
12
|
-
error_type: 'websocket',
|
|
13
|
-
connection_id: connection&.id,
|
|
14
|
-
error_class: error.class.name
|
|
15
|
-
}
|
|
16
|
-
handle_error(error, context)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def handle_test_execution_error(error, test_path = nil)
|
|
20
|
-
context = {
|
|
21
|
-
error_type: 'test_execution',
|
|
22
|
-
test_path: test_path,
|
|
23
|
-
error_class: error.class.name
|
|
24
|
-
}
|
|
25
|
-
handle_error(error, context)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def handle_instrumentation_error(error, method_name = nil)
|
|
29
|
-
context = {
|
|
30
|
-
error_type: 'instrumentation',
|
|
31
|
-
method_name: method_name,
|
|
32
|
-
error_class: error.class.name
|
|
33
|
-
}
|
|
34
|
-
handle_error(error, context)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def log_error(error, context)
|
|
40
|
-
Logger.error(
|
|
41
|
-
"Error occurred: #{error.message}",
|
|
42
|
-
context.merge(
|
|
43
|
-
backtrace: error.backtrace&.first(5),
|
|
44
|
-
error_class: error.class.name
|
|
45
|
-
)
|
|
46
|
-
)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def notify_error(error, context)
|
|
50
|
-
# In a production app, this would send notifications to monitoring services
|
|
51
|
-
# like Sentry, Bugsnag, or custom webhooks
|
|
52
|
-
puts "🚨 CRITICAL ERROR: #{error.message}"
|
|
53
|
-
puts "Context: #{context}"
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def should_notify?(error)
|
|
57
|
-
# Only notify for critical errors
|
|
58
|
-
error.is_a?(StandardError) &&
|
|
59
|
-
!error.is_a?(ArgumentError) &&
|
|
60
|
-
!error.is_a?(NoMethodError)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def recover_from_error(error, context)
|
|
64
|
-
case context[:error_type]
|
|
65
|
-
when 'websocket'
|
|
66
|
-
recover_websocket_connection
|
|
67
|
-
when 'test_execution'
|
|
68
|
-
recover_test_execution
|
|
69
|
-
when 'instrumentation'
|
|
70
|
-
recover_instrumentation
|
|
71
|
-
else
|
|
72
|
-
# Generic recovery
|
|
73
|
-
Logger.warn("No specific recovery strategy for error type: #{context[:error_type]}")
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def recover_websocket_connection
|
|
78
|
-
Logger.info("Attempting to recover WebSocket connection")
|
|
79
|
-
# In a real implementation, this would attempt to reconnect
|
|
80
|
-
# or notify the dashboard to refresh the connection
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def recover_test_execution
|
|
84
|
-
Logger.info("Test execution error recovered")
|
|
85
|
-
# Could implement retry logic or cleanup
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def recover_instrumentation
|
|
89
|
-
Logger.info("Instrumentation error recovered")
|
|
90
|
-
# Could disable problematic instrumentation or use fallbacks
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Custom error classes for better error handling
|
|
96
|
-
class CapyDashError < StandardError; end
|
|
97
|
-
class ConfigurationError < CapyDashError; end
|
|
98
|
-
class WebSocketError < CapyDashError; end
|
|
99
|
-
class TestExecutionError < CapyDashError; end
|
|
100
|
-
class InstrumentationError < CapyDashError; end
|
|
101
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
require 'capydash/dashboard_server'
|
|
2
|
-
require 'capydash/forwarder'
|
|
3
|
-
|
|
4
|
-
module CapyDash
|
|
5
|
-
module EventEmitter
|
|
6
|
-
@clients = []
|
|
7
|
-
|
|
8
|
-
class << self
|
|
9
|
-
attr_accessor :clients
|
|
10
|
-
|
|
11
|
-
def subscribe(&block)
|
|
12
|
-
@on_event = block
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def broadcast(event)
|
|
16
|
-
@on_event&.call(event)
|
|
17
|
-
|
|
18
|
-
port = CapyDash.configuration&.port
|
|
19
|
-
if ENV["CAPYDASH_EXTERNAL_WS"] == "1"
|
|
20
|
-
forwarder = CapyDash::Forwarder.instance(port: port)
|
|
21
|
-
forwarder.send_message(event)
|
|
22
|
-
else
|
|
23
|
-
server = CapyDash::DashboardServer.instance(port: port)
|
|
24
|
-
server.broadcast(event.to_json)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|