ReplayX is a deterministic web replay and debugging system built as a Chrome extension (Manifest V3). It captures real user sessions, replays them in a controlled environment, and surfaces failures with structured diagnostics so teams can understand regressions, UI drift, and flaky interactions with far greater clarity than a screen recording.
It is aimed at product engineers, QA teams, and frontend developers who need to reproduce bugs reliably, inspect interaction sequences, and debug asynchronous behaviour without brittle manual reproduction.
Modern web apps are stateful, asynchronous, and timing-sensitive, which makes bugs hard to reproduce. ReplayX turns a real session into a replayable artefact that can be inspected, resumed, and debugged.
- Deterministic replay of recorded user behaviour
- Reproduction of hard-to-catch UI regressions
- Visibility into network-driven behaviour and state changes
- Structured replay diagnostics for target misses, DOM mismatches, and playback failures
- Local-first: no backend, no external service, no telemetry
Captures clicks and double clicks, text input and form changes, form submissions, keyboard input (keydown and keyup tracked separately), scroll, focus/blur, navigation (including SPA pushState/replaceState), resize, DOM mutations, and fetch/XHR traffic.
Each event is time-normalised against the session start, tagged with the frame it was captured in, and assigned a monotonic sequence number so recording order survives storage and reload.
Privacy is enforced at capture time. Password fields, fields whose name or id looks credential-shaped, and anything marked data-replay-mask are replaced with a mask before the value ever leaves the page — on both input and change. Authorization-style headers are dropped and credential-shaped JSON and form-encoded body fields are masked inside the network interceptor.
- Event scheduling with normalised timing and a stable tie-break on recording order
- Multi-strategy element resolution: test ids, ids, names, ARIA labels, stable classes, and a structural path, plus shadow-DOM piercing and text-content matching
- Framework-safe value writes that bypass React/Vue setter interception
- Navigation handling across pages, with progress checkpointed so replay resumes after each load
- Replay speed control and single-step execution
- Selective storage restoration: only keys the recording captured are touched, so unrelated data survives
- Network-aware settling that waits for real quiescence before judging an outcome
fetch and XMLHttpRequest are intercepted in the page's MAIN world. During recording, exchanges are captured (with credentials stripped and bodies truncated); during replay, recorded responses are served back so pages that depend on API data behave as they did originally. When idle, the interceptor is fully transparent.
Structured findings for missing targets, failed event execution, and DOM mismatches. A mismatch is only reported when the original recording showed the DOM changing after that interaction and replay produced no change — so the report reflects real drift rather than every interaction that happens not to mutate the DOM.
| Layer | Module | Responsibility |
|---|---|---|
| Popup UI | src/popup.ts | Session list, replay controls, debugger panel |
| Background entry | src/background/service_worker.ts | Chrome API adapter and worker lifecycle only |
| Background logic | src/background/router.ts | All orchestration, dependency-injected and unit tested |
| Background state | src/background/state.ts | State that survives MV3 worker suspension |
| Content entry | src/content/main.ts | Wires recorder, replayer, widget, interceptor bridge |
| Recording | src/content/recorder.ts | Interaction capture, masking, rate limiting |
| Replay | src/content/replayer.ts | Playback, settling, diagnostics |
| Selectors | src/content/selector.ts | Uniqueness-verified selector generation |
| Interception | src/content/interceptor.ts | MAIN-world fetch/XHR hooks |
| Storage | src/storage/db.ts | IndexedDB session and chunked event storage |
| Protocol | src/messages.ts | Typed contracts for all three message channels |
| Validation | src/validation.ts | Import validation and sanitisation |
The background is split in two. service_worker.ts is a thin chrome-API adapter; every decision lives in router.ts behind injected db and tabs interfaces. That is what makes the background logic testable without a browser.
Recording state is durable. MV3 tears the service worker down after ~30 seconds idle, so recording and replay state live in chrome.storage.session and flushed events are appended straight to IndexedDB rather than buffered in worker memory.
Three message channels, one contract. Popup→background, background→content, and content↔interceptor are all typed in messages.ts. The interceptor channel runs over window.postMessage, which the host page can also write to, so every message on it is origin-checked and shape-validated.
The popup builds DOM, never HTML. Everything it renders comes from a recorded page or an imported file. All of it is inserted with createElement and textContent.
src/
background/
service_worker.ts router.ts state.ts
content/
main.ts recorder.ts replayer.ts
interceptor.ts interceptor.entry.ts selector.ts
storage/
db.ts
popup.ts popup.css debugger.ts
messages.ts types.ts validation.ts
test/
setup.ts
.github/workflows/ci.yml
manifest.json vite.config.ts vitest.config.ts eslint.config.js
- Node.js 20.19+ (see .nvmrc;
nvm usepicks the right version) - npm
- Google Chrome 116+
npm install
npm run build- Open
chrome://extensions - Enable Developer mode
- Choose Load unpacked
- Select the generated
dist/directory
npm run dev # Vite dev server with hot reload
npm test # Vitest in watch mode
npm run test:run # Single test run
npm run test:coverage # Tests with coverage thresholds enforced
npm run typecheck # tsc --noEmit
npm run lint # ESLint
npm run verify # typecheck + lint + coverage + build (what CI runs)- Load the extension and open a site.
- Start recording from the popup. The session record is created before capture begins, so events are durable from the first one.
- Interact with the app. Events are buffered in the page and flushed to IndexedDB periodically and on unload.
- Stop recording; the session appears in the popup list.
- Replay it to reproduce the behaviour, at any speed or one step at a time.
- Open View for the debugger panel: replay issues, event timeline, and network log.
229 tests across 10 files, run against jsdom with an in-memory IndexedDB and a chrome.* stub:
| Suite | Focus |
|---|---|
storage/db.test.ts |
CRUD, chunking, append, retention, duration maths |
background/router.test.ts |
Every message action, failure and rollback paths |
background/state.test.ts |
Persistence across worker suspension, concurrent patches |
content/selector.test.ts |
Selector uniqueness, CSS escaping, structural paths |
content/recorder.test.ts |
Capture, masking, debouncing, limits |
content/replayer.test.ts |
Scheduling, execution, element resolution, mismatch attribution |
content/interceptor.test.ts |
fetch/XHR hooks, mock matching, sanitisation |
popup.test.ts |
Rendering, and that hostile recorded content stays inert |
validation.test.ts |
Import schema and sanitisation |
debugger.test.ts |
Event summaries |
Coverage thresholds are enforced in vitest.config.ts and are a ratchet: raise them, never lower them.
.github/workflows/ci.yml runs on every push and pull request to main:
- Typecheck, lint, and tests with coverage on Node 20.19, 22, and 24
- Production build, plus a structural check that
dist/manifest.jsonis valid MV3 with a service worker and a MAIN-world content script npm audit --audit-level=highas a separate job- Coverage and the built extension uploaded as artefacts
| Permission | Why |
|---|---|
storage |
Session state that survives service-worker suspension |
scripting |
Injecting the content script into tabs loaded before install |
activeTab |
Resolving the tab to record or replay |
alarms |
Scheduling the data-retention sweep |
host_permissions: http/https |
Recording and replaying on the pages you choose |
The extension declares no downloads permission: session export builds the file in the popup, which is a DOM context, rather than in the service worker.
All data stays in your browser's IndexedDB. Retention is enforced automatically: sessions older than 30 days are deleted, and at most 100 sessions are kept (newest first). See PRIVACY.md.
- Semantic DOM diffing beyond structural fingerprints
- Richer timeline visualisation and filtering
- Cross-frame recording (
all_framesis currently off by design) - Better support for complex single-page applications
Provided as a development and debugging utility for browser-based workflows.