Skip to content

PRD: Record & replay the CDP screencast as a video frame track #74

@reynsu

Description

@reynsu

Generated from a grilling session. Decision record: docs/adr/0012-record-and-replay-screencast.md. Terminology note: in this codebase Snapshot = the component-tree capture per step (CLAUDE.md §14); the browser image is a frame; this PRD adds a frame track (the recorded per-frame stream for a test).

Problem Statement

When I triage a failing E2E test I can only see a frozen frame of the browser once the test ends, and per-step stills when I replay a past run. I cannot re-watch what the test actually did — the clicks, navigations, and form fills that led to the failure. The live screencast does show the real browser in motion, but only during the few seconds the test runs and only if I happen to be watching; it is never recorded.

Solution

Record the CDP screencast that already streams during a run as a replayable frame track (a timestamped sequence of jpeg frames) and play it back in the Preview panel with a video transport (play/pause + time scrubber) — for both the live run and past runs. No ffmpeg, no new native binary; reuse the existing jpeg-frame pipeline.

User Stories

  1. As a developer, I want the Preview to record the whole browser stream of a test, so that I can re-watch the run after it finishes instead of seeing only the last frame.
  2. As a developer, I want to press play and watch a finished test's clicks, navigations and form fills in motion, so that I understand how it reached the failure.
  3. As a developer, I want a time scrubber on the Preview, so that I can jump to the exact moment something went wrong.
  4. As a developer, I want to scrub the recorded run of a past run (not just the live one), so that I can review history without re-running.
  5. As a developer, I want the video transport to appear only when a recorded frame track exists, so that the UI doesn't promise playback it can't deliver.
  6. As a developer, I want scrubbing the video to optionally move the step slider to the step active at that timestamp, so that the component tree / a11y view stays in sync with what I'm watching.
  7. As a developer, I want the step slider to keep working as today for the component-tree dimension, so that per-step semantics aren't lost.
  8. As a developer watching live, I want the live Preview to keep streaming in real time, so that behaviour during the run is unchanged (now at ~15fps).
  9. As a developer, I want playback to honour the recorded cadence (timestamps), so that the replay matches the real timing of the test rather than a flat frame-per-tick.
  10. As a maintainer, I want recorded frame tracks kept only for the most recent runs, so that .reactlens/runs/ doesn't grow without bound.
  11. As a maintainer, I want older runs to degrade gracefully to the last-frame-per-step they already have, so that history still shows something after its video is pruned.
  12. As a maintainer, I want the screencast to run at a throttled rate (~15fps) relying on CDP's change-only emission, so that disk and WS cost stay reasonable.
  13. As a maintainer, I want the new per-frame data to flow through the existing parseRunEvent boundary, so that the runtime-validation invariant (CLAUDE.md §13) holds.
  14. As a developer on a slow machine, I want idle periods of a test to cost ~nothing on disk, so that a test that waits doesn't bloat the recording (CDP emits only on visual change).
  15. As a developer, I want playback to start from the test's first recorded frame, so that I see the run from the beginning.
  16. As a developer replaying a run that predates this feature, I want the Preview to still show the per-step stills, so that old runs don't break.
  17. As a developer, I want the Preview to clearly indicate live vs recorded mode, so that I know whether I'm watching a stream or a recording.

Implementation Decisions

(Full rationale + rejected alternatives in ADR 0012.)

  • Storage: timestamped jpeg sequence, no ffmpeg. Each screencast frame persists as a monotonic per-test sequence (e.g. `frames//.jpg`) plus a per-frame JSONL index line carrying `ts`, `stepId`, and the frame ref. The persistor stops overwriting one file per step.
  • Protocol (§9) extended additively. The `frame` event gains an optional `timestamp?: number` (from `Page.screencastFrame` metadata), validated through `parseRunEvent`. A new dedicated event type was rejected (would duplicate the "frame" concept across all 5 ingestion points).
  • Deep module `frame-track` (host). Owns the per-frame sequence numbering, path, and JSONL index line; extracted from the persistor so it is testable in isolation.
  • Deep module `frame-track-retention` (host, pure). Given the list of runs, decides which frame tracks to keep (most recent 5) and which to degrade to last-frame-per-step. Pure input→output.
  • Deep module frame-track builder (frontend, pure). Transforms the per-frame JSONL lines of a run into an ordered frame track with timestamps, consumed by the player. Lives alongside replay-timeline.
  • `VideoTransport` (frontend). Play/pause + time scrubber in the Preview panel, independent of the step `TimelineSlider`; scrubbing may sync the step slider to the step containing the current timestamp.
  • Single ~15fps stream. `Page.startScreencast` throttled via `everyNthFrame`; the same stream is shown live and recorded (no separate sampling path). Live drops from ~30 to ~15fps.
  • Reducer ingests per-frame frames into a per-test frame track (append on live, hydrate on replay). `run-paths` grows a per-frame path; the runs REST surface exposes the frame-track index for a run.

Testing Decisions

Good tests assert external behaviour, not internals: given inputs, assert the produced track / pruning plan / parsed event — never private fields.

  • `frame-track-retention` (unit, pure): given N runs, asserts exactly the most-recent-5 keep their tracks and the rest are marked for degrade. Highest-value, fully deterministic.
  • frame-track builder (unit, pure): given per-frame JSONL lines (out of order, with timestamps), asserts an ordered track with correct timings; tolerates pre-feature runs (per-step stills) without throwing.
  • `frame` schema round-trip (unit): `frame` with `timestamp` survives `parseRunEvent`; a `frame` without it still parses (back-compat). Prior art: existing `src/runner/events` boundary tests.
  • `frame-track` persistor (integration): drive the persistor with a frame stream and assert the per-frame sequence + JSONL index land on disk without overwriting. Prior art: `tests/integration/replay-from-disk.test.ts`.
  • Plus manual verification on the live dashboard (:5173): record a run, play it back, scrub, confirm live still streams and old runs still render.

Out of Scope

  • An interactive embedded browser / `<iframe>` of the app under test (a different, un-driven Chromium — shows none of the test's actions).
  • Encoding to a real video format (webm/mp4) via ffmpeg.
  • Embedding the Playwright trace viewer (trace.zip — a different, heavier artifact).
  • Changing the component-tree Snapshot capture or the diagnosis path.

Further Notes

  • This is moat-adjacent (§10 "capture is sacred"): the persistor + run layout change must be done carefully and behind `parseRunEvent`.
  • Cross-repo: protocol + capture + persistence + retention land in `reactlens`; the `VideoTransport` player lands in `@reynsu/reactlens-dashboard-ui`. They ship coordinated.
  • Related existing pain: issue Integration tests fail in CI: chromium-headless probe + CDP screencast empty #40 (CI screencast empty in chromium-headless) — the recording path should not assume frames always arrive.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentPRD/issue is fully specified and ready for an agent to pick up and execute

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions