cyborg-hunter 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/CITATION.cff +2 -2
- package/README.md +21 -6
- package/dist/cyborg-hunter-replay.js +3 -3
- package/dist/cyborg-hunter.esm.js +3 -2
- package/dist/cyborg-hunter.min.js +3 -3
- package/dist/extension-cyborg-hunter.js +1 -1
- package/package.json +10 -2
- package/src/cli/ingest.js +311 -57
- package/src/cli/renderers/html-index-core.js +66 -12
- package/src/cli/renderers/html-index.js +7 -5
- package/src/cli/renderers/replay-assets.js +61 -7
- package/src/cli/renderers/replay-client-source.js +102 -0
- package/src/cli/renderers/replay-viewer.client.js +1750 -595
- package/src/cli/report.js +6 -0
- package/src/core/monitor.js +12 -13
- package/src/jspsych/extension-cyborg-hunter-replay.js +64 -3
- package/src/jspsych/extension-cyborg-hunter.js +1 -1
- package/src/replay/capture-dom.js +470 -458
- package/src/replay/capture-trace.js +688 -271
- package/src/replay/delivery.js +82 -0
- package/src/replay/dom-instantiate.js +779 -0
- package/src/replay/index.js +88 -5
- package/src/replay/initial-state.js +295 -0
- package/src/replay/mutations.js +668 -0
- package/src/replay/node-registry.js +116 -0
- package/src/replay/persistence.js +19 -6
- package/src/replay/recorder.js +342 -73
- package/src/replay/redaction.js +165 -0
- package/src/replay/serializer.js +148 -43
- package/src/replay/snapshot.js +409 -0
- package/src/replay/span.js +55 -0
- package/src/replay/viewer-model.js +293 -102
- package/src/shared/constants.js +1 -1
- package/src/shared/inline-safe.js +80 -0
- package/src/shared/schema-v2-validator.js +595 -0
- package/tools/convert/jspsych-v1-to-v2.mjs +432 -0
package/src/cli/report.js
CHANGED
|
@@ -150,6 +150,12 @@ export async function run(args) {
|
|
|
150
150
|
if (replayAssets.count > 0) {
|
|
151
151
|
console.log(` replay/ — ${replayAssets.count} session replays (${(replayAssets.totalBytes / 1024 / 1024).toFixed(1)} MB)`);
|
|
152
152
|
}
|
|
153
|
+
// A skipped artifact is already visible in the report (the participant's
|
|
154
|
+
// replay section says why), but an analyst watching the CLI must not have
|
|
155
|
+
// to open the HTML to learn a recording did not make it.
|
|
156
|
+
for (const s of replayAssets.skipped) {
|
|
157
|
+
console.log(` replay/ — skipped ${s.participantId}: ${s.reason}`);
|
|
158
|
+
}
|
|
153
159
|
|
|
154
160
|
// HTML index page — references images/ folder (not base64-embedded)
|
|
155
161
|
const { renderHtmlIndex } = await import('./renderers/html-index.js');
|
package/src/core/monitor.js
CHANGED
|
@@ -64,7 +64,7 @@ export function init(userConfig) {
|
|
|
64
64
|
}, userConfig.domProtection || {}),
|
|
65
65
|
collectForPostHoc: Object.assign({
|
|
66
66
|
fullKeystrokeTimestamps: false,
|
|
67
|
-
rawMouseTrack:
|
|
67
|
+
rawMouseTrack: true, // see the privacy note at endTrial (flipped on 2026-09-02)
|
|
68
68
|
responseText: false,
|
|
69
69
|
windowPositionLog: true,
|
|
70
70
|
elementTrace: false,
|
|
@@ -378,18 +378,17 @@ export function init(userConfig) {
|
|
|
378
378
|
report.mouseMetrics = mouseMetrics;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// toggle.
|
|
381
|
+
// Raw per-sample mouse coordinates persist under `mouseTrack` (the
|
|
382
|
+
// name extract-core's field map expects: mouseTrack → mouseEvents, so a
|
|
383
|
+
// saved report round-trips through extractIntegrityData() without new
|
|
384
|
+
// glue) unless collectForPostHoc.rawMouseTrack is set false, in which
|
|
385
|
+
// case only the derived mouseMetrics above ship and the {x,y,t,type}
|
|
386
|
+
// samples never leave the browser. ON by default since 2026-09-02: a
|
|
387
|
+
// researcher who adds CH to an experiment has already adjudicated
|
|
388
|
+
// collecting behavioural traces, and an off-by-default gate made the
|
|
389
|
+
// report's trajectory panels read "no mouse data" for everyone who
|
|
390
|
+
// never found the toggle (found by CK on the bench harness). The
|
|
391
|
+
// toggle stays for protocols that need less.
|
|
393
392
|
if (config.collectForPostHoc.rawMouseTrack) {
|
|
394
393
|
report.mouseTrack = report.mouseEvents;
|
|
395
394
|
}
|
|
@@ -35,7 +35,7 @@ import { buildReplayMeta } from '../replay/persistence.js';
|
|
|
35
35
|
class CyborgHunterReplayExtension {
|
|
36
36
|
static info = {
|
|
37
37
|
name: 'cyborg-hunter-replay',
|
|
38
|
-
version: '0.
|
|
38
|
+
version: '0.8.0',
|
|
39
39
|
data: {} // per-trial return is {}; session meta goes via addProperties
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -47,9 +47,37 @@ class CyborgHunterReplayExtension {
|
|
|
47
47
|
this._monitoring = false;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Keyframe cadence on a jsPsych host: every TRIAL segment, always.
|
|
51
|
+
//
|
|
52
|
+
// The standalone recorder keyframes adaptively (spec §3's size trigger plus a
|
|
53
|
+
// segment fallback) because a persistent DOM makes a continuation cheap: the
|
|
54
|
+
// player carries the previous segment's tree forward and applies deltas. A
|
|
55
|
+
// jsPsych trial wipes the display and builds a new one, so there is nothing
|
|
56
|
+
// to carry forward — a continuation would make the player reconstruct each
|
|
57
|
+
// trial by replaying the wipe as a removal of everything followed by an
|
|
58
|
+
// insertion of everything, and would leave a seek into any trial dependent on
|
|
59
|
+
// the trials before it. §3 says as much: wiping hosts SHOULD keyframe every
|
|
60
|
+
// segment, which is also jsPsych-v1's own behaviour.
|
|
61
|
+
//
|
|
62
|
+
// Forced over researcher config rather than merged under it: the reason is a
|
|
63
|
+
// property of the host, not a tuning preference, so a value passed here is a
|
|
64
|
+
// misunderstanding rather than a choice. It is not discarded silently.
|
|
65
|
+
//
|
|
66
|
+
// "Every TRIAL segment" is exact, not loose. A jsPsych recording can also
|
|
67
|
+
// contain UNBRACKETED segments — the inter-trial interval, where the display
|
|
68
|
+
// wipe fires mutations between one trial's `on_finish` and the next trial's
|
|
69
|
+
// `on_load` — and those are recorded as CONTINUATIONS whatever this setting
|
|
70
|
+
// says (capture-dom's implicit-segment rule). That is correct: an ITI segment
|
|
71
|
+
// genuinely continues the outgoing trial's DOM as it is torn down, and its
|
|
72
|
+
// patches were numbered against that span before the segment existed.
|
|
50
73
|
initialize(params) {
|
|
51
74
|
this.params = params || {};
|
|
52
|
-
this.
|
|
75
|
+
if (this.params.keyframeEvery != null && this.params.keyframeEvery !== 1) {
|
|
76
|
+
console.warn('[cyborg-hunter-replay] ignoring keyframeEvery: ' +
|
|
77
|
+
this.params.keyframeEvery + ' — jsPsych wipes the display between ' +
|
|
78
|
+
'trials, so every trial segment is recorded as a keyframe.');
|
|
79
|
+
}
|
|
80
|
+
this.api = CHReplay.attach(Object.assign({}, this.params, { keyframeEvery: 1 }));
|
|
53
81
|
this.api.startSession();
|
|
54
82
|
this._stashChMonitor();
|
|
55
83
|
}
|
|
@@ -64,6 +92,36 @@ class CyborgHunterReplayExtension {
|
|
|
64
92
|
} catch (e) { /* standalone replay is fine */ }
|
|
65
93
|
}
|
|
66
94
|
|
|
95
|
+
// Spec §2's `host`: the runtime the recorder was embedded in, as opposed to
|
|
96
|
+
// `recorder`, which is this library. The recorder core is host-agnostic by
|
|
97
|
+
// construction and never sniffs for globals, so the adapter — the one piece
|
|
98
|
+
// that exists because jsPsych does — is where the answer comes from.
|
|
99
|
+
//
|
|
100
|
+
// §2 types host as `{name, version} | null` with version a required STRING,
|
|
101
|
+
// so a runtime that reports no version gets no host record rather than a
|
|
102
|
+
// half-record a conforming consumer would trip over.
|
|
103
|
+
//
|
|
104
|
+
// `version()` is the only shape checked for, because it is the only shape
|
|
105
|
+
// jsPsych has: 7.3.1 defines `version() { return version; }` on the JsPsych
|
|
106
|
+
// class and 8.x keeps the same core-API accessor. An earlier draft also
|
|
107
|
+
// accepted a plain string field; nothing produces one, so it was speculative
|
|
108
|
+
// dead code and went.
|
|
109
|
+
//
|
|
110
|
+
// Never throws: finalize()'s outer catch turns any throw into
|
|
111
|
+
// replayFinalizeError with NO autosave, and the least important field in
|
|
112
|
+
// the file must not be able to cost the recording.
|
|
113
|
+
_detectHost() {
|
|
114
|
+
try {
|
|
115
|
+
var jp = this.jsPsych;
|
|
116
|
+
if (!jp || typeof jp.version !== 'function') return null;
|
|
117
|
+
var v = jp.version();
|
|
118
|
+
if (typeof v !== 'string' || !v) return null;
|
|
119
|
+
return { name: 'jspsych', version: v };
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
67
125
|
// jsPsych 7 calls on_start unconditionally for every trial that lists the
|
|
68
126
|
// extension — must exist even as a no-op (see extension-cyborg-hunter.js).
|
|
69
127
|
on_start(_params) {}
|
|
@@ -105,7 +163,10 @@ class CyborgHunterReplayExtension {
|
|
|
105
163
|
} catch (e) {
|
|
106
164
|
console.warn('[cyborg-hunter-replay] could not pull CH session report:', e);
|
|
107
165
|
}
|
|
108
|
-
var result = await this.api.autoSaveNow({
|
|
166
|
+
var result = await this.api.autoSaveNow({
|
|
167
|
+
chSessionReport: chReport,
|
|
168
|
+
host: this._detectHost()
|
|
169
|
+
});
|
|
109
170
|
this._lastRecording = result.recording;
|
|
110
171
|
this.jsPsych.data.addProperties({ integrityReplayMeta: result.meta });
|
|
111
172
|
} catch (e) {
|
|
@@ -10,7 +10,7 @@ class CyborgHunterExtension {
|
|
|
10
10
|
// and package.json). Hand-bumped on each release; if you forget, the
|
|
11
11
|
// jsPsych developer console shows a stale number — the actual version
|
|
12
12
|
// attached to data is read from window.CyborgHunter.VERSION at runtime.
|
|
13
|
-
version: '0.
|
|
13
|
+
version: '0.8.0',
|
|
14
14
|
data: { integrity: { type: 'object' } }
|
|
15
15
|
};
|
|
16
16
|
|