Class: Browserctl::Recording

Inherits:
Object
  • Object
show all
Defined in:
lib/browserctl/recording.rb,
lib/browserctl/recording/state.rb,
lib/browserctl/recording/redactor.rb,
lib/browserctl/recording/log_writer.rb,
lib/browserctl/recording/workflow_renderer.rb

Overview

Captures a sequence of daemon commands as a JSONL log and renders it back out as a Ruby workflow file. This class is the public facade; the focused responsibilities live under ‘Browserctl::Recording::*`:

  • ‘Recording::State` — singleton over the on-disk marker.

  • ‘Recording::Redactor` — secret-aware redaction.

  • ‘Recording::LogWriter` — log file I/O and JSONL formatting.

  • ‘Recording::WorkflowRenderer` — log-to-Ruby workflow translation.

Defined Under Namespace

Modules: LogWriter, Redactor, State, WorkflowRenderer

Constant Summary collapse

RECORDINGS_DIR =
File.join(Dir.tmpdir, "browserctl-recordings")
STATE_FILE =
File.expand_path("~/.browserctl/active_recording")
RECORDING_FORMAT_VERSION =

Recording-log format version, written into the ‘_meta` header and validated when generate_workflow loads a recording. See docs/reference/format-versions.md.

1
SUPPORTED_FORMAT_VERSIONS =
[RECORDING_FORMAT_VERSION].freeze
LOG_FORMAT =

Bumped when the recording log shape changes in a way that older tooling (workflow generate, replay) cannot read.

"v0.11"
RECORDABLE =
%w[page_open navigate fill click screenshot evaluate].freeze

Class Method Summary collapse

Class Method Details

.activeObject



42
43
44
# File 'lib/browserctl/recording.rb', line 42

def self.active
  State.active
end

.append(cmd, response: nil, **attrs) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/browserctl/recording.rb', line 46

def self.append(cmd, response: nil, **attrs)
  name = active
  return unless name
  return unless RECORDABLE.include?(cmd.to_s)

  if %w[click fill].include?(cmd.to_s) && attrs[:selector].nil?
    record_ref_interaction(name, cmd.to_s, attrs, response)
    return
  end

  attrs = prepare_attrs(cmd.to_s, attrs)
  entry = { cmd: cmd.to_s, ts: now }.merge(attrs.transform_keys(&:to_s))
  entry.merge!((response)) if response

  LogWriter.append_entry(name, entry)
end

.generate_workflow(name, output_path: nil, keep_log: false) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/browserctl/recording.rb', line 63

def self.generate_workflow(name, output_path: nil, keep_log: false)
  log = LogWriter.log_path(name)
  raise Browserctl::Error, "no recording found for '#{name}'" unless File.exist?(log)

  raw   = LogWriter.read_entries(name)
  LogWriter.verify_format_version!(raw, path: log)
  lines = raw.reject { |l| l[:cmd] == "_meta" }
  ruby  = WorkflowRenderer.render(name, lines)
  File.write(output_path, ruby) if output_path
  warn_about_ref_interactions(lines)
  ruby
ensure
  LogWriter.delete_log(name) unless keep_log
end

.start(name) ⇒ Object



32
33
34
35
36
# File 'lib/browserctl/recording.rb', line 32

def self.start(name)
  LogWriter.init_log(name)
  State.write(name)
  name
end

.stopObject



38
39
40
# File 'lib/browserctl/recording.rb', line 38

def self.stop
  State.clear!
end

.warn_about_ref_interactions(lines) ⇒ Object



78
79
80
81
82
83
84
# File 'lib/browserctl/recording.rb', line 78

def self.warn_about_ref_interactions(lines)
  ref_count = lines.count { |l| l[:cmd] == "_ref_interaction" }
  return unless ref_count.positive?

  warn "Warning: #{ref_count} ref-based interaction(s) were captured but cannot be replayed by ref."
  warn "Search the generated workflow for 'TODO: ref-based' and replace with stable CSS selectors."
end