@purespectrum1/purescore-client 0.7.0 → 1.0.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/README.md +135 -3
- package/lib/application/auto-init.d.ts +6 -3
- package/lib/application/auto-init.js +29 -20
- package/lib/application/device-identifier.d.ts +18 -1
- package/lib/application/device-identifier.js +43 -5
- package/lib/domain/errors/index.d.ts +1 -0
- package/lib/domain/errors/index.js +17 -0
- package/lib/domain/errors/transaction-rejected-error.d.ts +7 -0
- package/lib/domain/errors/transaction-rejected-error.js +14 -0
- package/lib/domain/models/crosscutting/device-intelligence.d.ts +3 -3
- package/lib/domain/models/crosscutting/device-intelligence.js +3 -3
- package/lib/domain/models/crosscutting/index.d.ts +1 -0
- package/lib/domain/models/crosscutting/index.js +1 -0
- package/lib/domain/models/crosscutting/verisoul.d.ts +19 -0
- package/lib/domain/models/crosscutting/verisoul.js +25 -0
- package/lib/domain/protocols/services/device-fingerprint/navigator.d.ts +1 -0
- package/lib/domain/protocols/services/device-identifier.d.ts +6 -6
- package/lib/domain/protocols/services/interaction-activity.d.ts +25 -0
- package/lib/domain/protocols/services/interaction-activity.js +2 -0
- package/lib/domain/protocols/services/single-script-injection/auto-init.d.ts +8 -8
- package/lib/domain/protocols/services/verisoul.d.ts +23 -0
- package/lib/domain/protocols/services/verisoul.js +2 -0
- package/lib/domain/services/device-fingerprint/index.d.ts +1 -0
- package/lib/domain/services/device-fingerprint/index.js +1 -0
- package/lib/domain/services/device-fingerprint/navigator.d.ts +2 -0
- package/lib/domain/services/device-fingerprint/navigator.js +7 -0
- package/lib/domain/services/device-fingerprint/webgl.d.ts +5 -0
- package/lib/domain/services/device-fingerprint/webgl.js +29 -0
- package/lib/domain/services/dom-mutation-tracker.d.ts +19 -0
- package/lib/domain/services/dom-mutation-tracker.js +47 -0
- package/lib/domain/services/interaction-activity.d.ts +43 -0
- package/lib/domain/services/interaction-activity.js +189 -0
- package/lib/domain/services/single-script-injection/checkpoint-scheduler.d.ts +1 -1
- package/lib/domain/services/single-script-injection/checkpoint-scheduler.js +3 -3
- package/lib/domain/services/single-script-injection/response-delivery.d.ts +3 -3
- package/lib/domain/services/single-script-injection/response-delivery.js +3 -3
- package/lib/domain/services/single-script-injection/schedule-store.d.ts +17 -7
- package/lib/domain/services/single-script-injection/schedule-store.js +11 -10
- package/lib/domain/services/snapshot-store.d.ts +1 -0
- package/lib/domain/services/snapshot-store.js +3 -0
- package/lib/domain/services/translation-detector.d.ts +20 -0
- package/lib/domain/services/translation-detector.js +119 -0
- package/lib/domain/services/verisoul.d.ts +27 -0
- package/lib/domain/services/verisoul.js +96 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -2
- package/package.json +1 -1
|
@@ -19,7 +19,7 @@ export interface DeviceIdentifierOptions {
|
|
|
19
19
|
canvas?: boolean;
|
|
20
20
|
/**
|
|
21
21
|
* Set by the `data-auto-init="true"` bootstrap. Sends
|
|
22
|
-
* `single_script_injection: true` on both
|
|
22
|
+
* `single_script_injection: true` on both PureScore-Intelligence calls.
|
|
23
23
|
*/
|
|
24
24
|
singleScriptInjection?: boolean;
|
|
25
25
|
}
|
|
@@ -28,7 +28,7 @@ export interface DeviceIdentifierConfig {
|
|
|
28
28
|
env?: 'staging' | 'production';
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Flushes the collected signals for a transaction to the
|
|
31
|
+
* Flushes the collected signals for a transaction to the PureScore-Intelligence
|
|
32
32
|
* service and resolves with its response.
|
|
33
33
|
*/
|
|
34
34
|
export interface TrackDetails {
|
|
@@ -38,13 +38,13 @@ export interface TrackDetails {
|
|
|
38
38
|
* What a `trackDetails` call knows about the transaction beyond its id.
|
|
39
39
|
*/
|
|
40
40
|
export interface TrackDetailsContext {
|
|
41
|
-
|
|
41
|
+
projectId?: string;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Survey details sent once to `POST /v1/transaction` at transaction start.
|
|
45
45
|
*/
|
|
46
46
|
export interface TransactionDetails {
|
|
47
|
-
|
|
47
|
+
projectId: string;
|
|
48
48
|
expectedSurveyLOI: number;
|
|
49
49
|
surveyCountry: string;
|
|
50
50
|
surveyLanguage: string;
|
|
@@ -58,10 +58,10 @@ export interface TransactionDetails {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* Registers a transaction with the
|
|
61
|
+
* Registers a transaction with the PureScore-Intelligence service.
|
|
62
62
|
*/
|
|
63
63
|
export interface CreateTransaction {
|
|
64
|
-
createTransaction: (details: TransactionDetails, config?: DeviceIdentifierConfig) => Promise<string | undefined>;
|
|
64
|
+
createTransaction: (details: TransactionDetails, config?: DeviceIdentifierConfig, metadata?: DeviceIdentifierMetadata) => Promise<string | undefined>;
|
|
65
65
|
}
|
|
66
66
|
export interface DeviceIdentifierMetadata {
|
|
67
67
|
[key: string]: any;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type ClipboardActivityType = 'copy' | 'cut' | 'paste';
|
|
2
|
+
export interface ClipboardActivity {
|
|
3
|
+
type: ClipboardActivityType;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
targetTag: string | null;
|
|
6
|
+
length?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TabSwitch {
|
|
9
|
+
leftAt: number;
|
|
10
|
+
returnedAt: number | null;
|
|
11
|
+
}
|
|
12
|
+
export interface InteractionActivity {
|
|
13
|
+
copyCount: number;
|
|
14
|
+
cutCount: number;
|
|
15
|
+
pasteCount: number;
|
|
16
|
+
tabSwitchCount: number;
|
|
17
|
+
timeAwayMs: number;
|
|
18
|
+
clipboardEvents: ClipboardActivity[];
|
|
19
|
+
tabSwitches: TabSwitch[];
|
|
20
|
+
trackingStartedAt: number | null;
|
|
21
|
+
totalDurationMs: number;
|
|
22
|
+
translationDetected: boolean;
|
|
23
|
+
translationDetectedAt: number | null;
|
|
24
|
+
translationPhrase: string | null;
|
|
25
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DeviceIdentifierMetadata } from '../device-identifier';
|
|
2
2
|
/**
|
|
3
|
-
* A single scheduled
|
|
3
|
+
* A single scheduled PureScore-Intelligence call.
|
|
4
4
|
*/
|
|
5
5
|
export interface ScheduleCheckpoint {
|
|
6
6
|
/**
|
|
@@ -14,9 +14,9 @@ export interface ScheduleCheckpoint {
|
|
|
14
14
|
dueAt: number;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* The `ps_di_schedule_<
|
|
17
|
+
* The `ps_di_schedule_<projectId>_<supplierId>_<supplierMemberId|emailHash>` record.
|
|
18
18
|
*
|
|
19
|
-
* It is keyed by survey
|
|
19
|
+
* It is keyed by survey and respondent and handed to a storage adapter rather than
|
|
20
20
|
* held in page memory, so that a page navigation resumes the schedule instead of
|
|
21
21
|
* restarting it: `startedAt` is written once, by the very first page load of the
|
|
22
22
|
* transaction.
|
|
@@ -25,7 +25,7 @@ export interface ScheduleRecord {
|
|
|
25
25
|
/**
|
|
26
26
|
* The survey the schedule belongs to.
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
projectId: string;
|
|
29
29
|
/**
|
|
30
30
|
* Length of the interview, in minutes, from `data-expected-survey-loi`. Null
|
|
31
31
|
* only between creating the record and applying the LOI to it.
|
|
@@ -50,7 +50,7 @@ export interface ScheduleRecord {
|
|
|
50
50
|
*/
|
|
51
51
|
expiry: number;
|
|
52
52
|
/**
|
|
53
|
-
* Replay buffer: the most recent
|
|
53
|
+
* Replay buffer: the most recent PureScore-Intelligence response, so a page that
|
|
54
54
|
* registers a listener after the checkpoint fired still receives the verdict.
|
|
55
55
|
*/
|
|
56
56
|
lastResponse?: Record<string, any> | null;
|
|
@@ -64,7 +64,7 @@ export interface ScheduleRecord {
|
|
|
64
64
|
*/
|
|
65
65
|
export interface AutoInitConfig {
|
|
66
66
|
/**
|
|
67
|
-
* `data-token` —
|
|
67
|
+
* `data-token` — PureScore-Intelligence bearer token.
|
|
68
68
|
*/
|
|
69
69
|
token: string;
|
|
70
70
|
/**
|
|
@@ -80,9 +80,9 @@ export interface AutoInitConfig {
|
|
|
80
80
|
*/
|
|
81
81
|
callbackName?: string;
|
|
82
82
|
/**
|
|
83
|
-
* `data-
|
|
83
|
+
* `data-project-id`.
|
|
84
84
|
*/
|
|
85
|
-
|
|
85
|
+
projectId: string;
|
|
86
86
|
/**
|
|
87
87
|
* `data-expected-survey-loi` — length of the interview, in minutes. Drives the
|
|
88
88
|
* checkpoint schedule.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The global the Verisoul SDK installs once its bundle has loaded.
|
|
3
|
+
*/
|
|
4
|
+
export interface VerisoulSdk {
|
|
5
|
+
session: () => Promise<{
|
|
6
|
+
session_id: string;
|
|
7
|
+
}>;
|
|
8
|
+
reinitialize: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
Verisoul?: VerisoulSdk;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Supplies the respondent's Verisoul session id. Never throws: a missing or
|
|
17
|
+
* blocked SDK resolves with undefined so it cannot hold up the
|
|
18
|
+
* PureScore-Intelligence call it is attached to.
|
|
19
|
+
*/
|
|
20
|
+
export interface VerisoulSessionProvider {
|
|
21
|
+
load: () => void;
|
|
22
|
+
getSessionId: () => Promise<string | undefined>;
|
|
23
|
+
}
|
|
@@ -107,4 +107,6 @@ export declare class Navigatior {
|
|
|
107
107
|
* @returns {number} The RTT in milliseconds, or -1 if the Network Information API is not supported.
|
|
108
108
|
*/
|
|
109
109
|
getEstimatedRoundTripTime(): number;
|
|
110
|
+
getCpuCores(): number | null;
|
|
111
|
+
getDeviceMemory(): number | null;
|
|
110
112
|
}
|
|
@@ -209,5 +209,12 @@ class Navigatior {
|
|
|
209
209
|
}
|
|
210
210
|
return -1;
|
|
211
211
|
}
|
|
212
|
+
getCpuCores() {
|
|
213
|
+
return navigator.hardwareConcurrency || null;
|
|
214
|
+
}
|
|
215
|
+
getDeviceMemory() {
|
|
216
|
+
var _a;
|
|
217
|
+
return (_a = navigator.deviceMemory) !== null && _a !== void 0 ? _a : null;
|
|
218
|
+
}
|
|
212
219
|
}
|
|
213
220
|
exports.Navigatior = Navigatior;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebGL = void 0;
|
|
4
|
+
class WebGL {
|
|
5
|
+
getGpuRenderer() {
|
|
6
|
+
if (this.gpuRenderer === undefined) {
|
|
7
|
+
this.gpuRenderer = this._readGpuRenderer();
|
|
8
|
+
}
|
|
9
|
+
return this.gpuRenderer;
|
|
10
|
+
}
|
|
11
|
+
_readGpuRenderer() {
|
|
12
|
+
var _a;
|
|
13
|
+
try {
|
|
14
|
+
const canvas = document.createElement('canvas');
|
|
15
|
+
const gl = (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
|
|
16
|
+
if (!gl) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
20
|
+
const renderer = gl.getParameter(debugInfo ? debugInfo.UNMASKED_RENDERER_WEBGL : gl.RENDERER);
|
|
21
|
+
(_a = gl.getExtension('WEBGL_lose_context')) === null || _a === void 0 ? void 0 : _a.loseContext();
|
|
22
|
+
return typeof renderer === 'string' && renderer ? renderer : null;
|
|
23
|
+
}
|
|
24
|
+
catch (_b) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.WebGL = WebGL;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GetStorage, SetStorage } from '../protocols/cache';
|
|
2
|
+
/**
|
|
3
|
+
* Counts `style` attribute mutations on the page's elements, a signal of AI
|
|
4
|
+
* agents driving the browser.
|
|
5
|
+
*
|
|
6
|
+
* The count is never reset: it is mirrored into storage on every change and
|
|
7
|
+
* hydrated on start, so it keeps accumulating across page navigations for the
|
|
8
|
+
* whole session instead of restarting on each page.
|
|
9
|
+
*/
|
|
10
|
+
export declare class DomMutationTracker {
|
|
11
|
+
private readonly storage;
|
|
12
|
+
private observer;
|
|
13
|
+
private count;
|
|
14
|
+
constructor(storage: GetStorage & SetStorage);
|
|
15
|
+
get domElementsMutationCount(): number;
|
|
16
|
+
start(): void;
|
|
17
|
+
stop(): void;
|
|
18
|
+
private onMutations;
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DomMutationTracker = void 0;
|
|
4
|
+
const DOM_MUTATION_PERSIST_KEY = 'ps_deviceIdentifier_domElementsMutationCount';
|
|
5
|
+
/**
|
|
6
|
+
* Counts `style` attribute mutations on the page's elements, a signal of AI
|
|
7
|
+
* agents driving the browser.
|
|
8
|
+
*
|
|
9
|
+
* The count is never reset: it is mirrored into storage on every change and
|
|
10
|
+
* hydrated on start, so it keeps accumulating across page navigations for the
|
|
11
|
+
* whole session instead of restarting on each page.
|
|
12
|
+
*/
|
|
13
|
+
class DomMutationTracker {
|
|
14
|
+
constructor(storage) {
|
|
15
|
+
this.storage = storage;
|
|
16
|
+
this.observer = null;
|
|
17
|
+
this.count = 0;
|
|
18
|
+
}
|
|
19
|
+
get domElementsMutationCount() {
|
|
20
|
+
return this.count;
|
|
21
|
+
}
|
|
22
|
+
start() {
|
|
23
|
+
if (this.observer || typeof document === 'undefined' || typeof MutationObserver === 'undefined')
|
|
24
|
+
return;
|
|
25
|
+
const stored = this.storage.get(DOM_MUTATION_PERSIST_KEY);
|
|
26
|
+
this.count = Math.max(this.count, Number(stored === null || stored === void 0 ? void 0 : stored.count) || 0);
|
|
27
|
+
this.observer = new MutationObserver(mutations => this.onMutations(mutations));
|
|
28
|
+
this.observer.observe(document.documentElement, {
|
|
29
|
+
attributes: true,
|
|
30
|
+
attributeFilter: ['style'],
|
|
31
|
+
subtree: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
stop() {
|
|
35
|
+
var _a;
|
|
36
|
+
(_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
37
|
+
this.observer = null;
|
|
38
|
+
}
|
|
39
|
+
onMutations(mutations) {
|
|
40
|
+
const counted = mutations.filter(mutation => mutation.type === 'attributes' && mutation.target instanceof HTMLElement).length;
|
|
41
|
+
if (!counted)
|
|
42
|
+
return;
|
|
43
|
+
this.count += counted;
|
|
44
|
+
this.storage.set(DOM_MUTATION_PERSIST_KEY, { count: this.count });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.DomMutationTracker = DomMutationTracker;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { GetStorage, RemoveStorage, SetStorage } from '../protocols/cache';
|
|
2
|
+
import { InteractionActivity } from '../protocols/services/interaction-activity';
|
|
3
|
+
export declare const INTERACTION_ACTIVITY_MAX_ENTRIES = 50;
|
|
4
|
+
export declare class InteractionActivityTracker {
|
|
5
|
+
private copyCount;
|
|
6
|
+
private cutCount;
|
|
7
|
+
private pasteCount;
|
|
8
|
+
private tabSwitchCount;
|
|
9
|
+
private timeAwayMs;
|
|
10
|
+
private clipboardEvents;
|
|
11
|
+
private tabSwitches;
|
|
12
|
+
private trackingStartedAt;
|
|
13
|
+
private translationDetected;
|
|
14
|
+
private translationDetectedAt;
|
|
15
|
+
private translationPhrase;
|
|
16
|
+
private isTracking;
|
|
17
|
+
private isUnloading;
|
|
18
|
+
private readonly clipboardHandler;
|
|
19
|
+
private readonly visibilityHandler;
|
|
20
|
+
private readonly blurHandler;
|
|
21
|
+
private readonly focusHandler;
|
|
22
|
+
private readonly pageHideHandler;
|
|
23
|
+
private readonly pageShowHandler;
|
|
24
|
+
private readonly snapshots;
|
|
25
|
+
private readonly translation;
|
|
26
|
+
constructor(storage: GetStorage & SetStorage & RemoveStorage);
|
|
27
|
+
startTracking(): void;
|
|
28
|
+
stopTracking(): void;
|
|
29
|
+
hasActiveListeners(): boolean;
|
|
30
|
+
getState(): InteractionActivity;
|
|
31
|
+
restoreState(state: Partial<InteractionActivity>): void;
|
|
32
|
+
getInteractionActivity(): InteractionActivity;
|
|
33
|
+
private handleClipboard;
|
|
34
|
+
private pastedLength;
|
|
35
|
+
private handleTranslation;
|
|
36
|
+
private handleTranslationSample;
|
|
37
|
+
private handleVisibilityChange;
|
|
38
|
+
private handleLeave;
|
|
39
|
+
private handleReturn;
|
|
40
|
+
private isAway;
|
|
41
|
+
private openTabSwitch;
|
|
42
|
+
private capped;
|
|
43
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InteractionActivityTracker = exports.INTERACTION_ACTIVITY_MAX_ENTRIES = void 0;
|
|
4
|
+
const snapshot_store_1 = require("./snapshot-store");
|
|
5
|
+
const translation_detector_1 = require("./translation-detector");
|
|
6
|
+
exports.INTERACTION_ACTIVITY_MAX_ENTRIES = 50;
|
|
7
|
+
const INTERACTION_ACTIVITY_PERSIST_KEY = 'ps_deviceIdentifier_interactionActivity';
|
|
8
|
+
const CLIPBOARD_EVENTS = ['copy', 'cut', 'paste'];
|
|
9
|
+
class InteractionActivityTracker {
|
|
10
|
+
constructor(storage) {
|
|
11
|
+
this.copyCount = 0;
|
|
12
|
+
this.cutCount = 0;
|
|
13
|
+
this.pasteCount = 0;
|
|
14
|
+
this.tabSwitchCount = 0;
|
|
15
|
+
this.timeAwayMs = 0;
|
|
16
|
+
this.clipboardEvents = [];
|
|
17
|
+
this.tabSwitches = [];
|
|
18
|
+
this.trackingStartedAt = null;
|
|
19
|
+
this.translationDetected = false;
|
|
20
|
+
this.translationDetectedAt = null;
|
|
21
|
+
this.translationPhrase = null;
|
|
22
|
+
this.isTracking = false;
|
|
23
|
+
this.isUnloading = false;
|
|
24
|
+
this.translation = new translation_detector_1.TranslationDetector();
|
|
25
|
+
this.clipboardHandler = this.handleClipboard.bind(this);
|
|
26
|
+
this.visibilityHandler = this.handleVisibilityChange.bind(this);
|
|
27
|
+
this.blurHandler = this.handleLeave.bind(this);
|
|
28
|
+
this.focusHandler = this.handleReturn.bind(this);
|
|
29
|
+
this.pageHideHandler = () => { this.isUnloading = true; };
|
|
30
|
+
this.pageShowHandler = () => { this.isUnloading = false; };
|
|
31
|
+
this.snapshots = new snapshot_store_1.SnapshotStore(INTERACTION_ACTIVITY_PERSIST_KEY, () => this.getState(), storage);
|
|
32
|
+
}
|
|
33
|
+
startTracking() {
|
|
34
|
+
var _a;
|
|
35
|
+
if (this.isTracking || typeof document === 'undefined' || typeof window === 'undefined')
|
|
36
|
+
return;
|
|
37
|
+
const stored = this.snapshots.read();
|
|
38
|
+
if (stored)
|
|
39
|
+
this.restoreState(stored);
|
|
40
|
+
this.isTracking = true;
|
|
41
|
+
this.isUnloading = false;
|
|
42
|
+
(_a = this.trackingStartedAt) !== null && _a !== void 0 ? _a : (this.trackingStartedAt = Date.now());
|
|
43
|
+
for (const type of CLIPBOARD_EVENTS) {
|
|
44
|
+
document.addEventListener(type, this.clipboardHandler);
|
|
45
|
+
}
|
|
46
|
+
document.addEventListener('visibilitychange', this.visibilityHandler);
|
|
47
|
+
window.addEventListener('blur', this.blurHandler);
|
|
48
|
+
window.addEventListener('focus', this.focusHandler);
|
|
49
|
+
window.addEventListener('pagehide', this.pageHideHandler);
|
|
50
|
+
window.addEventListener('pageshow', this.pageShowHandler);
|
|
51
|
+
if (!this.translationDetected)
|
|
52
|
+
this.translation.start({
|
|
53
|
+
onDetected: () => this.handleTranslation(),
|
|
54
|
+
onSample: sample => this.handleTranslationSample(sample),
|
|
55
|
+
});
|
|
56
|
+
if (this.isAway() && document.visibilityState === 'visible' && document.hasFocus()) {
|
|
57
|
+
this.handleReturn();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
stopTracking() {
|
|
61
|
+
if (!this.isTracking)
|
|
62
|
+
return;
|
|
63
|
+
for (const type of CLIPBOARD_EVENTS) {
|
|
64
|
+
document.removeEventListener(type, this.clipboardHandler);
|
|
65
|
+
}
|
|
66
|
+
document.removeEventListener('visibilitychange', this.visibilityHandler);
|
|
67
|
+
window.removeEventListener('blur', this.blurHandler);
|
|
68
|
+
window.removeEventListener('focus', this.focusHandler);
|
|
69
|
+
window.removeEventListener('pagehide', this.pageHideHandler);
|
|
70
|
+
window.removeEventListener('pageshow', this.pageShowHandler);
|
|
71
|
+
this.translation.stop();
|
|
72
|
+
this.isTracking = false;
|
|
73
|
+
}
|
|
74
|
+
hasActiveListeners() {
|
|
75
|
+
return this.isTracking;
|
|
76
|
+
}
|
|
77
|
+
getState() {
|
|
78
|
+
return this.getInteractionActivity();
|
|
79
|
+
}
|
|
80
|
+
restoreState(state) {
|
|
81
|
+
if (typeof state.copyCount === 'number')
|
|
82
|
+
this.copyCount = state.copyCount;
|
|
83
|
+
if (typeof state.cutCount === 'number')
|
|
84
|
+
this.cutCount = state.cutCount;
|
|
85
|
+
if (typeof state.pasteCount === 'number')
|
|
86
|
+
this.pasteCount = state.pasteCount;
|
|
87
|
+
if (typeof state.tabSwitchCount === 'number')
|
|
88
|
+
this.tabSwitchCount = state.tabSwitchCount;
|
|
89
|
+
if (typeof state.timeAwayMs === 'number')
|
|
90
|
+
this.timeAwayMs = state.timeAwayMs;
|
|
91
|
+
if (Array.isArray(state.clipboardEvents))
|
|
92
|
+
this.clipboardEvents = state.clipboardEvents.slice(-exports.INTERACTION_ACTIVITY_MAX_ENTRIES);
|
|
93
|
+
if (Array.isArray(state.tabSwitches))
|
|
94
|
+
this.tabSwitches = state.tabSwitches.slice(-exports.INTERACTION_ACTIVITY_MAX_ENTRIES);
|
|
95
|
+
if (typeof state.trackingStartedAt === 'number')
|
|
96
|
+
this.trackingStartedAt = state.trackingStartedAt;
|
|
97
|
+
if (typeof state.translationDetected === 'boolean')
|
|
98
|
+
this.translationDetected = state.translationDetected;
|
|
99
|
+
if (typeof state.translationDetectedAt === 'number')
|
|
100
|
+
this.translationDetectedAt = state.translationDetectedAt;
|
|
101
|
+
if (typeof state.translationPhrase === 'string')
|
|
102
|
+
this.translationPhrase = state.translationPhrase;
|
|
103
|
+
}
|
|
104
|
+
getInteractionActivity() {
|
|
105
|
+
return {
|
|
106
|
+
copyCount: this.copyCount,
|
|
107
|
+
cutCount: this.cutCount,
|
|
108
|
+
pasteCount: this.pasteCount,
|
|
109
|
+
tabSwitchCount: this.tabSwitchCount,
|
|
110
|
+
timeAwayMs: this.timeAwayMs,
|
|
111
|
+
clipboardEvents: this.clipboardEvents.map(event => (Object.assign({}, event))),
|
|
112
|
+
tabSwitches: this.tabSwitches.map(tabSwitch => (Object.assign({}, tabSwitch))),
|
|
113
|
+
trackingStartedAt: this.trackingStartedAt,
|
|
114
|
+
totalDurationMs: this.trackingStartedAt === null ? 0 : Math.max(0, Date.now() - this.trackingStartedAt),
|
|
115
|
+
translationDetected: this.translationDetected,
|
|
116
|
+
translationDetectedAt: this.translationDetectedAt,
|
|
117
|
+
translationPhrase: this.translationPhrase,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
handleClipboard(event) {
|
|
121
|
+
const type = event.type;
|
|
122
|
+
const entry = {
|
|
123
|
+
type,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
targetTag: event.target instanceof Element ? event.target.tagName.toLowerCase() : null,
|
|
126
|
+
};
|
|
127
|
+
if (type === 'copy')
|
|
128
|
+
this.copyCount++;
|
|
129
|
+
if (type === 'cut')
|
|
130
|
+
this.cutCount++;
|
|
131
|
+
if (type === 'paste') {
|
|
132
|
+
this.pasteCount++;
|
|
133
|
+
entry.length = this.pastedLength(event);
|
|
134
|
+
}
|
|
135
|
+
this.clipboardEvents = this.capped([...this.clipboardEvents, entry]);
|
|
136
|
+
this.snapshots.save();
|
|
137
|
+
}
|
|
138
|
+
pastedLength(event) {
|
|
139
|
+
const data = event.clipboardData;
|
|
140
|
+
return ((data === null || data === void 0 ? void 0 : data.getData('text/plain')) || (data === null || data === void 0 ? void 0 : data.getData('text')) || '').length;
|
|
141
|
+
}
|
|
142
|
+
handleTranslation() {
|
|
143
|
+
if (this.translationDetected)
|
|
144
|
+
return;
|
|
145
|
+
this.translationDetected = true;
|
|
146
|
+
this.translationDetectedAt = Date.now();
|
|
147
|
+
this.snapshots.save();
|
|
148
|
+
}
|
|
149
|
+
handleTranslationSample(sample) {
|
|
150
|
+
this.translationPhrase = sample;
|
|
151
|
+
this.snapshots.save();
|
|
152
|
+
}
|
|
153
|
+
handleVisibilityChange() {
|
|
154
|
+
if (document.visibilityState === 'hidden') {
|
|
155
|
+
this.handleLeave();
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.handleReturn();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
handleLeave() {
|
|
162
|
+
// A navigation within the tab hides the outgoing page after `pagehide`.
|
|
163
|
+
if (this.isUnloading || this.isAway())
|
|
164
|
+
return;
|
|
165
|
+
this.tabSwitchCount++;
|
|
166
|
+
this.tabSwitches = this.capped([...this.tabSwitches, { leftAt: Date.now(), returnedAt: null }]);
|
|
167
|
+
this.snapshots.save();
|
|
168
|
+
}
|
|
169
|
+
handleReturn() {
|
|
170
|
+
const open = this.openTabSwitch();
|
|
171
|
+
if (!open)
|
|
172
|
+
return;
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
open.returnedAt = now;
|
|
175
|
+
this.timeAwayMs += Math.max(0, now - open.leftAt);
|
|
176
|
+
this.snapshots.save();
|
|
177
|
+
}
|
|
178
|
+
isAway() {
|
|
179
|
+
return this.openTabSwitch() !== undefined;
|
|
180
|
+
}
|
|
181
|
+
openTabSwitch() {
|
|
182
|
+
const last = this.tabSwitches[this.tabSwitches.length - 1];
|
|
183
|
+
return (last === null || last === void 0 ? void 0 : last.returnedAt) === null ? last : undefined;
|
|
184
|
+
}
|
|
185
|
+
capped(entries) {
|
|
186
|
+
return entries.slice(-exports.INTERACTION_ACTIVITY_MAX_ENTRIES);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
exports.InteractionActivityTracker = InteractionActivityTracker;
|
|
@@ -10,7 +10,7 @@ import { ScheduleStore } from './schedule-store';
|
|
|
10
10
|
export declare const TICK_MS = 5000;
|
|
11
11
|
type CheckpointHandler = (n: number, record: ScheduleRecord) => Promise<void>;
|
|
12
12
|
/**
|
|
13
|
-
* Drives the
|
|
13
|
+
* Drives the PureScore-Intelligence calls off the schedule record.
|
|
14
14
|
*
|
|
15
15
|
* Every tick it resolves the next outstanding checkpoint and fires it if its due
|
|
16
16
|
* time has passed; otherwise it keeps waiting. Once every checkpoint of the
|
|
@@ -19,7 +19,7 @@ exports.CheckpointScheduler = exports.TICK_MS = void 0;
|
|
|
19
19
|
*/
|
|
20
20
|
exports.TICK_MS = 5000;
|
|
21
21
|
/**
|
|
22
|
-
* Drives the
|
|
22
|
+
* Drives the PureScore-Intelligence calls off the schedule record.
|
|
23
23
|
*
|
|
24
24
|
* Every tick it resolves the next outstanding checkpoint and fires it if its due
|
|
25
25
|
* time has passed; otherwise it keeps waiting. Once every checkpoint of the
|
|
@@ -55,7 +55,7 @@ class CheckpointScheduler {
|
|
|
55
55
|
}
|
|
56
56
|
/** Fires a tick without letting its promise escape as an unhandled rejection. */
|
|
57
57
|
runTick() {
|
|
58
|
-
this.tick().catch(error => console.error('Error in
|
|
58
|
+
this.tick().catch(error => console.error('Error in PureScore-Intelligence checkpoint tick:', error));
|
|
59
59
|
}
|
|
60
60
|
tick() {
|
|
61
61
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -83,7 +83,7 @@ class CheckpointScheduler {
|
|
|
83
83
|
catch (error) {
|
|
84
84
|
// Swallowed on purpose: a rejected checkpoint must not surface as an
|
|
85
85
|
// unhandled rejection on the buyer's survey page.
|
|
86
|
-
console.error(`Error delivering
|
|
86
|
+
console.error(`Error delivering PureScore-Intelligence checkpoint ${next.n}:`, error);
|
|
87
87
|
}
|
|
88
88
|
finally {
|
|
89
89
|
this.inFlight = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Name of the custom event carrying the
|
|
2
|
+
* Name of the custom event carrying the PureScore-Intelligence response.
|
|
3
3
|
*
|
|
4
4
|
* The `ps:` prefix namespaces it so it cannot collide with an event the buyer's
|
|
5
5
|
* own survey platform dispatches.
|
|
@@ -11,9 +11,9 @@ export declare const PS_RESULT_EVENT = "ps:purescore:result";
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const PS_GLOBAL = "psPureScore";
|
|
13
13
|
/**
|
|
14
|
-
* Hands the
|
|
14
|
+
* Hands the PureScore-Intelligence response back to the buyer.
|
|
15
15
|
*
|
|
16
|
-
* Once the
|
|
16
|
+
* Once the Purescore-Client owns the call it also owns the response, so it has
|
|
17
17
|
* to push it out. Three channels carry the same object and the buyer picks
|
|
18
18
|
* whichever suits their platform:
|
|
19
19
|
*
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ResponseDeliveryService = exports.PS_GLOBAL = exports.PS_RESULT_EVENT = void 0;
|
|
4
4
|
const validation_1 = require("../../../validation");
|
|
5
5
|
/**
|
|
6
|
-
* Name of the custom event carrying the
|
|
6
|
+
* Name of the custom event carrying the PureScore-Intelligence response.
|
|
7
7
|
*
|
|
8
8
|
* The `ps:` prefix namespaces it so it cannot collide with an event the buyer's
|
|
9
9
|
* own survey platform dispatches.
|
|
@@ -15,9 +15,9 @@ exports.PS_RESULT_EVENT = 'ps:purescore:result';
|
|
|
15
15
|
*/
|
|
16
16
|
exports.PS_GLOBAL = 'psPureScore';
|
|
17
17
|
/**
|
|
18
|
-
* Hands the
|
|
18
|
+
* Hands the PureScore-Intelligence response back to the buyer.
|
|
19
19
|
*
|
|
20
|
-
* Once the
|
|
20
|
+
* Once the Purescore-Client owns the call it also owns the response, so it has
|
|
21
21
|
* to push it out. Three channels carry the same object and the buyer picks
|
|
22
22
|
* whichever suits their platform:
|
|
23
23
|
*
|
|
@@ -12,15 +12,25 @@ export declare const LOI_CHECKPOINT_FRACTIONS: readonly [number, number, 1, 2];
|
|
|
12
12
|
/** Most calls a transaction can make: the initial one plus each LOI fraction. */
|
|
13
13
|
export declare const CHECKPOINT_COUNT: number;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
* Who a schedule belongs to: the survey plus whatever identifies the respondent.
|
|
16
|
+
*/
|
|
17
|
+
export interface ScheduleIdentity {
|
|
18
|
+
projectId: string;
|
|
19
|
+
supplierId?: string;
|
|
20
|
+
supplierMemberId?: string;
|
|
21
|
+
emailHash?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Owns the `ps_di_schedule_<projectId>_<supplierId>_<supplierMemberId|emailHash>`
|
|
25
|
+
* record: which checkpoints are still outstanding on a later page load. Parts
|
|
26
|
+
* that were not provided are left out of the key. Storage is injected, so this
|
|
27
|
+
* holds only the schedule rules, not where the record lives.
|
|
18
28
|
*/
|
|
19
29
|
export declare class ScheduleStore {
|
|
20
|
-
private readonly surveyId;
|
|
21
30
|
private readonly storage;
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
private readonly projectId;
|
|
32
|
+
readonly key: string;
|
|
33
|
+
constructor({ projectId, supplierId, supplierMemberId, emailHash }: ScheduleIdentity, storage: GetStorage & SetStorage & RemoveStorage);
|
|
24
34
|
/**
|
|
25
35
|
* Checkpoint 1 fires `INITIAL_CHECKPOINT_DELAY_MS` in regardless; the rest are
|
|
26
36
|
* LOI multiples, so an unknown LOI yields a one-checkpoint schedule rather
|
|
@@ -57,7 +67,7 @@ export declare class ScheduleStore {
|
|
|
57
67
|
*/
|
|
58
68
|
markSent(n: number): ScheduleRecord | null;
|
|
59
69
|
/**
|
|
60
|
-
* Stores the latest
|
|
70
|
+
* Stores the latest PureScore-Intelligence response alongside the schedule so it
|
|
61
71
|
* can be replayed on the next page load.
|
|
62
72
|
*/
|
|
63
73
|
storeResponse(response: Record<string, any>): void;
|