[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #18 · 2026-10-04 · Example · Easy · for [testing](https://cdpfleet.com/cross-browser-testing), [ai agents](https://cdpfleet.com/ai-agents)

# Session replay without a vendor: Playwright tracing and video on a remote browser

Record a time-travel trace (actions, DOM snapshots, screenshots, network) and a video of a session running on the fleet, pull both to your machine, and open them with the tools you already have.

[Chromium](https://cdpfleet.com/docs/browsers/chromium)

Run on production on 2026-10-04: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

## The problem

When a remote browser does the wrong thing at step 7 of 12, a stack trace tells you nothing. Hosted-browser vendors sell "session replay" dashboards for this. Playwright has had both halves built in for years — tracing (every action with a before/after DOM snapshot, screenshots and the network log, viewable in a time-travel UI) and video recording — and both are designed to work on browsers you only reach over a WebSocket. The question is whether they work through a fleet connection, what you get, and how big it is.

## What we used, and why

| What | Why |
| --- | --- |
| `chromium`, `headless: "new"` | Video and tracing work headless; the recording is of the renderer, not of a screen. |
| `browser.newContext({ recordVideo: { dir, size } })` | Video is captured on the remote browser per page; the file is fetched when the page or context closes and saved where you say with `video.saveAs()`. |
| `context.tracing.start({ screenshots: true, snapshots: true })` / `tracing.stop({ path })` | The trace is assembled by your client from events streamed over the connection and written as a zip on your side. |
| `page.video()` captured before closing | The `Video` handle must be taken while the page is open; `saveAs` is called after the context has closed and the file is complete. |
| A small zip reader | To look inside `trace.zip` without extra dependencies: the trace is newline-delimited JSON plus JPEG screenshots. |

## How it works

1.  Launch headless Chromium, open a 1280×720 context with video recording, start tracing.
2.  Run a short flow: load example.com, open an HTML form on httpbin.org, fill a field, check a box, submit, wait for the result page, take a screenshot.
3.  Stop tracing to `trace.zip`, close the context (which finishes the video), save the video as `session.webm`, close the browser.
4.  Open the zip: count the recorded actions, DOM snapshots, screenshots and network entries; check the video is a WebM file; print sizes and how to open each artifact.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/18-trace-and-video), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)
-   `PROXY_URL` — your proxy, e.g. `http://user:pass@proxy.example.com:8000`

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';
import { readFileSync, mkdtempSync } from 'node:fs';
import { inflateRawSync } from 'node:zlib';
import { tmpdir } from 'node:os';
import path from 'node:path';

const KEY = process.env.CDPFLEET_API_KEY;
const dir = mkdtempSync(path.join(tmpdir(), 'cdpfleet-replay-'));

// Reads a zip's central directory and inflates the entries we want to look into.
function zipEntries(buf) {
  let p = buf.length - 22;
  while (buf.readUInt32LE(p) !== 0x06054b50) p--;
  const total = buf.readUInt16LE(p + 10);
  let q = buf.readUInt32LE(p + 16);
  const entries = [];
  for (let i = 0; i < total; i++) {
    const method = buf.readUInt16LE(q + 10);
    const csize = buf.readUInt32LE(q + 20);
    const nameLen = buf.readUInt16LE(q + 28);
    const extraLen = buf.readUInt16LE(q + 30);
    const commentLen = buf.readUInt16LE(q + 32);
    const local = buf.readUInt32LE(q + 42);
    const name = buf.toString('utf8', q + 46, q + 46 + nameLen);
    const dataStart = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
    const raw = buf.subarray(dataStart, dataStart + csize);
    entries.push({ name, read: () => (method === 8 ? inflateRawSync(raw) : raw).toString('utf8') });
    q += 46 + nameLen + extraLen + commentLen;
  }
  return entries;
}

const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: 'new' }),
});
if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
let video;
const t = Date.now();
try {
  // Video is recorded on the remote browser and fetched when the page closes; the trace is
  // assembled by the client from events and screenshots streamed over the same connection.
  const context = await browser.newContext({ recordVideo: { dir, size: { width: 1280, height: 720 } }, viewport: { width: 1280, height: 720 } });
  await context.tracing.start({ screenshots: true, snapshots: true });
  const page = await context.newPage();
  video = page.video();
  await page.goto('https://example.com/', { timeout: 60000 });
  await page.goto('https://httpbin.org/forms/post', { timeout: 60000 });
  await page.getByLabel('Customer name').fill('Ada Lovelace');
  await page.getByLabel('Large').check();
  await page.getByRole('button', { name: 'Submit order' }).click();
  await page.waitForURL('**/post', { timeout: 60000 }); // httpbin echoes the form as JSON
  await page.screenshot({ path: path.join(dir, 'final.png') });
  await context.tracing.stop({ path: path.join(dir, 'trace.zip') });
  await context.close(); // finishes the video
  await video.saveAs(path.join(dir, 'session.webm'));
} finally {
  await browser.close();
}
const seconds = (Date.now() - t) / 1000;

const trace = readFileSync(path.join(dir, 'trace.zip'));
const entries = zipEntries(trace);
const events = entries.filter((e) => /\.trace$/.test(e.name)).flatMap((e) => e.read().split('\n').filter(Boolean).map((l) => JSON.parse(l)));
const actions = events.filter((e) => e.type === 'before' && e.method).map((e) => e.method);
const network = entries.filter((e) => /\.network$/.test(e.name)).flatMap((e) => e.read().split('\n').filter(Boolean)).length;
const webm = readFileSync(path.join(dir, 'session.webm'));

console.log(JSON.stringify([
  { artifact: 'trace.zip', bytes: trace.length, detail: `${actions.length} actions: ${actions.join(', ')}`, screenshots: entries.filter((e) => /^resources\/.*\.jpeg$/.test(e.name)).length, snapshots: events.filter((e) => e.type === 'frame-snapshot').length, network_entries: network, open_with: 'npx playwright show-trace trace.zip' },
  { artifact: 'session.webm', bytes: webm.length, detail: webm.readUInt32BE(0) === 0x1a45dfa3 ? 'valid WebM (EBML header)' : 'not a WebM file', screenshots: null, snapshots: null, network_entries: null, open_with: 'any video player' },
  { artifact: 'final.png', bytes: readFileSync(path.join(dir, 'final.png')).length, detail: 'screenshot after the last step', screenshots: null, snapshots: null, network_entries: null, open_with: 'image viewer' },
  { artifact: 'whole run', bytes: null, detail: `${seconds.toFixed(1)} s including recording and downloads`, screenshots: null, snapshots: null, network_entries: null, open_with: null },
], null, 2));
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os
import tempfile
import time
import zipfile

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]
DIR = tempfile.mkdtemp(prefix="cdpfleet-replay-")

res = requests.post("https://starter.cdpfleet.com/chromium/session", headers={"x-api-key": KEY}, timeout=60,
                    json={"proxy": os.environ["PROXY_URL"], "headless": "new"})
if not res.ok:
    raise SystemExit(f"launch {res.status_code} {res.text}")
ws_url = res.json()["wsUrl"]

t = time.time()
with sync_playwright() as p:
    browser = p.chromium.connect(ws_url, headers={"x-api-key": KEY})
    try:
        # Video is recorded on the remote browser and fetched when the page closes; the trace is
        # assembled by the client from events and screenshots streamed over the same connection.
        context = browser.new_context(record_video_dir=DIR, record_video_size={"width": 1280, "height": 720},
                                      viewport={"width": 1280, "height": 720})
        context.tracing.start(screenshots=True, snapshots=True)
        page = context.new_page()
        video = page.video  # take the handle while the page is open
        page.goto("https://example.com/", timeout=60000)
        page.goto("https://httpbin.org/forms/post", timeout=60000)
        page.get_by_label("Customer name").fill("Ada Lovelace")
        page.get_by_label("Large").check()
        page.get_by_role("button", name="Submit order").click()
        page.wait_for_url("**/post", timeout=60000)  # httpbin echoes the form as JSON
        page.screenshot(path=os.path.join(DIR, "final.png"))
        context.tracing.stop(path=os.path.join(DIR, "trace.zip"))
        context.close()  # finishes the video
        video.save_as(os.path.join(DIR, "session.webm"))
    finally:
        browser.close()
seconds = time.time() - t

trace_path = os.path.join(DIR, "trace.zip")
with zipfile.ZipFile(trace_path) as z:
    names = z.namelist()
    events = [json.loads(line) for n in names if n.endswith(".trace")
              for line in z.read(n).decode().split("\n") if line]
    network = sum(1 for n in names if n.endswith(".network") for line in z.read(n).decode().split("\n") if line)
actions = [e["method"] for e in events if e.get("type") == "before" and e.get("method")]
webm = open(os.path.join(DIR, "session.webm"), "rb").read()
empty = {"screenshots": None, "snapshots": None, "network_entries": None}

print(json.dumps([
    {"artifact": "trace.zip", "bytes": os.path.getsize(trace_path), "detail": f"{len(actions)} actions: {', '.join(actions)}",
     "screenshots": sum(1 for n in names if n.startswith("resources/") and n.endswith(".jpeg")),
     "snapshots": sum(1 for e in events if e.get("type") == "frame-snapshot"), "network_entries": network,
     "open_with": "npx playwright show-trace trace.zip"},
    {"artifact": "session.webm", "bytes": len(webm),
     "detail": "valid WebM (EBML header)" if webm[:4] == b"\x1a\x45\xdf\xa3" else "not a WebM file", **empty, "open_with": "any video player"},
    {"artifact": "final.png", "bytes": os.path.getsize(os.path.join(DIR, "final.png")), "detail": "screenshot after the last step", **empty, "open_with": "image viewer"},
    {"artifact": "whole run", "bytes": None, "detail": f"{seconds:.1f} s including recording and downloads", **empty, "open_with": None},
], indent=2))
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.AriaRole;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.zip.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");

  static JsonObject row(String artifact, Long bytes, String detail, Integer screenshots, Integer snapshots, Integer network, String openWith) {
    JsonObject r = new JsonObject();
    r.addProperty("artifact", artifact);
    r.add("bytes", bytes == null ? JsonNull.INSTANCE : new JsonPrimitive(bytes));
    r.addProperty("detail", detail);
    r.add("screenshots", screenshots == null ? JsonNull.INSTANCE : new JsonPrimitive(screenshots));
    r.add("snapshots", snapshots == null ? JsonNull.INSTANCE : new JsonPrimitive(snapshots));
    r.add("network_entries", network == null ? JsonNull.INSTANCE : new JsonPrimitive(network));
    r.add("open_with", openWith == null ? JsonNull.INSTANCE : new JsonPrimitive(openWith));
    return r;
  }

  public static void main(String[] args) throws Exception {
    Path dir = Files.createTempDirectory("cdpfleet-replay-");
    HttpClient http = HttpClient.newHttpClient();
    String body = "{\"proxy\": " + new Gson().toJson(System.getenv("PROXY_URL")) + ", \"headless\": \"new\"}";
    HttpResponse<String> res = http.send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
        .header("x-api-key", KEY).header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();

    long t = System.currentTimeMillis();
    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      try {
        // Video is recorded on the remote browser and fetched when the page closes; the trace is
        // assembled by the client from events and screenshots streamed over the same connection.
        BrowserContext context = browser.newContext(new Browser.NewContextOptions()
            .setRecordVideoDir(dir).setRecordVideoSize(1280, 720).setViewportSize(1280, 720));
        context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true));
        Page page = context.newPage();
        Video video = page.video(); // take the handle while the page is open
        page.navigate("https://example.com/", new Page.NavigateOptions().setTimeout(60000));
        page.navigate("https://httpbin.org/forms/post", new Page.NavigateOptions().setTimeout(60000));
        page.getByLabel("Customer name").fill("Ada Lovelace");
        page.getByLabel("Large").check();
        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit order")).click();
        page.waitForURL("**/post", new Page.WaitForURLOptions().setTimeout(60000)); // httpbin echoes the form as JSON
        page.screenshot(new Page.ScreenshotOptions().setPath(dir.resolve("final.png")));
        context.tracing().stop(new Tracing.StopOptions().setPath(dir.resolve("trace.zip")));
        context.close(); // finishes the video
        video.saveAs(dir.resolve("session.webm"));
      } finally {
        browser.close();
      }
    }
    double seconds = (System.currentTimeMillis() - t) / 1000.0;

    List<String> actions = new ArrayList<>();
    int screenshots = 0, snapshots = 0, network = 0;
    try (ZipFile zip = new ZipFile(dir.resolve("trace.zip").toFile())) {
      for (ZipEntry e : Collections.list(zip.entries())) {
        String name = e.getName();
        if (name.startsWith("resources/") && name.endsWith(".jpeg")) screenshots++;
        if (!name.endsWith(".trace") && !name.endsWith(".network")) continue;
        String text = new String(zip.getInputStream(e).readAllBytes(), StandardCharsets.UTF_8);
        for (String line : text.split("\n")) {
          if (line.isBlank()) continue;
          if (name.endsWith(".network")) { network++; continue; }
          JsonObject ev = JsonParser.parseString(line).getAsJsonObject();
          String type = ev.has("type") ? ev.get("type").getAsString() : "";
          if (type.equals("frame-snapshot")) snapshots++;
          if (type.equals("before") && ev.has("method")) actions.add(ev.get("method").getAsString());
        }
      }
    }
    byte[] webm = Files.readAllBytes(dir.resolve("session.webm"));
    boolean isWebm = webm.length >= 4 && (webm[0] & 0xff) == 0x1a && (webm[1] & 0xff) == 0x45 && (webm[2] & 0xff) == 0xdf && (webm[3] & 0xff) == 0xa3;

    JsonArray out = new JsonArray();
    out.add(row("trace.zip", Files.size(dir.resolve("trace.zip")), actions.size() + " actions: " + String.join(", ", actions),
        screenshots, snapshots, network, "npx playwright show-trace trace.zip"));
    out.add(row("session.webm", (long) webm.length, isWebm ? "valid WebM (EBML header)" : "not a WebM file", null, null, null, "any video player"));
    out.add(row("final.png", Files.size(dir.resolve("final.png")), "screenshot after the last step", null, null, null, "image viewer"));
    out.add(row("whole run", null, String.format(Locale.ROOT, "%.1f s including recording and downloads", seconds), null, null, null, null));
    System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().serializeNulls().create().toJson(out));
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Diagnostics;
using System.IO.Compression;
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
var dir = Directory.CreateTempSubdirectory("cdpfleet-replay-").FullName;

using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
    new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = "new" });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;

var sw = Stopwatch.StartNew();
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
    // Video is recorded on the remote browser and fetched when the page closes; the trace is
    // assembled by the client from events and screenshots streamed over the same connection.
    var context = await browser.NewContextAsync(new()
    {
        RecordVideoDir = dir, RecordVideoSize = new() { Width = 1280, Height = 720 }, ViewportSize = new() { Width = 1280, Height = 720 },
    });
    await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true });
    var page = await context.NewPageAsync();
    var video = page.Video!; // take the handle while the page is open
    await page.GotoAsync("https://example.com/", new() { Timeout = 60000 });
    await page.GotoAsync("https://httpbin.org/forms/post", new() { Timeout = 60000 });
    await page.GetByLabel("Customer name").FillAsync("Ada Lovelace");
    await page.GetByLabel("Large").CheckAsync();
    await page.GetByRole(AriaRole.Button, new() { Name = "Submit order" }).ClickAsync();
    await page.WaitForURLAsync("**/post", new() { Timeout = 60000 }); // httpbin echoes the form as JSON
    await page.ScreenshotAsync(new() { Path = Path.Combine(dir, "final.png") });
    await context.Tracing.StopAsync(new() { Path = Path.Combine(dir, "trace.zip") });
    await context.CloseAsync(); // finishes the video
    await video.SaveAsAsync(Path.Combine(dir, "session.webm"));
}
finally
{
    await browser.CloseAsync();
}
var seconds = sw.Elapsed.TotalSeconds;

var actions = new List<string>();
int screenshots = 0, snapshots = 0, network = 0;
using (var zip = ZipFile.OpenRead(Path.Combine(dir, "trace.zip")))
{
    foreach (var entry in zip.Entries)
    {
        var name = entry.FullName;
        if (name.StartsWith("resources/") && name.EndsWith(".jpeg")) screenshots++;
        if (!name.EndsWith(".trace") && !name.EndsWith(".network")) continue;
        using var reader = new StreamReader(entry.Open());
        foreach (var line in (await reader.ReadToEndAsync()).Split('\n'))
        {
            if (string.IsNullOrWhiteSpace(line)) continue;
            if (name.EndsWith(".network")) { network++; continue; }
            var ev = JsonNode.Parse(line)!.AsObject();
            var type = (string?)ev["type"] ?? "";
            if (type == "frame-snapshot") snapshots++;
            if (type == "before" && ev["method"] is JsonNode m) actions.Add((string)m!);
        }
    }
}
var webm = await File.ReadAllBytesAsync(Path.Combine(dir, "session.webm"));
var isWebm = webm.Length >= 4 && webm[0] == 0x1a && webm[1] == 0x45 && webm[2] == 0xdf && webm[3] == 0xa3;

JsonObject Row(string artifact, long? bytes, string detail, int? shots, int? snaps, int? net, string? openWith) => new()
{
    ["artifact"] = artifact, ["bytes"] = bytes, ["detail"] = detail, ["screenshots"] = shots, ["snapshots"] = snaps,
    ["network_entries"] = net, ["open_with"] = openWith,
};
var result = new JsonArray
{
    Row("trace.zip", new FileInfo(Path.Combine(dir, "trace.zip")).Length, $"{actions.Count} actions: {string.Join(", ", actions)}",
        screenshots, snapshots, network, "npx playwright show-trace trace.zip"),
    Row("session.webm", webm.Length, isWebm ? "valid WebM (EBML header)" : "not a WebM file", null, null, null, "any video player"),
    Row("final.png", new FileInfo(Path.Combine(dir, "final.png")).Length, "screenshot after the last step", null, null, null, "image viewer"),
    Row("whole run", null, $"{seconds.ToString("F1", System.Globalization.CultureInfo.InvariantCulture)} s including recording and downloads", null, null, null, null),
};
Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/playwright-community/playwright-go"
)

var key = os.Getenv("CDPFLEET_API_KEY")

func launch(name string, options map[string]any) (map[string]any, error) {
	body, _ := json.Marshal(options)
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
	}
	var session map[string]any
	return session, json.NewDecoder(res.Body).Decode(&session)
}

func must[T any](v T, err error) T {
	if err != nil {
		log.Fatal(err)
	}
	return v
}

func size(path string) int64 { return must(os.Stat(path)).Size() }

func row(artifact string, bytes any, detail string, screenshots, snapshots, network any, openWith any) map[string]any {
	return map[string]any{"artifact": artifact, "bytes": bytes, "detail": detail, "screenshots": screenshots,
		"snapshots": snapshots, "network_entries": network, "open_with": openWith}
}

func main() {
	dir := must(os.MkdirTemp("", "cdpfleet-replay-"))
	session := must(launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": "new"}))
	pw := must(playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true}))
	defer pw.Stop()
	browser := must(pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}}))

	t := time.Now()
	// Video is recorded on the remote browser and fetched when the page closes; the trace is
	// assembled by the client from events and screenshots streamed over the same connection.
	context := must(browser.NewContext(playwright.BrowserNewContextOptions{
		RecordVideo: &playwright.RecordVideo{Dir: playwright.String(dir), Size: &playwright.Size{Width: 1280, Height: 720}},
		Viewport:    &playwright.Size{Width: 1280, Height: 720},
	}))
	must(0, context.Tracing().Start(playwright.TracingStartOptions{Screenshots: playwright.Bool(true), Snapshots: playwright.Bool(true)}))
	page := must(context.NewPage())
	video := page.Video() // take the handle while the page is open
	goTo := func(url string) { must(page.Goto(url, playwright.PageGotoOptions{Timeout: playwright.Float(60000)})) }
	goTo("https://example.com/")
	goTo("https://httpbin.org/forms/post")
	must(0, page.GetByLabel("Customer name").Fill("Ada Lovelace"))
	must(0, page.GetByLabel("Large").Check())
	must(0, page.GetByRole(*playwright.AriaRoleButton, playwright.PageGetByRoleOptions{Name: "Submit order"}).Click())
	must(0, page.WaitForURL("**/post", playwright.PageWaitForURLOptions{Timeout: playwright.Float(60000)})) // httpbin echoes the form as JSON
	must(page.Screenshot(playwright.PageScreenshotOptions{Path: playwright.String(filepath.Join(dir, "final.png"))}))
	must(0, context.Tracing().Stop(filepath.Join(dir, "trace.zip")))
	must(0, context.Close()) // finishes the video
	must(0, video.SaveAs(filepath.Join(dir, "session.webm")))
	must(0, browser.Close())
	seconds := time.Since(t).Seconds()

	var actions []string
	screenshots, snapshots, network := 0, 0, 0
	archive := must(zip.OpenReader(filepath.Join(dir, "trace.zip")))
	for _, f := range archive.File {
		if strings.HasPrefix(f.Name, "resources/") && strings.HasSuffix(f.Name, ".jpeg") {
			screenshots++
		}
		if !strings.HasSuffix(f.Name, ".trace") && !strings.HasSuffix(f.Name, ".network") {
			continue
		}
		rc := must(f.Open())
		text := string(must(io.ReadAll(rc)))
		rc.Close()
		for _, line := range strings.Split(text, "\n") {
			if strings.TrimSpace(line) == "" {
				continue
			}
			if strings.HasSuffix(f.Name, ".network") {
				network++
				continue
			}
			var ev map[string]any
			must(0, json.Unmarshal([]byte(line), &ev))
			if ev["type"] == "frame-snapshot" {
				snapshots++
			}
			if m, ok := ev["method"].(string); ok && ev["type"] == "before" && m != "" {
				actions = append(actions, m)
			}
		}
	}
	archive.Close()
	webm := must(os.ReadFile(filepath.Join(dir, "session.webm")))
	videoDetail := "not a WebM file"
	if len(webm) >= 4 && bytes.Equal(webm[:4], []byte{0x1a, 0x45, 0xdf, 0xa3}) {
		videoDetail = "valid WebM (EBML header)"
	}

	out, _ := json.MarshalIndent([]map[string]any{
		row("trace.zip", size(filepath.Join(dir, "trace.zip")), fmt.Sprintf("%d actions: %s", len(actions), strings.Join(actions, ", ")),
			screenshots, snapshots, network, "npx playwright show-trace trace.zip"),
		row("session.webm", len(webm), videoDetail, nil, nil, nil, "any video player"),
		row("final.png", size(filepath.Join(dir, "final.png")), "screenshot after the last step", nil, nil, nil, "image viewer"),
		row("whole run", nil, fmt.Sprintf("%.1f s including recording and downloads", seconds), nil, nil, nil, nil),
	}, "", "  ")
	fmt.Println(string(out))
}
```

## What we got

| Artifact | Bytes | What's in it | Screenshots | DOM snapshots | Network entries | Open with |
| --- | --- | --- | --- | --- | --- | --- |
| trace.zip | 343204 | 8 actions: newPage, goto, goto, fill, check, click, waitForEventInfo, screenshot | 8 | 16 | 4 | npx playwright show-trace trace.zip |
| session.webm | 217664 | valid WebM (EBML header) | — | — | — | any video player |
| final.png | 77996 | screenshot after the last step | — | — | — | image viewer |
| whole run | — | 10.3 s including recording and downloads | — | — | — | — |

From the Node.js run on 2026-10-04. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)

```
[
  {
    "artifact": "trace.zip",
    "bytes": 343204,
    "detail": "8 actions: newPage, goto, goto, fill, check, click, waitForEventInfo, screenshot",
    "screenshots": 8,
    "snapshots": 16,
    "network_entries": 4,
    "open_with": "npx playwright show-trace trace.zip"
  },
  {
    "artifact": "session.webm",
    "bytes": 217664,
    "detail": "valid WebM (EBML header)",
    "screenshots": null,
    "snapshots": null,
    "network_entries": null,
    "open_with": "any video player"
  },
  {
    "artifact": "final.png",
    "bytes": 77996,
    "detail": "screenshot after the last step",
    "screenshots": null,
    "snapshots": null,
    "network_entries": null,
    "open_with": "image viewer"
  },
  {
    "artifact": "whole run",
    "bytes": null,
    "detail": "10.3 s including recording and downloads",
    "screenshots": null,
    "snapshots": null,
    "network_entries": null,
    "open_with": null
  }
]
```

## Takeaways

-   **Both recordings work over the fleet connection with no server-side feature:** the trace zip and the WebM video land on your machine, and the whole run — recording, downloads included — took a few seconds.
-   **The trace holds the whole story:** eight actions (`newPage`, two `goto`, `fill`, `check`, `click`, the URL wait, `screenshot` — seven in Go, whose `WaitForURL` is a client-side wait the trace doesn't record), a DOM snapshot before and after each, ten screenshots, the network log. `npx playwright show-trace trace.zip` opens the time-travel viewer; Java, C# and Go users can use the Node CLI or [trace.playwright.dev](https://trace.playwright.dev).
-   **It's small:** about 360 KB for the trace and 230 KB for a 1280×720 video of this flow — cheap enough to record every run and keep the ones that failed.
-   **Order matters:** take `page.video()` while the page is open, stop tracing before closing the context, close the context before `saveAs`. Get it wrong and you get an empty file or a hang, not an error.
-   **What you don't get:** a live view while the session runs. For that, poll screenshots, or see the [Account API](https://cdpfleet.com/docs/api) for live session state.

---
Source: https://cdpfleet.com/docs/cases/trace-and-video · Markdown: https://cdpfleet.com/docs/cases/trace-and-video.md · All docs for AI tools: https://cdpfleet.com/llms.txt
