activeagent 1.1.0 → 1.2.0
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/CHANGELOG.md +61 -0
- data/README.md +24 -14
- data/lib/active_agent/telemetry/configuration.rb +13 -9
- data/lib/active_agent/telemetry/instrumentation.rb +4 -0
- data/lib/active_agent/telemetry/tool_origin.rb +90 -0
- data/lib/active_agent/telemetry.rb +1 -0
- data/lib/active_agent/version.rb +1 -1
- data/lib/active_agent.rb +0 -5
- metadata +22 -32
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +0 -138
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +0 -64
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +0 -129
- data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +0 -123
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +0 -56
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +0 -14
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +0 -49
- data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +0 -65
- data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +0 -86
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +0 -256
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +0 -113
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +0 -208
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +0 -60
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +0 -46
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +0 -125
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +0 -83
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +0 -52
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +0 -169
- data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +0 -193
- data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +0 -214
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +0 -117
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +0 -135
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +0 -145
- data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +0 -36
- data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +0 -94
- data/lib/active_agent/dashboard/config/routes.rb +0 -19
- data/lib/active_agent/dashboard/engine.rb +0 -43
- data/lib/active_agent/dashboard.rb +0 -161
- data/lib/generators/active_agent/dashboard/install_generator.rb +0 -92
- data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +0 -67
- data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +0 -46
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveAgent
|
|
4
|
-
module Dashboard
|
|
5
|
-
# Represents a single browser action within a session recording.
|
|
6
|
-
#
|
|
7
|
-
# Actions capture user and agent interactions like clicks, typing,
|
|
8
|
-
# navigation, and form submissions.
|
|
9
|
-
#
|
|
10
|
-
class RecordingAction < ApplicationRecord
|
|
11
|
-
belongs_to :session_recording, class_name: "ActiveAgent::Dashboard::SessionRecording"
|
|
12
|
-
has_one :snapshot, class_name: "ActiveAgent::Dashboard::RecordingSnapshot", dependent: :nullify
|
|
13
|
-
|
|
14
|
-
ACTION_TYPES = %w[
|
|
15
|
-
navigate
|
|
16
|
-
click
|
|
17
|
-
type
|
|
18
|
-
scroll
|
|
19
|
-
snapshot
|
|
20
|
-
select
|
|
21
|
-
hover
|
|
22
|
-
drag
|
|
23
|
-
file_upload
|
|
24
|
-
dialog
|
|
25
|
-
evaluate
|
|
26
|
-
wait
|
|
27
|
-
form_fill
|
|
28
|
-
key_press
|
|
29
|
-
focus
|
|
30
|
-
submit
|
|
31
|
-
handoff
|
|
32
|
-
user_action
|
|
33
|
-
completion
|
|
34
|
-
].freeze
|
|
35
|
-
|
|
36
|
-
validates :action_type, presence: true, inclusion: { in: ACTION_TYPES }
|
|
37
|
-
validates :sequence, presence: true, uniqueness: { scope: :session_recording_id }
|
|
38
|
-
validates :timestamp_ms, presence: true
|
|
39
|
-
|
|
40
|
-
scope :ordered, -> { order(:sequence) }
|
|
41
|
-
scope :with_screenshots, -> { where.not(screenshot_key: nil) }
|
|
42
|
-
|
|
43
|
-
# Get the screenshot URL (signed URL from storage)
|
|
44
|
-
def screenshot_url(expires_in: 15.minutes)
|
|
45
|
-
return nil unless screenshot_key.present?
|
|
46
|
-
|
|
47
|
-
ActiveAgent::Dashboard.storage_service&.signed_url_for(screenshot_key, expires_in: expires_in)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Get the DOM snapshot content
|
|
51
|
-
def dom_snapshot_content
|
|
52
|
-
return nil unless dom_snapshot_key.present?
|
|
53
|
-
|
|
54
|
-
ActiveAgent::Dashboard.storage_service&.fetch_snapshot(dom_snapshot_key)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Format for API response
|
|
58
|
-
def as_json_for_api
|
|
59
|
-
{
|
|
60
|
-
id: id,
|
|
61
|
-
action_type: action_type,
|
|
62
|
-
sequence: sequence,
|
|
63
|
-
timestamp_ms: timestamp_ms,
|
|
64
|
-
selector: selector,
|
|
65
|
-
value: redacted_value,
|
|
66
|
-
screenshot_url: screenshot_url,
|
|
67
|
-
has_dom_snapshot: dom_snapshot_key.present?,
|
|
68
|
-
metadata: safe_metadata,
|
|
69
|
-
created_at: created_at.iso8601
|
|
70
|
-
}
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Get browser state at this action (for handoff)
|
|
74
|
-
def browser_state
|
|
75
|
-
{
|
|
76
|
-
url: extract_url,
|
|
77
|
-
form_values: metadata["form_values"],
|
|
78
|
-
scroll_position: metadata["scroll_position"],
|
|
79
|
-
active_element: selector,
|
|
80
|
-
action_type: action_type
|
|
81
|
-
}
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
private
|
|
85
|
-
|
|
86
|
-
def redacted_value
|
|
87
|
-
return value unless should_redact?
|
|
88
|
-
|
|
89
|
-
"[REDACTED]"
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def should_redact?
|
|
93
|
-
return false unless value.present?
|
|
94
|
-
|
|
95
|
-
sensitive_patterns = [
|
|
96
|
-
/password/i,
|
|
97
|
-
/credit.?card/i,
|
|
98
|
-
/cvv/i,
|
|
99
|
-
/ssn/i,
|
|
100
|
-
/social.?security/i,
|
|
101
|
-
/\b\d{16}\b/,
|
|
102
|
-
/\b\d{3}-\d{2}-\d{4}\b/
|
|
103
|
-
]
|
|
104
|
-
|
|
105
|
-
selector_is_sensitive = sensitive_patterns.any? { |p| selector&.match?(p) }
|
|
106
|
-
value_is_sensitive = sensitive_patterns.any? { |p| value.match?(p) }
|
|
107
|
-
|
|
108
|
-
selector_is_sensitive || value_is_sensitive
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def safe_metadata
|
|
112
|
-
metadata.except("password", "credit_card", "cvv", "ssn")
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def extract_url
|
|
116
|
-
case action_type
|
|
117
|
-
when "navigate"
|
|
118
|
-
value
|
|
119
|
-
else
|
|
120
|
-
metadata["url"] || metadata["page_url"]
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveAgent
|
|
4
|
-
module Dashboard
|
|
5
|
-
# Stores screenshots and DOM snapshots from session recordings.
|
|
6
|
-
#
|
|
7
|
-
# Supports Active Storage for file attachment and provides
|
|
8
|
-
# signed URLs for secure access.
|
|
9
|
-
#
|
|
10
|
-
class RecordingSnapshot < ApplicationRecord
|
|
11
|
-
belongs_to :session_recording, class_name: "ActiveAgent::Dashboard::SessionRecording"
|
|
12
|
-
belongs_to :recording_action, class_name: "ActiveAgent::Dashboard::RecordingAction", optional: true
|
|
13
|
-
|
|
14
|
-
has_one_attached :file if defined?(ActiveStorage)
|
|
15
|
-
|
|
16
|
-
SNAPSHOT_TYPES = %w[screenshot dom full_page].freeze
|
|
17
|
-
|
|
18
|
-
validates :storage_key, presence: true, uniqueness: true
|
|
19
|
-
validates :snapshot_type, presence: true, inclusion: { in: SNAPSHOT_TYPES }
|
|
20
|
-
|
|
21
|
-
scope :screenshots, -> { where(snapshot_type: "screenshot") }
|
|
22
|
-
scope :dom_snapshots, -> { where(snapshot_type: "dom") }
|
|
23
|
-
scope :ordered, -> { order(:created_at) }
|
|
24
|
-
|
|
25
|
-
# Get a signed URL for the file
|
|
26
|
-
def signed_url(expires_in: 15.minutes)
|
|
27
|
-
return nil unless respond_to?(:file) && file.attached?
|
|
28
|
-
|
|
29
|
-
file.url(expires_in: expires_in)
|
|
30
|
-
rescue StandardError
|
|
31
|
-
nil
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Store file data
|
|
35
|
-
def store!(data, filename: nil, content_type: nil)
|
|
36
|
-
return unless respond_to?(:file)
|
|
37
|
-
|
|
38
|
-
content_type ||= infer_content_type
|
|
39
|
-
filename ||= generate_filename
|
|
40
|
-
|
|
41
|
-
file.attach(
|
|
42
|
-
io: StringIO.new(data),
|
|
43
|
-
filename: filename,
|
|
44
|
-
content_type: content_type
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
update!(file_size_bytes: data.bytesize)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# For API response
|
|
51
|
-
def as_json_for_api
|
|
52
|
-
{
|
|
53
|
-
id: id,
|
|
54
|
-
storage_key: storage_key,
|
|
55
|
-
snapshot_type: snapshot_type,
|
|
56
|
-
width: width,
|
|
57
|
-
height: height,
|
|
58
|
-
file_size_bytes: file_size_bytes,
|
|
59
|
-
url: signed_url,
|
|
60
|
-
created_at: created_at.iso8601
|
|
61
|
-
}
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def infer_content_type
|
|
67
|
-
case snapshot_type
|
|
68
|
-
when "screenshot", "full_page"
|
|
69
|
-
"image/png"
|
|
70
|
-
when "dom"
|
|
71
|
-
"text/html"
|
|
72
|
-
else
|
|
73
|
-
"application/octet-stream"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def generate_filename
|
|
78
|
-
ext = snapshot_type == "dom" ? "html" : "png"
|
|
79
|
-
"#{storage_key.tr('/', '_')}.#{ext}"
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveAgent
|
|
4
|
-
module Dashboard
|
|
5
|
-
# Tracks individual task executions within a sandbox session.
|
|
6
|
-
#
|
|
7
|
-
# Each sandbox run represents a single agent task execution,
|
|
8
|
-
# capturing input, output, timing, and any errors.
|
|
9
|
-
#
|
|
10
|
-
class SandboxRun < ApplicationRecord
|
|
11
|
-
belongs_to :sandbox_session, class_name: "ActiveAgent::Dashboard::SandboxSession", optional: true
|
|
12
|
-
|
|
13
|
-
enum :status, {
|
|
14
|
-
pending: 0,
|
|
15
|
-
running: 1,
|
|
16
|
-
completed: 2,
|
|
17
|
-
failed: 3,
|
|
18
|
-
cancelled: 4
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
validates :task, presence: true
|
|
22
|
-
validates :status, presence: true
|
|
23
|
-
|
|
24
|
-
scope :recent, -> { order(created_at: :desc) }
|
|
25
|
-
scope :completed_runs, -> { where(status: [ :completed, :failed ]) }
|
|
26
|
-
|
|
27
|
-
# Summary for API responses
|
|
28
|
-
def summary
|
|
29
|
-
{
|
|
30
|
-
id: id,
|
|
31
|
-
task: task.truncate(100),
|
|
32
|
-
status: status,
|
|
33
|
-
duration_ms: duration_ms,
|
|
34
|
-
tokens_used: tokens_used,
|
|
35
|
-
created_at: created_at&.iso8601,
|
|
36
|
-
completed_at: completed_at&.iso8601
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Detailed info including full result
|
|
41
|
-
def details
|
|
42
|
-
summary.merge(
|
|
43
|
-
task: task,
|
|
44
|
-
result: result,
|
|
45
|
-
error: error,
|
|
46
|
-
screenshots: screenshots || [],
|
|
47
|
-
started_at: started_at&.iso8601
|
|
48
|
-
)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveAgent
|
|
4
|
-
module Dashboard
|
|
5
|
-
# Manages sandbox execution sessions for agent runs.
|
|
6
|
-
#
|
|
7
|
-
# Sandbox sessions provide isolated execution environments for running
|
|
8
|
-
# agents with tools like browser automation, file system access, etc.
|
|
9
|
-
#
|
|
10
|
-
# Supports both local (Docker/Incus) and cloud (Cloud Run) sandbox providers.
|
|
11
|
-
#
|
|
12
|
-
# @example Creating a sandbox session
|
|
13
|
-
# session = ActiveAgent::Dashboard::SandboxSession.create!(
|
|
14
|
-
# sandbox_type: "playwright_mcp"
|
|
15
|
-
# )
|
|
16
|
-
# session.provision!
|
|
17
|
-
#
|
|
18
|
-
class SandboxSession < ApplicationRecord
|
|
19
|
-
# Owner associations - optional to support anonymous users
|
|
20
|
-
belongs_to :user, class_name: ActiveAgent::Dashboard.user_class, optional: true if ActiveAgent::Dashboard.user_class
|
|
21
|
-
belongs_to :account, class_name: ActiveAgent::Dashboard.account_class, optional: true if ActiveAgent::Dashboard.multi_tenant?
|
|
22
|
-
belongs_to :agent_template, class_name: "ActiveAgent::Dashboard::AgentTemplate", optional: true
|
|
23
|
-
|
|
24
|
-
has_many :sandbox_runs, class_name: "ActiveAgent::Dashboard::SandboxRun", dependent: :destroy
|
|
25
|
-
has_one :session_recording, class_name: "ActiveAgent::Dashboard::SessionRecording", dependent: :nullify
|
|
26
|
-
|
|
27
|
-
# Session statuses
|
|
28
|
-
enum :status, {
|
|
29
|
-
pending: 0,
|
|
30
|
-
provisioning: 1,
|
|
31
|
-
ready: 2,
|
|
32
|
-
running: 3,
|
|
33
|
-
completed: 4,
|
|
34
|
-
expired: 5,
|
|
35
|
-
failed: 6
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# Sandbox types
|
|
39
|
-
SANDBOX_TYPES = %w[playwright_mcp terminal research].freeze
|
|
40
|
-
|
|
41
|
-
# Default limits (can be overridden by platform tier limits)
|
|
42
|
-
DEFAULT_LIMITS = {
|
|
43
|
-
max_runs: 10,
|
|
44
|
-
timeout_seconds: 300,
|
|
45
|
-
max_tokens: 50_000,
|
|
46
|
-
session_duration_minutes: 15
|
|
47
|
-
}.freeze
|
|
48
|
-
|
|
49
|
-
# Validations
|
|
50
|
-
validates :session_id, presence: true, uniqueness: true
|
|
51
|
-
validates :sandbox_type, inclusion: { in: SANDBOX_TYPES }
|
|
52
|
-
|
|
53
|
-
# Callbacks
|
|
54
|
-
before_validation :generate_session_id, on: :create
|
|
55
|
-
before_create :set_defaults
|
|
56
|
-
|
|
57
|
-
# Scopes
|
|
58
|
-
scope :active, -> { where(status: [ :pending, :provisioning, :ready, :running ]) }
|
|
59
|
-
scope :expired_sessions, -> { where("expires_at < ?", Time.current) }
|
|
60
|
-
scope :by_type, ->(type) { where(sandbox_type: type) }
|
|
61
|
-
scope :anonymous, -> { where(user_id: nil) }
|
|
62
|
-
scope :recent, -> { order(created_at: :desc) }
|
|
63
|
-
|
|
64
|
-
# Check if session is still valid
|
|
65
|
-
def active?
|
|
66
|
-
!expired? && !failed? && !completed? && expires_at > Time.current
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Check if can run more tasks
|
|
70
|
-
def can_run?
|
|
71
|
-
active? && runs_count < max_runs
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Record a new run (thread-safe for parallel execution)
|
|
75
|
-
def record_run!(task:, result:, duration_ms:, tokens:, screenshots: [], provider: nil)
|
|
76
|
-
run = {
|
|
77
|
-
id: SecureRandom.uuid,
|
|
78
|
-
task: task,
|
|
79
|
-
result: result,
|
|
80
|
-
duration_ms: duration_ms,
|
|
81
|
-
tokens: tokens,
|
|
82
|
-
screenshots: screenshots,
|
|
83
|
-
provider: provider,
|
|
84
|
-
status: "completed",
|
|
85
|
-
created_at: Time.current.iso8601
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
with_lock do
|
|
89
|
-
reload
|
|
90
|
-
self.runs = runs + [ run ]
|
|
91
|
-
self.runs_count = runs.size
|
|
92
|
-
self.total_tokens += tokens
|
|
93
|
-
self.total_duration_ms += duration_ms
|
|
94
|
-
self.last_activity_at = Time.current
|
|
95
|
-
save!
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
run
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Provision the sandbox
|
|
102
|
-
def provision!
|
|
103
|
-
return if provisioning? || ready?
|
|
104
|
-
|
|
105
|
-
update!(status: :provisioning)
|
|
106
|
-
|
|
107
|
-
# Use configured sandbox service
|
|
108
|
-
if Rails.env.development? || Rails.env.test?
|
|
109
|
-
ActiveAgent::Dashboard::SandboxProvisionJob.perform_now(id)
|
|
110
|
-
else
|
|
111
|
-
ActiveAgent::Dashboard::SandboxProvisionJob.perform_later(id)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Mark as ready with sandbox URL
|
|
116
|
-
def mark_ready!(sandbox_url:, sandbox_job_id: nil)
|
|
117
|
-
update!(
|
|
118
|
-
status: :ready,
|
|
119
|
-
cloud_run_url: sandbox_url,
|
|
120
|
-
cloud_run_job_id: sandbox_job_id
|
|
121
|
-
)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Expire the session
|
|
125
|
-
def expire!
|
|
126
|
-
update!(status: :expired)
|
|
127
|
-
ActiveAgent::Dashboard::SandboxCleanupJob.perform_later(id) if cloud_run_job_id.present?
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Summary for API responses
|
|
131
|
-
def summary
|
|
132
|
-
{
|
|
133
|
-
id: id,
|
|
134
|
-
session_id: session_id,
|
|
135
|
-
sandbox_type: sandbox_type,
|
|
136
|
-
status: status,
|
|
137
|
-
runs_count: runs_count,
|
|
138
|
-
max_runs: max_runs,
|
|
139
|
-
total_tokens: total_tokens,
|
|
140
|
-
expires_at: expires_at&.iso8601,
|
|
141
|
-
created_at: created_at.iso8601,
|
|
142
|
-
cloud_run_url: cloud_run_url
|
|
143
|
-
}
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Detailed info including runs
|
|
147
|
-
def details
|
|
148
|
-
summary.merge(
|
|
149
|
-
runs: runs,
|
|
150
|
-
total_duration_ms: total_duration_ms,
|
|
151
|
-
last_activity_at: last_activity_at&.iso8601
|
|
152
|
-
)
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
private
|
|
156
|
-
|
|
157
|
-
def generate_session_id
|
|
158
|
-
self.session_id ||= SecureRandom.uuid
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def set_defaults
|
|
162
|
-
limits = ActiveAgent::Dashboard.sandbox_limits || DEFAULT_LIMITS
|
|
163
|
-
self.expires_at ||= limits[:session_duration_minutes].minutes.from_now
|
|
164
|
-
self.max_runs ||= limits[:max_runs]
|
|
165
|
-
self.timeout_seconds ||= limits[:timeout_seconds]
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveAgent
|
|
4
|
-
module Dashboard
|
|
5
|
-
# Records browser sessions for playback and analysis.
|
|
6
|
-
#
|
|
7
|
-
# Session recordings capture the sequence of actions taken during
|
|
8
|
-
# an agent run or sandbox session, including screenshots, DOM snapshots,
|
|
9
|
-
# and timing information.
|
|
10
|
-
#
|
|
11
|
-
# @example Starting a recording
|
|
12
|
-
# recording = ActiveAgent::Dashboard::SessionRecording.start!(
|
|
13
|
-
# agent_run: run,
|
|
14
|
-
# name: "checkout_flow"
|
|
15
|
-
# )
|
|
16
|
-
#
|
|
17
|
-
# @example Recording an action
|
|
18
|
-
# recording.record_action!(
|
|
19
|
-
# action_type: "click",
|
|
20
|
-
# selector: "button.submit",
|
|
21
|
-
# screenshot: screenshot_data
|
|
22
|
-
# )
|
|
23
|
-
#
|
|
24
|
-
class SessionRecording < ApplicationRecord
|
|
25
|
-
belongs_to :agent_run, class_name: "ActiveAgent::Dashboard::AgentRun", optional: true
|
|
26
|
-
belongs_to :sandbox_session, class_name: "ActiveAgent::Dashboard::SandboxSession", optional: true
|
|
27
|
-
|
|
28
|
-
has_many :recording_actions, class_name: "ActiveAgent::Dashboard::RecordingAction", dependent: :destroy
|
|
29
|
-
has_many :recording_snapshots, class_name: "ActiveAgent::Dashboard::RecordingSnapshot", dependent: :destroy
|
|
30
|
-
|
|
31
|
-
enum :status, { recording: 0, completed: 1, failed: 2 }
|
|
32
|
-
|
|
33
|
-
validates :status, presence: true
|
|
34
|
-
validate :must_have_parent, unless: -> { demo_recording? || user_session? }
|
|
35
|
-
|
|
36
|
-
scope :recent, -> { order(created_at: :desc) }
|
|
37
|
-
scope :for_agent, ->(agent_id) { joins(:agent_run).where(agent_runs: { agent_id: agent_id }) }
|
|
38
|
-
scope :demo, -> { where(name: "lander_demo") }
|
|
39
|
-
scope :user_sessions, -> { where("name LIKE ?", "user_takeover_%") }
|
|
40
|
-
|
|
41
|
-
# Check if this is a demo recording (doesn't require parent)
|
|
42
|
-
def demo_recording?
|
|
43
|
-
name&.start_with?("lander_") || name == "demo"
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Check if this is a user takeover session (doesn't require parent)
|
|
47
|
-
def user_session?
|
|
48
|
-
name&.start_with?("user_takeover_")
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Start a new recording session
|
|
52
|
-
def self.start!(agent_run: nil, sandbox_session: nil, name: nil)
|
|
53
|
-
create!(
|
|
54
|
-
agent_run: agent_run,
|
|
55
|
-
sandbox_session: sandbox_session,
|
|
56
|
-
name: name || generate_name(agent_run, sandbox_session),
|
|
57
|
-
status: :recording,
|
|
58
|
-
metadata: { started_at: Time.current.iso8601 }
|
|
59
|
-
)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Start a user takeover session (for lander demo analytics)
|
|
63
|
-
def self.start_user_session!(visitor_id: nil, parent_demo_id: nil, page_url: nil)
|
|
64
|
-
create!(
|
|
65
|
-
name: "user_takeover_#{Time.current.strftime('%Y%m%d_%H%M%S')}_#{SecureRandom.hex(4)}",
|
|
66
|
-
status: :recording,
|
|
67
|
-
metadata: {
|
|
68
|
-
started_at: Time.current.iso8601,
|
|
69
|
-
session_type: "user_takeover",
|
|
70
|
-
visitor_id: visitor_id,
|
|
71
|
-
parent_demo_id: parent_demo_id,
|
|
72
|
-
page_url: page_url,
|
|
73
|
-
user_agent: nil
|
|
74
|
-
}
|
|
75
|
-
)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Record a browser action
|
|
79
|
-
def record_action!(action_type:, selector: nil, value: nil, screenshot: nil, dom_snapshot: nil, metadata: {})
|
|
80
|
-
raise "Recording already completed" unless recording?
|
|
81
|
-
|
|
82
|
-
action = recording_actions.create!(
|
|
83
|
-
action_type: action_type,
|
|
84
|
-
sequence: next_sequence,
|
|
85
|
-
timestamp_ms: elapsed_ms,
|
|
86
|
-
selector: selector,
|
|
87
|
-
value: value,
|
|
88
|
-
metadata: metadata
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
if screenshot.present?
|
|
92
|
-
snapshot = store_snapshot(screenshot, :screenshot, action)
|
|
93
|
-
action.update!(screenshot_key: snapshot.storage_key)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
if dom_snapshot.present?
|
|
97
|
-
snapshot = store_snapshot(dom_snapshot, :dom, action)
|
|
98
|
-
action.update!(dom_snapshot_key: snapshot.storage_key)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
increment!(:action_count)
|
|
102
|
-
action
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Complete the recording
|
|
106
|
-
def complete!
|
|
107
|
-
return unless recording?
|
|
108
|
-
|
|
109
|
-
update!(
|
|
110
|
-
status: :completed,
|
|
111
|
-
duration_ms: elapsed_ms,
|
|
112
|
-
metadata: metadata.merge(completed_at: Time.current.iso8601)
|
|
113
|
-
)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Mark recording as failed
|
|
117
|
-
def fail!(error_message = nil)
|
|
118
|
-
return unless recording?
|
|
119
|
-
|
|
120
|
-
update!(
|
|
121
|
-
status: :failed,
|
|
122
|
-
duration_ms: elapsed_ms,
|
|
123
|
-
metadata: metadata.merge(
|
|
124
|
-
failed_at: Time.current.iso8601,
|
|
125
|
-
error: error_message
|
|
126
|
-
)
|
|
127
|
-
)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Get timeline data for playback
|
|
131
|
-
def timeline
|
|
132
|
-
recording_actions.order(:sequence).map do |action|
|
|
133
|
-
{
|
|
134
|
-
id: action.id,
|
|
135
|
-
type: action.action_type,
|
|
136
|
-
sequence: action.sequence,
|
|
137
|
-
timestamp_ms: action.timestamp_ms,
|
|
138
|
-
selector: action.selector,
|
|
139
|
-
value: action.value,
|
|
140
|
-
screenshot_key: action.screenshot_key,
|
|
141
|
-
metadata: action.metadata
|
|
142
|
-
}
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
private
|
|
147
|
-
|
|
148
|
-
def must_have_parent
|
|
149
|
-
return if agent_run.present? || sandbox_session.present?
|
|
150
|
-
|
|
151
|
-
errors.add(:base, "must belong to an agent_run or sandbox_session")
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def self.generate_name(agent_run, sandbox_session)
|
|
155
|
-
prefix = if agent_run&.agent
|
|
156
|
-
agent_run.agent.name.parameterize
|
|
157
|
-
elsif sandbox_session&.agent_template
|
|
158
|
-
sandbox_session.agent_template.name.parameterize
|
|
159
|
-
else
|
|
160
|
-
"session"
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
"#{prefix}_#{Time.current.strftime('%Y%m%d_%H%M%S')}"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def next_sequence
|
|
167
|
-
(recording_actions.maximum(:sequence) || 0) + 1
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def elapsed_ms
|
|
171
|
-
((Time.current - created_at) * 1000).to_i
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def store_snapshot(data, snapshot_type, action = nil)
|
|
175
|
-
storage_key = generate_storage_key(snapshot_type, action&.sequence)
|
|
176
|
-
|
|
177
|
-
recording_snapshots.create!(
|
|
178
|
-
recording_action: action,
|
|
179
|
-
storage_key: storage_key,
|
|
180
|
-
snapshot_type: snapshot_type,
|
|
181
|
-
file_size_bytes: data.bytesize
|
|
182
|
-
)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def generate_storage_key(snapshot_type, sequence = nil)
|
|
186
|
-
parts = [ "recordings", id, snapshot_type.to_s ]
|
|
187
|
-
parts << sequence.to_s if sequence
|
|
188
|
-
parts << SecureRandom.hex(8)
|
|
189
|
-
parts.join("/")
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
end
|