@interfere/react 9.0.2 → 10.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 +4 -4
- package/dist/api.d.mts +25 -0
- package/dist/api.d.mts.map +1 -0
- package/dist/api.mjs +68 -0
- package/dist/api.mjs.map +1 -0
- package/dist/error-boundary.d.mts +11 -4
- package/dist/error-boundary.d.mts.map +1 -1
- package/dist/error-boundary.mjs +6 -3
- package/dist/error-boundary.mjs.map +1 -1
- package/dist/internal/browser-context.d.mts +6 -0
- package/dist/internal/browser-context.d.mts.map +1 -0
- package/dist/internal/browser-context.mjs +59 -0
- package/dist/internal/browser-context.mjs.map +1 -0
- package/dist/internal/capture-boundary.d.mts +5 -1
- package/dist/internal/capture-boundary.d.mts.map +1 -1
- package/dist/internal/capture-boundary.mjs +9 -5
- package/dist/internal/capture-boundary.mjs.map +1 -1
- package/dist/internal/capture.d.mts +16 -5
- package/dist/internal/capture.d.mts.map +1 -1
- package/dist/internal/capture.mjs +20 -16
- package/dist/internal/capture.mjs.map +1 -1
- package/dist/internal/config.d.mts +20 -4
- package/dist/internal/config.d.mts.map +1 -1
- package/dist/internal/config.mjs +12 -12
- package/dist/internal/config.mjs.map +1 -1
- package/dist/internal/consent.d.mts.map +1 -1
- package/dist/internal/consent.mjs +3 -1
- package/dist/internal/consent.mjs.map +1 -1
- package/dist/internal/console-patch.d.mts +19 -0
- package/dist/internal/console-patch.d.mts.map +1 -0
- package/dist/internal/console-patch.mjs +62 -0
- package/dist/internal/console-patch.mjs.map +1 -0
- package/dist/internal/dom/actionable.d.mts +27 -0
- package/dist/internal/dom/actionable.d.mts.map +1 -0
- package/dist/internal/dom/actionable.mjs +62 -0
- package/dist/internal/dom/actionable.mjs.map +1 -0
- package/dist/internal/kernel-registry.d.mts +8 -0
- package/dist/internal/kernel-registry.d.mts.map +1 -0
- package/dist/internal/kernel-registry.mjs +31 -0
- package/dist/internal/kernel-registry.mjs.map +1 -0
- package/dist/internal/kernel.d.mts +267 -0
- package/dist/internal/kernel.d.mts.map +1 -0
- package/dist/internal/kernel.mjs +322 -0
- package/dist/internal/kernel.mjs.map +1 -0
- package/dist/internal/otel/exporter.d.mts +93 -0
- package/dist/internal/otel/exporter.d.mts.map +1 -0
- package/dist/internal/otel/exporter.mjs +212 -0
- package/dist/internal/otel/exporter.mjs.map +1 -0
- package/dist/internal/otel/index.d.mts +6 -0
- package/dist/internal/otel/index.mjs +6 -0
- package/dist/internal/otel/instrumentations.d.mts +42 -0
- package/dist/internal/otel/instrumentations.d.mts.map +1 -0
- package/dist/internal/otel/instrumentations.mjs +150 -0
- package/dist/internal/otel/instrumentations.mjs.map +1 -0
- package/dist/internal/otel/page-scope-context-manager.d.mts +32 -0
- package/dist/internal/otel/page-scope-context-manager.d.mts.map +1 -0
- package/dist/internal/otel/page-scope-context-manager.mjs +36 -0
- package/dist/internal/otel/page-scope-context-manager.mjs.map +1 -0
- package/dist/internal/otel/propagation.d.mts +21 -0
- package/dist/internal/otel/propagation.d.mts.map +1 -0
- package/dist/internal/otel/propagation.mjs +40 -0
- package/dist/internal/otel/propagation.mjs.map +1 -0
- package/dist/internal/otel/provider.d.mts +107 -0
- package/dist/internal/otel/provider.d.mts.map +1 -0
- package/dist/internal/otel/provider.mjs +151 -0
- package/dist/internal/otel/provider.mjs.map +1 -0
- package/dist/internal/otel/web-vitals.d.mts +35 -0
- package/dist/internal/otel/web-vitals.d.mts.map +1 -0
- package/dist/internal/otel/web-vitals.mjs +162 -0
- package/dist/internal/otel/web-vitals.mjs.map +1 -0
- package/dist/internal/page-lifecycle.d.mts +21 -0
- package/dist/internal/page-lifecycle.d.mts.map +1 -0
- package/dist/internal/page-lifecycle.mjs +33 -0
- package/dist/internal/page-lifecycle.mjs.map +1 -0
- package/dist/internal/plugin-runtime.d.mts +0 -2
- package/dist/internal/plugin-runtime.d.mts.map +1 -1
- package/dist/internal/plugin-runtime.mjs +1 -7
- package/dist/internal/plugin-runtime.mjs.map +1 -1
- package/dist/internal/react-context.d.mts +45 -0
- package/dist/internal/react-context.d.mts.map +1 -0
- package/dist/internal/react-context.mjs +34 -0
- package/dist/internal/react-context.mjs.map +1 -0
- package/dist/internal/sw.d.mts +22 -2
- package/dist/internal/sw.d.mts.map +1 -1
- package/dist/internal/sw.mjs +30 -3
- package/dist/internal/sw.mjs.map +1 -1
- package/dist/internal/version.d.mts +3 -1
- package/dist/internal/version.d.mts.map +1 -1
- package/dist/internal/version.mjs +4 -2
- package/dist/internal/version.mjs.map +1 -1
- package/dist/internal/wrapper-singleton.d.mts +47 -0
- package/dist/internal/wrapper-singleton.d.mts.map +1 -0
- package/dist/internal/wrapper-singleton.mjs +73 -0
- package/dist/internal/wrapper-singleton.mjs.map +1 -0
- package/dist/package.mjs +1 -1
- package/dist/plugins/errors.d.mts.map +1 -1
- package/dist/plugins/errors.mjs +18 -25
- package/dist/plugins/errors.mjs.map +1 -1
- package/dist/plugins/lib/loader.d.mts +1 -2
- package/dist/plugins/lib/loader.d.mts.map +1 -1
- package/dist/plugins/lib/loader.mjs +2 -11
- package/dist/plugins/lib/loader.mjs.map +1 -1
- package/dist/plugins/lib/types.d.mts +3 -2
- package/dist/plugins/lib/types.d.mts.map +1 -1
- package/dist/plugins/logs.d.mts +13 -0
- package/dist/plugins/logs.d.mts.map +1 -0
- package/dist/plugins/logs.mjs +53 -0
- package/dist/plugins/logs.mjs.map +1 -0
- package/dist/plugins/rage-clicks.d.mts.map +1 -1
- package/dist/plugins/rage-clicks.mjs +12 -10
- package/dist/plugins/rage-clicks.mjs.map +1 -1
- package/dist/plugins/replay.d.mts.map +1 -1
- package/dist/plugins/replay.mjs +58 -19
- package/dist/plugins/replay.mjs.map +1 -1
- package/dist/provider.d.mts +11 -20
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +13 -14
- package/dist/provider.mjs.map +1 -1
- package/dist/react-error-handler.d.mts +21 -5
- package/dist/react-error-handler.d.mts.map +1 -1
- package/dist/react-error-handler.mjs +15 -7
- package/dist/react-error-handler.mjs.map +1 -1
- package/dist/sw.d.mts +2 -0
- package/dist/sw.mjs +2 -0
- package/dist/tracking/api.d.mts +41 -15
- package/dist/tracking/api.d.mts.map +1 -1
- package/dist/tracking/api.mjs +122 -104
- package/dist/tracking/api.mjs.map +1 -1
- package/dist/tracking/device.d.mts +30 -7
- package/dist/tracking/device.d.mts.map +1 -1
- package/dist/tracking/device.mjs +70 -46
- package/dist/tracking/device.mjs.map +1 -1
- package/dist/tracking/geo.d.mts +11 -3
- package/dist/tracking/geo.d.mts.map +1 -1
- package/dist/tracking/geo.mjs +33 -29
- package/dist/tracking/geo.mjs.map +1 -1
- package/dist/tracking/session.d.mts +3 -1
- package/dist/tracking/session.d.mts.map +1 -1
- package/dist/tracking/session.mjs.map +1 -1
- package/dist/util/bot.d.mts +10 -0
- package/dist/util/bot.d.mts.map +1 -0
- package/dist/util/bot.mjs +14 -0
- package/dist/util/bot.mjs.map +1 -0
- package/dist/util/global.d.mts +10 -0
- package/dist/util/global.d.mts.map +1 -0
- package/dist/util/global.mjs +12 -0
- package/dist/util/global.mjs.map +1 -0
- package/dist/util/log.d.mts.map +1 -1
- package/dist/util/log.mjs +8 -1
- package/dist/util/log.mjs.map +1 -1
- package/dist/util/stringify.d.mts +9 -0
- package/dist/util/stringify.d.mts.map +1 -0
- package/dist/util/stringify.mjs +16 -0
- package/dist/util/stringify.mjs.map +1 -0
- package/package.json +73 -20
- package/dist/internal/client.d.mts +0 -48
- package/dist/internal/client.d.mts.map +0 -1
- package/dist/internal/client.mjs +0 -146
- package/dist/internal/client.mjs.map +0 -1
- package/dist/internal/context.d.mts +0 -6
- package/dist/internal/context.d.mts.map +0 -1
- package/dist/internal/context.mjs +0 -32
- package/dist/internal/context.mjs.map +0 -1
- package/dist/internal/envelope.d.mts +0 -15
- package/dist/internal/envelope.d.mts.map +0 -1
- package/dist/internal/envelope.mjs +0 -24
- package/dist/internal/envelope.mjs.map +0 -1
- package/dist/internal/errors.d.mts +0 -4
- package/dist/internal/errors.d.mts.map +0 -1
- package/dist/internal/errors.mjs +0 -4
- package/dist/internal/errors.mjs.map +0 -1
- package/dist/plugins/device.d.mts +0 -6
- package/dist/plugins/device.d.mts.map +0 -1
- package/dist/plugins/device.mjs +0 -13
- package/dist/plugins/device.mjs.map +0 -1
- package/dist/plugins/pages.d.mts +0 -6
- package/dist/plugins/pages.d.mts.map +0 -1
- package/dist/plugins/pages.mjs +0 -102
- package/dist/plugins/pages.mjs.map +0 -1
- package/dist/transport/http.d.mts +0 -25
- package/dist/transport/http.d.mts.map +0 -1
- package/dist/transport/http.mjs +0 -80
- package/dist/transport/http.mjs.map +0 -1
- package/dist/transport/queue.d.mts +0 -34
- package/dist/transport/queue.d.mts.map +0 -1
- package/dist/transport/queue.mjs +0 -100
- package/dist/transport/queue.mjs.map +0 -1
package/dist/tracking/api.d.mts
CHANGED
|
@@ -1,21 +1,47 @@
|
|
|
1
|
-
import { IngestTarget } from "../
|
|
1
|
+
import { IngestTarget } from "../internal/config.mjs";
|
|
2
|
+
import { DeviceManager } from "./device.mjs";
|
|
3
|
+
import { GeoDetector } from "./geo.mjs";
|
|
4
|
+
import { SessionId } from "@interfere/types/data/session";
|
|
2
5
|
import { IdentifyParams } from "@interfere/types/sdk/identify";
|
|
3
6
|
|
|
4
7
|
//#region src/tracking/api.d.ts
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
interface SessionTrackerOptions {
|
|
9
|
+
device?: DeviceManager;
|
|
10
|
+
fetcher?: typeof globalThis.fetch;
|
|
11
|
+
geo?: GeoDetector;
|
|
12
|
+
target: IngestTarget;
|
|
13
|
+
}
|
|
14
|
+
declare class SessionTracker {
|
|
15
|
+
private readonly target;
|
|
16
|
+
private readonly fetcher;
|
|
17
|
+
private readonly device;
|
|
18
|
+
private readonly geo;
|
|
19
|
+
private mgr;
|
|
20
|
+
private currentIdentity;
|
|
21
|
+
private identifiedSessionId;
|
|
22
|
+
private syncedSessionId;
|
|
23
|
+
private syncAttemptMs;
|
|
24
|
+
private generation;
|
|
25
|
+
constructor(opts: SessionTrackerOptions);
|
|
26
|
+
start(): void;
|
|
27
|
+
sessionId(): SessionId | null;
|
|
28
|
+
windowId(): string | null;
|
|
7
29
|
getDeviceId(): string | null;
|
|
8
30
|
getFpHash(): string | null;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Identity headers (session / window / device). Layered onto the
|
|
33
|
+
* session/identify POSTs alongside the target's static headers
|
|
34
|
+
* (content-type + auth + force-enable).
|
|
35
|
+
*/
|
|
36
|
+
headers(): Record<string, string>;
|
|
37
|
+
getIdentity(): IdentifyParams | null;
|
|
38
|
+
identify(params: IdentifyParams): Promise<void>;
|
|
39
|
+
clearIdentity(): void;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
private requestHeaders;
|
|
42
|
+
private ensureSynced;
|
|
43
|
+
private syncSession;
|
|
44
|
+
private onRotate;
|
|
45
|
+
}
|
|
20
46
|
//#endregion
|
|
21
|
-
export {
|
|
47
|
+
export { SessionTracker, SessionTrackerOptions };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;;;;UAaiB,qBAAA;EACf,MAAA,GAAS,aAAA;EACT,OAAA,UAAiB,UAAA,CAAW,KAAA;EAC5B,GAAA,GAAM,WAAA;EACN,MAAA,EAAQ,YAAA;AAAA;AAAA,cAGG,cAAA;EAAA,iBACM,MAAA;EAAA,iBACA,OAAA;EAAA,iBACA,MAAA;EAAA,iBACA,GAAA;EAAA,QAET,GAAA;EAAA,QACA,eAAA;EAAA,QACA,mBAAA;EAAA,QACA,eAAA;EAAA,QACA,aAAA;EAAA,QACA,UAAA;cAEI,IAAA,EAAM,qBAAA;EAOlB,KAAA,CAAA;EAaA,SAAA,CAAA,GAAa,SAAA;EAQb,QAAA,CAAA;EAIA,WAAA,CAAA;EAIA,SAAA,CAAA;EAjDyB;;;;;EA0DzB,OAAA,CAAA,GAAW,MAAA;EAiBX,WAAA,CAAA,GAAe,cAAA;EAIT,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,OAAA;EAiDxC,aAAA,CAAA;EAKA,OAAA,CAAA;EAAA,QAQQ,cAAA;EAAA,QAOA,YAAA;EAAA,QAcA,WAAA;EAAA,QA0BM,QAAA;AAAA"}
|
package/dist/tracking/api.mjs
CHANGED
|
@@ -1,107 +1,86 @@
|
|
|
1
1
|
import { createLogger } from "../util/log.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { detectCountryCode, resetGeo } from "./geo.mjs";
|
|
2
|
+
import { DeviceManager } from "./device.mjs";
|
|
3
|
+
import { GeoDetector } from "./geo.mjs";
|
|
5
4
|
import { SessionManager } from "./session.mjs";
|
|
6
5
|
//#region src/tracking/api.ts
|
|
7
6
|
const log = createLogger("tracking");
|
|
8
|
-
let mgr = null;
|
|
9
|
-
let target = null;
|
|
10
|
-
let currentIdentity = null;
|
|
11
|
-
let identifiedSessionId = null;
|
|
12
|
-
let syncedSessionId = null;
|
|
13
|
-
let syncAttemptMs = 0;
|
|
14
|
-
let generation = 0;
|
|
15
7
|
const SYNC_COOLDOWN_MS = 5e3;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
sessionId,
|
|
25
|
-
deviceId,
|
|
26
|
-
fpHash
|
|
27
|
-
}),
|
|
28
|
-
keepalive: true,
|
|
29
|
-
signal: AbortSignal.timeout(1e4)
|
|
30
|
-
}).then((res) => {
|
|
31
|
-
if (!res.ok) syncedSessionId = null;
|
|
32
|
-
}).catch(() => {
|
|
33
|
-
syncedSessionId = null;
|
|
34
|
-
log.warn("session sync failed, will retry");
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
function ensureSynced(sessionId) {
|
|
38
|
-
if (syncedSessionId === sessionId) return;
|
|
39
|
-
if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) return;
|
|
40
|
-
syncSession(sessionId, getDeviceId(), getFpHash());
|
|
41
|
-
}
|
|
42
|
-
async function onRotate(sessionId) {
|
|
8
|
+
var SessionTracker = class {
|
|
9
|
+
target;
|
|
10
|
+
fetcher;
|
|
11
|
+
device;
|
|
12
|
+
geo;
|
|
13
|
+
mgr = null;
|
|
14
|
+
currentIdentity = null;
|
|
15
|
+
identifiedSessionId = null;
|
|
43
16
|
syncedSessionId = null;
|
|
44
|
-
syncAttemptMs =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const
|
|
17
|
+
syncAttemptMs = 0;
|
|
18
|
+
generation = 0;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
this.target = opts.target;
|
|
21
|
+
this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
22
|
+
this.device = opts.device ?? new DeviceManager();
|
|
23
|
+
this.geo = opts.geo ?? new GeoDetector();
|
|
24
|
+
}
|
|
25
|
+
start() {
|
|
26
|
+
this.device.init();
|
|
27
|
+
this.geo.detect();
|
|
28
|
+
this.mgr = new SessionManager((id) => {
|
|
29
|
+
this.onRotate(id).catch(() => {});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
sessionId() {
|
|
33
|
+
const id = this.mgr?.getSessionId() ?? null;
|
|
34
|
+
if (id) this.ensureSynced(id);
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
windowId() {
|
|
38
|
+
return this.mgr?.getWindowId() ?? null;
|
|
39
|
+
}
|
|
61
40
|
getDeviceId() {
|
|
62
|
-
return getDeviceId();
|
|
63
|
-
}
|
|
41
|
+
return this.device.getDeviceId();
|
|
42
|
+
}
|
|
64
43
|
getFpHash() {
|
|
65
|
-
return getFpHash();
|
|
44
|
+
return this.device.getFpHash();
|
|
66
45
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Identity headers (session / window / device). Layered onto the
|
|
48
|
+
* session/identify POSTs alongside the target's static headers
|
|
49
|
+
* (content-type + auth + force-enable).
|
|
50
|
+
*/
|
|
51
|
+
headers() {
|
|
52
|
+
const h = {};
|
|
53
|
+
const sid = this.mgr?.getSessionId() ?? null;
|
|
54
|
+
if (sid) h["x-interfere-session"] = sid;
|
|
55
|
+
const wid = this.mgr?.getWindowId() ?? null;
|
|
56
|
+
if (wid) h["x-interfere-window"] = wid;
|
|
57
|
+
const did = this.device.getDeviceId();
|
|
58
|
+
if (did) h["x-interfere-device"] = did;
|
|
59
|
+
return h;
|
|
76
60
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
const sessionId = mgr.getSessionId();
|
|
85
|
-
if (identifiedSessionId === sessionId) {
|
|
61
|
+
getIdentity() {
|
|
62
|
+
return this.currentIdentity;
|
|
63
|
+
}
|
|
64
|
+
async identify(params) {
|
|
65
|
+
if (!this.mgr) return;
|
|
66
|
+
const sessionId = this.mgr.getSessionId();
|
|
67
|
+
if (this.identifiedSessionId === sessionId) {
|
|
86
68
|
log.debug("skipped, already identified for session %s", sessionId);
|
|
87
69
|
return;
|
|
88
70
|
}
|
|
89
|
-
currentIdentity = params;
|
|
90
|
-
identifiedSessionId = sessionId;
|
|
91
|
-
const gen = generation;
|
|
92
|
-
const [
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
]);
|
|
97
|
-
if (gen !== generation || !target) {
|
|
98
|
-
identifiedSessionId = null;
|
|
71
|
+
this.currentIdentity = params;
|
|
72
|
+
this.identifiedSessionId = sessionId;
|
|
73
|
+
const gen = this.generation;
|
|
74
|
+
const [fpHash, country] = await Promise.all([this.device.whenFingerprintReady(), this.geo.detect()]);
|
|
75
|
+
const deviceId = this.device.getDeviceId();
|
|
76
|
+
if (gen !== this.generation) {
|
|
77
|
+
this.identifiedSessionId = null;
|
|
99
78
|
return;
|
|
100
79
|
}
|
|
101
|
-
log.info("
|
|
102
|
-
|
|
103
|
-
method: "
|
|
104
|
-
headers:
|
|
80
|
+
log.info("POST session %s → user %s", sessionId, params.identifier);
|
|
81
|
+
this.fetcher(`${this.target.url}/identify`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: this.requestHeaders(),
|
|
105
84
|
body: JSON.stringify({
|
|
106
85
|
sessionId,
|
|
107
86
|
deviceId,
|
|
@@ -112,23 +91,62 @@ const identity = {
|
|
|
112
91
|
keepalive: true,
|
|
113
92
|
signal: AbortSignal.timeout(1e4)
|
|
114
93
|
}).catch(() => {
|
|
115
|
-
identifiedSessionId = null;
|
|
94
|
+
this.identifiedSessionId = null;
|
|
116
95
|
log.warn("identify failed for session %s", sessionId);
|
|
117
96
|
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
currentIdentity = null;
|
|
121
|
-
identifiedSessionId = null;
|
|
97
|
+
}
|
|
98
|
+
clearIdentity() {
|
|
99
|
+
this.currentIdentity = null;
|
|
100
|
+
this.identifiedSessionId = null;
|
|
101
|
+
}
|
|
102
|
+
dispose() {
|
|
103
|
+
this.generation += 1;
|
|
104
|
+
this.clearIdentity();
|
|
105
|
+
this.syncedSessionId = null;
|
|
106
|
+
this.syncAttemptMs = 0;
|
|
107
|
+
this.mgr = null;
|
|
108
|
+
}
|
|
109
|
+
requestHeaders() {
|
|
110
|
+
return {
|
|
111
|
+
...Object.fromEntries(this.target.headers.entries()),
|
|
112
|
+
...this.headers()
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
ensureSynced(sessionId) {
|
|
116
|
+
if (this.syncedSessionId === sessionId) return;
|
|
117
|
+
if (Date.now() - this.syncAttemptMs < SYNC_COOLDOWN_MS) return;
|
|
118
|
+
this.syncSession(sessionId, this.device.getDeviceId(), this.device.getFpHash());
|
|
119
|
+
}
|
|
120
|
+
syncSession(sessionId, deviceId, fpHash) {
|
|
121
|
+
this.syncAttemptMs = Date.now();
|
|
122
|
+
this.syncedSessionId = sessionId;
|
|
123
|
+
this.fetcher(this.target.url, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: this.requestHeaders(),
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
sessionId,
|
|
128
|
+
deviceId,
|
|
129
|
+
fpHash
|
|
130
|
+
}),
|
|
131
|
+
keepalive: true,
|
|
132
|
+
signal: AbortSignal.timeout(1e4)
|
|
133
|
+
}).then((res) => {
|
|
134
|
+
if (!res.ok) this.syncedSessionId = null;
|
|
135
|
+
}).catch(() => {
|
|
136
|
+
this.syncedSessionId = null;
|
|
137
|
+
log.warn("session sync failed, will retry");
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async onRotate(sessionId) {
|
|
141
|
+
this.syncedSessionId = null;
|
|
142
|
+
this.syncAttemptMs = Date.now();
|
|
143
|
+
const gen = this.generation;
|
|
144
|
+
const fpHash = await this.device.whenFingerprintReady();
|
|
145
|
+
const deviceId = this.device.getDeviceId();
|
|
146
|
+
if (gen !== this.generation) return;
|
|
147
|
+
log.debug("POST session %s (device=%s fp=%s)", sessionId, deviceId ?? "pending", fpHash ?? "none");
|
|
148
|
+
this.syncSession(sessionId, deviceId, fpHash);
|
|
122
149
|
}
|
|
123
150
|
};
|
|
124
|
-
function teardown() {
|
|
125
|
-
generation += 1;
|
|
126
|
-
identity.clear();
|
|
127
|
-
resetGeo();
|
|
128
|
-
syncedSessionId = null;
|
|
129
|
-
syncAttemptMs = 0;
|
|
130
|
-
mgr = null;
|
|
131
|
-
target = null;
|
|
132
|
-
}
|
|
133
151
|
//#endregion
|
|
134
|
-
export {
|
|
152
|
+
export { SessionTracker };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport {
|
|
1
|
+
{"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { SessionId } from \"@interfere/types/data/session\";\nimport type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport type { IngestTarget } from \"../internal/config.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { DeviceManager } from \"./device.js\";\nimport { GeoDetector } from \"./geo.js\";\nimport { SessionManager } from \"./session.js\";\n\nconst log = createLogger(\"tracking\");\n\nconst SYNC_COOLDOWN_MS = 5000;\n\nexport interface SessionTrackerOptions {\n device?: DeviceManager;\n fetcher?: typeof globalThis.fetch;\n geo?: GeoDetector;\n target: IngestTarget;\n}\n\nexport class SessionTracker {\n private readonly target: IngestTarget;\n private readonly fetcher: typeof globalThis.fetch;\n private readonly device: DeviceManager;\n private readonly geo: GeoDetector;\n\n private mgr: SessionManager | null = null;\n private currentIdentity: IdentifyParams | null = null;\n private identifiedSessionId: string | null = null;\n private syncedSessionId: string | null = null;\n private syncAttemptMs = 0;\n private generation = 0;\n\n constructor(opts: SessionTrackerOptions) {\n this.target = opts.target;\n this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);\n this.device = opts.device ?? new DeviceManager();\n this.geo = opts.geo ?? new GeoDetector();\n }\n\n start(): void {\n this.device.init();\n // Best-effort warm-up; `GeoDetector.detect()` swallows its own errors and\n // resolves to `null`, so we don't need to attach a `.catch`. If `identify`\n // runs before the geo result lands, the country field is just omitted.\n this.geo.detect();\n this.mgr = new SessionManager((id) => {\n this.onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n }\n\n sessionId(): SessionId | null {\n const id = this.mgr?.getSessionId() ?? null;\n if (id) {\n this.ensureSynced(id);\n }\n return id;\n }\n\n windowId(): string | null {\n return this.mgr?.getWindowId() ?? null;\n }\n\n getDeviceId(): string | null {\n return this.device.getDeviceId();\n }\n\n getFpHash(): string | null {\n return this.device.getFpHash();\n }\n\n /**\n * Identity headers (session / window / device). Layered onto the\n * session/identify POSTs alongside the target's static headers\n * (content-type + auth + force-enable).\n */\n headers(): Record<string, string> {\n const h: Record<string, string> = {};\n const sid = this.mgr?.getSessionId() ?? null;\n if (sid) {\n h[\"x-interfere-session\"] = sid;\n }\n const wid = this.mgr?.getWindowId() ?? null;\n if (wid) {\n h[\"x-interfere-window\"] = wid;\n }\n const did = this.device.getDeviceId();\n if (did) {\n h[\"x-interfere-device\"] = did;\n }\n return h;\n }\n\n getIdentity(): IdentifyParams | null {\n return this.currentIdentity;\n }\n\n async identify(params: IdentifyParams): Promise<void> {\n if (!this.mgr) {\n return;\n }\n const sessionId = this.mgr.getSessionId();\n if (this.identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n this.currentIdentity = params;\n this.identifiedSessionId = sessionId;\n\n const gen = this.generation;\n const [fpHash, country] = await Promise.all([\n this.device.whenFingerprintReady(),\n this.geo.detect(),\n ]);\n const deviceId = this.device.getDeviceId();\n if (gen !== this.generation) {\n this.identifiedSessionId = null;\n return;\n }\n // Append the path to the existing target. `new URL(\"/identify\", base)`\n // is wrong on both axes: in proxy mode `target.url` is relative\n // (`/api/interfere/v1/session`) so the URL constructor throws\n // synchronously — `currentIdentity` is set but no fetch fires; in\n // pub-token mode the path-absolute \"/identify\" replaces the target's\n // path, hitting `/identify` at the origin instead of the collector's\n // `/v1/session/identify` route. Plain string concatenation gets both.\n log.info(\"POST session %s → user %s\", sessionId, params.identifier);\n this.fetcher(`${this.target.url}/identify`, {\n method: \"POST\",\n headers: this.requestHeaders(),\n body: JSON.stringify({\n sessionId,\n deviceId,\n fpHash,\n ...params,\n ...(country && { country }),\n }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n }).catch(() => {\n this.identifiedSessionId = null;\n log.warn(\"identify failed for session %s\", sessionId);\n });\n }\n\n clearIdentity(): void {\n this.currentIdentity = null;\n this.identifiedSessionId = null;\n }\n\n dispose(): void {\n this.generation += 1;\n this.clearIdentity();\n this.syncedSessionId = null;\n this.syncAttemptMs = 0;\n this.mgr = null;\n }\n\n private requestHeaders(): Record<string, string> {\n return {\n ...Object.fromEntries(this.target.headers.entries()),\n ...this.headers(),\n };\n }\n\n private ensureSynced(sessionId: string): void {\n if (this.syncedSessionId === sessionId) {\n return;\n }\n if (Date.now() - this.syncAttemptMs < SYNC_COOLDOWN_MS) {\n return;\n }\n this.syncSession(\n sessionId,\n this.device.getDeviceId(),\n this.device.getFpHash()\n );\n }\n\n private syncSession(\n sessionId: string,\n deviceId: string | null,\n fpHash: string | null\n ): void {\n this.syncAttemptMs = Date.now();\n this.syncedSessionId = sessionId;\n\n this.fetcher(this.target.url, {\n method: \"POST\",\n headers: this.requestHeaders(),\n body: JSON.stringify({ sessionId, deviceId, fpHash }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n this.syncedSessionId = null;\n }\n })\n .catch(() => {\n this.syncedSessionId = null;\n log.warn(\"session sync failed, will retry\");\n });\n }\n\n private async onRotate(sessionId: string): Promise<void> {\n this.syncedSessionId = null;\n this.syncAttemptMs = Date.now();\n const gen = this.generation;\n const fpHash = await this.device.whenFingerprintReady();\n const deviceId = this.device.getDeviceId();\n if (gen !== this.generation) {\n return;\n }\n log.debug(\n \"POST session %s (device=%s fp=%s)\",\n sessionId,\n deviceId ?? \"pending\",\n fpHash ?? \"none\"\n );\n this.syncSession(sessionId, deviceId, fpHash);\n }\n}\n"],"mappings":";;;;;AASA,MAAM,MAAM,aAAa,WAAW;AAEpC,MAAM,mBAAmB;AASzB,IAAa,iBAAb,MAA4B;CAC1B;CACA;CACA;CACA;CAEA,MAAqC;CACrC,kBAAiD;CACjD,sBAA6C;CAC7C,kBAAyC;CACzC,gBAAwB;CACxB,aAAqB;CAErB,YAAY,MAA6B;EACvC,KAAK,SAAS,KAAK;EACnB,KAAK,UAAU,KAAK,WAAW,WAAW,MAAM,KAAK,WAAW;EAChE,KAAK,SAAS,KAAK,UAAU,IAAI,eAAe;EAChD,KAAK,MAAM,KAAK,OAAO,IAAI,aAAa;;CAG1C,QAAc;EACZ,KAAK,OAAO,MAAM;EAIlB,KAAK,IAAI,QAAQ;EACjB,KAAK,MAAM,IAAI,gBAAgB,OAAO;GACpC,KAAK,SAAS,GAAG,CAAC,YAAY,GAE5B;IACF;;CAGJ,YAA8B;EAC5B,MAAM,KAAK,KAAK,KAAK,cAAc,IAAI;EACvC,IAAI,IACF,KAAK,aAAa,GAAG;EAEvB,OAAO;;CAGT,WAA0B;EACxB,OAAO,KAAK,KAAK,aAAa,IAAI;;CAGpC,cAA6B;EAC3B,OAAO,KAAK,OAAO,aAAa;;CAGlC,YAA2B;EACzB,OAAO,KAAK,OAAO,WAAW;;;;;;;CAQhC,UAAkC;EAChC,MAAM,IAA4B,EAAE;EACpC,MAAM,MAAM,KAAK,KAAK,cAAc,IAAI;EACxC,IAAI,KACF,EAAE,yBAAyB;EAE7B,MAAM,MAAM,KAAK,KAAK,aAAa,IAAI;EACvC,IAAI,KACF,EAAE,wBAAwB;EAE5B,MAAM,MAAM,KAAK,OAAO,aAAa;EACrC,IAAI,KACF,EAAE,wBAAwB;EAE5B,OAAO;;CAGT,cAAqC;EACnC,OAAO,KAAK;;CAGd,MAAM,SAAS,QAAuC;EACpD,IAAI,CAAC,KAAK,KACR;EAEF,MAAM,YAAY,KAAK,IAAI,cAAc;EACzC,IAAI,KAAK,wBAAwB,WAAW;GAC1C,IAAI,MAAM,8CAA8C,UAAU;GAClE;;EAGF,KAAK,kBAAkB;EACvB,KAAK,sBAAsB;EAE3B,MAAM,MAAM,KAAK;EACjB,MAAM,CAAC,QAAQ,WAAW,MAAM,QAAQ,IAAI,CAC1C,KAAK,OAAO,sBAAsB,EAClC,KAAK,IAAI,QAAQ,CAClB,CAAC;EACF,MAAM,WAAW,KAAK,OAAO,aAAa;EAC1C,IAAI,QAAQ,KAAK,YAAY;GAC3B,KAAK,sBAAsB;GAC3B;;EASF,IAAI,KAAK,6BAA6B,WAAW,OAAO,WAAW;EACnE,KAAK,QAAQ,GAAG,KAAK,OAAO,IAAI,YAAY;GAC1C,QAAQ;GACR,SAAS,KAAK,gBAAgB;GAC9B,MAAM,KAAK,UAAU;IACnB;IACA;IACA;IACA,GAAG;IACH,GAAI,WAAW,EAAE,SAAS;IAC3B,CAAC;GACF,WAAW;GACX,QAAQ,YAAY,QAAQ,IAAO;GACpC,CAAC,CAAC,YAAY;GACb,KAAK,sBAAsB;GAC3B,IAAI,KAAK,kCAAkC,UAAU;IACrD;;CAGJ,gBAAsB;EACpB,KAAK,kBAAkB;EACvB,KAAK,sBAAsB;;CAG7B,UAAgB;EACd,KAAK,cAAc;EACnB,KAAK,eAAe;EACpB,KAAK,kBAAkB;EACvB,KAAK,gBAAgB;EACrB,KAAK,MAAM;;CAGb,iBAAiD;EAC/C,OAAO;GACL,GAAG,OAAO,YAAY,KAAK,OAAO,QAAQ,SAAS,CAAC;GACpD,GAAG,KAAK,SAAS;GAClB;;CAGH,aAAqB,WAAyB;EAC5C,IAAI,KAAK,oBAAoB,WAC3B;EAEF,IAAI,KAAK,KAAK,GAAG,KAAK,gBAAgB,kBACpC;EAEF,KAAK,YACH,WACA,KAAK,OAAO,aAAa,EACzB,KAAK,OAAO,WAAW,CACxB;;CAGH,YACE,WACA,UACA,QACM;EACN,KAAK,gBAAgB,KAAK,KAAK;EAC/B,KAAK,kBAAkB;EAEvB,KAAK,QAAQ,KAAK,OAAO,KAAK;GAC5B,QAAQ;GACR,SAAS,KAAK,gBAAgB;GAC9B,MAAM,KAAK,UAAU;IAAE;IAAW;IAAU;IAAQ,CAAC;GACrD,WAAW;GACX,QAAQ,YAAY,QAAQ,IAAO;GACpC,CAAC,CACC,MAAM,QAAQ;GACb,IAAI,CAAC,IAAI,IACP,KAAK,kBAAkB;IAEzB,CACD,YAAY;GACX,KAAK,kBAAkB;GACvB,IAAI,KAAK,kCAAkC;IAC3C;;CAGN,MAAc,SAAS,WAAkC;EACvD,KAAK,kBAAkB;EACvB,KAAK,gBAAgB,KAAK,KAAK;EAC/B,MAAM,MAAM,KAAK;EACjB,MAAM,SAAS,MAAM,KAAK,OAAO,sBAAsB;EACvD,MAAM,WAAW,KAAK,OAAO,aAAa;EAC1C,IAAI,QAAQ,KAAK,YACf;EAEF,IAAI,MACF,qCACA,WACA,YAAY,WACZ,UAAU,OACX;EACD,KAAK,YAAY,WAAW,UAAU,OAAO"}
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
//#region src/tracking/device.d.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Producer of an opaque, stable per-browser fingerprint hash. The default
|
|
4
|
+
* dynamic-imports the FingerprintJS OSS library and self-defers via
|
|
5
|
+
* `requestIdleCallback` so the ~1.5s of synchronous font / canvas / WebGL
|
|
6
|
+
* probing the library performs on a cold load doesn't land on the
|
|
7
|
+
* hydration tick. Tests inject a fixed-value provider that resolves
|
|
8
|
+
* immediately — they pay nothing for the production deferral.
|
|
9
|
+
*/
|
|
10
|
+
type FingerprintProvider = () => Promise<string | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves on the next browser idle frame, capped at 5s. Falls back to
|
|
13
|
+
* `setTimeout(0)` (next macrotask) on hosts without
|
|
14
|
+
* `requestIdleCallback`. Exported only for the unit test that pins the
|
|
15
|
+
* "default provider gates fingerprint work via RIC" contract — not part
|
|
16
|
+
* of the package's public surface.
|
|
17
|
+
*/
|
|
18
|
+
declare function whenIdle(): Promise<void>;
|
|
19
|
+
declare class DeviceManager {
|
|
20
|
+
private deviceId;
|
|
21
|
+
private fpHash;
|
|
22
|
+
private fpPending;
|
|
23
|
+
private readonly fingerprintProvider;
|
|
24
|
+
constructor(fingerprintProvider?: FingerprintProvider);
|
|
25
|
+
init(): void;
|
|
26
|
+
private startFingerprint;
|
|
27
|
+
getDeviceId(): string | null;
|
|
28
|
+
getFpHash(): string | null;
|
|
29
|
+
whenFingerprintReady(): Promise<string | null>;
|
|
30
|
+
}
|
|
8
31
|
//#endregion
|
|
9
|
-
export {
|
|
32
|
+
export { DeviceManager, FingerprintProvider, whenIdle };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device.d.mts","names":[],"sources":["../../src/tracking/device.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"device.d.mts","names":[],"sources":["../../src/tracking/device.ts"],"mappings":";;AAgBA;;;;;AAWA;;KAXY,mBAAA,SAA4B,OAAA;;;AAkFxC;;;;;iBAvEgB,QAAA,CAAA,GAAY,OAAA;AAAA,cAuEf,aAAA;EAAA,QACH,QAAA;EAAA,QACA,MAAA;EAAA,QACA,SAAA;EAAA,iBACS,mBAAA;cAEL,mBAAA,GAAsB,mBAAA;EAKlC,IAAA,CAAA;EAAA,QAkCQ,gBAAA;EAkBR,WAAA,CAAA;EAIA,SAAA,CAAA;EAIA,oBAAA,CAAA,GAAwB,OAAA;AAAA"}
|
package/dist/tracking/device.mjs
CHANGED
|
@@ -4,9 +4,37 @@ const log = createLogger("device");
|
|
|
4
4
|
const LS_KEY = "interfere:device_id";
|
|
5
5
|
const COOKIE_NAME = "interfere_did";
|
|
6
6
|
const COOKIE_MAX_AGE_DAYS = 400;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const FP_IDLE_TIMEOUT_MS = 5e3;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves on the next browser idle frame, capped at 5s. Falls back to
|
|
10
|
+
* `setTimeout(0)` (next macrotask) on hosts without
|
|
11
|
+
* `requestIdleCallback`. Exported only for the unit test that pins the
|
|
12
|
+
* "default provider gates fingerprint work via RIC" contract — not part
|
|
13
|
+
* of the package's public surface.
|
|
14
|
+
*/
|
|
15
|
+
function whenIdle() {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
if (typeof globalThis.requestIdleCallback === "function") {
|
|
18
|
+
globalThis.requestIdleCallback(() => resolve(), { timeout: FP_IDLE_TIMEOUT_MS });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
setTimeout(resolve, 0);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const defaultFingerprintProvider = async () => {
|
|
25
|
+
await whenIdle();
|
|
26
|
+
try {
|
|
27
|
+
const result = await (await (await import("@fingerprintjs/fingerprintjs")).load()).get();
|
|
28
|
+
if (!result.visitorId) {
|
|
29
|
+
log.error("Fingerprinting returned an empty visitor id; visitor identity will be cookie-only");
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return result.visitorId;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
log.error("fingerprint init failed; visitor identity will be cookie-only", err);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
10
38
|
function tryLocalStorage() {
|
|
11
39
|
try {
|
|
12
40
|
const s = globalThis.localStorage;
|
|
@@ -31,50 +59,46 @@ function setCookie(name, value) {
|
|
|
31
59
|
function generateId() {
|
|
32
60
|
return crypto.randomUUID();
|
|
33
61
|
}
|
|
34
|
-
|
|
35
|
-
if (deviceId) return;
|
|
36
|
-
const ls = tryLocalStorage();
|
|
37
|
-
const fromLs = ls?.getItem(LS_KEY) ?? null;
|
|
38
|
-
const fromCookie = getCookie(COOKIE_NAME);
|
|
39
|
-
deviceId = fromLs ?? fromCookie ?? generateId();
|
|
40
|
-
if (!fromLs && ls) ls.setItem(LS_KEY, deviceId);
|
|
41
|
-
if (!fromCookie) setCookie(COOKIE_NAME, deviceId);
|
|
42
|
-
if (fromLs && !fromCookie) setCookie(COOKIE_NAME, deviceId);
|
|
43
|
-
if (fromCookie && !fromLs && ls) ls.setItem(LS_KEY, deviceId);
|
|
44
|
-
log.debug("device %s (ls=%s cookie=%s)", deviceId, !!fromLs, !!fromCookie);
|
|
45
|
-
initFpHash();
|
|
46
|
-
}
|
|
47
|
-
function initFpHash() {
|
|
48
|
-
if (fpHash || fpPending) return;
|
|
49
|
-
fpPending = (async () => {
|
|
50
|
-
try {
|
|
51
|
-
fpHash = (await (await (await import("@fingerprintjs/fingerprintjs")).load()).get()).visitorId;
|
|
52
|
-
log.debug("fpHash %s", fpHash);
|
|
53
|
-
return fpHash;
|
|
54
|
-
} catch {
|
|
55
|
-
log.warn("fp hash failed");
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
})();
|
|
59
|
-
}
|
|
60
|
-
function getDeviceId() {
|
|
61
|
-
return deviceId;
|
|
62
|
-
}
|
|
63
|
-
function getFpHash() {
|
|
64
|
-
return fpHash;
|
|
65
|
-
}
|
|
66
|
-
function whenDeviceReady() {
|
|
67
|
-
if (deviceId) return Promise.resolve(deviceId);
|
|
68
|
-
return Promise.resolve(null);
|
|
69
|
-
}
|
|
70
|
-
function whenFingerprintReady() {
|
|
71
|
-
if (fpHash) return Promise.resolve(fpHash);
|
|
72
|
-
return fpPending ?? Promise.resolve(null);
|
|
73
|
-
}
|
|
74
|
-
function resetDevice() {
|
|
62
|
+
var DeviceManager = class {
|
|
75
63
|
deviceId = null;
|
|
76
64
|
fpHash = null;
|
|
77
65
|
fpPending = null;
|
|
78
|
-
|
|
66
|
+
fingerprintProvider;
|
|
67
|
+
constructor(fingerprintProvider) {
|
|
68
|
+
this.fingerprintProvider = fingerprintProvider ?? defaultFingerprintProvider;
|
|
69
|
+
}
|
|
70
|
+
init() {
|
|
71
|
+
if (this.deviceId) return;
|
|
72
|
+
const ls = tryLocalStorage();
|
|
73
|
+
const fromLs = ls?.getItem(LS_KEY) ?? null;
|
|
74
|
+
const fromCookie = getCookie(COOKIE_NAME);
|
|
75
|
+
this.deviceId = fromLs ?? fromCookie ?? generateId();
|
|
76
|
+
if (!fromLs && ls) ls.setItem(LS_KEY, this.deviceId);
|
|
77
|
+
if (!fromCookie) setCookie(COOKIE_NAME, this.deviceId);
|
|
78
|
+
if (fromLs && !fromCookie) setCookie(COOKIE_NAME, this.deviceId);
|
|
79
|
+
if (fromCookie && !fromLs && ls) ls.setItem(LS_KEY, this.deviceId);
|
|
80
|
+
log.debug("device %s (ls=%s cookie=%s)", this.deviceId, !!fromLs, !!fromCookie);
|
|
81
|
+
this.startFingerprint();
|
|
82
|
+
}
|
|
83
|
+
startFingerprint() {
|
|
84
|
+
if (this.fpHash || this.fpPending) return;
|
|
85
|
+
this.fpPending = (async () => {
|
|
86
|
+
const hash = await this.fingerprintProvider();
|
|
87
|
+
this.fpHash = hash;
|
|
88
|
+
if (hash) log.debug("fpHash %s", hash);
|
|
89
|
+
return hash;
|
|
90
|
+
})();
|
|
91
|
+
}
|
|
92
|
+
getDeviceId() {
|
|
93
|
+
return this.deviceId;
|
|
94
|
+
}
|
|
95
|
+
getFpHash() {
|
|
96
|
+
return this.fpHash;
|
|
97
|
+
}
|
|
98
|
+
whenFingerprintReady() {
|
|
99
|
+
if (this.fpHash) return Promise.resolve(this.fpHash);
|
|
100
|
+
return this.fpPending ?? Promise.resolve(null);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
79
103
|
//#endregion
|
|
80
|
-
export {
|
|
104
|
+
export { DeviceManager, whenIdle };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\
|
|
1
|
+
{"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\n/**\n * Producer of an opaque, stable per-browser fingerprint hash. The default\n * dynamic-imports the FingerprintJS OSS library and self-defers via\n * `requestIdleCallback` so the ~1.5s of synchronous font / canvas / WebGL\n * probing the library performs on a cold load doesn't land on the\n * hydration tick. Tests inject a fixed-value provider that resolves\n * immediately — they pay nothing for the production deferral.\n */\nexport type FingerprintProvider = () => Promise<string | null>;\n\nconst FP_IDLE_TIMEOUT_MS = 5000;\n\n/**\n * Resolves on the next browser idle frame, capped at 5s. Falls back to\n * `setTimeout(0)` (next macrotask) on hosts without\n * `requestIdleCallback`. Exported only for the unit test that pins the\n * \"default provider gates fingerprint work via RIC\" contract — not part\n * of the package's public surface.\n */\nexport function whenIdle(): Promise<void> {\n return new Promise((resolve) => {\n if (typeof globalThis.requestIdleCallback === \"function\") {\n globalThis.requestIdleCallback(() => resolve(), {\n timeout: FP_IDLE_TIMEOUT_MS,\n });\n return;\n }\n setTimeout(resolve, 0);\n });\n}\n\nconst defaultFingerprintProvider: FingerprintProvider = async () => {\n await whenIdle();\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs\");\n const fp = await FingerprintJS.load();\n const result = await fp.get();\n if (!result.visitorId) {\n log.error(\n \"Fingerprinting returned an empty visitor id; visitor identity will be cookie-only\"\n );\n\n return null;\n }\n return result.visitorId;\n } catch (err) {\n log.error(\n \"fingerprint init failed; visitor identity will be cookie-only\",\n err\n );\n\n return null;\n }\n};\n\nfunction tryLocalStorage(): Storage | null {\n try {\n const s = globalThis.localStorage;\n const key = \"__interfere_device_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const match = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.split(\"=\")[1] ?? \"\") : null;\n}\n\nfunction setCookie(name: string, value: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;\n // biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API is async and not universally supported; synchronous access is required here\n document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;\n}\n\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\nexport class DeviceManager {\n private deviceId: string | null = null;\n private fpHash: string | null = null;\n private fpPending: Promise<string | null> | null = null;\n private readonly fingerprintProvider: FingerprintProvider;\n\n constructor(fingerprintProvider?: FingerprintProvider) {\n this.fingerprintProvider =\n fingerprintProvider ?? defaultFingerprintProvider;\n }\n\n init(): void {\n if (this.deviceId) {\n return;\n }\n\n const ls = tryLocalStorage();\n const fromLs = ls?.getItem(LS_KEY) ?? null;\n const fromCookie = getCookie(COOKIE_NAME);\n\n this.deviceId = fromLs ?? fromCookie ?? generateId();\n\n if (!fromLs && ls) {\n ls.setItem(LS_KEY, this.deviceId);\n }\n if (!fromCookie) {\n setCookie(COOKIE_NAME, this.deviceId);\n }\n if (fromLs && !fromCookie) {\n setCookie(COOKIE_NAME, this.deviceId);\n }\n if (fromCookie && !fromLs && ls) {\n ls.setItem(LS_KEY, this.deviceId);\n }\n\n log.debug(\n \"device %s (ls=%s cookie=%s)\",\n this.deviceId,\n !!fromLs,\n !!fromCookie\n );\n\n this.startFingerprint();\n }\n\n private startFingerprint(): void {\n if (this.fpHash || this.fpPending) {\n return;\n }\n\n this.fpPending = (async () => {\n const hash = await this.fingerprintProvider();\n this.fpHash = hash;\n if (hash) {\n log.debug(\"fpHash %s\", hash);\n }\n // No log on null — the provider is responsible for surfacing its own\n // failure mode (the default provider logs at error level; custom\n // providers can choose). Logging here too would double-count.\n return hash;\n })();\n }\n\n getDeviceId(): string | null {\n return this.deviceId;\n }\n\n getFpHash(): string | null {\n return this.fpHash;\n }\n\n whenFingerprintReady(): Promise<string | null> {\n if (this.fpHash) {\n return Promise.resolve(this.fpHash);\n }\n return this.fpPending ?? Promise.resolve(null);\n }\n}\n"],"mappings":";;AAEA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,SAAS;AACf,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAY5B,MAAM,qBAAqB;;;;;;;;AAS3B,SAAgB,WAA0B;CACxC,OAAO,IAAI,SAAS,YAAY;EAC9B,IAAI,OAAO,WAAW,wBAAwB,YAAY;GACxD,WAAW,0BAA0B,SAAS,EAAE,EAC9C,SAAS,oBACV,CAAC;GACF;;EAEF,WAAW,SAAS,EAAE;GACtB;;AAGJ,MAAM,6BAAkD,YAAY;CAClE,MAAM,UAAU;CAChB,IAAI;EAGF,MAAM,SAAS,OAAM,OADJ,MADW,OAAO,iCACJ,MAAM,EACb,KAAK;EAC7B,IAAI,CAAC,OAAO,WAAW;GACrB,IAAI,MACF,oFACD;GAED,OAAO;;EAET,OAAO,OAAO;UACP,KAAK;EACZ,IAAI,MACF,iEACA,IACD;EAED,OAAO;;;AAIX,SAAS,kBAAkC;CACzC,IAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;EACZ,EAAE,QAAQ,KAAK,IAAI;EACnB,EAAE,WAAW,IAAI;EACjB,OAAO;SACD;EACN,OAAO;;;AAIX,SAAS,UAAU,MAA6B;CAC9C,IAAI,OAAO,aAAa,aACtB,OAAO;CAET,MAAM,QAAQ,SAAS,OACpB,MAAM,KAAK,CACX,MAAM,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,CAAC;CACxC,OAAO,QAAQ,mBAAmB,MAAM,MAAM,IAAI,CAAC,MAAM,GAAG,GAAG;;AAGjE,SAAS,UAAU,MAAc,OAAqB;CACpD,IAAI,OAAO,aAAa,aACtB;CAEF,MAAM,SAAS,sBAAsB,KAAK,KAAK;CAE/C,SAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,MAAM,CAAC,kBAAkB,OAAO;;AAGlF,SAAS,aAAqB;CAC5B,OAAO,OAAO,YAAY;;AAG5B,IAAa,gBAAb,MAA2B;CACzB,WAAkC;CAClC,SAAgC;CAChC,YAAmD;CACnD;CAEA,YAAY,qBAA2C;EACrD,KAAK,sBACH,uBAAuB;;CAG3B,OAAa;EACX,IAAI,KAAK,UACP;EAGF,MAAM,KAAK,iBAAiB;EAC5B,MAAM,SAAS,IAAI,QAAQ,OAAO,IAAI;EACtC,MAAM,aAAa,UAAU,YAAY;EAEzC,KAAK,WAAW,UAAU,cAAc,YAAY;EAEpD,IAAI,CAAC,UAAU,IACb,GAAG,QAAQ,QAAQ,KAAK,SAAS;EAEnC,IAAI,CAAC,YACH,UAAU,aAAa,KAAK,SAAS;EAEvC,IAAI,UAAU,CAAC,YACb,UAAU,aAAa,KAAK,SAAS;EAEvC,IAAI,cAAc,CAAC,UAAU,IAC3B,GAAG,QAAQ,QAAQ,KAAK,SAAS;EAGnC,IAAI,MACF,+BACA,KAAK,UACL,CAAC,CAAC,QACF,CAAC,CAAC,WACH;EAED,KAAK,kBAAkB;;CAGzB,mBAAiC;EAC/B,IAAI,KAAK,UAAU,KAAK,WACtB;EAGF,KAAK,aAAa,YAAY;GAC5B,MAAM,OAAO,MAAM,KAAK,qBAAqB;GAC7C,KAAK,SAAS;GACd,IAAI,MACF,IAAI,MAAM,aAAa,KAAK;GAK9B,OAAO;MACL;;CAGN,cAA6B;EAC3B,OAAO,KAAK;;CAGd,YAA2B;EACzB,OAAO,KAAK;;CAGd,uBAA+C;EAC7C,IAAI,KAAK,QACP,OAAO,QAAQ,QAAQ,KAAK,OAAO;EAErC,OAAO,KAAK,aAAa,QAAQ,QAAQ,KAAK"}
|
package/dist/tracking/geo.d.mts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
//#region src/tracking/geo.d.ts
|
|
2
|
-
declare
|
|
3
|
-
|
|
2
|
+
declare class GeoDetector {
|
|
3
|
+
private cached;
|
|
4
|
+
private pending;
|
|
5
|
+
private failed;
|
|
6
|
+
private readonly fetcher;
|
|
7
|
+
constructor(fetcher?: typeof globalThis.fetch);
|
|
8
|
+
detect(): Promise<string | null>;
|
|
9
|
+
getCountry(): string | null;
|
|
10
|
+
private fetchCountryCode;
|
|
11
|
+
}
|
|
4
12
|
//#endregion
|
|
5
|
-
export {
|
|
13
|
+
export { GeoDetector };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"geo.d.mts","names":[],"sources":["../../src/tracking/geo.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"geo.d.mts","names":[],"sources":["../../src/tracking/geo.ts"],"mappings":";cAea,WAAA;EAAA,QACH,MAAA;EAAA,QACA,OAAA;EAAA,QACA,MAAA;EAAA,iBACS,OAAA;cAEL,OAAA,UAAiB,UAAA,CAAW,KAAA;EAIxC,MAAA,CAAA,GAAU,OAAA;EAuBV,UAAA,CAAA;EAAA,QAIc,gBAAA;AAAA"}
|