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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/replay/node-registry.js
|
|
2
|
+
// Node-ID registry for the v2 recording format. Pure — no imports, no DOM
|
|
3
|
+
// APIs — so it runs identically in the browser, in node tests against
|
|
4
|
+
// fake-DOM fixtures, and (later) inside the shared engine.
|
|
5
|
+
//
|
|
6
|
+
// Spec §4: every node in a recording carries an integer id; ids are scoped to
|
|
7
|
+
// a KEYFRAME SPAN (a keyframe segment plus its continuation segments) and are
|
|
8
|
+
// assigned in first-seen pre-order, restarting at 1 at each keyframe. The
|
|
9
|
+
// snapshot walk numbers the tree; mid-span `dom.add` subtrees continue the
|
|
10
|
+
// same counter (canonical-core pins ids 6,7 after a 5-node keyframe).
|
|
11
|
+
//
|
|
12
|
+
// Identity, not content: a WeakMap keyed by the LIVE node object. Nothing is
|
|
13
|
+
// stamped into the DOM (the v1 nonce-marker attribute mechanism, which had to
|
|
14
|
+
// dodge its own MutationObserver echo, is retired by this module), and a
|
|
15
|
+
// dropped span's entries become garbage-collectable the moment resetSpan()
|
|
16
|
+
// releases the map.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a node-ID registry for one keyframe span.
|
|
20
|
+
*
|
|
21
|
+
* @returns {{
|
|
22
|
+
* idFor: (node: object) => number,
|
|
23
|
+
* assignTree: (root: object) => number,
|
|
24
|
+
* peekId: (node: object) => number | null,
|
|
25
|
+
* resetSpan: () => void,
|
|
26
|
+
* count: number
|
|
27
|
+
* }}
|
|
28
|
+
*/
|
|
29
|
+
export function createRegistry() {
|
|
30
|
+
let ids = new WeakMap();
|
|
31
|
+
let nextId = 1;
|
|
32
|
+
|
|
33
|
+
// Assign-if-absent for a SINGLE node (no descendant walk). In normal
|
|
34
|
+
// operation every node of the observed tree is numbered by assignTree
|
|
35
|
+
// first — snapshot walk at the keyframe, dom.add walks mid-span — so this
|
|
36
|
+
// is the fallback path for a node that WILL be serialized but has not been
|
|
37
|
+
// walked yet.
|
|
38
|
+
//
|
|
39
|
+
// NOT the way to resolve an event target or anchor: use peekId there. A
|
|
40
|
+
// target outside the observed root must resolve to null (design §2), and
|
|
41
|
+
// minting an id for it would put a number in the file that names nothing in
|
|
42
|
+
// the player's tree. dom.remove (Task 3) and anchors (Task 5) are peekId
|
|
43
|
+
// callers for the same reason.
|
|
44
|
+
//
|
|
45
|
+
// ids.set BEFORE the counter advances, so a call that throws (a non-object
|
|
46
|
+
// node) burns no id and leaves the span's numbering intact.
|
|
47
|
+
function idFor(node) {
|
|
48
|
+
const existing = ids.get(node);
|
|
49
|
+
if (existing !== undefined) return existing;
|
|
50
|
+
ids.set(node, nextId);
|
|
51
|
+
return nextId++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Pre-order walk: root, then each child's whole subtree, left to right.
|
|
55
|
+
// Numbers EVERY node the DomNode serialization can emit — elements, text,
|
|
56
|
+
// comments — because spec §4 gives all three an id.
|
|
57
|
+
//
|
|
58
|
+
// Boundary: the registry numbers whatever tree it is handed. Exclusion
|
|
59
|
+
// (data-record-exclude placeholders) and redaction filtering are the
|
|
60
|
+
// SERIALIZER's concern (Task 2); an excluded element still occupies its
|
|
61
|
+
// position and still needs an id, and the serializer decides what of the
|
|
62
|
+
// subtree reaches the file. Keeping the walk unconditional means ids stay
|
|
63
|
+
// stable regardless of what the serializer later chooses to emit.
|
|
64
|
+
//
|
|
65
|
+
// Precondition for the fixture's tidy 1..N numbering: the keyframe snapshot
|
|
66
|
+
// walk must be the FIRST allocation after each resetSpan(). Spec §4 only
|
|
67
|
+
// demands unique first-seen ids, and this registry stays correct either way
|
|
68
|
+
// (an idFor between reset and snapshot just shifts the tree to 2..N+1), but
|
|
69
|
+
// the canonical-core keyframe reads 1..5 only because nothing allocates
|
|
70
|
+
// ahead of its walk. Callers: reset, then snapshot, then everything else.
|
|
71
|
+
function assignTree(root) {
|
|
72
|
+
const rootId = idFor(root);
|
|
73
|
+
const children = root.childNodes;
|
|
74
|
+
if (children) {
|
|
75
|
+
for (let i = 0; i < children.length; i++) assignTree(children[i]);
|
|
76
|
+
}
|
|
77
|
+
return rootId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Read-only lookup: never allocates. Used wherever minting an id would be
|
|
81
|
+
// wrong rather than merely early — `dom.remove` targets, event anchors —
|
|
82
|
+
// because a fresh id there names a node the player was never sent.
|
|
83
|
+
//
|
|
84
|
+
// What it does NOT tell you: a non-null id does not mean the player has the
|
|
85
|
+
// node. assignTree numbers unconditionally, so descendants of a
|
|
86
|
+
// data-record-exclude placeholder, script/noscript elements the serializer
|
|
87
|
+
// skips, and anything numbered by an idFor fallback outside the observed
|
|
88
|
+
// root all peek non-null while appearing nowhere in the file. Strict
|
|
89
|
+
// validation only number-checks `node` fields, so a dangling reference
|
|
90
|
+
// survives validation and shows up as broken replay. Resolving a hit up to
|
|
91
|
+
// the nearest EMITTED ancestor (the placeholder, per spec §4) is the
|
|
92
|
+
// caller's job — this function answers "was it numbered", not "was it sent".
|
|
93
|
+
function peekId(node) {
|
|
94
|
+
const id = ids.get(node);
|
|
95
|
+
return id === undefined ? null : id;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Keyframe boundary: drop the whole span's assignments and restart at 1.
|
|
99
|
+
// A fresh WeakMap rather than per-entry deletion — the old map is simply
|
|
100
|
+
// released along with the span it described.
|
|
101
|
+
function resetSpan() {
|
|
102
|
+
ids = new WeakMap();
|
|
103
|
+
nextId = 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
idFor,
|
|
108
|
+
assignTree,
|
|
109
|
+
peekId,
|
|
110
|
+
resetSpan,
|
|
111
|
+
// Ids are handed out only by the counter, one per node, so the number
|
|
112
|
+
// assigned in this span is always nextId − 1 — no second tally to keep in
|
|
113
|
+
// sync. A getter (not a snapshot property) so callers read it live.
|
|
114
|
+
get count() { return nextId - 1; },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -10,14 +10,21 @@ var DATAPIPE_URL = 'https://pipe.jspsych.org/api/data/';
|
|
|
10
10
|
|
|
11
11
|
import { sanitizeId } from '../shared/constants.js';
|
|
12
12
|
|
|
13
|
+
// The CH vendor namespace of a v2 recording (spec §9), where `metadata.tier`
|
|
14
|
+
// and the `ch_extensions` diagnostics moved.
|
|
15
|
+
function chExt(recording) {
|
|
16
|
+
var ext = recording && recording.extensions;
|
|
17
|
+
return (ext && ext['cyborg-hunter']) || {};
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
|
-
* <pid>-replay-<
|
|
21
|
+
* <pid>-replay-<recordingStartEpochMs>.json — the epoch suffix makes reloads
|
|
15
22
|
* produce distinct artifacts instead of overwriting (CLI picks the latest
|
|
16
23
|
* and warns on multiples).
|
|
17
24
|
*/
|
|
18
25
|
export function replayFilename(recording) {
|
|
19
|
-
var pid = sanitizeId(recording.
|
|
20
|
-
var epoch = Date.parse(recording.
|
|
26
|
+
var pid = sanitizeId(recording.participant_id);
|
|
27
|
+
var epoch = Date.parse(recording.recording_started_at);
|
|
21
28
|
return pid + '-replay-' + epoch + '.json';
|
|
22
29
|
}
|
|
23
30
|
|
|
@@ -25,16 +32,22 @@ export function replayFilename(recording) {
|
|
|
25
32
|
* The small pointer that rides along in the jsPsych data (one
|
|
26
33
|
* addProperties call) so the analyst can tell from the CSV alone whether a
|
|
27
34
|
* replay exists, how big it was, and where it went.
|
|
35
|
+
*
|
|
36
|
+
* The OUTPUT shape is a stable contract with the CLI ingest and the HTML index
|
|
37
|
+
* (they read these six keys); only where the values are read FROM moved to v2.
|
|
28
38
|
*/
|
|
29
39
|
export function buildReplayMeta(recording, savedTo) {
|
|
40
|
+
var ext = chExt(recording);
|
|
30
41
|
return {
|
|
31
42
|
schema_version: recording.schema_version,
|
|
32
|
-
tier:
|
|
43
|
+
tier: ext.tier != null ? ext.tier : null,
|
|
33
44
|
saved_to: savedTo,
|
|
34
45
|
bytes_uncompressed: JSON.stringify(recording).length,
|
|
35
|
-
capture_failures: (
|
|
46
|
+
capture_failures: (ext.capture_failures || [])
|
|
36
47
|
.map(function (f) { return f.channel; }),
|
|
37
|
-
|
|
48
|
+
// Spec §5.7's standard flag, which mirrors the vendor one; reading the
|
|
49
|
+
// standard field keeps the pointer honest for any v2 producer.
|
|
50
|
+
capture_stopped: !!recording.truncated
|
|
38
51
|
};
|
|
39
52
|
}
|
|
40
53
|
|