Skip to content

feat: jsPsych session recording replay SPA (v0 prototype)#1

Merged
jodeleeuw merged 3 commits intomainfrom
copilot/build-single-page-replayer-app
May 1, 2026
Merged

feat: jsPsych session recording replay SPA (v0 prototype)#1
jodeleeuw merged 3 commits intomainfrom
copilot/build-single-page-replayer-app

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

Implements a static single-page replay app that consumes SessionRecording JSON produced by jsPsych.getSessionRecording() (record_session: true) and reconstructs the participant's session visually — no plugin re-execution.

Schema layer

Replay engine (src/replay/)

  • dom.tsinstantiateDom() walks DomNode trees into real DOM; blocks on* attributes; maintains Map<number, Node> for event targeting
  • engine.tsReplayEngine drives playback via scheduleEvents() (one setTimeout per event) and applyEventsSync() for seek fast-forward; handles every event type: dom.*, mouse.*, touch.*, key.*, scroll.*, focus/blur, clipboard.*, media.* (media logged only in v0)
  • viewport.tsViewportManager resizes the iframe from viewport_changes; viewportAt(t) resolves the active viewport at any trial time

UI (src/ui/)

  • player.ts — play/pause/restart, prev/next/dropdown trial navigation, scrub bar with synchronous seek, speed control (0.25×–4×)
  • sidebar.ts — trial list with plugin labels; trial_data JSON viewer on selection
  • overlay.ts — cursor dot tracking mouse/touch position, keystroke badges (modifier symbols + key, last 3), focus/blur stage overlay

Entry point

  • index.html + src/main.ts — file picker + drag-drop loads a .json, validates, and wires all components; auto-loads public/examples/sample-recording.json on startup
  • public/examples/sample-recording.json — 4-trial synthetic fixture (keyboard response, survey text, button response) for development

Tests

21 vitest unit tests covering validate() edge cases, instantiateDom() outerHTML fixture matching, and ReplayEngine event dispatch (including security: on* attrs suppressed through both DOM instantiation and dom.attr events).

Original prompt

jspsych/replay — v0 Prototype Plan
Mission
Build a single-page web app that loads a JSON recording produced by jsPsych.getSessionRecording() (record_session: true) and reconstructs a visual replay of the participant's session. The replayer is purely observational — it does not re-run plugin code; it applies the recorded DOM mutations and visualizes the recorded input events.

This is the canonical first replayer for the recording format. Treat the schema as the contract; the recording library in jspsych/jspsych produces it, this app consumes it.

Authoritative references
The source of truth lives in jspsych/jspsych. Read these first:

docs/reference/session-recording-schema.md — full prose schema reference with replayer guidance, including the per-trial replay procedure, node-id semantics, and end-reason meanings.
packages/jspsych/src/modules/recording.ts — exported TypeScript types (SessionRecording, TrialRecording, DomNode, RecordedEvent, etc.). These are the wire types; copy them into the replay project rather than depending on jspsych directly.
packages/jspsych/tests/core/record-session.test.ts — test cases that exercise each capture path. Useful as known-good fixtures for replay.
Schema version at the time of writing: 1. Reject anything else.

Tech stack
Vite + TypeScript for build and dev server. No framework — vanilla TS keeps the prototype small and the code obvious.
No runtime dependencies beyond the recording's TypeScript types (copied in).
pnpm or npm — match the parent project's tooling.
Single output: a static SPA that can be opened locally or hosted on GitHub Pages.
File layout
jspsych/replay/
├── README.md
├── package.json
├── tsconfig.json
├── vite.config.ts
├── index.html
├── public/
│ └── examples/
│ └── sample-recording.json # capture once from a real session
├── src/
│ ├── main.ts # bootstrap, file picker
│ ├── ui/
│ │ ├── player.ts # play/pause/scrub/jump-to-trial
│ │ ├── sidebar.ts # trial list + trial_data viewer
│ │ └── overlay.ts # cursor + keystroke indicator
│ ├── replay/
│ │ ├── engine.ts # scheduling + event dispatch
│ │ ├── dom.ts # initial_dom → real DOM, mutation apply
│ │ ├── input.ts # input event visualization
│ │ └── viewport.ts # iframe sizing from viewport_changes
│ └── schema/
│ └── types.ts # types copied from jspsych/jspsych
└── tests/
└── engine.test.ts # vitest, mostly deterministic replay paths
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Host page │
│ │
│ ┌────────────────────────┐ ┌─────────────────────────────┐ │
│ │ Player UI │ │ Trial sidebar │ │
│ │ ─ Play/pause/seek │ │ ─ List of trials │ │
│ │ ─ Time readout │ │ ─ Selected trial's │ │
│ │ ─ Trial dropdown │ │ trial_data viewer │ │
│ └────────────────────────┘ └─────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Replay stage (iframe, sandboxed) │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ #jspsych-content reconstructed from initial_dom + │ │ │
│ │ │ events. Cursor overlay + keystroke indicator on top. │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Use a sandboxed <iframe sandbox="allow-same-origin"> so the recorded DOM can't fire scripts. The engine writes into the iframe's document directly via DOM APIs.

Replay model
For each trial:

Clear the iframe's #jspsych-content.
Walk trial.initial_dom and instantiate it as real DOM nodes inside #jspsych-content. Maintain a Map<number, Node> from recorded node id → live node.
Schedule trial.events by their t field. At each fire:
dom.add: instantiate node (recursively assigning ids), insert before sibling before (or append if null).
dom.remove: remove the live node mapped to node id; clean up its descendants from the id map.
dom.attr: set/remove the attribute.
dom.text: replace the node's data.
mouse.* / touch.: update the cursor overlay (no real input dispatch into the iframe).
key.
: append to the keystroke indicator.
scroll.window: scroll the iframe's window.
scroll.element: set scrollLeft/scrollTop on the mapped element.
media.: log to a sidebar (optional in v0). Don't actually play media.
focus/blur/fullscreen.
: visual indicator on the stage.
clipboard.*: log to sidebar.
Phasing
Phase 1 — Plumbing (target: ~2 hours)
not done
npm create vi...

Copilot AI changed the title [WIP] Build single-page app to replay jsPsych session recordings feat: jsPsych session recording replay SPA (v0 prototype) May 1, 2026
Copilot AI requested a review from jodeleeuw May 1, 2026 12:31
@jodeleeuw
Copy link
Copy Markdown
Member

@copilot configure this for gh pages deploy

Agent-Logs-Url: https://github.com/jspsych/replay/sessions/8fa4fd72-d057-4ff2-982f-e5de4ea5f7dc

Co-authored-by: jodeleeuw <595524+jodeleeuw@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot configure this for gh pages deploy

Done in 6fa1ae5. Added .github/workflows/deploy.yml that:

  • Triggers on push to main and workflow_dispatch
  • Installs deps with pnpm, runs pnpm build, uploads dist/ as a Pages artifact
  • Deploys to GitHub Pages via actions/deploy-pages

Also updated vite.config.ts so base is /replay/ in production builds (matching the jspsych/replay repo path on jspsych.github.io) and ./ during local dev.

After merging, enable GitHub Pages in the repo settings (Settings → Pages → Source: GitHub Actions) to activate the deployment.

@jodeleeuw jodeleeuw marked this pull request as ready for review May 1, 2026 12:37
@jodeleeuw jodeleeuw merged commit 7e09f2a into main May 1, 2026
Copilot stopped work on behalf of jodeleeuw due to an error May 1, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants