Class: Browserctl::Recording

Inherits:
Object
  • Object
show all
Defined in:
lib/browserctl/recording.rb

Overview

rubocop:disable Metrics/ClassLength

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. Distinct from LOG_FORMAT below — that string (“v0.11”) tracks the human-readable log shape; this integer is the machine-readable schema gate per the WS-1 format-version convention. See docs/reference/format-versions.md.

1
SUPPORTED_FORMAT_VERSIONS =
[RECORDING_FORMAT_VERSION].freeze
RECORDABLE =
%w[page_open navigate fill click screenshot evaluate].freeze
SENSITIVE_PARAM_PATTERN =
/\A(token|key|secret|auth|code|access_token|api_key|client_secret|state)\z/ix
SECRET_FIELD_PATTERN =

Selector tokens that signal a fill is targeting a secret-shaped field. The captured group (or matched substring) is used as the inferred field name; that name later drives the generated ‘secret_ref:` placeholder.

/\b(password|passwd|api[_-]?key|token|secret|otp|pin|client[_-]?secret|access[_-]?token)\b/i
WAIT_THRESHOLD_SECONDS =

Conservative thresholds for inferring an explicit wait between recorded steps. Gaps shorter than the threshold come from natural input cadence; gaps above it usually mean the page actually had work to do.

1.5
WAIT_PADDING_SECONDS =
5
WAIT_FLOOR_SECONDS =
5
LOG_FORMAT =

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

"v0.11"

Class Method Summary collapse

Class Method Details

.activeObject



72
73
74
# File 'lib/browserctl/recording.rb', line 72

def self.active
  File.exist?(STATE_FILE) ? File.read(STATE_FILE).strip : nil
end

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



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/browserctl/recording.rb', line 76

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

  File.open(log_path(name), "a") do |f|
    f.puts JSON.generate(entry)
  end
end

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



95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/browserctl/recording.rb', line 95

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

  raw   = File.readlines(log).map { |l| JSON.parse(l, symbolize_names: true) }
  verify_format_version!(raw, path: log)
  lines = raw.reject { |l| l[:cmd] == "_meta" }
  ruby  = build_workflow_ruby(name, lines)
  File.write(output_path, ruby) if output_path
  warn_about_ref_interactions(lines)
  ruby
ensure
  FileUtils.rm_f(log) if log && !keep_log
end

.start(name) ⇒ Object



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

def self.start(name)
  FileUtils.mkdir_p(RECORDINGS_DIR, mode: 0o700)
  FileUtils.mkdir_p(File.dirname(STATE_FILE))
  File.write(STATE_FILE, name)
  FileUtils.rm_f(log_path(name))
  FileUtils.touch(log_path(name))
  File.chmod(0o600, log_path(name))
  File.open(log_path(name), "a") do |f|
    f.puts JSON.generate(
      cmd: "_meta",
      format_version: RECORDING_FORMAT_VERSION,
      log_format: LOG_FORMAT,
      recording: name,
      started_at: Time.now.utc.iso8601
    )
  end
  name
end

.stopObject

Raises:



64
65
66
67
68
69
70
# File 'lib/browserctl/recording.rb', line 64

def self.stop
  name = active
  raise Browserctl::Error, "no active recording — run: browserctl record start <name>" unless name

  File.unlink(STATE_FILE)
  name
end

.warn_about_ref_interactions(lines) ⇒ Object



110
111
112
113
114
115
116
# File 'lib/browserctl/recording.rb', line 110

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