cyborg-hunter 0.9.2 → 0.10.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.
@@ -0,0 +1,68 @@
1
+ // src/oneliner/segment-diff.js
2
+ // Turns the monitor's cumulative session report into per-segment deltas.
3
+ // The session arrays are append-only (every write in src/core is a .push), so
4
+ // "what happened since the last cut" is report[key].slice(lastSeen[key]).
5
+ // Keys are discovered at every cut, not fixed at build time, so a signal added
6
+ // to the core later flows through without touching this file.
7
+ //
8
+ // createSegmentDiffer(monitor) → { cut(meta) → segment, seen() → {[key]: number} }
9
+ // monitor: anything with getSessionReport() and getSessionScore()
10
+ // meta: { segmentIndex, source: 'manual'|'host'|'page'|'final', trialId,
11
+ // pageOrigin, trialReport?, gapReports? }
12
+ // segment: { segmentIndex, source, trialId, pageOrigin,
13
+ // deltas: { [arrayKey]: any[] }, // every array key except aliases
14
+ // counters: { pasteCount, copyCount, dropCount },
15
+ // score: getSessionScore() verbatim,
16
+ // gap?: [...], // non-empty gap reports only
17
+ // config?, libraryVersion? } // segmentIndex === 0 only
18
+ //
19
+ // The CLI side (src/cli/segment-reassembly.js) concatenates the deltas back
20
+ // into the session shape finalize() dumps.
21
+
22
+ // Keys that are the same array as another key inside the monitor (monitor.js
23
+ // keeps layoutShifts as a deprecated alias of viewportWidthShifts). Skipped on
24
+ // the way out so the entries are not shipped twice; restored on the way in
25
+ // (src/cli/segment-reassembly.js keeps a copy; a test pins the two equal).
26
+ export const ALIAS_KEYS = { layoutShifts: 'viewportWidthShifts' };
27
+
28
+ export function createSegmentDiffer(monitor) {
29
+ var lastSeen = {};
30
+ function arrayKeys(report) {
31
+ return Object.keys(report).filter(function (k) {
32
+ return Array.isArray(report[k]) && !(k in ALIAS_KEYS);
33
+ });
34
+ }
35
+ // A gap report covers the time between two host trials. Most are empty;
36
+ // only those with paste/copy/drop/synthetic-insertion evidence are kept,
37
+ // and only those four arrays plus the duration (no mouse trace).
38
+ function nonEmptyGap(r) {
39
+ var keys = ['pasteEvents', 'copyEvents', 'dropEvents', 'syntheticInsertions'];
40
+ var any = keys.some(function (k) { return Array.isArray(r[k]) && r[k].length > 0; });
41
+ if (!any) return null;
42
+ var out = { duration_ms: r.duration_ms };
43
+ keys.forEach(function (k) { out[k] = r[k] || []; });
44
+ return out;
45
+ }
46
+ return {
47
+ seen: function () { return Object.assign({}, lastSeen); },
48
+ cut: function (meta) {
49
+ var report = monitor.getSessionReport(); // one deep copy per cut (monitor.js getSessionReport); O(session size)
50
+ var deltas = {};
51
+ arrayKeys(report).forEach(function (k) {
52
+ var from = lastSeen[k] || 0;
53
+ deltas[k] = report[k].slice(from);
54
+ lastSeen[k] = report[k].length;
55
+ });
56
+ var seg = {
57
+ segmentIndex: meta.segmentIndex, source: meta.source, trialId: meta.trialId, pageOrigin: meta.pageOrigin,
58
+ deltas: deltas,
59
+ counters: { pasteCount: report.pasteCount, copyCount: report.copyCount, dropCount: report.dropCount },
60
+ score: monitor.getSessionScore()
61
+ };
62
+ var gaps = (meta.gapReports || []).map(nonEmptyGap).filter(Boolean);
63
+ if (gaps.length) seg.gap = gaps;
64
+ if (meta.segmentIndex === 0) { seg.config = report.config; seg.libraryVersion = report.libraryVersion; }
65
+ return seg;
66
+ }
67
+ };
68
+ }
@@ -0,0 +1,186 @@
1
+ // src/oneliner/segmenter.js
2
+ // Keeps the monitor permanently inside a trial. The core attaches its paste,
3
+ // typing, mouse, idle-gap and element-trace listeners only between startTrial
4
+ // and endTrial (monitor.js startTrial), so a paste outside a trial is never
5
+ // recorded. The one-line setup therefore opens a span at boot and, at every
6
+ // boundary, closes the current trial and opens the next one in the same call.
7
+ //
8
+ // createSegmenter({ monitor, differ, clock, sourceDefault }) → {
9
+ // start({ trialId }) open the boot span; no-op if already open
10
+ // → null | { error }
11
+ // rotate(opts) close the span (a "gap"), open the host's trial;
12
+ // no segment; the gap report is buffered for the
13
+ // next cut() and also returned
14
+ // → gapReport | null (nothing was open) | { error }
15
+ // cut({ source, nextTrialId?, nextOpts? }) close + segment + open next
16
+ // → { segment, trialReport } success
17
+ // → { segment, trialReport, error } cut OK, reopen failed
18
+ // → { error } nothing was cut
19
+ // finish({ source }) close + segment, nothing reopened
20
+ // → { segment, trialReport } | null (closed) | { error }
21
+ // abandon() close without a segment (manual-mode hand-over)
22
+ // → null | { error }
23
+ // state() { open, segmentIndex, currentTrialId }
24
+ // setSegmentIndex(n) multi-page restore
25
+ // }
26
+ // differ: createSegmentDiffer(monitor) (src/oneliner/segment-diff.js)
27
+ // clock: () => number, the page origin (performance.timeOrigin); injectable
28
+ //
29
+ // Contract for the host adapters: a result with `segment` must be saved even
30
+ // when it also carries `error` (the data is complete; only the next span failed
31
+ // to open, and the error becomes the cyborgHunterError marker). A result with
32
+ // `error` alone means nothing was cut.
33
+ //
34
+ // None of these throws into the host: a monitor or differ failure is logged
35
+ // and returned as { error: message }. After finish() or abandon() the segmenter
36
+ // is latched: start/rotate/cut return { error: 'finished' | 'abandoned' }
37
+ // without touching the monitor, and finish() returns null.
38
+ //
39
+ // Keeping `open` true to the monitor. The monitor has no state getter, but
40
+ // transition() is the FIRST statement of both startTrial and endTrial
41
+ // (monitor.js), and its rejection message names the current state. So:
42
+ // - any endTrial throw leaves the monitor outside a trial (rejected, or
43
+ // already transitioned) → open = false;
44
+ // - a startTrial throw that is not a lifecycle rejection means the
45
+ // transition happened → the trial IS open (listeners maybe partly
46
+ // attached) → open = true, so cut()/finish() still segment it rather than
47
+ // drop it, and endTrial's removeTrialListeners cleans up whatever attached;
48
+ // - a startTrial rejected "from 'trial'" means a trial we thought closed is
49
+ // still open → close it, keep its report as a gap, and retry once.
50
+
51
+ // Mirrors the message thrown by monitor.js transition(); null when `e` is not
52
+ // a lifecycle rejection.
53
+ var LIFECYCLE_FROM = /invalid lifecycle call: cannot transition from '(\w+)'/;
54
+ function lifecycleFrom(e) {
55
+ var m = LIFECYCLE_FROM.exec(String((e && e.message) || e));
56
+ return m ? m[1] : null;
57
+ }
58
+
59
+ export function createSegmenter(opts) {
60
+ var monitor = opts.monitor;
61
+ var differ = opts.differ;
62
+ var clock = opts.clock || function () { return performance.timeOrigin; };
63
+ var sourceDefault = opts.sourceDefault || 'host';
64
+
65
+ var open = false;
66
+ var latched = null; // 'finished' | 'abandoned': the segmenter no longer drives the monitor
67
+ var segmentIndex = 0;
68
+ var currentTrialId = null;
69
+ var gapReports = []; // gap trial reports since the last cut
70
+
71
+ function fail(what, e) {
72
+ var message = String((e && e.message) || e);
73
+ console.error('[cyborg-hunter] ' + what + ' failed: ' + message);
74
+ return { error: message };
75
+ }
76
+
77
+ // A span the host did not name is called after the segment it will become.
78
+ function spanId() { return 'span-' + segmentIndex; }
79
+
80
+ function closeTrial() {
81
+ try { return monitor.endTrial(); }
82
+ finally { open = false; }
83
+ }
84
+
85
+ // The explicit trialId always wins: it is assigned last, so a trialId inside
86
+ // `extra` (nextOpts, rotate's opts) can never rename the span.
87
+ function openTrial(trialId, extra) {
88
+ var args = Object.assign({}, extra || {}, { trialId: trialId });
89
+ try {
90
+ monitor.startTrial(args);
91
+ } catch (e) {
92
+ var from = lifecycleFrom(e);
93
+ if (from === 'trial') {
94
+ gapReports.push(closeTrial());
95
+ monitor.startTrial(args);
96
+ } else {
97
+ if (from === null) { currentTrialId = trialId; open = true; }
98
+ throw e;
99
+ }
100
+ }
101
+ currentTrialId = trialId;
102
+ open = true;
103
+ }
104
+
105
+ // Close the open trial and turn everything since the last cut into a segment.
106
+ function closeAndSegment(source) {
107
+ var report = closeTrial();
108
+ var segment = differ.cut({
109
+ segmentIndex: segmentIndex, source: source, trialId: currentTrialId,
110
+ pageOrigin: clock(), trialReport: report, gapReports: gapReports
111
+ });
112
+ gapReports = [];
113
+ segmentIndex += 1;
114
+ return { segment: segment, trialReport: report };
115
+ }
116
+
117
+ return {
118
+ start: function (o) {
119
+ if (latched) return { error: latched };
120
+ if (open) return null;
121
+ try {
122
+ openTrial((o && o.trialId) || spanId());
123
+ return null;
124
+ } catch (e) { return fail('start', e); }
125
+ },
126
+
127
+ rotate: function (o) {
128
+ if (latched) return { error: latched };
129
+ o = o || {};
130
+ var gap = null;
131
+ try {
132
+ if (open) {
133
+ gap = closeTrial();
134
+ gapReports.push(gap);
135
+ }
136
+ } catch (e) { return fail('trial rotation', e); }
137
+ var extra = Object.assign({}, o);
138
+ delete extra.trialId;
139
+ try {
140
+ openTrial(o.trialId || spanId(), extra);
141
+ } catch (e) { return fail('trial rotation', e); } // the gap, if any, stays buffered
142
+ return gap;
143
+ },
144
+
145
+ cut: function (o) {
146
+ if (latched) return { error: latched };
147
+ o = o || {};
148
+ var out;
149
+ try {
150
+ out = closeAndSegment(o.source || sourceDefault);
151
+ } catch (e) { return fail('segment write', e); }
152
+ try {
153
+ openTrial(o.nextTrialId || spanId(), o.nextOpts);
154
+ } catch (e) { out.error = fail('trial reopen', e).error; }
155
+ return out;
156
+ },
157
+
158
+ finish: function (o) {
159
+ if (latched) return null;
160
+ latched = 'finished';
161
+ if (!open) return null;
162
+ try {
163
+ return closeAndSegment((o && o.source) || 'final');
164
+ } catch (e) { return fail('segment write', e); }
165
+ },
166
+
167
+ // Hands the monitor to the host (manual mode). Gap reports still buffered
168
+ // are intentionally discarded: no cut() follows, and the abandoned spans
169
+ // carry no host trial to attach them to.
170
+ abandon: function () {
171
+ if (latched) return null;
172
+ latched = 'abandoned';
173
+ if (!open) return null;
174
+ try {
175
+ closeTrial();
176
+ return null;
177
+ } catch (e) { return fail('abandon', e); }
178
+ },
179
+
180
+ state: function () {
181
+ return { open: open, segmentIndex: segmentIndex, currentTrialId: currentTrialId };
182
+ },
183
+
184
+ setSegmentIndex: function (n) { segmentIndex = n; }
185
+ };
186
+ }
@@ -360,7 +360,11 @@ export function createRecorder(userConfig) {
360
360
  ? { w: de.clientWidth || 0, h: de.clientHeight || 0 } : null;
361
361
  session.userAgent = typeof navigator !== 'undefined' && navigator.userAgent
362
362
  ? String(navigator.userAgent) : '';
363
- if (config.autoSave.mode === 'none') {
363
+ // _ownerSavesRecording (internal): the embedding library takes the
364
+ // recording itself and tells its user how to save it (the one-line
365
+ // setup's CyborgHunter.replay(), src/oneliner/replay-loader.js), so
366
+ // this warning, which names getRecording(), would only mislead there.
367
+ if (config.autoSave.mode === 'none' && !config._ownerSavesRecording) {
364
368
  console.warn('[cyborg-hunter-replay] autoSave.mode is "none" — the recording will be lost unless you call getRecording() yourself.');
365
369
  }
366
370
  },
@@ -2,7 +2,7 @@
2
2
  // Default thresholds and preset configurations for Cyborg Hunter.
3
3
  // Single source of truth — used by both the browser library and CLI report tool.
4
4
 
5
- export const VERSION = "0.9.2";
5
+ export const VERSION = "0.10.0";
6
6
 
7
7
  // Default detection thresholds shared across presets.
8
8
  // Researchers can override any value at init() time.