feat: jsPsych session recording replay SPA (v0 prototype)#1
Merged
Conversation
Agent-Logs-Url: https://github.com/jspsych/replay/sessions/06486b86-7d28-49a3-817c-65e64897b8ca Co-authored-by: jodeleeuw <595524+jodeleeuw@users.noreply.github.com>
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
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>
Contributor
Author
Done in 6fa1ae5. Added
Also updated After merging, enable GitHub Pages in the repo settings (Settings → Pages → Source: GitHub Actions) to activate the deployment. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements a static single-page replay app that consumes
SessionRecordingJSON produced byjsPsych.getSessionRecording()(record_session: true) and reconstructs the participant's session visually — no plugin re-execution.Schema layer
src/schema/types.ts— wire types copied from feat(jspsych): add record_session option for high-fidelity replay capture jsPsych#3661;validate()enforcesschema_version === 1and throws descriptively on any mismatchReplay engine (
src/replay/)dom.ts—instantiateDom()walksDomNodetrees into real DOM; blockson*attributes; maintainsMap<number, Node>for event targetingengine.ts—ReplayEnginedrives playback viascheduleEvents()(onesetTimeoutper event) andapplyEventsSync()for seek fast-forward; handles every event type:dom.*,mouse.*,touch.*,key.*,scroll.*,focus/blur,clipboard.*,media.*(media logged only in v0)viewport.ts—ViewportManagerresizes the iframe fromviewport_changes;viewportAt(t)resolves the active viewport at any trial timeUI (
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_dataJSON viewer on selectionoverlay.ts— cursor dot tracking mouse/touch position, keystroke badges (modifier symbols + key, last 3), focus/blur stage overlayEntry point
index.html+src/main.ts— file picker + drag-drop loads a.json, validates, and wires all components; auto-loadspublic/examples/sample-recording.jsonon startuppublic/examples/sample-recording.json— 4-trial synthetic fixture (keyboard response, survey text, button response) for developmentTests
21 vitest unit tests covering
validate()edge cases,instantiateDom()outerHTML fixture matching, andReplayEngineevent dispatch (including security:on*attrs suppressed through both DOM instantiation anddom.attrevents).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...