Log in once, reuse every workflow. Record, replay, annotate, and ship — all from the browser you already know.
Capture authenticated browser sessions with manual login, record user flows, annotate pages with notes, and replay everything in CI. Supports both Puppeteer and Playwright.
Designed for test automation, QA workflows, and internal tooling — without scraping, bot detection bypass, or fingerprint spoofing.
- Human-in-the-loop — You log in manually (CAPTCHA / 2FA friendly)
- Persistent profiles — Sessions saved to disk and reusable across runs
- Auto-detect login completion — URL and cookie heuristics, optional regex target
- Session auto-update — Cookies + localStorage saved periodically and after every command
- Automatic screenshots — Captures a PNG after login, on session open, and on record
- QA toolbar — On-page toolbar with screenshot, annotation, and recording controls
- Manual screenshots — One-click screenshot from the toolbar (toolbar hidden in output)
- Page annotations — Click anywhere to leave numbered notes with auto-screenshot
- Interactive recording — On-page overlay UI with start/stop controls and named recordings
- Named recordings — Save recordings with names, replay by name
- Action replay — Replay recorded actions with configurable speed and visual cursor
- Parametric replay — Feed mock data (FakerJS) or custom data (CSV/JSON) into input fields during replay
- Schema extraction — Auto-detect input fields and infer data types for mock generation
- File upload replay — Programmatically upload files during replay (Puppeteer + Playwright)
- Desktop viewport — 1440×900 default for realistic rendering
- Dual runner support — Puppeteer (via puppeteer-core) and Playwright (optional)
- Works for any website — SaaS dashboards, admin panels, internal tools
npm installpuppeteer-core is included as a direct dependency and uses your system's Chrome/Chromium. For Playwright support, install it separately:
npm install playwrightLaunch a headed browser, log in manually, and save the session:
sessionsnap capture <url> \
--profile <name> \
[--runner puppeteer|playwright] \
[--out <file>] \
[--wait <minutes>] \
[--target <regex>]--runnerdefaults topuppeteer--targetallows specifying a regex pattern for the post-login URL (e.g."/dashboard"or"^https://.*/app")
Example:
sessionsnap capture https://app.acmecorp.io/login --profile acmeThe tool will:
- Open a headed browser (1440×900 viewport)
- Navigate to the URL
- Wait for you to log in manually
- Detect login completion (URL no longer contains
login/signin/auth, or cookie count increases) - Take a screenshot of the post-login state
- Capture cookies and save a session snapshot
With a specific target URL pattern:
sessionsnap capture https://app.acmecorp.io/login --profile acme --target "/dashboard"sessionsnap open <url> \
--profile <name> \
[--runner puppeteer|playwright]Example:
sessionsnap open https://app.acmecorp.io/dashboard --profile acmeThe browser opens with the QA toolbar (top-right corner) providing:
- 📸 Screenshot button — capture a clean screenshot (toolbar hidden in output)
- 📌 Annotation button — enter annotation mode to leave notes on the page
When you close the browser, the session snapshot is automatically updated with the latest cookies.
Open a browser with a saved session and record user actions via an on-page overlay UI:
sessionsnap record <url> \
--profile <name> \
[--runner puppeteer|playwright] \
[--name <recording-name>]The browser opens with the QA toolbar containing screenshot, annotation, and recording controls:
[ 📸 ] [ 📌 ] | [ ⏺ Start Recording ]
Interactive mode (no --name):
- Browser opens with the toolbar showing 📸 Screenshot, 📌 Annotate, and ⏺ Start Recording buttons
- Click Start → toolbar switches to recording mode (timer + action counter + Stop button)
- Perform your actions on the page
- Click Stop → a dialog prompts you for a recording name
- Enter a name and click Save → recording saved to disk
Screenshot and annotation are always available, even during recording. Actions on the toolbar itself are never recorded.
Pre-named mode (--name provided):
- Recording starts automatically with the given name
- Click Stop when done → recording saved immediately
Examples:
# Interactive: name the recording when you stop
sessionsnap record https://app.acmecorp.io/dashboard --profile acme
# Pre-named: recording starts immediately as "checkout-flow"
sessionsnap record https://app.acmecorp.io/dashboard --profile acme --name "checkout-flow"The recorder captures 12 action types:
| Action | What it records | Replay |
|---|---|---|
| click | selector, tag, text, coordinates | CSS selector → text → coordinates fallback |
| dblclick | selector, tag, text, coordinates | Same 3-tier fallback as click |
| hover | selector, tag, text, coordinates | CSS selector → text → coordinates fallback |
| mousemove | x, y coordinates | page.mouse.move(x, y) |
| input | selector, value (passwords masked) | Clear + type / fill |
| select | selector, selected value & text | page.select / selectOption |
| checkbox | selector, checked state, type | Smart toggle (checks current state) |
| keydown | key, combo (Ctrl/Meta/Alt+key) | page.keyboard.press with modifiers |
| submit | form selector, action, method | form.requestSubmit() / Enter fallback |
| scroll | scroll position (window + inner elements) | window.scrollTo / element.scrollTo |
| file | selector, file names, count | Log only (cannot replay file selection) |
| navigation | from/to URLs (SPA & traditional) | page.goto(url) |
Noise reduction: hover debounced (150ms, interactive elements only), scroll debounced (400ms), mousemove throttled (~60fps), keydown only captures special keys and modifier combos.
Recordings are tied to a specific viewport size (default 1440×900). If the browser window is resized during an active recording, the recording is automatically cancelled to prevent unreliable replay.
- A red warning banner appears on the page
- All recorded actions are discarded
- The process exits with code
1
Both CSS viewport (innerWidth/innerHeight) and physical window (outerWidth/outerHeight) dimensions are monitored — this works correctly even when the viewport is emulated via CDP. Minor fluctuations (±10px viewport, ±30px window) are tolerated.
Named recordings are tracked in recordings.json inside the profile directory:
~/.sessionsnap/profiles/acme/
recordings.json # metadata: name → file mapping
actions-checkout-flow.json # named recording
actions-recording-2026-...json # unnamed recording
sessionsnap replay \
--profile <name> \
[--name <recording-name>] \
[--file <actions-file>] \
[--runner puppeteer|playwright] \
[--speed <multiplier>] \
[--headless] \
[--bail] \
[--visual]Examples:
# Replay a named recording
sessionsnap replay --profile acme --name "checkout-flow"
# Replay the latest recording
sessionsnap replay --profile acme
# Replay at 2x speed with visual cursor
sessionsnap replay --profile acme --name "checkout-flow" --speed 2 --visual
# Replay a specific file
sessionsnap replay --profile acme --file actions-recording-2026-02-09T12-00-00-000Z.json
# Replay in headless mode
sessionsnap replay --profile acme --name "checkout-flow" --headless
# Fail fast — stop on first failure (exit code 1)
sessionsnap replay --profile acme --name "checkout-flow" --bailThe replayer:
- Launches a browser with the saved session profile
- Navigates to the starting URL from the recording
- Executes each action in order
- Uses real timing from the recording (clamped to 50ms–5s per action)
- Takes a screenshot after replay completes
- Password fields are skipped during replay for security
By default, replay is fault-tolerant. If an individual action fails (element not found, timeout, etc.), it is logged as FAILED and the replay continues with the next action.
Use --bail for fail-fast mode: the replay stops immediately on the first failure and exits with code 1. Useful in CI/CD pipelines.
For click actions, there is a 3-step fallback strategy:
- CSS selector — tries the recorded selector (2s timeout)
- Text content — searches all visible elements for matching text
- Coordinates — clicks at the recorded x/y position
[sessionsnap] [1/5] click: #submit-btn "Submit"
[sessionsnap] [2/5] click FAILED: Element not found: #old-selector (text: "Gone")
[sessionsnap] [3/5] input: input[name="email"] → "user@example.com"
[sessionsnap] [4/5] navigation: /login → /dashboard
[sessionsnap] [5/5] click: a[href="/settings"] "Settings"
[sessionsnap] Replay complete.
sessionsnap recordings --profile <name> [--json]Shows all recordings with name (if named), action count, age, action type summary, and a replay hint:
[sessionsnap] 3 recording(s) for profile "acme":
1. checkout-flow (actions-checkout-flow.json)
42 actions (2h ago) replay: --name "checkout-flow"
click:12 input:8 navigation:3 hover:15 scroll:4
2. actions-recording-2026-02-11T14-30-00-000Z.json
18 actions (1d ago) replay: --file "actions-recording-2026-02-11T14-30-00-000Z.json"
click:6 input:4 navigation:2 mousemove:6
sessionsnap annotations --profile <name> [--json]Shows all annotations with note text, URL, coordinates, age, and linked screenshot:
[sessionsnap] 3 annotation(s) for profile "acme":
1. 📌 "Login button does not respond on first click" (2h ago)
https://app.acmecorp.io/login (450, 320) #login-button
📸 annotation-ann_1739...-2026-02-12T....png
2. 📌 "Missing validation message for empty email" (1h ago)
https://app.acmecorp.io/login (380, 250) input[name="email"]
📸 annotation-ann_1739...-2026-02-12T....png
Analyses a recording to extract input field metadata and infer data types for parametric replay:
sessionsnap schema --profile <name> --name <recording-name> [--locale <locale>] [--json]The schema is automatically generated when a recording stops. You can also run it manually to regenerate or inspect:
# Extract and display schema
sessionsnap schema --profile acme --name checkout-flow
# Output:
# [sessionsnap] Schema for "checkout-flow" (5 fields, locale: en):
#
# 1. [text] firstName person.firstName
# selector: #firstName
# label: "First Name"
# original: "Ahmet"
# constraints: {"required":true}
#
# 2. [email] email internet.email
# selector: #email
# original: "ahmet@test.com"
#
# 3. [select] city helpers.arrayElement
# selector: select[name="city"]
# options: 2 choice(s)
# Export as JSON for editing
sessionsnap schema --profile acme --name checkout-flow --json > schema.jsonThe schema JSON is saved at ~/.sessionsnap/profiles/<profile>/schema-<name>.json and can be edited manually to fine-tune faker methods, add file paths, or adjust constraints.
Replay recordings with random mock data (FakerJS) or custom data (CSV/JSON) instead of the original values:
# Mock data with FakerJS (random values each run)
sessionsnap replay --profile acme --name checkout-flow --mock
# Turkish locale for realistic Turkish names/addresses
sessionsnap replay --profile acme --name checkout-flow --mock --locale tr
# Run 5 iterations with different random data each time
sessionsnap replay --profile acme --name checkout-flow --mock --iterations 5
# Custom data from a JSON file
sessionsnap replay --profile acme --name checkout-flow --data ./users.json
# Custom data from a CSV file
sessionsnap replay --profile acme --name checkout-flow --data ./users.csv
# Data file + iterations (rows cycle: 1,2,3,1,2,3,...)
sessionsnap replay --profile acme --name checkout-flow --data ./users.csv --iterations 10-
Schema extraction — When a recording stops, the tool automatically analyses all
input,select, andfileactions to infer field types using a priority chain:inputTypedirect mapping (email→internet.email,tel→phone.number)autocompleteattribute (given-name→person.firstName)name/placeholder/labelkeyword heuristics (firstName→person.firstName)- Value pattern analysis (email regex, phone regex, URL pattern)
- Fallback:
lorem.word()
-
Mock generation — FakerJS generates random values matching each field's detected type, respecting locale and constraints (minLength, maxLength, etc.)
-
Value override — During replay, input/select values are replaced with mock values. The original recording logic (click, navigate, scroll, etc.) is preserved.
JSON format — Array of objects keyed by field name:
[
{ "firstName": "Mehmet", "email": "mehmet@firma.com", "city": "34" },
{ "firstName": "Ayse", "email": "ayse@firma.com", "city": "06" }
]CSV format — Header row matches field name:
firstName,email,city
Mehmet,mehmet@firma.com,34
Ayse,ayse@firma.com,06Field matching priority: name attribute → selector's name= → label text (case-insensitive).
File inputs can be replayed with mock/custom data. In the schema JSON, set filePaths:
{
"actionIndex": 12,
"type": "file",
"name": "resume",
"filePaths": ["./test-files/resume.pdf"]
}In custom data files, specify file paths per row:
[
{ "firstName": "Mehmet", "resume": "./files/cv-mehmet.pdf" },
{ "firstName": "Ayse", "resume": "./files/cv-ayse.pdf" }
]CSV with multiple files (pipe-separated): ./a.jpg|./b.jpg
Both open and record commands inject a floating toolbar into the browser page (top-right corner). The toolbar provides instant access to screenshot and annotation features without leaving the browser.
Click the camera icon to capture a screenshot at any time.
- Toolbar and all sessionsnap UI elements are automatically hidden in the screenshot
- Annotation pins remain visible (useful for documenting issues)
- Screenshots are saved as
manual-<timestamp>.pngin the profile directory - A "Screenshot captured!" toast confirms the action
Click the pin icon to toggle annotation mode:
- Enter annotation mode — cursor changes to crosshair, toast confirms
- Click anywhere on the page — a popup appears near the click point
- Type your note — describe the issue, observation, or suggestion
- Save (
Ctrl/Cmd+Enteror click Save) — a numbered pin appears at that location - Exit annotation mode — click the pin icon again
Annotation features:
- Numbered pins (1, 2, 3...) placed directly on the page content
- Hover a pin to see the note text as a tooltip
- Click the X button on a pin to delete it
- Each annotation auto-captures a screenshot with the pin and note label visible (toolbar hidden)
- Annotations are persisted in
annotations.json— reloaded when you revisit the same URL - During recording, actions in annotation mode are not recorded (clean separation)
Annotation data model:
{
"id": "ann_1739..._1",
"x": 450,
"y": 320,
"text": "Login button does not respond on first click",
"url": "https://app.acmecorp.io/login",
"selector": "#login-button",
"timestamp": "2026-02-12T14:30:00.000Z",
"viewport": { "width": 1440, "height": 900 },
"screenshotFile": "annotation-ann_1739...-2026-02-12T....png"
}| Feature | open |
record |
|---|---|---|
| 📸 Screenshot | ✓ | ✓ |
| 📌 Annotations | ✓ | ✓ |
| ⏺ Start/Stop Recording | — | ✓ |
| Timer + Action Counter | — | ✓ |
| Viewport Resize Guard | — | ✓ |
sessionsnap list [--json]Shows all saved profiles with runner, URL, relative date, screenshot/recording counts:
[sessionsnap] 2 profile(s) found:
acme [puppeteer] https://app.acmecorp.io/dashboard (2h ago) 📸 3 🔴 2
staging [playwright] https://staging.acmecorp.io/home (1d ago) 📸 1
sessionsnap status --profile <name>Prints profile details: directory path, session metadata, cookie/origin counts, screenshots and recordings.
Check if a saved session is still valid (login not expired):
sessionsnap doctor \
--profile <name> \
[--runner puppeteer|playwright] \
[--url <url>] \
[--json]--urloverrides the default check URL (profile'sfinalUrl)--jsonoutputs structured JSON (useful for CI/CD)- Exit code
1if session is unhealthy
Health checks performed:
| Check | What it does |
|---|---|
| HTTP Status | Verifies the page returns 2xx/3xx (not 401/403) |
| Login Redirect | Detects if the page redirected to a login/auth URL |
| Cookie Count | Compares current cookies against the snapshot |
| Cookie Expiry | Checks if any saved cookies have expired |
| Page Content | Scans the page for login forms (password inputs, auth forms) |
Example:
sessionsnap doctor --profile acme[sessionsnap] Doctor — Profile: "acme"
URL: https://app.acmecorp.io/dashboard
✓ PASS HTTP Status
200
✓ PASS Login Redirect
No redirect — stayed on target URL
✓ PASS Cookie Count
15 cookies (snapshot had 12)
✓ PASS Cookie Expiry
No expired cookies in snapshot
✓ PASS Page Content
No login form detected
✓ Session is healthy — login appears valid.
If session is expired:
✗ FAIL Login Redirect
Redirected to login page: https://app.acmecorp.io/login
✗ FAIL Page Content
Login form detected on page — session likely expired
✗ Session may be expired — re-capture recommended.
Run: sessionsnap capture <url> --profile acme
Use in CI/CD:
# Check before running tests — exits with code 1 if expired
sessionsnap doctor --profile acme --json || sessionsnap capture https://app.acmecorp.io/login --profile acmesessionsnap clean --profile <name> [--force]Deletes a profile and all its data (snapshot, screenshots, recordings). Prompts for confirmation unless --force is used.
# 1. Capture — log in and save the session
sessionsnap capture https://app.acmecorp.io/login --profile acme
# 2. Check session health
sessionsnap doctor --profile acme
# 3. Record — open with saved session, record a user flow
sessionsnap record https://app.acmecorp.io/dashboard --profile acme --name "checkout-flow"
# 4. Replay — replay the recorded flow
sessionsnap replay --profile acme --name "checkout-flow"
# 5. Open — browse with saved session (QA toolbar available)
sessionsnap open https://app.acmecorp.io/settings --profile acme
# → Use 📸 to take manual screenshots
# → Use 📌 to annotate issues on the page
# 6. View annotations
sessionsnap annotations --profile acme
# 7. Export encrypted session for CI
sessionsnap export --profile acme --key "my-secret" --stdout
# 8. Import session in CI
echo "$BUNDLE" | sessionsnap import --profile acme --key "my-secret" --stdin
# 9. List recordings
sessionsnap recordings --profile acme
# 10. Clean up
sessionsnap clean --profile acme --force{
"meta": {
"runner": "puppeteer",
"profile": "acme",
"capturedAt": "2026-02-09T12:00:00.000Z",
"startUrl": "https://app.acmecorp.io/login",
"finalUrl": "https://app.acmecorp.io/dashboard"
},
"cookies": [
{
"name": "session_token",
"value": "abc123",
"domain": ".acmecorp.io",
"path": "/",
"expires": 1700000000,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "https://app.acmecorp.io",
"localStorage": [
{ "name": "auth_token", "value": "eyJhbGci..." },
{ "name": "user_prefs", "value": "{\"theme\":\"dark\"}" }
]
}
]
}After using open, record, or replay, an updatedAt field is added to meta.
sessionsnap keeps your session data (snapshot.json) up-to-date across all commands:
| Command | When snapshot is saved | What is saved |
|---|---|---|
| capture | After login detection | Cookies + localStorage |
| open | Every 30s + on close | Cookies + localStorage |
| record | When recording stops | Cookies + localStorage |
| replay | After replay completes | Cookies + localStorage |
How it works:
- The real session lives in Chrome's
userDataDir— Chrome manages cookies, localStorage, indexedDB, service workers, and cache automatically. Refresh tokens are handled natively by the browser. snapshot.jsonis a periodic mirror of that state. Theopencommand saves every 30 seconds while the browser is alive, so even if the final save fails (browser already closed), recent state is preserved.- For Puppeteer: cookies are extracted via CDP (
Network.getAllCookies), localStorage viapage.evaluate(). - For Playwright:
context.storageState()provides both cookies and localStorage natively.
Note:
userDataDiralways reflects the latest state — even ifsnapshot.jsonis slightly stale, the nextopen/record/replaysession will use the liveuserDataDirdata. Thedoctorcommand verifies whether the session is still valid.
Export encrypted session bundles for use in CI/CD pipelines. Sessions are encrypted with AES-256-GCM — zero external dependencies, uses Node.js built-in node:crypto.
sessionsnap export \
--profile <name> \
--key <passphrase> \
[--out <file>] \
[--stdout]--keyencryption passphrase (also readsSESSIONSNAP_KEYenv var)--outwrites to a.encfile (default:<profile>.enc)--stdoutoutputs base64 to stdout (for storing as CI secret)
The bundle includes: snapshot.json + recordings.json + all actions-*.json files + Chrome session files (Cookies, Local Storage, Preferences).
sessionsnap import \
--profile <name> \
--key <passphrase> \
[--file <path>] \
[--stdin]--filereads from an.encfile--stdinreads base64 from stdin
[16 bytes salt][12 bytes IV][16 bytes authTag][...ciphertext...]
Key derivation: PBKDF2(passphrase, salt, 100000 iterations, 32 bytes, SHA-256) produces a 256-bit AES key.
The plaintext payload is a JSON bundle:
{
"version": 2,
"profile": "acme",
"exportedAt": "2026-02-11T...",
"snapshot": { "meta": {}, "cookies": [], "origins": [] },
"recordings": { "checkout-flow": { "file": "actions-checkout_flow.json" } },
"actions": {
"actions-checkout_flow.json": [{ "type": "click", "..." : "..." }]
},
"sessionFiles": [
{ "path": "Default/Cookies", "data": "<base64>" },
{ "path": "Default/Local Storage/leveldb/000003.log", "data": "<base64>" },
{ "path": "Local State", "data": "<base64>" }
]
}Chrome session files included in the bundle:
| File | Content | Purpose |
|---|---|---|
Default/Cookies |
SQLite DB | HTTP cookies (session tokens, CSRF, auth) |
Default/Local Storage/ |
LevelDB | JWT tokens, auth state, user preferences |
Default/Preferences |
JSON | Chrome profile settings |
Default/Secure Preferences |
JSON | HMAC-signed preferences |
Default/Network Persistent State |
JSON | HSTS pins, security policies |
Local State |
JSON | OS keychain reference, encryption key |
These files are typically 20-100 KB total (compressed), making the bundle lightweight for CI secrets. On import, Chrome loads session data natively from these files — no cookie injection or localStorage hydration needed.
# Export to file
sessionsnap export --profile acme --key "my-secret-key" --out acme.enc
# Export as base64 (copy to clipboard on macOS)
sessionsnap export --profile acme --key "my-secret-key" --stdout | pbcopyname: E2E Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
# Import encrypted session from GitHub secret
- name: Import session
run: echo "$SESSIONSNAP_BUNDLE" | npx sessionsnap import --profile acme --key "$SESSIONSNAP_KEY" --stdin
env:
SESSIONSNAP_BUNDLE: ${{ secrets.SESSIONSNAP_BUNDLE }}
SESSIONSNAP_KEY: ${{ secrets.SESSIONSNAP_KEY }}
# Verify session is still valid
- name: Check session health
run: npx sessionsnap doctor --profile acme
# Run recorded test flow
- name: Replay test
run: npx sessionsnap replay --profile acme --name "checkout-flow" --headless --baile2e-test:
image: node:20
stage: test
script:
- npm ci
- echo "$SESSIONSNAP_BUNDLE" | npx sessionsnap import --profile acme --key "$SESSIONSNAP_KEY" --stdin
- npx sessionsnap doctor --profile acme
- npx sessionsnap replay --profile acme --name "checkout-flow" --headless --bail
variables:
SESSIONSNAP_BUNDLE: $SESSIONSNAP_BUNDLE
SESSIONSNAP_KEY: $SESSIONSNAP_KEYsequenceDiagram
actor Dev as Developer
participant Local as Local Machine
participant Secret as CI Secrets
participant CI as CI Pipeline
Dev->>Local: sessionsnap capture --profile acme
Dev->>Local: sessionsnap record --profile acme --name "flow"
Dev->>Local: sessionsnap export --profile acme --key $KEY --stdout
Local-->>Secret: Store base64 as SESSIONSNAP_BUNDLE
Note over Secret,CI: Pipeline triggered
CI->>Secret: Read SESSIONSNAP_BUNDLE + SESSIONSNAP_KEY
CI->>CI: sessionsnap import --profile acme --key $KEY --stdin
CI->>CI: sessionsnap doctor --profile acme
CI->>CI: sessionsnap replay --profile acme --name "flow" --headless --bail
alt Session expired
CI->>CI: Exit code 1 — notify developer
else Session valid
CI->>CI: Tests pass
end
graph TB
subgraph CLI["CLI Layer — bin/sessionsnap.js"]
CMD_CAPTURE["capture"]
CMD_OPEN["open"]
CMD_RECORD["record"]
CMD_REPLAY["replay"]
CMD_DOCTOR["doctor"]
CMD_EXPORT["export"]
CMD_IMPORT["import"]
CMD_LIST["list"]
CMD_RECORDINGS["recordings"]
CMD_ANNOTATIONS["annotations"]
CMD_STATUS["status"]
CMD_CLEAN["clean"]
end
subgraph RUNNERS["Runner Layer — Puppeteer / Playwright"]
subgraph PUPPETEER["puppeteer/"]
P_BROWSER["browser.js\n(Chrome discovery)"]
P_CAPTURE["capture.js"]
P_OPEN["open.js"]
P_RECORD["record.js"]
P_REPLAY["replay.js"]
P_DOCTOR["doctor.js"]
end
subgraph PLAYWRIGHT["playwright/"]
PW_CAPTURE["capture.js"]
PW_OPEN["open.js"]
PW_RECORD["record.js"]
PW_REPLAY["replay.js"]
PW_DOCTOR["doctor.js"]
end
end
subgraph CORE["Core Layer — Shared Logic"]
SNAPSHOT["snapshot.js\n(normalize cookies,\ncreate/update snapshot)"]
HEURISTICS["heuristics.js\n(login detection:\nURL + cookie + regex)"]
TOOLBAR["toolbar.js\n(QA toolbar: screenshot\n+ annotations)"]
RECORDER["recorder.js\n(recording controls +\naction capture + writeFileSync)"]
REPLAYER["replayer.js\n(action execution +\nvisual cursor + fallback)"]
DOCTOR_CORE["doctor.js\n(session health checks:\nHTTP + redirect + cookies\n+ page content)"]
CIPHER["cipher.js\n(AES-256-GCM\nencrypt/decrypt)"]
BUNDLE["bundle.js\n(profile JSON\nbundling)"]
CSS_GEN["css-selector-generator\n(robust CSS selectors)"]
end
subgraph STORAGE["Storage Layer — store.js"]
PROFILES["~/.sessionsnap/profiles/"]
SNAP_FILE["snapshot.json"]
SCREENSHOTS["*.png"]
ACTIONS["actions-*.json"]
REC_META["recordings.json"]
ANNOTATIONS["annotations.json"]
end
subgraph BROWSER["Browser"]
PAGE["Browser Page"]
QA_TB["QA Toolbar\n(screenshot + annotations)"]
OVERLAY["Recording Controls\n(start/stop + timer)"]
PINS["Annotation Pins"]
CDP["CDP / storageState"]
end
CMD_CAPTURE --> P_CAPTURE & PW_CAPTURE
CMD_OPEN --> P_OPEN & PW_OPEN
CMD_RECORD --> P_RECORD & PW_RECORD
CMD_REPLAY --> P_REPLAY & PW_REPLAY
CMD_DOCTOR --> P_DOCTOR & PW_DOCTOR
CMD_EXPORT --> BUNDLE
CMD_IMPORT --> BUNDLE
BUNDLE --> CIPHER
BUNDLE --> STORAGE
CMD_LIST & CMD_RECORDINGS & CMD_ANNOTATIONS & CMD_STATUS & CMD_CLEAN --> STORAGE
P_CAPTURE & P_OPEN & P_RECORD & P_REPLAY & P_DOCTOR --> P_BROWSER
P_BROWSER --> PAGE
P_CAPTURE --> HEURISTICS & SNAPSHOT
P_OPEN --> SNAPSHOT & TOOLBAR
P_RECORD --> RECORDER & TOOLBAR
P_REPLAY --> REPLAYER
P_DOCTOR --> DOCTOR_CORE
PW_CAPTURE --> HEURISTICS & SNAPSHOT
PW_OPEN --> SNAPSHOT & TOOLBAR
PW_RECORD --> RECORDER & TOOLBAR
PW_REPLAY --> REPLAYER
PW_DOCTOR --> DOCTOR_CORE
TOOLBAR --> QA_TB
TOOLBAR --> PINS
RECORDER --> CSS_GEN
RECORDER --> OVERLAY
RECORDER --> PAGE
REPLAYER --> PAGE
HEURISTICS --> CDP
SNAPSHOT --> STORAGE
DOCTOR_CORE --> PAGE & CDP
P_CAPTURE & P_OPEN & P_RECORD & P_REPLAY & P_DOCTOR --> STORAGE
PW_CAPTURE & PW_OPEN & PW_RECORD & PW_REPLAY & PW_DOCTOR --> STORAGE
STORAGE --> SNAP_FILE & SCREENSHOTS & ACTIONS & REC_META & ANNOTATIONS
style CLI fill:#1e293b,stroke:#475569,color:#f8fafc
style CORE fill:#1e3a5f,stroke:#3b82f6,color:#f8fafc
style RUNNERS fill:#1a2e1a,stroke:#22c55e,color:#f8fafc
style STORAGE fill:#3b1f0b,stroke:#f97316,color:#f8fafc
style BROWSER fill:#3b0764,stroke:#a855f7,color:#f8fafc
sequenceDiagram
actor User
participant CLI as CLI (commander)
participant Runner as Runner (Puppeteer/Playwright)
participant Browser as Headed Browser
participant Heuristics as heuristics.js
participant Store as store.js
participant Disk as ~/.sessionsnap/profiles/
User->>CLI: sessionsnap capture <url> --profile acme
CLI->>Store: ensureProfileDir("acme")
Store->>Disk: mkdir ~/.sessionsnap/profiles/acme/
CLI->>Runner: capture(url, opts)
Runner->>Browser: launch (headed, 1440×900)
Runner->>Browser: page.goto(url)
Browser-->>User: Login page displayed
User->>Browser: Manual login (CAPTCHA, 2FA, etc.)
loop Every 2 seconds
Runner->>Heuristics: waitForLoginComplete(page)
Heuristics->>Browser: Check URL pattern + cookie count
alt --target regex provided
Heuristics->>Browser: Check URL matches regex
end
end
Heuristics-->>Runner: Login detected ✓
Runner->>Browser: waitForNetworkIdle()
Runner->>Browser: page.screenshot()
Runner->>Store: saveScreenshot(capture-*.png)
Runner->>Browser: getAllCookies (CDP) / context.cookies()
Runner->>Store: createSnapshot + saveSnapshot
Store->>Disk: Write snapshot.json + screenshot
Runner->>Browser: browser.close()
CLI-->>User: Profile saved ✓
sequenceDiagram
actor User
participant CLI as CLI
participant Runner as Runner
participant Browser as Browser Page
participant Recorder as recorder.js
participant Replayer as replayer.js
participant Store as store.js
participant Disk as Disk
Note over User,Disk: ── RECORD PHASE ──
User->>CLI: sessionsnap record <url> --profile acme
CLI->>Store: loadSnapshot("acme") — verify session exists
CLI->>Runner: record(url, opts)
Runner->>Browser: launch with userDataDir
Runner->>Browser: injectToolbar(page) — QA toolbar (📸 + 📌)
Runner->>Recorder: setupRecorder(page, runner, dir)
Recorder->>Browser: inject css-selector-generator lib
Recorder->>Browser: inject action listener script
Recorder->>Browser: inject recording controls (⏺ Start/⏹ Stop)
Recorder->>Browser: exposeFunction(__sessionsnap_push)
Runner->>Browser: page.goto(url)
Browser-->>User: Page with QA toolbar displayed
User->>Browser: Click "Start Recording"
Browser->>Recorder: __sessionsnap_start_recording()
User->>Browser: Interact (click, type, hover, scroll...)
loop Each user action
Browser->>Recorder: __sessionsnap_report_action(action)
Recorder->>Disk: writeFileSync (crash-safe)
Recorder-->>CLI: Log: [click] #btn "Submit"
end
User->>Browser: Click "Stop"
Browser->>Browser: Show name dialog
User->>Browser: Enter "checkout-flow" → Save
Browser->>Recorder: __sessionsnap_stop_recording("checkout-flow")
Recorder->>Disk: Write actions-checkout_flow.json
Recorder->>Store: saveRecordingMetadata
Store->>Disk: Update recordings.json
Recorder->>Recorder: extractSchema(actions)
Recorder->>Disk: Write schema-checkout_flow.json (auto)
Note over User,Disk: ── REPLAY PHASE ──
User->>CLI: sessionsnap replay --profile acme --name "checkout-flow"
CLI->>Store: loadRecordingByName("acme", "checkout-flow")
Store-->>CLI: actions[] (N actions)
CLI->>Runner: replay(actions, opts)
Runner->>Browser: launch with userDataDir
Runner->>Browser: goto(startUrl)
Runner->>Replayer: replayActions(page, actions)
loop Each action
Replayer->>Replayer: Calculate delay from timestamps
alt click / dblclick
Replayer->>Browser: mouse.move(x, y) → CSS / text / coords fallback
else input
Replayer->>Browser: clear + type(value)
else hover
Replayer->>Browser: mouse.move(x, y) → hover(selector)
else navigation
Replayer->>Browser: page.goto(url) → waitForNetworkIdle
else scroll
Replayer->>Browser: scrollTo(x, y)
end
Replayer-->>CLI: Log: [1/N] action description
end
Replayer-->>Runner: Replay complete
Runner->>Browser: screenshot()
Runner->>Store: saveScreenshot(replay-*.png)
Runner->>Browser: browser.close()
CLI-->>User: Replay finished ✓
Note over User,Disk: ── PARAMETRIC REPLAY (--mock / --data) ──
User->>CLI: sessionsnap replay --name "checkout-flow" --mock --locale tr
CLI->>Store: loadSchema("acme", "checkout-flow")
Store-->>CLI: schema (field types + faker methods)
CLI->>CLI: FakerFill.generate(schema, locale=tr)
CLI-->>CLI: mockValues {actionIndex → {value}}
CLI->>Runner: replay(actions, {mockValues})
Runner->>Replayer: replayActions(page, actions, {mockValues})
loop Each action
alt input/select with mockValues[i]
Replayer->>Browser: type(mockValue) instead of original
Replayer-->>CLI: 🎲 Mock: #email → "baydar@gmail.com"
else file with mockValues[i].filePaths
Replayer->>Browser: uploadFile(filePaths)
Replayer-->>CLI: 📎 Uploading 1 file(s) to input[name="resume"]
else other actions
Replayer->>Browser: execute as normal
end
end
graph LR
subgraph HOME["~/.sessionsnap/"]
subgraph PROFILES["profiles/"]
subgraph ACME["acme/"]
SNAP["snapshot.json\n(cookies, meta, origins)"]
REC_JSON["recordings.json\n(name → file mapping)"]
ANN_JSON["annotations.json\n(notes, pins, screenshots)"]
CAP_PNG["capture-2026-*.png"]
OPEN_PNG["open-2026-*.png"]
MANUAL_PNG["manual-2026-*.png"]
ANN_PNG["annotation-ann_*-2026-*.png"]
REPLAY_PNG["replay-2026-*.png"]
RECORD_PNG["record-2026-*.png"]
ACTIONS1["actions-checkout_flow.json"]
ACTIONS2["actions-recording-*.json"]
USERDATA["Chrome userDataDir\n(cookies, localStorage,\nindexedDB, cache)"]
end
subgraph STAGING["staging/"]
SNAP2["snapshot.json"]
OTHER["..."]
end
end
end
style HOME fill:#1c1917,stroke:#78716c,color:#f5f5f4
style PROFILES fill:#292524,stroke:#a8a29e,color:#f5f5f4
style ACME fill:#1e3a5f,stroke:#3b82f6,color:#f8fafc
style STAGING fill:#1e3a5f,stroke:#3b82f6,color:#f8fafc
flowchart LR
subgraph PAGE["Browser Page Context"]
TOOLBAR_UI["QA Toolbar\n📸 Screenshot · 📌 Annotate\n⏺ Start / ⏹ Stop"]
EVENTS["DOM Events\nclick · input · hover\nscroll · keydown\ndblclick · submit\ncheckbox · file\nmousemove"]
CSS["css-selector-generator\n(robust selectors)"]
DEBOUNCE["Debounce / Throttle\nhover: 150ms\nscroll: 400ms\nmousemove: ~16ms"]
FILTER["Filter\n- isOverlayEvent?\n- isRecording?\n- isAnnotateMode?\n- noise reduction"]
end
subgraph BRIDGE["Page ↔ Node.js Bridge"]
EXPOSE_REC["exposeFunction\n__sessionsnap_push"]
EXPOSE_SS["exposeFunction\n__sessionsnap_take_screenshot"]
EXPOSE_ANN["exposeFunction\n__sessionsnap_save_annotation"]
end
subgraph NODEJS["Node.js Process"]
HANDLER["Action Handler\n(log + accumulate)"]
SYNC_WRITE["writeFileSync\n(crash-safe)"]
METADATA["saveRecordingMetadata\n(recordings.json)"]
SS_HANDLER["Screenshot Handler\n(page.screenshot)"]
ANN_HANDLER["Annotation Handler\n(annotations.json)"]
end
EVENTS --> CSS --> DEBOUNCE --> FILTER --> EXPOSE_REC
TOOLBAR_UI -.->|start/stop| FILTER
TOOLBAR_UI -.->|📸 click| EXPOSE_SS
TOOLBAR_UI -.->|📌 save| EXPOSE_ANN
EXPOSE_REC --> HANDLER --> SYNC_WRITE
HANDLER --> METADATA
EXPOSE_SS --> SS_HANDLER
EXPOSE_ANN --> ANN_HANDLER
style PAGE fill:#3b0764,stroke:#a855f7,color:#f8fafc
style BRIDGE fill:#713f12,stroke:#eab308,color:#f8fafc
style NODEJS fill:#1e3a5f,stroke:#3b82f6,color:#f8fafc
flowchart TD
START["Replay Action"] --> TYPE{Action Type?}
TYPE -->|click / dblclick| MOVE["mouse.move(x, y)"]
MOVE --> SEL{"CSS Selector\nfound?"}
SEL -->|Yes| CLICK_SEL["Click via selector ✓"]
SEL -->|No / timeout| TEXT{"Text content\nmatch?"}
TEXT -->|Yes| CLICK_TEXT["Click via text ✓"]
TEXT -->|No| COORDS{"Coordinates\navailable?"}
COORDS -->|Yes| CLICK_XY["Click at (x, y) ✓"]
COORDS -->|No| FAIL["FAILED ✗"]
TYPE -->|input| INPUT["Clear field → type value"]
TYPE -->|hover| HOVER["mouse.move → hover(selector)"]
TYPE -->|navigation| NAV["page.goto → waitForNetworkIdle"]
TYPE -->|scroll| SCROLL["window.scrollTo / element.scrollTo"]
TYPE -->|keydown| KEY["keyboard.press with modifiers"]
TYPE -->|checkbox| CHECK["Check state → toggle if needed"]
TYPE -->|submit| SUBMIT["requestSubmit / Enter key"]
TYPE -->|mousemove| MMOVE["mouse.move(x, y) per point"]
FAIL --> BAIL{--bail?}
BAIL -->|Yes| EXIT["Exit code 1"]
BAIL -->|No| NEXT["Continue to next action"]
style START fill:#1e3a5f,stroke:#3b82f6,color:#f8fafc
style FAIL fill:#7f1d1d,stroke:#ef4444,color:#f8fafc
style EXIT fill:#7f1d1d,stroke:#ef4444,color:#f8fafc
style CLICK_SEL fill:#14532d,stroke:#22c55e,color:#f8fafc
style CLICK_TEXT fill:#14532d,stroke:#22c55e,color:#f8fafc
style CLICK_XY fill:#14532d,stroke:#22c55e,color:#f8fafc
style NEXT fill:#713f12,stroke:#eab308,color:#f8fafc
sessionsnap/
bin/
sessionsnap.js # CLI entry point
src/
core/
snapshot.js # Canonical session format & normalization
heuristics.js # Login detection logic (URL + cookie polling)
toolbar.js # QA toolbar: screenshot + annotation injection
recorder.js # Recording controls + action capture + writeFileSync
replayer.js # Replay engine for recorded actions + mock value override
schema.js # Schema extraction from recordings (field types + faker mapping)
faker-fill.js # FakerJS mock data generation from schema
data-loader.js # Custom CSV/JSON data loading + field matching
doctor.js # Session health checks (HTTP, redirect, cookies, content)
cipher.js # AES-256-GCM encrypt/decrypt (node:crypto)
bundle.js # Profile JSON bundling for export/import
puppeteer/
browser.js # Chrome/Chromium executable discovery
capture.js # Capture session via CDP
open.js # Open URL with saved profile + update on close
record.js # Record user actions via Puppeteer
replay.js # Replay recorded actions via Puppeteer
doctor.js # Health check via Puppeteer
playwright/
capture.js # Capture session via storageState
open.js # Open URL with saved profile + update on close
record.js # Record user actions via Playwright
replay.js # Replay recorded actions via Playwright
doctor.js # Health check via Playwright
store.js # Profile directory, metadata & JSON I/O
test/
store.test.js
snapshot.test.js
heuristics.test.js
cli.test.js
package.json
npm testUses the Node.js built-in test runner (node:test). No additional test dependencies required.
MIT