CloudClawer/CloudClawerBlog
DocsSign In
All PostsTutorialsDeep Dives
Blog
Deep Dive 8 min read2026-05-14

How CloudClawer Browser Recordings Work Under the Hood

A look at how the Playwright recording helper injects rrweb, how events flow from your agent's browser to S3, and how to use the result to debug something Claude did two days ago.

What CloudClawer recordings actually capture

A CloudClawer recording is an rrweb capture of one Playwright-driven browser session. Not a Claude Code tool trace, not a desktop screen recording — the actual DOM, mouse, input, and (optionally) canvas pixels of a Chromium page your agent drove.

That distinction matters. When the agent says "I clicked Save and the toast appeared", the recording is the only artifact that can tell you whether the toast showed the right text, whether the save button was disabled, whether the form had the right inputs filled in. The agent might claim success; the rrweb timeline doesn't lie.

The rrweb foundation

rrweb ("record and replay the web") serializes a page's DOM and watches for mutations. A recording is just a stream of typed events:

  • FullSnapshot — a complete serialized DOM at one moment
  • IncrementalSnapshot — a diff (DOM mutation, mouse move, input change, scroll, viewport resize, …)
  • Meta — viewport size, URL, timestamp

CloudClawer doesn't add a parallel event schema on top of these — the recordings are the raw rrweb stream. Replay just hands them back to rrweb's player.

How the Playwright helper works

The cloudclawer-hooks/record.mjs module exports startRecording(page, opts). Under the hood it does three things:

  1. Injects rrweb into the page via page.addScriptTag, pinned to rrweb@2.0.0-alpha.4 via jsDelivr so the events the recorder emits stay compatible with the replay player.
  2. Starts capture in the page context: it calls rrweb.record() and buffers each event into window.__rrwebEvents.
  3. Re-injects on navigation. Playwright fires a load event on every nav; the helper re-injects rrweb and restarts capture so a multi-page flow stays in one continuous recording.

When you call rec.stop(), the helper drains window.__rrwebEvents, wraps it with metadata (title, final URL, duration), and writes the JSON to ~/.cloudclawer-recordings/<id>.json.

import { chromium } from 'playwright';
import { startRecording } from 'cloudclawer-hooks/record.mjs';

const browser = await chromium.launch();
const page    = await browser.newPage();
const rec     = await startRecording(page, {
  title: 'Memory sharing E2E',
  recordCanvas: false,   // turn on for WebGL — needs preserveDrawingBuffer
});

await page.goto('http://localhost:3000');
await page.getByRole('button', { name: 'Share memory' }).click();
// … walk the UI …
const { filePath, eventCount } = await rec.stop();
console.log(`Wrote ${eventCount} events → ${filePath}`);

await browser.close();

Asset inlining (and why localhost recordings still play back)

The recorder defaults inlineImages: true and inlineFonts: true. That matters because rrweb's replay page fetches asset URLs at replay time. If you recorded against localhost:3000and try to play back on a different machine, un-inlined assets would 404 — you'd see DOM mutations but no images, no fonts, no icons.

With inlining on, those assets are converted to base64 data URLs at record time and embedded directly in the rrweb event stream. The cost is a larger file; the upside is a recording that plays back the same on a teammate's laptop, a CI replay viewer, or your phone.

Uploading and serving

Recordings don't stream — they're uploaded after stop(). The cloudclawer-hooks record-upload command takes the local JSON file and:

  1. Hits POST /u/dkr/recordings/presign with your X-API-Key and gets back a presigned S3 PUT URL.
  2. Streams the file directly to S3 at users/{username}/recordings/{recordingId}.json.
  3. Returns the dashboard play URL.

The system-router never holds the file in memory — only the metadata. That lets recordings scale to whatever S3 charges per GB without spending compute on the upload path. The 90-day TTL is an S3 lifecycle rule, not application code.

Replay in the dashboard

When you open a recording in the Recordings dashboard, the page fetches the events JSON, hands it to rrweb's Replayer inside an iframe, and renders the same controls you'd expect from a session-replay tool: scrub bar, speed (0.5×–4×), skip-inactive, and pause.

Because the replay is a real DOM (rebuilt from the snapshots), you can open browser devtools on the replay iframe and inspect elements as if the page were live. That frequently is the fastest way to debug an agent failure: replay the moment of failure, inspect the actual DOM the agent saw.

Fetching the raw stream

The API surface is intentionally small:

# List your recordings (newest first)
GET    /u/dkr/recordings
X-API-Key: YOUR_CLOUDCLAWER_KEY

# Fetch one — returns metadata + a presigned download URL for the events JSON
GET    /u/dkr/recordings/{recordingId}

# Delete (also removes the S3 object)
DELETE /u/dkr/recordings/{recordingId}

Once you have the events JSON, you can feed it to rrweb's Replayer in any page, ship it to a teammate, or run your own analysis (e.g. summarize the URLs visited, count form submissions, diff DOM snapshots over time).

Privacy and what to mask

Form inputs are captured by default — including password fields. If your recording will end up in any shared dashboard, mask them. rrweb supports per-element masks (the data-rr-block attribute) and call-site options (maskInputOptions). The recorder accepts the same options via the second argument; pass anything supported by upstream rrweb.

Where recordings fit in CloudClawer's workflow

Recordings pair well with Guided Routines: schedule a routine that drives a Playwright check (e.g. "every morning, log into the dashboard and verify the deploy was successful"), upload the recording at the end, and your agent leaves a visual breadcrumb of every run. When something breaks, you scrub the timeline rather than re-running the whole thing locally.

◢ Signal subscribe

New posts & releases, in your inbox

Get notified when we ship a new blog post or product release. At most one email per week. Unsubscribe anytime.

© NeuralAccel 2026