@humanjs/recorder 0.1.1 → 0.3.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.
package/README.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @humanjs/recorder
2
2
 
3
+ <p>
4
+ <a href="https://www.npmjs.com/package/@humanjs/recorder"><img alt="npm" src="https://img.shields.io/npm/v/@humanjs/recorder"></a>
5
+ <a href="https://www.npmjs.com/package/@humanjs/recorder"><img alt="downloads" src="https://img.shields.io/npm/dt/@humanjs/recorder"></a>
6
+ <a href="https://github.com/totigm/humanjs"><img alt="GitHub" src="https://img.shields.io/badge/GitHub-totigm%2Fhumanjs-181717?logo=github"></a>
7
+ <a href="https://github.com/totigm/humanjs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/totigm/humanjs/actions/workflows/ci.yml/badge.svg"></a>
8
+ <a href="https://github.com/totigm/humanjs/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/npm/l/@humanjs/recorder"></a>
9
+ <a href="https://humanjs.dev"><img alt="docs" src="https://img.shields.io/badge/docs-humanjs.dev-emerald"></a>
10
+ </p>
11
+
3
12
  One-call session recording for [HumanJS](https://humanjs.dev) — turn a humanized Playwright session into an mp4, an animated GIF, a structured JSON timeline, or any combination.
4
13
 
5
14
  ## Install
@@ -49,6 +58,8 @@ The returned `Recording` has:
49
58
  | `rec.toVideo(path, options?)` | Write an mp4 or webm. Repeatable. |
50
59
  | `rec.toGif(path, options?)` | Write an animated GIF (palette-optimized, defaults to 15fps). Repeatable. |
51
60
  | `rec.toTimeline(path)` | Write the structured JSON timeline. Repeatable. |
61
+ | `rec.toHumanJS(path)` | Write a runnable HumanJS script that replays the session. |
62
+ | `rec.toPlaywright(path)` | Write a `@playwright/test` spec (humanized — uses HumanJS). |
52
63
  | `rec.timeline` | Read the in-memory `Timeline` object. |
53
64
  | `rec.durationMs` | Wall-clock duration of the recorded window. |
54
65
  | `rec.hasVideo` | True if frames were captured (i.e. `output` was set). |
@@ -72,12 +83,16 @@ await record(
72
83
  {
73
84
  output: 'demo.mp4', // .mp4 / .webm / .gif — omit to skip video entirely
74
85
  quality: 'high', // 'fast' | 'standard' | 'high' (default) | 'lossless'
86
+ captureInputs: true, // capture typed/pasted text for code export (default; passwords masked)
75
87
  url: 'https://example.com', // optional — navigate before the callback
76
88
  personality: 'careful', // any PersonalityConfig
77
89
  seed: 'session-42', // deterministic when set
78
- viewport: { width: 1920, height: 1080 },
90
+ viewport: { width: 1920, height: 1080 }, // ephemeral/persistent only — CDP uses the real window
79
91
  headless: false, // defaults false so you can watch the recording
80
92
  cursor: true, // auto-install visible cursor overlay (default true)
93
+ userDataDir: './.profile', // persistent profile — stay logged in across runs
94
+ cdpUrl: 'http://localhost:9222', // OR attach to a browser you launched (precedence)
95
+ channel: 'chrome', // launch installed Chrome instead of bundled Chromium
81
96
  launch: { args: ['--no-sandbox'] }, // forwarded to chromium.launch()
82
97
  context: { locale: 'en-US' }, // forwarded to browser.newContext()
83
98
  },
@@ -93,6 +108,31 @@ await record(
93
108
 
94
109
  **No-video mode**: omit `output` entirely (or call `record(fn)` with no options). No screenshot polling, no temp files, no encoding overhead. The structured timeline is still captured.
95
110
 
111
+ ## Recording a logged-in flow
112
+
113
+ By default each `record()` call uses a fresh, signed-out browser. Two ways to record something behind a login:
114
+
115
+ ```ts
116
+ // Persistent profile — sign in once (in a headed run), reuse it forever
117
+ await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {
118
+ await human.goto('https://app.example.com/dashboard');
119
+ });
120
+
121
+ // Or attach to a browser you already launched (real logins/tabs)
122
+ // chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.humanjs-chrome"
123
+ await record({ output: 'flow.mp4', cdpUrl: 'http://localhost:9222' }, async (human) => {
124
+ await human.goto('https://app.example.com/dashboard');
125
+ });
126
+ ```
127
+
128
+ - **`userDataDir`** keeps cookies/logins across runs (a dedicated profile, starts empty).
129
+ - **`cdpUrl`** records a browser you launched yourself — its existing session. HumanJS **never closes** a browser it attached to; it only borrows it. Takes precedence over `userDataDir`.
130
+ - **`channel`** (`'chrome'` / `'msedge'`) swaps the binary but, on its own, still uses a fresh profile — pair it with `userDataDir` or `cdpUrl` for real logins.
131
+
132
+ > You can't attach to your everyday Chrome — it only exposes a CDP port when launched with `--remote-debugging-port`, and Chrome refuses that on the default profile. Use a dedicated `--user-data-dir` (you sign in once there), or `userDataDir` to let HumanJS manage one.
133
+
134
+ > **Recording resolution in CDP mode:** the attached browser's real window size wins — `viewport` is intentionally not applied. HumanJS is borrowing a window it doesn't own, and forcing a size would only *emulate* one (letterboxing the capture so the frames don't match what you see). Need a specific resolution? Launch the browser with `--window-size=1920,1080` to size the real window, or use the default/persistent modes where HumanJS owns the window and `viewport` applies directly.
135
+
96
136
  ## Quality presets
97
137
 
98
138
  Pick the preset that matches the recording's purpose:
package/dist/index.cjs CHANGED
@@ -19,55 +19,98 @@ async function record(optionsOrFn, maybeFn) {
19
19
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
20
20
  const {
21
21
  output,
22
+ name,
22
23
  quality,
24
+ captureInputs,
23
25
  url,
24
26
  viewport,
25
27
  headless,
26
28
  launch,
27
- context,
29
+ context: contextOptions,
30
+ userDataDir,
31
+ cdpUrl,
32
+ channel,
28
33
  cursor,
29
34
  ...createHumanOptions
30
35
  } = options;
31
36
  const resolvedQuality = quality ?? "high";
32
37
  const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];
33
- const resolvedViewport = viewport ?? context?.viewport ?? browserPreset.viewport;
38
+ const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;
34
39
  const wantsCapture = output !== void 0;
35
- const browser = await playwright.chromium.launch({
36
- ...launch,
37
- headless: headless ?? false
40
+ const { page, dispose } = await acquireRecordingContext({
41
+ cdpUrl,
42
+ userDataDir,
43
+ channel,
44
+ headless: headless ?? false,
45
+ viewport: resolvedViewport,
46
+ launch,
47
+ context: contextOptions,
48
+ cursor
38
49
  });
39
50
  try {
40
- const browserContext = await browser.newContext({
41
- ...context,
42
- viewport: resolvedViewport
43
- });
44
- try {
45
- if (cursor !== false) {
46
- const cursorOptions = typeof cursor === "object" ? cursor : void 0;
47
- await playwright.installMouseHelper(browserContext, cursorOptions);
51
+ if (url) await page.goto(url);
52
+ const human = await playwright.createHuman(page, createHumanOptions);
53
+ const recording = await human.record(
54
+ { name, video: wantsCapture, quality: resolvedQuality, captureInputs },
55
+ () => fn(human, page)
56
+ );
57
+ if (wantsCapture && output) {
58
+ const ext = path.extname(output).toLowerCase();
59
+ if (ext === ".gif") {
60
+ await recording.toGif(output);
61
+ } else {
62
+ await recording.toVideo(output, { quality: resolvedQuality });
48
63
  }
49
- const page = await browserContext.newPage();
50
- if (url) await page.goto(url);
51
- const human = await playwright.createHuman(page, createHumanOptions);
52
- const recording = await human.record(
53
- { video: wantsCapture, quality: resolvedQuality },
54
- () => fn(human, page)
55
- );
56
- if (wantsCapture && output) {
57
- const ext = path.extname(output).toLowerCase();
58
- if (ext === ".gif") {
59
- await recording.toGif(output);
60
- } else {
61
- await recording.toVideo(output, { quality: resolvedQuality });
62
- }
63
- }
64
- return recording;
65
- } finally {
66
- await browserContext.close().catch(() => void 0);
67
64
  }
65
+ return recording;
68
66
  } finally {
69
- await browser.close();
67
+ await dispose();
68
+ }
69
+ }
70
+ async function acquireRecordingContext(opts) {
71
+ const cursorOptions = typeof opts.cursor === "object" ? opts.cursor : void 0;
72
+ const installCursor = async (ctx) => {
73
+ if (opts.cursor !== false) await playwright.installMouseHelper(ctx, cursorOptions);
74
+ };
75
+ if (opts.cdpUrl) {
76
+ const browser2 = await playwright.chromium.connectOverCDP(opts.cdpUrl);
77
+ const context2 = browser2.contexts()[0] ?? await browser2.newContext({ ...opts.context });
78
+ await installCursor(context2);
79
+ const page2 = context2.pages()[0] ?? await context2.newPage();
80
+ return { page: page2, dispose: async () => void 0 };
70
81
  }
82
+ if (opts.userDataDir) {
83
+ const context2 = await playwright.chromium.launchPersistentContext(opts.userDataDir, {
84
+ ...opts.launch,
85
+ ...opts.context,
86
+ channel: opts.channel ?? opts.launch?.channel,
87
+ headless: opts.headless,
88
+ viewport: opts.viewport
89
+ });
90
+ await installCursor(context2);
91
+ const page2 = context2.pages()[0] ?? await context2.newPage();
92
+ return {
93
+ page: page2,
94
+ dispose: async () => {
95
+ await context2.close().catch(() => void 0);
96
+ }
97
+ };
98
+ }
99
+ const browser = await playwright.chromium.launch({
100
+ ...opts.launch,
101
+ channel: opts.channel ?? opts.launch?.channel,
102
+ headless: opts.headless
103
+ });
104
+ const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });
105
+ await installCursor(context);
106
+ const page = await context.newPage();
107
+ return {
108
+ page,
109
+ dispose: async () => {
110
+ await context.close().catch(() => void 0);
111
+ await browser.close().catch(() => void 0);
112
+ }
113
+ };
71
114
  }
72
115
 
73
116
  exports.record = record;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/record/index.ts"],"names":["chromium","installMouseHelper","createHuman","extname"],"mappings":"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","file":"index.cjs","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /** Viewport dimensions. Overrides the quality preset's viewport. */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches a browser, opens a page, creates a\n * humanized session, runs `fn`, and returns a {@link Recording} you can\n * export to video, JSON timeline, or read in-memory.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Timeline only, no video overhead\n * const rec = await record(async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n quality,\n url,\n viewport,\n headless,\n launch,\n context,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? context?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const browser = await chromium.launch({\n ...launch,\n headless: headless ?? false,\n });\n try {\n const browserContext = await browser.newContext({\n ...context,\n viewport: resolvedViewport,\n });\n try {\n // Install the visible-cursor overlay before any page is created so\n // the addInitScript runs on the initial page render — and on any\n // page.setContent() inside the callback too.\n if (cursor !== false) {\n const cursorOptions = typeof cursor === 'object' ? cursor : undefined;\n await installMouseHelper(browserContext, cursorOptions);\n }\n\n const page = await browserContext.newPage();\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record({ video: wantsCapture, quality: resolvedQuality }, () =>\n fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await browserContext.close().catch(() => undefined);\n }\n } finally {\n await browser.close();\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/record/index.ts"],"names":["createHuman","extname","installMouseHelper","browser","chromium","context","page"],"mappings":"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","file":"index.cjs","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContext,\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Optional label for the recording — becomes the title of a generated\n * `toPlaywright()` test. See `@humanjs/playwright`'s `HumanRecordOptions.name`.\n */\n readonly name?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /**\n * Capture actual typed/pasted text into the timeline, so `toHumanJS()` /\n * `toPlaywright()` exports include the values. Defaults to `true`; password\n * fields are always masked. Set `false` to record no input values (exports\n * emit empty-string placeholders). See `@humanjs/playwright`'s\n * `HumanRecordOptions.captureInputs`.\n */\n readonly captureInputs?: boolean;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /**\n * Viewport dimensions. Overrides the quality preset's viewport. Applies to\n * the default and persistent (`userDataDir`) modes; ignored when attaching\n * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).\n */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Record in a **persistent profile** at this directory so logins and\n * cookies survive across runs — sign in once (in a headed run), and later\n * recordings start authenticated. Uses `launchPersistentContext` under the\n * hood (a single context); `headless`, `launch`, `channel`, and `viewport`\n * still apply. Starts empty the first time.\n */\n readonly userDataDir?: string;\n /**\n * Record by **attaching to a browser you already launched** over CDP (e.g.\n * `\"http://localhost:9222\"`). Reuses that browser's existing context — your\n * real logins, tabs, extensions. Start the browser yourself with\n * `--remote-debugging-port`. HumanJS never closes a browser it attached to;\n * it only borrows it. Takes precedence over `userDataDir`.\n *\n * `launch` / `headless` / `channel` / `viewport` don't apply here — you're\n * borrowing a window HumanJS doesn't own, so the browser's real window size\n * wins. Forcing a `viewport` would only *emulate* one and letterbox the\n * capture. For a fixed recording resolution, launch the browser yourself\n * with `--window-size=1920,1080`, or use the default / persistent modes.\n */\n readonly cdpUrl?: string;\n /**\n * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that\n * installed browser's binary instead of bundled Chromium (applies to the\n * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse\n * your existing profile — pair it with `userDataDir` (persistent) or\n * `cdpUrl` (attach) for real logins.\n */\n readonly channel?: string;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches (or attaches to) a browser, opens a\n * page, creates a humanized session, runs `fn`, and returns a\n * {@link Recording} you can export to video, JSON timeline, or read in-memory.\n *\n * Browser source (default → ephemeral fresh profile):\n * - `userDataDir` — a persistent profile that keeps logins across runs.\n * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);\n * never closed on finish, only released.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Stay signed in across runs (persistent profile)\n * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {\n * await human.goto('https://app.example.com/dashboard');\n * });\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n name,\n quality,\n captureInputs,\n url,\n viewport,\n headless,\n launch,\n context: contextOptions,\n userDataDir,\n cdpUrl,\n channel,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const { page, dispose } = await acquireRecordingContext({\n cdpUrl,\n userDataDir,\n channel,\n headless: headless ?? false,\n viewport: resolvedViewport,\n launch,\n context: contextOptions,\n cursor,\n });\n\n try {\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record(\n { name, video: wantsCapture, quality: resolvedQuality, captureInputs },\n () => fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await dispose();\n }\n}\n\n/** Internal options for {@link acquireRecordingContext}. */\ninterface AcquireOptions {\n readonly cdpUrl?: string;\n readonly userDataDir?: string;\n readonly channel?: string;\n readonly headless: boolean;\n readonly viewport: { readonly width: number; readonly height: number };\n readonly launch?: LaunchOptions;\n readonly context?: BrowserContextOptions;\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/**\n * Resolves the browser source — CDP attach > persistent profile > ephemeral\n * — and returns the page to drive plus a `dispose` that tears down only what\n * we created. A CDP-attached browser is borrowed: `dispose` leaves it (and\n * its context) untouched so we never close the caller's real browser.\n */\nasync function acquireRecordingContext(\n opts: AcquireOptions,\n): Promise<{ page: Page; dispose: () => Promise<void> }> {\n const cursorOptions = typeof opts.cursor === 'object' ? opts.cursor : undefined;\n const installCursor = async (ctx: BrowserContext): Promise<void> => {\n if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);\n };\n\n // Attach to a browser the caller launched. Reuse its existing context so we\n // record their real session; only make a fresh one if there's none. Never\n // close it on dispose — it's borrowed.\n if (opts.cdpUrl) {\n const browser = await chromium.connectOverCDP(opts.cdpUrl);\n const context = browser.contexts()[0] ?? (await browser.newContext({ ...opts.context }));\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return { page, dispose: async () => undefined };\n }\n\n // Persistent profile — the context owns its browser, so closing it on\n // dispose tears everything down.\n if (opts.userDataDir) {\n const context = await chromium.launchPersistentContext(opts.userDataDir, {\n ...opts.launch,\n ...opts.context,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n viewport: opts.viewport,\n });\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n },\n };\n }\n\n // Ephemeral (default) — a fresh throwaway browser + context.\n const browser = await chromium.launch({\n ...opts.launch,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n });\n const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });\n await installCursor(context);\n const page = await context.newPage();\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n await browser.close().catch(() => undefined);\n },\n };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -14,6 +14,11 @@ interface RecordOptions extends CreateHumanOptions {
14
14
  * the structured action timeline via `.toTimeline()` / `.timeline`.
15
15
  */
16
16
  readonly output?: string;
17
+ /**
18
+ * Optional label for the recording — becomes the title of a generated
19
+ * `toPlaywright()` test. See `@humanjs/playwright`'s `HumanRecordOptions.name`.
20
+ */
21
+ readonly name?: string;
17
22
  /**
18
23
  * Quality preset. Picks both source viewport and ffmpeg encoding settings.
19
24
  * Defaults to `'high'` (visually-lossless 1080p).
@@ -24,9 +29,21 @@ interface RecordOptions extends CreateHumanOptions {
24
29
  * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation
25
30
  */
26
31
  readonly quality?: RecordingQuality;
32
+ /**
33
+ * Capture actual typed/pasted text into the timeline, so `toHumanJS()` /
34
+ * `toPlaywright()` exports include the values. Defaults to `true`; password
35
+ * fields are always masked. Set `false` to record no input values (exports
36
+ * emit empty-string placeholders). See `@humanjs/playwright`'s
37
+ * `HumanRecordOptions.captureInputs`.
38
+ */
39
+ readonly captureInputs?: boolean;
27
40
  /** Optional URL to navigate to before the callback runs. */
28
41
  readonly url?: string;
29
- /** Viewport dimensions. Overrides the quality preset's viewport. */
42
+ /**
43
+ * Viewport dimensions. Overrides the quality preset's viewport. Applies to
44
+ * the default and persistent (`userDataDir`) modes; ignored when attaching
45
+ * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).
46
+ */
30
47
  readonly viewport?: {
31
48
  readonly width: number;
32
49
  readonly height: number;
@@ -37,6 +54,36 @@ interface RecordOptions extends CreateHumanOptions {
37
54
  readonly launch?: LaunchOptions;
38
55
  /** Forwarded to `browser.newContext()` (alongside `viewport`). */
39
56
  readonly context?: BrowserContextOptions;
57
+ /**
58
+ * Record in a **persistent profile** at this directory so logins and
59
+ * cookies survive across runs — sign in once (in a headed run), and later
60
+ * recordings start authenticated. Uses `launchPersistentContext` under the
61
+ * hood (a single context); `headless`, `launch`, `channel`, and `viewport`
62
+ * still apply. Starts empty the first time.
63
+ */
64
+ readonly userDataDir?: string;
65
+ /**
66
+ * Record by **attaching to a browser you already launched** over CDP (e.g.
67
+ * `"http://localhost:9222"`). Reuses that browser's existing context — your
68
+ * real logins, tabs, extensions. Start the browser yourself with
69
+ * `--remote-debugging-port`. HumanJS never closes a browser it attached to;
70
+ * it only borrows it. Takes precedence over `userDataDir`.
71
+ *
72
+ * `launch` / `headless` / `channel` / `viewport` don't apply here — you're
73
+ * borrowing a window HumanJS doesn't own, so the browser's real window size
74
+ * wins. Forcing a `viewport` would only *emulate* one and letterbox the
75
+ * capture. For a fixed recording resolution, launch the browser yourself
76
+ * with `--window-size=1920,1080`, or use the default / persistent modes.
77
+ */
78
+ readonly cdpUrl?: string;
79
+ /**
80
+ * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that
81
+ * installed browser's binary instead of bundled Chromium (applies to the
82
+ * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse
83
+ * your existing profile — pair it with `userDataDir` (persistent) or
84
+ * `cdpUrl` (attach) for real logins.
85
+ */
86
+ readonly channel?: string;
40
87
  /**
41
88
  * Install the HumanJS visible cursor overlay so recorded videos show
42
89
  * mouse motion — Playwright's synthetic mouse doesn't render a cursor
@@ -58,9 +105,14 @@ interface RecordOptions extends CreateHumanOptions {
58
105
  /** The callback shape both overloads of {@link record} accept. */
59
106
  type RecordCallback = (human: Human, page: Page) => Promise<void>;
60
107
  /**
61
- * One-call session recording. Launches a browser, opens a page, creates a
62
- * humanized session, runs `fn`, and returns a {@link Recording} you can
63
- * export to video, JSON timeline, or read in-memory.
108
+ * One-call session recording. Launches (or attaches to) a browser, opens a
109
+ * page, creates a humanized session, runs `fn`, and returns a
110
+ * {@link Recording} you can export to video, JSON timeline, or read in-memory.
111
+ *
112
+ * Browser source (default → ephemeral fresh profile):
113
+ * - `userDataDir` — a persistent profile that keeps logins across runs.
114
+ * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);
115
+ * never closed on finish, only released.
64
116
  *
65
117
  * If `options.output` is set, the output file is written to that path before
66
118
  * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)
@@ -81,11 +133,10 @@ type RecordCallback = (human: Human, page: Page) => Promise<void>;
81
133
  *
82
134
  * @example
83
135
  * ```ts
84
- * // Timeline only, no video overhead
85
- * const rec = await record(async (human) => {
86
- * await human.click('#login');
136
+ * // Stay signed in across runs (persistent profile)
137
+ * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {
138
+ * await human.goto('https://app.example.com/dashboard');
87
139
  * });
88
- * await rec.toTimeline('demo.json');
89
140
  * ```
90
141
  *
91
142
  * For multi-page flows or recording a slice of a larger session, use
package/dist/index.d.ts CHANGED
@@ -14,6 +14,11 @@ interface RecordOptions extends CreateHumanOptions {
14
14
  * the structured action timeline via `.toTimeline()` / `.timeline`.
15
15
  */
16
16
  readonly output?: string;
17
+ /**
18
+ * Optional label for the recording — becomes the title of a generated
19
+ * `toPlaywright()` test. See `@humanjs/playwright`'s `HumanRecordOptions.name`.
20
+ */
21
+ readonly name?: string;
17
22
  /**
18
23
  * Quality preset. Picks both source viewport and ffmpeg encoding settings.
19
24
  * Defaults to `'high'` (visually-lossless 1080p).
@@ -24,9 +29,21 @@ interface RecordOptions extends CreateHumanOptions {
24
29
  * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation
25
30
  */
26
31
  readonly quality?: RecordingQuality;
32
+ /**
33
+ * Capture actual typed/pasted text into the timeline, so `toHumanJS()` /
34
+ * `toPlaywright()` exports include the values. Defaults to `true`; password
35
+ * fields are always masked. Set `false` to record no input values (exports
36
+ * emit empty-string placeholders). See `@humanjs/playwright`'s
37
+ * `HumanRecordOptions.captureInputs`.
38
+ */
39
+ readonly captureInputs?: boolean;
27
40
  /** Optional URL to navigate to before the callback runs. */
28
41
  readonly url?: string;
29
- /** Viewport dimensions. Overrides the quality preset's viewport. */
42
+ /**
43
+ * Viewport dimensions. Overrides the quality preset's viewport. Applies to
44
+ * the default and persistent (`userDataDir`) modes; ignored when attaching
45
+ * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).
46
+ */
30
47
  readonly viewport?: {
31
48
  readonly width: number;
32
49
  readonly height: number;
@@ -37,6 +54,36 @@ interface RecordOptions extends CreateHumanOptions {
37
54
  readonly launch?: LaunchOptions;
38
55
  /** Forwarded to `browser.newContext()` (alongside `viewport`). */
39
56
  readonly context?: BrowserContextOptions;
57
+ /**
58
+ * Record in a **persistent profile** at this directory so logins and
59
+ * cookies survive across runs — sign in once (in a headed run), and later
60
+ * recordings start authenticated. Uses `launchPersistentContext` under the
61
+ * hood (a single context); `headless`, `launch`, `channel`, and `viewport`
62
+ * still apply. Starts empty the first time.
63
+ */
64
+ readonly userDataDir?: string;
65
+ /**
66
+ * Record by **attaching to a browser you already launched** over CDP (e.g.
67
+ * `"http://localhost:9222"`). Reuses that browser's existing context — your
68
+ * real logins, tabs, extensions. Start the browser yourself with
69
+ * `--remote-debugging-port`. HumanJS never closes a browser it attached to;
70
+ * it only borrows it. Takes precedence over `userDataDir`.
71
+ *
72
+ * `launch` / `headless` / `channel` / `viewport` don't apply here — you're
73
+ * borrowing a window HumanJS doesn't own, so the browser's real window size
74
+ * wins. Forcing a `viewport` would only *emulate* one and letterbox the
75
+ * capture. For a fixed recording resolution, launch the browser yourself
76
+ * with `--window-size=1920,1080`, or use the default / persistent modes.
77
+ */
78
+ readonly cdpUrl?: string;
79
+ /**
80
+ * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that
81
+ * installed browser's binary instead of bundled Chromium (applies to the
82
+ * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse
83
+ * your existing profile — pair it with `userDataDir` (persistent) or
84
+ * `cdpUrl` (attach) for real logins.
85
+ */
86
+ readonly channel?: string;
40
87
  /**
41
88
  * Install the HumanJS visible cursor overlay so recorded videos show
42
89
  * mouse motion — Playwright's synthetic mouse doesn't render a cursor
@@ -58,9 +105,14 @@ interface RecordOptions extends CreateHumanOptions {
58
105
  /** The callback shape both overloads of {@link record} accept. */
59
106
  type RecordCallback = (human: Human, page: Page) => Promise<void>;
60
107
  /**
61
- * One-call session recording. Launches a browser, opens a page, creates a
62
- * humanized session, runs `fn`, and returns a {@link Recording} you can
63
- * export to video, JSON timeline, or read in-memory.
108
+ * One-call session recording. Launches (or attaches to) a browser, opens a
109
+ * page, creates a humanized session, runs `fn`, and returns a
110
+ * {@link Recording} you can export to video, JSON timeline, or read in-memory.
111
+ *
112
+ * Browser source (default → ephemeral fresh profile):
113
+ * - `userDataDir` — a persistent profile that keeps logins across runs.
114
+ * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);
115
+ * never closed on finish, only released.
64
116
  *
65
117
  * If `options.output` is set, the output file is written to that path before
66
118
  * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)
@@ -81,11 +133,10 @@ type RecordCallback = (human: Human, page: Page) => Promise<void>;
81
133
  *
82
134
  * @example
83
135
  * ```ts
84
- * // Timeline only, no video overhead
85
- * const rec = await record(async (human) => {
86
- * await human.click('#login');
136
+ * // Stay signed in across runs (persistent profile)
137
+ * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {
138
+ * await human.goto('https://app.example.com/dashboard');
87
139
  * });
88
- * await rec.toTimeline('demo.json');
89
140
  * ```
90
141
  *
91
142
  * For multi-page flows or recording a slice of a larger session, use
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { extname } from 'path';
2
- import { chromium, installMouseHelper, createHuman } from '@humanjs/playwright';
2
+ import { createHuman, chromium, installMouseHelper } from '@humanjs/playwright';
3
3
 
4
4
  // src/record/index.ts
5
5
  var QUALITY_BROWSER_PRESETS = {
@@ -17,55 +17,98 @@ async function record(optionsOrFn, maybeFn) {
17
17
  const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
18
18
  const {
19
19
  output,
20
+ name,
20
21
  quality,
22
+ captureInputs,
21
23
  url,
22
24
  viewport,
23
25
  headless,
24
26
  launch,
25
- context,
27
+ context: contextOptions,
28
+ userDataDir,
29
+ cdpUrl,
30
+ channel,
26
31
  cursor,
27
32
  ...createHumanOptions
28
33
  } = options;
29
34
  const resolvedQuality = quality ?? "high";
30
35
  const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];
31
- const resolvedViewport = viewport ?? context?.viewport ?? browserPreset.viewport;
36
+ const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;
32
37
  const wantsCapture = output !== void 0;
33
- const browser = await chromium.launch({
34
- ...launch,
35
- headless: headless ?? false
38
+ const { page, dispose } = await acquireRecordingContext({
39
+ cdpUrl,
40
+ userDataDir,
41
+ channel,
42
+ headless: headless ?? false,
43
+ viewport: resolvedViewport,
44
+ launch,
45
+ context: contextOptions,
46
+ cursor
36
47
  });
37
48
  try {
38
- const browserContext = await browser.newContext({
39
- ...context,
40
- viewport: resolvedViewport
41
- });
42
- try {
43
- if (cursor !== false) {
44
- const cursorOptions = typeof cursor === "object" ? cursor : void 0;
45
- await installMouseHelper(browserContext, cursorOptions);
49
+ if (url) await page.goto(url);
50
+ const human = await createHuman(page, createHumanOptions);
51
+ const recording = await human.record(
52
+ { name, video: wantsCapture, quality: resolvedQuality, captureInputs },
53
+ () => fn(human, page)
54
+ );
55
+ if (wantsCapture && output) {
56
+ const ext = extname(output).toLowerCase();
57
+ if (ext === ".gif") {
58
+ await recording.toGif(output);
59
+ } else {
60
+ await recording.toVideo(output, { quality: resolvedQuality });
46
61
  }
47
- const page = await browserContext.newPage();
48
- if (url) await page.goto(url);
49
- const human = await createHuman(page, createHumanOptions);
50
- const recording = await human.record(
51
- { video: wantsCapture, quality: resolvedQuality },
52
- () => fn(human, page)
53
- );
54
- if (wantsCapture && output) {
55
- const ext = extname(output).toLowerCase();
56
- if (ext === ".gif") {
57
- await recording.toGif(output);
58
- } else {
59
- await recording.toVideo(output, { quality: resolvedQuality });
60
- }
61
- }
62
- return recording;
63
- } finally {
64
- await browserContext.close().catch(() => void 0);
65
62
  }
63
+ return recording;
66
64
  } finally {
67
- await browser.close();
65
+ await dispose();
66
+ }
67
+ }
68
+ async function acquireRecordingContext(opts) {
69
+ const cursorOptions = typeof opts.cursor === "object" ? opts.cursor : void 0;
70
+ const installCursor = async (ctx) => {
71
+ if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);
72
+ };
73
+ if (opts.cdpUrl) {
74
+ const browser2 = await chromium.connectOverCDP(opts.cdpUrl);
75
+ const context2 = browser2.contexts()[0] ?? await browser2.newContext({ ...opts.context });
76
+ await installCursor(context2);
77
+ const page2 = context2.pages()[0] ?? await context2.newPage();
78
+ return { page: page2, dispose: async () => void 0 };
68
79
  }
80
+ if (opts.userDataDir) {
81
+ const context2 = await chromium.launchPersistentContext(opts.userDataDir, {
82
+ ...opts.launch,
83
+ ...opts.context,
84
+ channel: opts.channel ?? opts.launch?.channel,
85
+ headless: opts.headless,
86
+ viewport: opts.viewport
87
+ });
88
+ await installCursor(context2);
89
+ const page2 = context2.pages()[0] ?? await context2.newPage();
90
+ return {
91
+ page: page2,
92
+ dispose: async () => {
93
+ await context2.close().catch(() => void 0);
94
+ }
95
+ };
96
+ }
97
+ const browser = await chromium.launch({
98
+ ...opts.launch,
99
+ channel: opts.channel ?? opts.launch?.channel,
100
+ headless: opts.headless
101
+ });
102
+ const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });
103
+ await installCursor(context);
104
+ const page = await context.newPage();
105
+ return {
106
+ page,
107
+ dispose: async () => {
108
+ await context.close().catch(() => void 0);
109
+ await browser.close().catch(() => void 0);
110
+ }
111
+ };
69
112
  }
70
113
 
71
114
  export { record };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/record/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /** Viewport dimensions. Overrides the quality preset's viewport. */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches a browser, opens a page, creates a\n * humanized session, runs `fn`, and returns a {@link Recording} you can\n * export to video, JSON timeline, or read in-memory.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Timeline only, no video overhead\n * const rec = await record(async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n quality,\n url,\n viewport,\n headless,\n launch,\n context,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? context?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const browser = await chromium.launch({\n ...launch,\n headless: headless ?? false,\n });\n try {\n const browserContext = await browser.newContext({\n ...context,\n viewport: resolvedViewport,\n });\n try {\n // Install the visible-cursor overlay before any page is created so\n // the addInitScript runs on the initial page render — and on any\n // page.setContent() inside the callback too.\n if (cursor !== false) {\n const cursorOptions = typeof cursor === 'object' ? cursor : undefined;\n await installMouseHelper(browserContext, cursorOptions);\n }\n\n const page = await browserContext.newPage();\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record({ video: wantsCapture, quality: resolvedQuality }, () =>\n fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await browserContext.close().catch(() => undefined);\n }\n } finally {\n await browser.close();\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/record/index.ts"],"names":["browser","context","page"],"mappings":"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","file":"index.js","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContext,\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Optional label for the recording — becomes the title of a generated\n * `toPlaywright()` test. See `@humanjs/playwright`'s `HumanRecordOptions.name`.\n */\n readonly name?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /**\n * Capture actual typed/pasted text into the timeline, so `toHumanJS()` /\n * `toPlaywright()` exports include the values. Defaults to `true`; password\n * fields are always masked. Set `false` to record no input values (exports\n * emit empty-string placeholders). See `@humanjs/playwright`'s\n * `HumanRecordOptions.captureInputs`.\n */\n readonly captureInputs?: boolean;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /**\n * Viewport dimensions. Overrides the quality preset's viewport. Applies to\n * the default and persistent (`userDataDir`) modes; ignored when attaching\n * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).\n */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Record in a **persistent profile** at this directory so logins and\n * cookies survive across runs — sign in once (in a headed run), and later\n * recordings start authenticated. Uses `launchPersistentContext` under the\n * hood (a single context); `headless`, `launch`, `channel`, and `viewport`\n * still apply. Starts empty the first time.\n */\n readonly userDataDir?: string;\n /**\n * Record by **attaching to a browser you already launched** over CDP (e.g.\n * `\"http://localhost:9222\"`). Reuses that browser's existing context — your\n * real logins, tabs, extensions. Start the browser yourself with\n * `--remote-debugging-port`. HumanJS never closes a browser it attached to;\n * it only borrows it. Takes precedence over `userDataDir`.\n *\n * `launch` / `headless` / `channel` / `viewport` don't apply here — you're\n * borrowing a window HumanJS doesn't own, so the browser's real window size\n * wins. Forcing a `viewport` would only *emulate* one and letterbox the\n * capture. For a fixed recording resolution, launch the browser yourself\n * with `--window-size=1920,1080`, or use the default / persistent modes.\n */\n readonly cdpUrl?: string;\n /**\n * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that\n * installed browser's binary instead of bundled Chromium (applies to the\n * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse\n * your existing profile — pair it with `userDataDir` (persistent) or\n * `cdpUrl` (attach) for real logins.\n */\n readonly channel?: string;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches (or attaches to) a browser, opens a\n * page, creates a humanized session, runs `fn`, and returns a\n * {@link Recording} you can export to video, JSON timeline, or read in-memory.\n *\n * Browser source (default → ephemeral fresh profile):\n * - `userDataDir` — a persistent profile that keeps logins across runs.\n * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);\n * never closed on finish, only released.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Stay signed in across runs (persistent profile)\n * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {\n * await human.goto('https://app.example.com/dashboard');\n * });\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n name,\n quality,\n captureInputs,\n url,\n viewport,\n headless,\n launch,\n context: contextOptions,\n userDataDir,\n cdpUrl,\n channel,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const { page, dispose } = await acquireRecordingContext({\n cdpUrl,\n userDataDir,\n channel,\n headless: headless ?? false,\n viewport: resolvedViewport,\n launch,\n context: contextOptions,\n cursor,\n });\n\n try {\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record(\n { name, video: wantsCapture, quality: resolvedQuality, captureInputs },\n () => fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await dispose();\n }\n}\n\n/** Internal options for {@link acquireRecordingContext}. */\ninterface AcquireOptions {\n readonly cdpUrl?: string;\n readonly userDataDir?: string;\n readonly channel?: string;\n readonly headless: boolean;\n readonly viewport: { readonly width: number; readonly height: number };\n readonly launch?: LaunchOptions;\n readonly context?: BrowserContextOptions;\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/**\n * Resolves the browser source — CDP attach > persistent profile > ephemeral\n * — and returns the page to drive plus a `dispose` that tears down only what\n * we created. A CDP-attached browser is borrowed: `dispose` leaves it (and\n * its context) untouched so we never close the caller's real browser.\n */\nasync function acquireRecordingContext(\n opts: AcquireOptions,\n): Promise<{ page: Page; dispose: () => Promise<void> }> {\n const cursorOptions = typeof opts.cursor === 'object' ? opts.cursor : undefined;\n const installCursor = async (ctx: BrowserContext): Promise<void> => {\n if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);\n };\n\n // Attach to a browser the caller launched. Reuse its existing context so we\n // record their real session; only make a fresh one if there's none. Never\n // close it on dispose — it's borrowed.\n if (opts.cdpUrl) {\n const browser = await chromium.connectOverCDP(opts.cdpUrl);\n const context = browser.contexts()[0] ?? (await browser.newContext({ ...opts.context }));\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return { page, dispose: async () => undefined };\n }\n\n // Persistent profile — the context owns its browser, so closing it on\n // dispose tears everything down.\n if (opts.userDataDir) {\n const context = await chromium.launchPersistentContext(opts.userDataDir, {\n ...opts.launch,\n ...opts.context,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n viewport: opts.viewport,\n });\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n },\n };\n }\n\n // Ephemeral (default) — a fresh throwaway browser + context.\n const browser = await chromium.launch({\n ...opts.launch,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n });\n const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });\n await installCursor(context);\n const page = await context.newPage();\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n await browser.close().catch(() => undefined);\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanjs/recorder",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "One-call session recording for HumanJS — capture a humanized Playwright session as mp4, webm, gif, or a structured JSON timeline.",
5
5
  "keywords": [
6
6
  "humanjs",
@@ -49,7 +49,7 @@
49
49
  "node": ">=20"
50
50
  },
51
51
  "dependencies": {
52
- "@humanjs/playwright": "0.5.0"
52
+ "@humanjs/playwright": "0.7.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "playwright": ">=1.40.0"