Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions src/content/docs/browser-rendering/features/session-recording.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
pcx_content_type: how-to
title: Session recording
description: Record and replay Browser Rendering sessions to visually debug browser automation scripts.
sidebar:
order: 2
---

import { Details, Tabs, TabItem, DashButton } from "~/components";

When browser automation fails or behaves unexpectedly, it can be difficult to understand what happened. Session recording captures DOM changes, mouse and keyboard events, and page navigation as structured JSON events — not a video — so it is lightweight and easy to inspect. Recordings are powered by [rrweb](https://github.com/rrweb-io/rrweb) and are opt-in per session.

## Enable session recording

Pass `recording: true` to `puppeteer.launch()` or `playwright.launch()`:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@omarmosid will this session recordings work for CDP? if so we should include


<Tabs>
<TabItem label="Puppeteer">

```ts
import puppeteer from "@cloudflare/puppeteer";

interface Env {
MYBROWSER: Fetcher;
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const browser = await puppeteer.launch(env.MYBROWSER, { recording: true });
const page = await browser.newPage();

await page.goto("https://example.com");
// ... your automation steps ...

const sessionId = browser.sessionId();
await browser.close();

return new Response(`Session recorded: ${sessionId}`);
},
};
```

</TabItem>
<TabItem label="Playwright">

```ts
import { launch } from "@cloudflare/playwright";

interface Env {
MYBROWSER: Fetcher;
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const browser = await launch(env.MYBROWSER, { recording: true });
const page = await browser.newPage();

await page.goto("https://example.com");
// ... your automation steps ...

const sessionId = browser.sessionId();
await browser.close();

return new Response(`Session recorded: ${sessionId}`);
},
};
```

</TabItem>
</Tabs>

:::note
The recording is finalized when the browser session closes — whether you call `browser.close()` explicitly, the session reaches its idle timeout, or the Worker terminates for any other reason. The recording is not available until after the session ends.
:::

## View recordings
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will need to change to Runs instead of Logs

for the dash link, check with Visal to confirm if its just


After a session closes, its recording is available in the Cloudflare dashboard under **Browser Rendering** > **Logs**. Select a session to open the recording viewer, where you can scrub the timeline and inspect DOM state, console output, and browser interactions at each point in time.

<DashButton url="/?to=/:account/workers/browser-rendering/logs" />

## Retrieve a recording via API

You can also retrieve a recording programmatically using the session ID. Use `browser.sessionId()` to capture the session ID before closing the browser, then pass it to the recordings endpoint.

```bash
curl https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/browser-rendering/recording/<SESSION_ID> \
-H "Authorization: Bearer <API_TOKEN>"
```

A successful response returns the following fields:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think typically we don't have a table showing response fields. typically its an example response in a code block


| Field | Type | Description |
| ----------- | ------ | ----------------------------------------- |
| `sessionId` | string | Unique identifier for the session. |
| `startTime` | string | ISO timestamp when the recording started. |
| `endTime` | string | ISO timestamp when the recording ended. |
| `events` | object | rrweb events keyed by target ID. |

## Replay a recording locally
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dumb q - why might someone want to replay locally? bc they dont want to use our dash? does that mean they need to have downloaded the recording file?


<Details header="Replaying recordings with rrweb-player">

The `events` values in the API response are standard rrweb event arrays. You can pass them directly to [`rrweb-player`](https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-player) to self-host a replay UI with a timeline scrubber, DOM inspection, and playback controls.

</Details>

## Limits

- Recording is opt-in. It is not enabled by default.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should mention it's opt-in in the intro!

- Session recording is only available with Workers Bindings via `launch()`. It is not available with the REST API.
- The minimum recording duration is 1 second. Sessions shorter than 1 second will not produce a viewable recording.
- The maximum recording duration is 2 hours.

## rrweb limitations

Session recording uses [rrweb](https://github.com/rrweb-io/rrweb), which records DOM state and events rather than pixels. This approach is lightweight but has the following limitations:

- **Canvas elements** — The content of `<canvas>` elements is not captured. The element itself appears in the recording as a blank placeholder.
- **Cross-origin iframes** — Content inside cross-origin `<iframe>` elements is not recorded. Same-origin iframes are recorded normally.
- **Video and audio** — The DOM structure of `<video>` and `<audio>` elements is captured, but media playback state and content are not.
- **WebGL** — WebGL rendering is not captured.
- **Input fields** — The content of all input fields is masked by default and will not be visible in the replay.
- **Large or complex pages** — Pages with frequent DOM mutations (for example, pages with real-time data feeds or heavy animations) can generate a high volume of events, which increases the size of the recording.
Loading