@reopt-ai/data-sdk-client 0.1.5 → 0.2.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 +141 -7
- package/dist/client-B1GlWwVq.d.ts +744 -0
- package/dist/client-B1GlWwVq.d.ts.map +1 -0
- package/dist/client-BCrui21p.d.cts +744 -0
- package/dist/client-BCrui21p.d.cts.map +1 -0
- package/dist/exceptions-C9Fh0BIR.js +285 -0
- package/dist/exceptions-C9Fh0BIR.js.map +1 -0
- package/dist/exceptions-CuhGo9A5.cjs +289 -0
- package/dist/exceptions-CuhGo9A5.cjs.map +1 -0
- package/dist/index.cjs +75 -1874
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +35 -33
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -70
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +51 -1919
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +31 -0
- package/dist/next.d.cts.map +1 -0
- package/dist/next.d.ts +8 -10
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +36 -49
- package/dist/next.js.map +1 -1
- package/dist/observe-D-BHMwrO.cjs +54 -0
- package/dist/observe-D-BHMwrO.cjs.map +1 -0
- package/dist/observe-D1pNg1qL.js +54 -0
- package/dist/observe-D1pNg1qL.js.map +1 -0
- package/dist/react.cjs +105 -1863
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +80 -0
- package/dist/react.d.cts.map +1 -0
- package/dist/react.d.ts +53 -47
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +118 -23
- package/dist/react.js.map +1 -1
- package/dist/registry-BPhiH_l9.js +1775 -0
- package/dist/registry-BPhiH_l9.js.map +1 -0
- package/dist/registry-CGqrOtf4.cjs +1810 -0
- package/dist/registry-CGqrOtf4.cjs.map +1 -0
- package/dist/tracing-DAhaKkl0.cjs +93 -0
- package/dist/tracing-DAhaKkl0.cjs.map +1 -0
- package/dist/tracing-Dt1RVuQ9.js +93 -0
- package/dist/tracing-Dt1RVuQ9.js.map +1 -0
- package/package.json +31 -16
- package/dist/chunk-2UPF3C7D.js +0 -106
- package/dist/chunk-2UPF3C7D.js.map +0 -1
- package/dist/chunk-4MTDZBRS.js +0 -57
- package/dist/chunk-4MTDZBRS.js.map +0 -1
- package/dist/chunk-BJGIHFDA.js +0 -1627
- package/dist/chunk-BJGIHFDA.js.map +0 -1
- package/dist/client-TesmBLXd.d.ts +0 -581
- package/dist/exceptions-NQHZUDYO.js +0 -9
- package/dist/exceptions-NQHZUDYO.js.map +0 -1
- package/dist/tracing-LC3NZND7.js +0 -91
- package/dist/tracing-LC3NZND7.js.map +0 -1
package/dist/chunk-BJGIHFDA.js
DELETED
|
@@ -1,1627 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
describeError
|
|
3
|
-
} from "./chunk-4MTDZBRS.js";
|
|
4
|
-
|
|
5
|
-
// src/capture/utm.ts
|
|
6
|
-
var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
|
|
7
|
-
function extractUTMParams(search = typeof location !== "undefined" ? location.search : "") {
|
|
8
|
-
try {
|
|
9
|
-
const params = new URLSearchParams(search);
|
|
10
|
-
const utm = {};
|
|
11
|
-
for (const key of UTM_KEYS) {
|
|
12
|
-
const value = params.get(key);
|
|
13
|
-
if (value) utm[key] = value;
|
|
14
|
-
}
|
|
15
|
-
return utm;
|
|
16
|
-
} catch {
|
|
17
|
-
return {};
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// src/client.ts
|
|
22
|
-
import {
|
|
23
|
-
formatSessionHeader,
|
|
24
|
-
isOptedOut,
|
|
25
|
-
isValidIdentityId as isValidIdentityId2,
|
|
26
|
-
parseSessionHeader
|
|
27
|
-
} from "@reopt-ai/data-contract/identity";
|
|
28
|
-
|
|
29
|
-
// ../data-sdk-core/src/circuit-breaker.ts
|
|
30
|
-
var DEFAULT_CIRCUIT_BREAKER = {
|
|
31
|
-
failureThreshold: 5,
|
|
32
|
-
recoveryTimeout: 6e4
|
|
33
|
-
};
|
|
34
|
-
var CircuitBreaker = class {
|
|
35
|
-
state = "closed";
|
|
36
|
-
consecutiveFailures = 0;
|
|
37
|
-
openedAt = 0;
|
|
38
|
-
failureThreshold;
|
|
39
|
-
recoveryTimeout;
|
|
40
|
-
now;
|
|
41
|
-
constructor(config, now) {
|
|
42
|
-
this.failureThreshold = config?.failureThreshold ?? DEFAULT_CIRCUIT_BREAKER.failureThreshold;
|
|
43
|
-
this.recoveryTimeout = config?.recoveryTimeout ?? DEFAULT_CIRCUIT_BREAKER.recoveryTimeout;
|
|
44
|
-
this.now = now;
|
|
45
|
-
}
|
|
46
|
-
getState() {
|
|
47
|
-
return this.state;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Whether a request may go out right now. Moves an expired `open` circuit
|
|
51
|
-
* to `half-open` as a side effect, so exactly one caller gets to probe.
|
|
52
|
-
*/
|
|
53
|
-
allowRequest() {
|
|
54
|
-
if (this.state !== "open") return true;
|
|
55
|
-
if (this.now() - this.openedAt < this.recoveryTimeout) return false;
|
|
56
|
-
this.state = "half-open";
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
recordSuccess() {
|
|
60
|
-
this.state = "closed";
|
|
61
|
-
this.consecutiveFailures = 0;
|
|
62
|
-
}
|
|
63
|
-
/** A transient failure. Returns `true` if this one opened the circuit. */
|
|
64
|
-
recordFailure() {
|
|
65
|
-
this.consecutiveFailures++;
|
|
66
|
-
if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
|
|
67
|
-
const wasOpen = this.state === "open";
|
|
68
|
-
this.state = "open";
|
|
69
|
-
this.openedAt = this.now();
|
|
70
|
-
return !wasOpen;
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* A permanent rejection: the server is up. Close a half-open circuit and
|
|
76
|
-
* reset the failure streak, but do not count it against the threshold.
|
|
77
|
-
*/
|
|
78
|
-
recordRejection() {
|
|
79
|
-
if (this.state === "half-open") this.state = "closed";
|
|
80
|
-
this.consecutiveFailures = 0;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// ../data-sdk-core/src/consent.ts
|
|
85
|
-
var DEFAULT_CONSENT_CATEGORY = "analytics";
|
|
86
|
-
var CONSENT_STORAGE_KEY = "consent";
|
|
87
|
-
var ConsentManager = class {
|
|
88
|
-
state = {};
|
|
89
|
-
paused = false;
|
|
90
|
-
storage;
|
|
91
|
-
onChange;
|
|
92
|
-
constructor(config, storage, onChange) {
|
|
93
|
-
const persist = config?.persist ?? true;
|
|
94
|
-
this.storage = persist ? storage : void 0;
|
|
95
|
-
this.onChange = onChange;
|
|
96
|
-
const defaultConsent = config?.defaultConsent ?? true;
|
|
97
|
-
for (const category of config?.categories ?? [DEFAULT_CONSENT_CATEGORY]) {
|
|
98
|
-
this.state[category] = defaultConsent;
|
|
99
|
-
}
|
|
100
|
-
const stored = this.storage ? readStoredConsent(this.storage) : null;
|
|
101
|
-
if (stored) Object.assign(this.state, stored);
|
|
102
|
-
}
|
|
103
|
-
get(category) {
|
|
104
|
-
return this.state[category] ?? false;
|
|
105
|
-
}
|
|
106
|
-
set(category, allowed) {
|
|
107
|
-
this.state[category] = allowed;
|
|
108
|
-
this.persist();
|
|
109
|
-
}
|
|
110
|
-
setAll(allowed) {
|
|
111
|
-
for (const key of Object.keys(this.state)) {
|
|
112
|
-
this.state[key] = allowed;
|
|
113
|
-
}
|
|
114
|
-
this.persist();
|
|
115
|
-
}
|
|
116
|
-
snapshot() {
|
|
117
|
-
return { ...this.state };
|
|
118
|
-
}
|
|
119
|
-
/** Overlay decisions from elsewhere (a server bootstrap) on the current state. */
|
|
120
|
-
replace(state) {
|
|
121
|
-
this.state = { ...this.state, ...state };
|
|
122
|
-
this.persist();
|
|
123
|
-
}
|
|
124
|
-
pause() {
|
|
125
|
-
this.paused = true;
|
|
126
|
-
}
|
|
127
|
-
resume() {
|
|
128
|
-
this.paused = false;
|
|
129
|
-
}
|
|
130
|
-
isPaused() {
|
|
131
|
-
return this.paused;
|
|
132
|
-
}
|
|
133
|
-
/** Why an event in `category` may not be queued right now, or `null`. */
|
|
134
|
-
enqueueBlockReason(category) {
|
|
135
|
-
if (this.paused) return "tracking_paused";
|
|
136
|
-
if (!this.get(category)) return "consent_denied";
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
/** Why nothing may be sent right now, or `null`. */
|
|
140
|
-
flushBlockReason() {
|
|
141
|
-
if (this.paused) return "tracking_paused";
|
|
142
|
-
if (!Object.values(this.state).some((allowed) => allowed === true)) return "consent_denied";
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
persist() {
|
|
146
|
-
this.onChange?.(this.snapshot());
|
|
147
|
-
if (!this.storage) return;
|
|
148
|
-
try {
|
|
149
|
-
this.storage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(this.state));
|
|
150
|
-
} catch {
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
function readStoredConsent(storage) {
|
|
155
|
-
try {
|
|
156
|
-
const raw = storage.getItem(CONSENT_STORAGE_KEY);
|
|
157
|
-
if (!raw) return null;
|
|
158
|
-
const parsed = JSON.parse(raw);
|
|
159
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
160
|
-
const state = {};
|
|
161
|
-
for (const [category, allowed] of Object.entries(parsed)) {
|
|
162
|
-
if (typeof allowed === "boolean") state[category] = allowed;
|
|
163
|
-
}
|
|
164
|
-
return state;
|
|
165
|
-
} catch {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ../data-sdk-core/src/events.ts
|
|
171
|
-
import { zTrackHandlerPayload } from "@reopt-ai/data-contract/ingest";
|
|
172
|
-
|
|
173
|
-
// ../data-sdk-core/src/validate.ts
|
|
174
|
-
import { RESERVED_EVENT_NAMES } from "@reopt-ai/data-contract/events";
|
|
175
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
176
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
177
|
-
var isId = (value, required) => value === void 0 ? !required : typeof value === "number" || typeof value === "string" && value.length <= 500 && (!required || value.length > 0);
|
|
178
|
-
var isName = (value, max = 200) => typeof value === "string" && value.length > 0 && value.length <= max;
|
|
179
|
-
function validateStructurally(event) {
|
|
180
|
-
const bad = [];
|
|
181
|
-
if (!isRecord(event)) return [{ field: "", message: "not an object" }];
|
|
182
|
-
if (!UUID.test(String(event.eventId))) bad.push("eventId");
|
|
183
|
-
if (!Number.isInteger(event.timestamp) || event.timestamp < 0) bad.push("timestamp");
|
|
184
|
-
const p = event.payload;
|
|
185
|
-
if (!isRecord(p)) return [...bad, "payload"].map(field);
|
|
186
|
-
switch (event.type) {
|
|
187
|
-
case "track":
|
|
188
|
-
if (!isName(p.name)) bad.push("payload.name");
|
|
189
|
-
else if (RESERVED_EVENT_NAMES.includes(p.name))
|
|
190
|
-
bad.push("payload.name:reserved");
|
|
191
|
-
if (!isId(p.profileId, false)) bad.push("payload.profileId");
|
|
192
|
-
break;
|
|
193
|
-
case "identify":
|
|
194
|
-
if (!isId(p.profileId, true)) bad.push("payload.profileId");
|
|
195
|
-
break;
|
|
196
|
-
case "increment":
|
|
197
|
-
case "decrement":
|
|
198
|
-
if (!isId(p.profileId, true)) bad.push("payload.profileId");
|
|
199
|
-
if (!isName(p.property)) bad.push("payload.property");
|
|
200
|
-
if (p.value !== void 0 && !(typeof p.value === "number" && p.value > 0)) bad.push("payload.value");
|
|
201
|
-
break;
|
|
202
|
-
default:
|
|
203
|
-
bad.push("type");
|
|
204
|
-
}
|
|
205
|
-
if (p.properties !== void 0 && !isRecord(p.properties)) bad.push("payload.properties");
|
|
206
|
-
return bad.map(field);
|
|
207
|
-
}
|
|
208
|
-
function field(name) {
|
|
209
|
-
const [path, reason] = name.split(":");
|
|
210
|
-
return { field: path, message: reason ?? "invalid" };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ../data-sdk-core/src/events.ts
|
|
214
|
-
function validateEvent(event) {
|
|
215
|
-
const errors = validateStructurally(event);
|
|
216
|
-
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
217
|
-
const result = zTrackHandlerPayload.safeParse(event);
|
|
218
|
-
if (!result.success) {
|
|
219
|
-
return {
|
|
220
|
-
valid: false,
|
|
221
|
-
errors: result.error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message }))
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
if (errors.length > 0) {
|
|
225
|
-
console.warn("[reopt] structural validator disagrees with the contract schema \u2014 report this", errors);
|
|
226
|
-
return { valid: true };
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
230
|
-
}
|
|
231
|
-
function stamp(deps, draft) {
|
|
232
|
-
return { ...draft, eventId: deps.createId(), timestamp: deps.now() };
|
|
233
|
-
}
|
|
234
|
-
function buildTrackEvent(deps, options, fallbackProfileId) {
|
|
235
|
-
return stamp(deps, {
|
|
236
|
-
type: "track",
|
|
237
|
-
payload: {
|
|
238
|
-
name: options.name,
|
|
239
|
-
properties: options.properties,
|
|
240
|
-
profileId: options.profileId ?? fallbackProfileId ?? void 0
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
function buildIdentifyEvent(deps, options) {
|
|
245
|
-
return stamp(deps, {
|
|
246
|
-
type: "identify",
|
|
247
|
-
payload: {
|
|
248
|
-
profileId: options.profileId,
|
|
249
|
-
...options.firstName && { firstName: options.firstName },
|
|
250
|
-
...options.lastName && { lastName: options.lastName },
|
|
251
|
-
...options.email && { email: options.email },
|
|
252
|
-
...options.avatar && { avatar: options.avatar },
|
|
253
|
-
properties: options.properties
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
function buildCounterEvent(deps, type, options, fallbackProfileId) {
|
|
258
|
-
return stamp(deps, {
|
|
259
|
-
type,
|
|
260
|
-
payload: {
|
|
261
|
-
profileId: options.profileId ?? fallbackProfileId ?? "",
|
|
262
|
-
property: options.property,
|
|
263
|
-
value: options.value ?? 1
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
function buildPageViewOptions(options, context) {
|
|
268
|
-
const utm = options.utm ?? context.utm ?? {};
|
|
269
|
-
return {
|
|
270
|
-
name: "$pageview",
|
|
271
|
-
consentCategory: options.consentCategory,
|
|
272
|
-
identity: options.identity,
|
|
273
|
-
properties: {
|
|
274
|
-
path: options.path ?? context.path ?? "/",
|
|
275
|
-
origin: options.origin ?? context.origin ?? "",
|
|
276
|
-
title: options.title ?? context.title ?? "",
|
|
277
|
-
referrer: options.referrer ?? context.referrer ?? "",
|
|
278
|
-
...Object.fromEntries(Object.entries(utm).filter(([, value]) => value)),
|
|
279
|
-
...context.properties,
|
|
280
|
-
...options.properties
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
function normalizePersistedEvent(deps, event) {
|
|
285
|
-
if (!event || typeof event !== "object") return null;
|
|
286
|
-
const candidate = event;
|
|
287
|
-
if (candidate.type !== "track" && candidate.type !== "identify" && candidate.type !== "increment" && candidate.type !== "decrement") {
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
if (!candidate.payload || typeof candidate.payload !== "object") return null;
|
|
291
|
-
return {
|
|
292
|
-
...candidate,
|
|
293
|
-
eventId: candidate.eventId ?? deps.createId(),
|
|
294
|
-
timestamp: candidate.timestamp ?? deps.now()
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ../data-sdk-core/src/ids.ts
|
|
299
|
-
var lastTimestamp = -1;
|
|
300
|
-
var sequence = 0;
|
|
301
|
-
var HEX = "0123456789abcdef";
|
|
302
|
-
function randomBytes(length) {
|
|
303
|
-
const bytes = new Uint8Array(length);
|
|
304
|
-
const cryptoApi = globalThis.crypto;
|
|
305
|
-
if (cryptoApi && typeof cryptoApi.getRandomValues === "function") {
|
|
306
|
-
cryptoApi.getRandomValues(bytes);
|
|
307
|
-
return bytes;
|
|
308
|
-
}
|
|
309
|
-
for (let index = 0; index < length; index++) {
|
|
310
|
-
bytes[index] = Math.floor(Math.random() * 256);
|
|
311
|
-
}
|
|
312
|
-
return bytes;
|
|
313
|
-
}
|
|
314
|
-
function hex(bytes) {
|
|
315
|
-
let out = "";
|
|
316
|
-
for (const byte of bytes) {
|
|
317
|
-
out += HEX[byte >> 4] + HEX[byte & 15];
|
|
318
|
-
}
|
|
319
|
-
return out;
|
|
320
|
-
}
|
|
321
|
-
function uuidv7(now = Date.now()) {
|
|
322
|
-
let timestamp = Math.max(0, Math.floor(now));
|
|
323
|
-
if (timestamp === lastTimestamp) {
|
|
324
|
-
sequence = sequence + 1 & 4095;
|
|
325
|
-
if (sequence === 0) {
|
|
326
|
-
timestamp = lastTimestamp + 1;
|
|
327
|
-
lastTimestamp = timestamp;
|
|
328
|
-
}
|
|
329
|
-
} else if (timestamp < lastTimestamp) {
|
|
330
|
-
timestamp = lastTimestamp;
|
|
331
|
-
sequence = sequence + 1 & 4095;
|
|
332
|
-
if (sequence === 0) {
|
|
333
|
-
timestamp = lastTimestamp + 1;
|
|
334
|
-
lastTimestamp = timestamp;
|
|
335
|
-
}
|
|
336
|
-
} else {
|
|
337
|
-
lastTimestamp = timestamp;
|
|
338
|
-
sequence = randomBytes(2)[0] & 15;
|
|
339
|
-
}
|
|
340
|
-
const bytes = new Uint8Array(16);
|
|
341
|
-
bytes[0] = timestamp / 2 ** 40 & 255;
|
|
342
|
-
bytes[1] = timestamp / 2 ** 32 & 255;
|
|
343
|
-
bytes[2] = timestamp / 2 ** 24 & 255;
|
|
344
|
-
bytes[3] = timestamp / 2 ** 16 & 255;
|
|
345
|
-
bytes[4] = timestamp / 2 ** 8 & 255;
|
|
346
|
-
bytes[5] = timestamp & 255;
|
|
347
|
-
bytes[6] = 112 | sequence >> 8;
|
|
348
|
-
bytes[7] = sequence & 255;
|
|
349
|
-
const random = randomBytes(8);
|
|
350
|
-
bytes[8] = 128 | random[0] & 63;
|
|
351
|
-
for (let index = 1; index < 8; index++) {
|
|
352
|
-
bytes[8 + index] = random[index];
|
|
353
|
-
}
|
|
354
|
-
const text = hex(bytes);
|
|
355
|
-
return `${text.slice(0, 8)}-${text.slice(8, 12)}-${text.slice(12, 16)}-${text.slice(16, 20)}-${text.slice(20)}`;
|
|
356
|
-
}
|
|
357
|
-
function uuidv4() {
|
|
358
|
-
const cryptoApi = globalThis.crypto;
|
|
359
|
-
if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
|
|
360
|
-
return cryptoApi.randomUUID();
|
|
361
|
-
}
|
|
362
|
-
const bytes = randomBytes(16);
|
|
363
|
-
bytes[6] = 64 | bytes[6] & 15;
|
|
364
|
-
bytes[8] = 128 | bytes[8] & 63;
|
|
365
|
-
const text = hex(bytes);
|
|
366
|
-
return `${text.slice(0, 8)}-${text.slice(8, 12)}-${text.slice(12, 16)}-${text.slice(16, 20)}-${text.slice(20)}`;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ../data-sdk-core/src/queue.ts
|
|
370
|
-
var QUEUE_STORAGE_KEY = "queue";
|
|
371
|
-
var PERSIST_DEBOUNCE_MS = 250;
|
|
372
|
-
function serializedSize(value) {
|
|
373
|
-
const text = JSON.stringify(value);
|
|
374
|
-
if (typeof TextEncoder !== "undefined") {
|
|
375
|
-
return new TextEncoder().encode(text).length;
|
|
376
|
-
}
|
|
377
|
-
let bytes = 0;
|
|
378
|
-
for (let index = 0; index < text.length; index++) {
|
|
379
|
-
const code = text.charCodeAt(index);
|
|
380
|
-
bytes += code < 128 ? 1 : code < 2048 ? 2 : 3;
|
|
381
|
-
}
|
|
382
|
-
return bytes;
|
|
383
|
-
}
|
|
384
|
-
function identityKey(identity) {
|
|
385
|
-
if (!identity) return "";
|
|
386
|
-
return `${identity.deviceId ?? ""}\0${identity.sessionId ?? ""}`;
|
|
387
|
-
}
|
|
388
|
-
var EventQueue = class {
|
|
389
|
-
entries = [];
|
|
390
|
-
/** The batch currently on the wire. Persisted with the queue so an unload mid-request loses nothing; the server dedupes by eventId. */
|
|
391
|
-
inFlight = [];
|
|
392
|
-
persistTimeout = null;
|
|
393
|
-
options;
|
|
394
|
-
constructor(options) {
|
|
395
|
-
this.options = options;
|
|
396
|
-
}
|
|
397
|
-
get length() {
|
|
398
|
-
return this.entries.length;
|
|
399
|
-
}
|
|
400
|
-
/** Adds an entry, dropping the oldest past `maxSize`. Returns how many were dropped. */
|
|
401
|
-
push(entry) {
|
|
402
|
-
let dropped = 0;
|
|
403
|
-
if (this.entries.length >= this.options.maxSize) {
|
|
404
|
-
dropped = this.entries.length - this.options.maxSize + 1;
|
|
405
|
-
this.entries.splice(0, dropped);
|
|
406
|
-
}
|
|
407
|
-
this.entries.push(entry);
|
|
408
|
-
this.schedulePersist();
|
|
409
|
-
return dropped;
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Removes and returns the next batch: a run of head entries that share
|
|
413
|
-
* one identity, at most `maxCount` long, whose combined serialized size
|
|
414
|
-
* stays under `maxBytes` (including the array brackets and commas).
|
|
415
|
-
*
|
|
416
|
-
* Always returns at least one entry when the queue is non-empty — a single
|
|
417
|
-
* oversized event must be attempted, and rejected by the server, rather
|
|
418
|
-
* than sit at the head of the queue forever.
|
|
419
|
-
*/
|
|
420
|
-
take(maxCount, maxBytes) {
|
|
421
|
-
if (this.entries.length === 0) return [];
|
|
422
|
-
const key = identityKey(this.entries[0].identity);
|
|
423
|
-
let count = 0;
|
|
424
|
-
let bytes = 2;
|
|
425
|
-
while (count < this.entries.length && count < maxCount) {
|
|
426
|
-
const entry = this.entries[count];
|
|
427
|
-
if (count > 0 && identityKey(entry.identity) !== key) break;
|
|
428
|
-
const size = serializedSize(entry.event) + (count > 0 ? 1 : 0);
|
|
429
|
-
if (count > 0 && bytes + size > maxBytes) break;
|
|
430
|
-
bytes += size;
|
|
431
|
-
count++;
|
|
432
|
-
}
|
|
433
|
-
this.inFlight = this.entries.splice(0, count);
|
|
434
|
-
return this.inFlight;
|
|
435
|
-
}
|
|
436
|
-
/** The batch returned by the last `take()` has been settled — sent, dropped or requeued. */
|
|
437
|
-
settle() {
|
|
438
|
-
this.inFlight = [];
|
|
439
|
-
}
|
|
440
|
-
/** Puts a batch back at the head, preserving order, after a transient failure. */
|
|
441
|
-
requeue(entries) {
|
|
442
|
-
this.entries.unshift(...entries);
|
|
443
|
-
this.inFlight = [];
|
|
444
|
-
}
|
|
445
|
-
clear() {
|
|
446
|
-
this.entries = [];
|
|
447
|
-
}
|
|
448
|
-
peekAll() {
|
|
449
|
-
return this.entries;
|
|
450
|
-
}
|
|
451
|
-
schedulePersist() {
|
|
452
|
-
if (!this.options.persist || !this.options.storage) return;
|
|
453
|
-
if (this.persistTimeout) return;
|
|
454
|
-
this.persistTimeout = setTimeout(() => {
|
|
455
|
-
this.persistTimeout = null;
|
|
456
|
-
this.persistNow();
|
|
457
|
-
}, PERSIST_DEBOUNCE_MS);
|
|
458
|
-
}
|
|
459
|
-
persistNow() {
|
|
460
|
-
if (this.persistTimeout) {
|
|
461
|
-
clearTimeout(this.persistTimeout);
|
|
462
|
-
this.persistTimeout = null;
|
|
463
|
-
}
|
|
464
|
-
if (!this.options.persist || !this.options.storage) return;
|
|
465
|
-
try {
|
|
466
|
-
this.options.storage.setItem(
|
|
467
|
-
QUEUE_STORAGE_KEY,
|
|
468
|
-
JSON.stringify([...this.inFlight, ...this.entries].map((entry) => entry.event))
|
|
469
|
-
);
|
|
470
|
-
} catch {
|
|
471
|
-
this.options.log?.("Failed to persist queue");
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Loads whatever a previous page left behind and removes it from storage,
|
|
476
|
-
* so two tabs restoring the same buffer do not both send it.
|
|
477
|
-
*/
|
|
478
|
-
restore() {
|
|
479
|
-
if (!this.options.persist || !this.options.storage) return 0;
|
|
480
|
-
try {
|
|
481
|
-
const raw = this.options.storage.getItem(QUEUE_STORAGE_KEY);
|
|
482
|
-
if (!raw) return 0;
|
|
483
|
-
const payloads = JSON.parse(raw);
|
|
484
|
-
if (!Array.isArray(payloads)) return 0;
|
|
485
|
-
const restored = payloads.flatMap((payload) => {
|
|
486
|
-
const event = this.options.normalize(payload);
|
|
487
|
-
return event ? [{ event }] : [];
|
|
488
|
-
});
|
|
489
|
-
this.entries.push(...restored);
|
|
490
|
-
this.options.storage.removeItem(QUEUE_STORAGE_KEY);
|
|
491
|
-
return restored.length;
|
|
492
|
-
} catch {
|
|
493
|
-
this.options.log?.("Failed to load persisted queue");
|
|
494
|
-
return 0;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
dispose() {
|
|
498
|
-
if (this.persistTimeout) {
|
|
499
|
-
clearTimeout(this.persistTimeout);
|
|
500
|
-
this.persistTimeout = null;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
// ../data-sdk-core/src/retry.ts
|
|
506
|
-
var TransportError = class extends Error {
|
|
507
|
-
status;
|
|
508
|
-
/**
|
|
509
|
-
* Keep the batch in the durable queue even though it is not retried now.
|
|
510
|
-
* Used for quota exhaustion and contract drift — conditions that need a
|
|
511
|
-
* human, after which an explicit flush should deliver the backlog.
|
|
512
|
-
*/
|
|
513
|
-
preserveBatch;
|
|
514
|
-
constructor(message, status, preserveBatch = false) {
|
|
515
|
-
super(message);
|
|
516
|
-
this.name = "TransportError";
|
|
517
|
-
this.status = status;
|
|
518
|
-
this.preserveBatch = preserveBatch;
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
function isRetryableTransportError(error) {
|
|
522
|
-
if (error instanceof TransportError && typeof error.status === "number") {
|
|
523
|
-
if (error.status === 429) return true;
|
|
524
|
-
if (error.status >= 400 && error.status < 500) return false;
|
|
525
|
-
}
|
|
526
|
-
return true;
|
|
527
|
-
}
|
|
528
|
-
var DEFAULT_RETRY = {
|
|
529
|
-
maxRetries: 3,
|
|
530
|
-
baseDelay: 1e3,
|
|
531
|
-
maxDelay: 3e4,
|
|
532
|
-
jitter: 0.1
|
|
533
|
-
};
|
|
534
|
-
function resolveRetryConfig(config) {
|
|
535
|
-
return {
|
|
536
|
-
maxRetries: config?.maxRetries ?? DEFAULT_RETRY.maxRetries,
|
|
537
|
-
baseDelay: config?.baseDelay ?? DEFAULT_RETRY.baseDelay,
|
|
538
|
-
maxDelay: config?.maxDelay ?? DEFAULT_RETRY.maxDelay,
|
|
539
|
-
jitter: config?.jitter ?? DEFAULT_RETRY.jitter
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
function computeBackoff(attempt, config, random = Math.random) {
|
|
543
|
-
const delay = Math.min(Math.pow(2, attempt) * config.baseDelay, config.maxDelay);
|
|
544
|
-
const jitterAmount = delay * config.jitter * (random() * 2 - 1);
|
|
545
|
-
return Math.max(0, delay + jitterAmount);
|
|
546
|
-
}
|
|
547
|
-
async function withRetry(operation, config, hooks = {}) {
|
|
548
|
-
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
549
|
-
try {
|
|
550
|
-
return await operation(attempt);
|
|
551
|
-
} catch (error) {
|
|
552
|
-
if (!isRetryableTransportError(error) || attempt === config.maxRetries) {
|
|
553
|
-
throw error;
|
|
554
|
-
}
|
|
555
|
-
const waitMs = computeBackoff(attempt, config, hooks.random);
|
|
556
|
-
hooks.onRetry?.(attempt + 1, waitMs, error);
|
|
557
|
-
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
throw new TransportError("Retry loop exhausted", 500, true);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// ../data-sdk-core/src/storage.ts
|
|
564
|
-
function memoryStorage() {
|
|
565
|
-
const store = /* @__PURE__ */ new Map();
|
|
566
|
-
return {
|
|
567
|
-
getItem: (key) => store.get(key) ?? null,
|
|
568
|
-
setItem: (key, value) => {
|
|
569
|
-
store.set(key, value);
|
|
570
|
-
},
|
|
571
|
-
removeItem: (key) => {
|
|
572
|
-
store.delete(key);
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
function safeStorage(backend, onError) {
|
|
577
|
-
return {
|
|
578
|
-
getItem: (key) => {
|
|
579
|
-
try {
|
|
580
|
-
return backend.getItem(key);
|
|
581
|
-
} catch (error) {
|
|
582
|
-
onError?.("getItem", error);
|
|
583
|
-
return null;
|
|
584
|
-
}
|
|
585
|
-
},
|
|
586
|
-
setItem: (key, value) => {
|
|
587
|
-
try {
|
|
588
|
-
backend.setItem(key, value);
|
|
589
|
-
} catch (error) {
|
|
590
|
-
onError?.("setItem", error);
|
|
591
|
-
}
|
|
592
|
-
},
|
|
593
|
-
removeItem: (key) => {
|
|
594
|
-
try {
|
|
595
|
-
backend.removeItem(key);
|
|
596
|
-
} catch (error) {
|
|
597
|
-
onError?.("removeItem", error);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
function prefixedStorage(backend, prefix) {
|
|
603
|
-
return {
|
|
604
|
-
getItem: (key) => backend.getItem(prefix + key),
|
|
605
|
-
setItem: (key, value) => backend.setItem(prefix + key, value),
|
|
606
|
-
removeItem: (key) => backend.removeItem(prefix + key)
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// ../data-sdk-core/src/transport.ts
|
|
611
|
-
import {
|
|
612
|
-
CLIENT_ID_HEADER,
|
|
613
|
-
CLIENT_SECRET_HEADER,
|
|
614
|
-
CONTRACT_VERSION,
|
|
615
|
-
CONTRACT_VERSION_HEADER,
|
|
616
|
-
DEVICE_ID_HEADER,
|
|
617
|
-
SESSION_ID_HEADER,
|
|
618
|
-
WRITE_KEY_HEADER
|
|
619
|
-
} from "@reopt-ai/data-contract/identity";
|
|
620
|
-
|
|
621
|
-
// ../data-sdk-core/src/types.ts
|
|
622
|
-
function isWriteKeyAuth(auth) {
|
|
623
|
-
return "writeKey" in auth;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// ../data-sdk-core/src/transport.ts
|
|
627
|
-
function readIngestCounts(rawBody) {
|
|
628
|
-
let body;
|
|
629
|
-
try {
|
|
630
|
-
body = JSON.parse(rawBody);
|
|
631
|
-
} catch (error) {
|
|
632
|
-
throw new TransportError(
|
|
633
|
-
`Track response did not match contract: ${error instanceof Error ? error.message : String(error)}`,
|
|
634
|
-
409,
|
|
635
|
-
true
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
const record = typeof body === "object" && body !== null ? body : null;
|
|
639
|
-
const count = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
640
|
-
if (!record || record.status !== "ok" && record.status !== "accepted" || !count(record.accepted) || !count(record.duplicates) || !Array.isArray(record.rejected)) {
|
|
641
|
-
throw new TransportError("Track response did not match contract: unexpected shape", 409, true);
|
|
642
|
-
}
|
|
643
|
-
const session = record.session;
|
|
644
|
-
const credential = session && typeof session.id === "string" && typeof session.token === "string" ? { id: session.id, token: session.token } : void 0;
|
|
645
|
-
return {
|
|
646
|
-
accepted: record.accepted,
|
|
647
|
-
duplicates: record.duplicates,
|
|
648
|
-
rejected: record.rejected.length,
|
|
649
|
-
...credential ? { session: credential } : {}
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
function resolveBaseUrl(value) {
|
|
653
|
-
if (!value) {
|
|
654
|
-
throw new Error("[reopt] `baseUrl` is required (your reopt-data origin, or the /ingest proxy prefix)");
|
|
655
|
-
}
|
|
656
|
-
return value.replace(/\/+$/, "");
|
|
657
|
-
}
|
|
658
|
-
function createTransport(options) {
|
|
659
|
-
const url = `${resolveBaseUrl(options.baseUrl)}/api/track`;
|
|
660
|
-
const headers = (identity) => {
|
|
661
|
-
const result = { "Content-Type": "application/json" };
|
|
662
|
-
const deviceId = identity?.deviceId ?? options.getDeviceId();
|
|
663
|
-
if (deviceId) result[DEVICE_ID_HEADER] = deviceId;
|
|
664
|
-
const sessionId = identity?.sessionId !== void 0 ? identity.sessionId : options.getSessionId?.();
|
|
665
|
-
if (sessionId) result[SESSION_ID_HEADER] = sessionId;
|
|
666
|
-
if (isWriteKeyAuth(options.auth)) {
|
|
667
|
-
result[WRITE_KEY_HEADER] = options.auth.writeKey;
|
|
668
|
-
} else {
|
|
669
|
-
result[CLIENT_ID_HEADER] = options.auth.clientId;
|
|
670
|
-
result[CLIENT_SECRET_HEADER] = options.auth.clientSecret;
|
|
671
|
-
}
|
|
672
|
-
return result;
|
|
673
|
-
};
|
|
674
|
-
let reportedVersion;
|
|
675
|
-
const send = async (batch, sendOptions = {}) => {
|
|
676
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
677
|
-
if (!fetchImpl) {
|
|
678
|
-
throw new TransportError("No fetch implementation available", void 0, true);
|
|
679
|
-
}
|
|
680
|
-
const response = await fetchImpl(url, {
|
|
681
|
-
method: "POST",
|
|
682
|
-
headers: headers(sendOptions.identity),
|
|
683
|
-
body: JSON.stringify(batch),
|
|
684
|
-
...sendOptions.keepalive ? { keepalive: true } : {}
|
|
685
|
-
});
|
|
686
|
-
const rawBody = await response.text();
|
|
687
|
-
if (!response.ok) {
|
|
688
|
-
let code;
|
|
689
|
-
try {
|
|
690
|
-
const parsed = JSON.parse(rawBody);
|
|
691
|
-
if (typeof parsed.code === "string") code = parsed.code;
|
|
692
|
-
} catch {
|
|
693
|
-
}
|
|
694
|
-
const preserveBatch = code === "quota_exceeded";
|
|
695
|
-
const effectiveStatus = code === "quota_exceeded" ? 402 : response.status;
|
|
696
|
-
throw new TransportError(`Failed to send batch: ${rawBody}`, effectiveStatus, preserveBatch);
|
|
697
|
-
}
|
|
698
|
-
const serverVersion = response.headers.get(CONTRACT_VERSION_HEADER);
|
|
699
|
-
if (serverVersion !== CONTRACT_VERSION && reportedVersion !== serverVersion) {
|
|
700
|
-
reportedVersion = serverVersion;
|
|
701
|
-
options.onVersionMismatch?.(serverVersion);
|
|
702
|
-
}
|
|
703
|
-
const counts = readIngestCounts(rawBody);
|
|
704
|
-
if (counts.accepted + counts.duplicates + counts.rejected !== batch.length) {
|
|
705
|
-
throw new TransportError("Track response counts do not reconcile with the submitted batch", 409, true);
|
|
706
|
-
}
|
|
707
|
-
return {
|
|
708
|
-
sent: counts.accepted + counts.duplicates,
|
|
709
|
-
failed: counts.rejected,
|
|
710
|
-
...counts.session ? { session: counts.session } : {}
|
|
711
|
-
};
|
|
712
|
-
};
|
|
713
|
-
return { url, headers, send };
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// ../data-sdk-core/src/client.ts
|
|
717
|
-
var DEFAULT_BATCH = {
|
|
718
|
-
size: 100,
|
|
719
|
-
intervalMs: 1e3,
|
|
720
|
-
maxBytes: 4e5
|
|
721
|
-
};
|
|
722
|
-
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
723
|
-
var DEFAULT_MAX_QUEUE_SIZE = 1e4;
|
|
724
|
-
var DEFAULT_STORAGE_PREFIX = "reopt_";
|
|
725
|
-
var ReoptCore = class {
|
|
726
|
-
runtime;
|
|
727
|
-
transport;
|
|
728
|
-
queue;
|
|
729
|
-
consent;
|
|
730
|
-
breaker;
|
|
731
|
-
retry;
|
|
732
|
-
batch;
|
|
733
|
-
factory;
|
|
734
|
-
debug;
|
|
735
|
-
deviceId;
|
|
736
|
-
profileId = null;
|
|
737
|
-
globalProperties = {};
|
|
738
|
-
activeFlush = null;
|
|
739
|
-
flushTimeout = null;
|
|
740
|
-
removeFlushSignal = null;
|
|
741
|
-
closed = false;
|
|
742
|
-
/** Bumped by `reset()`; a batch from an older generation is never requeued. */
|
|
743
|
-
generation = 0;
|
|
744
|
-
lastFlushRequeued = false;
|
|
745
|
-
constructor(config) {
|
|
746
|
-
this.runtime = config.runtime;
|
|
747
|
-
this.debug = config.debug ?? false;
|
|
748
|
-
this.deviceId = config.runtime.deviceId;
|
|
749
|
-
const now = config.runtime.now ?? (() => Date.now());
|
|
750
|
-
this.factory = { now, createId: config.runtime.createId ?? (() => uuidv7(now())) };
|
|
751
|
-
const rawStorage = config.runtime.storage;
|
|
752
|
-
const storage = rawStorage ? prefixedStorage(
|
|
753
|
-
safeStorage(rawStorage, (operation) => this.log(`storage ${operation} failed`)),
|
|
754
|
-
config.storagePrefix ?? DEFAULT_STORAGE_PREFIX
|
|
755
|
-
) : void 0;
|
|
756
|
-
this.batch = {
|
|
757
|
-
size: config.batch?.size ?? DEFAULT_BATCH.size,
|
|
758
|
-
intervalMs: config.batch?.intervalMs ?? DEFAULT_BATCH.intervalMs,
|
|
759
|
-
maxBytes: config.batch?.maxBytes ?? DEFAULT_BATCH.maxBytes
|
|
760
|
-
};
|
|
761
|
-
this.retry = resolveRetryConfig(config.retry);
|
|
762
|
-
this.breaker = new CircuitBreaker(config.circuitBreaker, now);
|
|
763
|
-
this.transport = createTransport({
|
|
764
|
-
baseUrl: config.baseUrl,
|
|
765
|
-
auth: config.auth,
|
|
766
|
-
getDeviceId: () => this.deviceId,
|
|
767
|
-
getSessionId: config.runtime.getSessionId,
|
|
768
|
-
fetch: config.runtime.fetch,
|
|
769
|
-
onVersionMismatch: (serverVersion) => this.log("server contract version", serverVersion)
|
|
770
|
-
});
|
|
771
|
-
this.consent = new ConsentManager(config.consent, storage);
|
|
772
|
-
this.queue = new EventQueue({
|
|
773
|
-
maxSize: config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
|
|
774
|
-
storage,
|
|
775
|
-
persist: config.enableOfflineBuffer ?? storage !== void 0,
|
|
776
|
-
normalize: (entry) => normalizePersistedEvent(this.factory, entry),
|
|
777
|
-
log: (...args) => this.log(...args)
|
|
778
|
-
});
|
|
779
|
-
const restored = this.queue.restore();
|
|
780
|
-
if (restored > 0) {
|
|
781
|
-
this.log(`restored ${restored}`);
|
|
782
|
-
this.scheduleFlush();
|
|
783
|
-
}
|
|
784
|
-
const autoFlush = config.autoFlushOnUnload ?? config.runtime.onFlushSignal !== void 0;
|
|
785
|
-
if (autoFlush && config.runtime.onFlushSignal) {
|
|
786
|
-
this.removeFlushSignal = config.runtime.onFlushSignal(() => this.flushOnSignal());
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
log(...args) {
|
|
790
|
-
if (this.debug) {
|
|
791
|
-
console.log("[reopt]", ...args);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
// --- Identity ---
|
|
795
|
-
getDeviceId() {
|
|
796
|
-
return this.deviceId;
|
|
797
|
-
}
|
|
798
|
-
/** Swap the device id, e.g. after a server bootstrap or a `reset()`. */
|
|
799
|
-
setDeviceId(deviceId) {
|
|
800
|
-
this.deviceId = deviceId;
|
|
801
|
-
}
|
|
802
|
-
setProfileId(profileId) {
|
|
803
|
-
this.profileId = profileId;
|
|
804
|
-
this.log("profile", profileId);
|
|
805
|
-
}
|
|
806
|
-
getProfileId() {
|
|
807
|
-
return this.profileId;
|
|
808
|
-
}
|
|
809
|
-
// --- Consent ---
|
|
810
|
-
setConsent(category, allowed) {
|
|
811
|
-
this.consent.set(category, allowed);
|
|
812
|
-
this.log("consent", category, allowed);
|
|
813
|
-
}
|
|
814
|
-
getConsent(category) {
|
|
815
|
-
return this.consent.get(category);
|
|
816
|
-
}
|
|
817
|
-
setAllConsent(allowed) {
|
|
818
|
-
this.consent.setAll(allowed);
|
|
819
|
-
this.log("consent all", allowed);
|
|
820
|
-
}
|
|
821
|
-
getConsentState() {
|
|
822
|
-
return this.consent.snapshot();
|
|
823
|
-
}
|
|
824
|
-
replaceConsentState(state) {
|
|
825
|
-
this.consent.replace(state);
|
|
826
|
-
}
|
|
827
|
-
pauseTracking() {
|
|
828
|
-
this.consent.pause();
|
|
829
|
-
this.log("paused");
|
|
830
|
-
}
|
|
831
|
-
resumeTracking() {
|
|
832
|
-
this.consent.resume();
|
|
833
|
-
this.log("resumed");
|
|
834
|
-
if (this.queue.length > 0) this.scheduleFlush();
|
|
835
|
-
}
|
|
836
|
-
// --- Global properties ---
|
|
837
|
-
/**
|
|
838
|
-
* Properties attached to every `track` event (and so every automatic
|
|
839
|
-
* event) from now on. An event's own properties win on conflict. This is
|
|
840
|
-
* how a host adds its breakdown axis — a `page_id`, a tenant — to
|
|
841
|
-
* `$web_vitals` and `$pageleave`, which the SDK otherwise stamps with
|
|
842
|
-
* only a path.
|
|
843
|
-
*/
|
|
844
|
-
register(properties) {
|
|
845
|
-
this.globalProperties = { ...this.globalProperties, ...properties };
|
|
846
|
-
}
|
|
847
|
-
unregister(...keys) {
|
|
848
|
-
for (const key of keys) delete this.globalProperties[key];
|
|
849
|
-
}
|
|
850
|
-
getGlobalProperties() {
|
|
851
|
-
return { ...this.globalProperties };
|
|
852
|
-
}
|
|
853
|
-
// --- Events ---
|
|
854
|
-
/**
|
|
855
|
-
* `options.identity` files the event under another visitor than the
|
|
856
|
-
* runtime's — the server SDK's way of serving many requests from one
|
|
857
|
-
* engine. Such an event never reads or writes the engine's own profile:
|
|
858
|
-
* that belongs to the runtime's visitor, not the request's.
|
|
859
|
-
*/
|
|
860
|
-
track(options) {
|
|
861
|
-
const merged = Object.keys(this.globalProperties).length > 0 ? { ...options, properties: { ...this.globalProperties, ...options.properties } } : options;
|
|
862
|
-
const event = buildTrackEvent(this.factory, merged, options.identity ? null : this.profileId);
|
|
863
|
-
return this.enqueue(event, options.consentCategory ?? DEFAULT_CONSENT_CATEGORY, options.identity);
|
|
864
|
-
}
|
|
865
|
-
identify(options) {
|
|
866
|
-
if (!options.identity) this.profileId = options.profileId;
|
|
867
|
-
const event = buildIdentifyEvent(this.factory, options);
|
|
868
|
-
return this.enqueue(event, options.consentCategory ?? DEFAULT_CONSENT_CATEGORY, options.identity);
|
|
869
|
-
}
|
|
870
|
-
increment(options) {
|
|
871
|
-
const event = buildCounterEvent(this.factory, "increment", options, options.identity ? null : this.profileId);
|
|
872
|
-
return this.enqueue(event, options.consentCategory ?? DEFAULT_CONSENT_CATEGORY, options.identity);
|
|
873
|
-
}
|
|
874
|
-
decrement(options) {
|
|
875
|
-
const event = buildCounterEvent(this.factory, "decrement", options, options.identity ? null : this.profileId);
|
|
876
|
-
return this.enqueue(event, options.consentCategory ?? DEFAULT_CONSENT_CATEGORY, options.identity);
|
|
877
|
-
}
|
|
878
|
-
pageView(options = {}) {
|
|
879
|
-
return this.track(buildPageViewOptions(options, this.runtime.getPageContext?.() ?? {}));
|
|
880
|
-
}
|
|
881
|
-
screenView(screenName, properties, identity) {
|
|
882
|
-
return this.track({ name: "$screen_view", properties: { screen_name: screenName, ...properties }, identity });
|
|
883
|
-
}
|
|
884
|
-
// --- Queue ---
|
|
885
|
-
get pending() {
|
|
886
|
-
return this.queue.length;
|
|
887
|
-
}
|
|
888
|
-
enqueue(event, category, identity) {
|
|
889
|
-
const blockReason = this.consent.enqueueBlockReason(category);
|
|
890
|
-
if (blockReason) {
|
|
891
|
-
this.log("dropped:", blockReason);
|
|
892
|
-
return { eventId: event.eventId, queued: false, reason: blockReason };
|
|
893
|
-
}
|
|
894
|
-
const validation = validateEvent(event);
|
|
895
|
-
if (!validation.valid) {
|
|
896
|
-
this.log("invalid:", validation.errors);
|
|
897
|
-
return { eventId: event.eventId, queued: false, reason: "validation_failed", errors: validation.errors };
|
|
898
|
-
}
|
|
899
|
-
if (serializedSize(event) + 2 > this.batch.maxBytes) {
|
|
900
|
-
this.log("too large");
|
|
901
|
-
return {
|
|
902
|
-
eventId: event.eventId,
|
|
903
|
-
queued: false,
|
|
904
|
-
reason: "payload_too_large",
|
|
905
|
-
errors: [{ field: "payload", message: `serialized event exceeds ${this.batch.maxBytes} bytes` }]
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
const entry = identity ? { event, identity } : { event };
|
|
909
|
-
const dropped = this.queue.push(entry);
|
|
910
|
-
if (dropped > 0) this.log("queue full, dropped", dropped);
|
|
911
|
-
if (this.queue.length >= this.batch.size) {
|
|
912
|
-
void this.flush();
|
|
913
|
-
} else {
|
|
914
|
-
this.scheduleFlush();
|
|
915
|
-
}
|
|
916
|
-
return { eventId: event.eventId, queued: true };
|
|
917
|
-
}
|
|
918
|
-
scheduleFlush() {
|
|
919
|
-
if (this.flushTimeout || this.closed) return;
|
|
920
|
-
this.flushTimeout = setTimeout(() => {
|
|
921
|
-
this.flushTimeout = null;
|
|
922
|
-
void this.flush();
|
|
923
|
-
}, this.batch.intervalMs);
|
|
924
|
-
}
|
|
925
|
-
clearScheduledFlush() {
|
|
926
|
-
if (!this.flushTimeout) return;
|
|
927
|
-
clearTimeout(this.flushTimeout);
|
|
928
|
-
this.flushTimeout = null;
|
|
929
|
-
}
|
|
930
|
-
// --- Flush ---
|
|
931
|
-
flush(options = {}) {
|
|
932
|
-
return options.drain ? this.flushAll(options) : this.flushExclusive(options);
|
|
933
|
-
}
|
|
934
|
-
/**
|
|
935
|
-
* The page (or process) is going away. Persist first so nothing is lost if
|
|
936
|
-
* the request never completes, then send what can be sent with keepalive.
|
|
937
|
-
*/
|
|
938
|
-
flushOnSignal() {
|
|
939
|
-
this.queue.persistNow();
|
|
940
|
-
if (this.queue.length === 0 || this.consent.flushBlockReason()) return;
|
|
941
|
-
const lastChance = this.runtime.sendLastChance;
|
|
942
|
-
if (lastChance) {
|
|
943
|
-
const batch = this.queue.take(this.batch.size, KEEPALIVE_MAX_BYTES);
|
|
944
|
-
const body = JSON.stringify(batch.map((entry) => entry.event));
|
|
945
|
-
if (lastChance(this.transport.url, body, this.transport.headers(batch[0]?.identity))) {
|
|
946
|
-
this.queue.settle();
|
|
947
|
-
this.queue.persistNow();
|
|
948
|
-
if (this.queue.length > 0) this.flushOnSignal();
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
this.queue.requeue(batch);
|
|
952
|
-
}
|
|
953
|
-
while (this.queue.length > 0) {
|
|
954
|
-
const batch = this.queue.take(this.batch.size, Math.min(this.batch.maxBytes, KEEPALIVE_MAX_BYTES));
|
|
955
|
-
const events = batch.map((entry) => entry.event);
|
|
956
|
-
const identity = this.identityFor(batch);
|
|
957
|
-
this.queue.settle();
|
|
958
|
-
void this.transport.send(events, { keepalive: true, identity }).catch((error) => {
|
|
959
|
-
this.log("keepalive failed", error);
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
this.queue.persistNow();
|
|
963
|
-
}
|
|
964
|
-
/**
|
|
965
|
-
* The identity a batch is sent under, fixed when it leaves the queue. A
|
|
966
|
-
* `reset()` during a retry must not re-address the previous visitor's
|
|
967
|
-
* events to the new device.
|
|
968
|
-
*/
|
|
969
|
-
identityFor(batch) {
|
|
970
|
-
return batch[0]?.identity ?? { deviceId: this.deviceId };
|
|
971
|
-
}
|
|
972
|
-
flushExclusive(options) {
|
|
973
|
-
if (this.activeFlush) return this.activeFlush;
|
|
974
|
-
const activeFlush = this.flushOnce(options).finally(() => {
|
|
975
|
-
if (this.activeFlush === activeFlush) this.activeFlush = null;
|
|
976
|
-
});
|
|
977
|
-
this.activeFlush = activeFlush;
|
|
978
|
-
return activeFlush;
|
|
979
|
-
}
|
|
980
|
-
async flushAll(options) {
|
|
981
|
-
let sent = 0;
|
|
982
|
-
let failed = 0;
|
|
983
|
-
let status = "idle";
|
|
984
|
-
while (this.queue.length > 0) {
|
|
985
|
-
const pendingBefore = this.queue.length;
|
|
986
|
-
const result = await this.flushExclusive({ ...options, drain: true });
|
|
987
|
-
sent += result.sent;
|
|
988
|
-
failed += result.failed;
|
|
989
|
-
status = result.status;
|
|
990
|
-
const madeProgress = !this.lastFlushRequeued && (result.sent > 0 || this.queue.length < pendingBefore);
|
|
991
|
-
if (!madeProgress) return { status, sent, failed, pending: result.pending };
|
|
992
|
-
}
|
|
993
|
-
return { status: sent > 0 ? "success" : status, sent, failed, pending: this.queue.length };
|
|
994
|
-
}
|
|
995
|
-
async flushOnce(options) {
|
|
996
|
-
if (this.queue.length === 0) {
|
|
997
|
-
this.clearScheduledFlush();
|
|
998
|
-
return { status: "idle", sent: 0, failed: 0, pending: 0 };
|
|
999
|
-
}
|
|
1000
|
-
if (this.consent.flushBlockReason()) {
|
|
1001
|
-
return { status: "skipped", sent: 0, failed: 0, pending: this.queue.length };
|
|
1002
|
-
}
|
|
1003
|
-
if (!this.breaker.allowRequest()) {
|
|
1004
|
-
this.log("breaker open");
|
|
1005
|
-
return { status: "skipped", sent: 0, failed: 0, pending: this.queue.length };
|
|
1006
|
-
}
|
|
1007
|
-
if (this.breaker.getState() === "half-open") {
|
|
1008
|
-
this.log("breaker half-open");
|
|
1009
|
-
}
|
|
1010
|
-
const maxBytes = options.keepalive ? Math.min(this.batch.maxBytes, KEEPALIVE_MAX_BYTES) : this.batch.maxBytes;
|
|
1011
|
-
const batch = this.queue.take(this.batch.size, maxBytes);
|
|
1012
|
-
const events = batch.map((entry) => entry.event);
|
|
1013
|
-
const identity = this.identityFor(batch);
|
|
1014
|
-
const generation = this.generation;
|
|
1015
|
-
this.lastFlushRequeued = false;
|
|
1016
|
-
try {
|
|
1017
|
-
const delivery = await withRetry(() => this.transport.send(events, { ...options, identity }), this.retry, {
|
|
1018
|
-
onRetry: (attempt, waitMs) => this.log("retry", attempt, Math.round(waitMs))
|
|
1019
|
-
});
|
|
1020
|
-
this.log("flushed", delivery.sent, delivery.failed);
|
|
1021
|
-
if (delivery.session && !batch[0]?.identity) {
|
|
1022
|
-
this.runtime.onSession?.(delivery.session);
|
|
1023
|
-
}
|
|
1024
|
-
if (this.breaker.getState() === "half-open") this.log("breaker closed");
|
|
1025
|
-
this.breaker.recordSuccess();
|
|
1026
|
-
this.queue.settle();
|
|
1027
|
-
this.queue.persistNow();
|
|
1028
|
-
this.continueOrSettle(options);
|
|
1029
|
-
return {
|
|
1030
|
-
status: delivery.failed > 0 ? "failed" : "success",
|
|
1031
|
-
sent: delivery.sent,
|
|
1032
|
-
failed: delivery.failed,
|
|
1033
|
-
pending: this.queue.length
|
|
1034
|
-
};
|
|
1035
|
-
} catch (error) {
|
|
1036
|
-
const preserve = error instanceof TransportError && error.preserveBatch;
|
|
1037
|
-
if (!isRetryableTransportError(error) && !preserve) {
|
|
1038
|
-
this.log("rejected", batch.length, error);
|
|
1039
|
-
this.breaker.recordRejection();
|
|
1040
|
-
this.queue.settle();
|
|
1041
|
-
this.queue.persistNow();
|
|
1042
|
-
this.continueOrSettle(options);
|
|
1043
|
-
return { status: "failed", sent: 0, failed: batch.length, pending: this.queue.length };
|
|
1044
|
-
}
|
|
1045
|
-
this.log("flush failed", error);
|
|
1046
|
-
if (generation !== this.generation) {
|
|
1047
|
-
this.queue.settle();
|
|
1048
|
-
return { status: "failed", sent: 0, failed: batch.length, pending: this.queue.length };
|
|
1049
|
-
}
|
|
1050
|
-
this.lastFlushRequeued = true;
|
|
1051
|
-
this.queue.requeue(batch);
|
|
1052
|
-
this.queue.persistNow();
|
|
1053
|
-
if (!isRetryableTransportError(error)) {
|
|
1054
|
-
return { status: "failed", sent: 0, failed: batch.length, pending: this.queue.length };
|
|
1055
|
-
}
|
|
1056
|
-
if (this.breaker.recordFailure()) {
|
|
1057
|
-
this.log("breaker opened");
|
|
1058
|
-
}
|
|
1059
|
-
return { status: "failed", sent: 0, failed: batch.length, pending: this.queue.length };
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
continueOrSettle(options) {
|
|
1063
|
-
if (!options.drain && this.queue.length > 0) {
|
|
1064
|
-
this.scheduleFlush();
|
|
1065
|
-
} else if (this.queue.length === 0) {
|
|
1066
|
-
this.clearScheduledFlush();
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
// --- Lifecycle ---
|
|
1070
|
-
/**
|
|
1071
|
-
* Forget who this is: profile, queued events and — when the runtime can
|
|
1072
|
-
* mint one — the device id. A shared computer that logs out must not keep
|
|
1073
|
-
* attributing the next person to the previous one.
|
|
1074
|
-
*/
|
|
1075
|
-
reset() {
|
|
1076
|
-
this.generation += 1;
|
|
1077
|
-
this.profileId = null;
|
|
1078
|
-
this.globalProperties = {};
|
|
1079
|
-
this.queue.clear();
|
|
1080
|
-
this.queue.settle();
|
|
1081
|
-
this.queue.persistNow();
|
|
1082
|
-
const regenerate = this.runtime.regenerateDeviceId;
|
|
1083
|
-
if (regenerate) {
|
|
1084
|
-
this.deviceId = regenerate();
|
|
1085
|
-
}
|
|
1086
|
-
this.log("reset");
|
|
1087
|
-
}
|
|
1088
|
-
async close() {
|
|
1089
|
-
this.closed = true;
|
|
1090
|
-
this.removeFlushSignal?.();
|
|
1091
|
-
this.removeFlushSignal = null;
|
|
1092
|
-
this.clearScheduledFlush();
|
|
1093
|
-
this.queue.dispose();
|
|
1094
|
-
this.queue.persistNow();
|
|
1095
|
-
const result = await this.flush({ drain: true });
|
|
1096
|
-
this.clearScheduledFlush();
|
|
1097
|
-
return result;
|
|
1098
|
-
}
|
|
1099
|
-
};
|
|
1100
|
-
|
|
1101
|
-
// src/capture/pageleave.ts
|
|
1102
|
-
import { scrollDepthBucket } from "@reopt-ai/data-contract/events";
|
|
1103
|
-
var PageLeaveTracker = class {
|
|
1104
|
-
current = null;
|
|
1105
|
-
now;
|
|
1106
|
-
constructor(now) {
|
|
1107
|
-
this.now = now;
|
|
1108
|
-
}
|
|
1109
|
-
/** Called on every `$pageview`. Returns the `$pageleave` for the page it replaces, if any. */
|
|
1110
|
-
enter(path, origin, scrollDepth) {
|
|
1111
|
-
const left = this.leave(scrollDepth);
|
|
1112
|
-
this.current = { path, origin, startedAt: this.now() };
|
|
1113
|
-
return left;
|
|
1114
|
-
}
|
|
1115
|
-
/** Called when the page is hidden. Returns the leave, or `null` if nothing was entered. */
|
|
1116
|
-
leave(scrollDepth) {
|
|
1117
|
-
const page = this.current;
|
|
1118
|
-
if (!page) return null;
|
|
1119
|
-
this.current = null;
|
|
1120
|
-
const seconds = Math.max(0, Math.round((this.now() - page.startedAt) / 1e3));
|
|
1121
|
-
return {
|
|
1122
|
-
path: page.path,
|
|
1123
|
-
origin: page.origin,
|
|
1124
|
-
duration: seconds,
|
|
1125
|
-
...scrollDepth !== null ? { scroll_depth: scrollDepth, scroll_depth_bucket: scrollDepthBucket(scrollDepth) } : {}
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
/**
|
|
1129
|
-
* After the page was hidden and the visitor came back, the same page is
|
|
1130
|
-
* on screen again with a fresh timer.
|
|
1131
|
-
*/
|
|
1132
|
-
resume() {
|
|
1133
|
-
}
|
|
1134
|
-
peek() {
|
|
1135
|
-
return this.current;
|
|
1136
|
-
}
|
|
1137
|
-
};
|
|
1138
|
-
|
|
1139
|
-
// src/capture/scroll-depth.ts
|
|
1140
|
-
function currentDepth() {
|
|
1141
|
-
if (typeof window === "undefined" || typeof document === "undefined") return null;
|
|
1142
|
-
const root = document.documentElement;
|
|
1143
|
-
const scrollable = Math.max(root.scrollHeight, document.body?.scrollHeight ?? 0) - window.innerHeight;
|
|
1144
|
-
if (!Number.isFinite(scrollable) || scrollable <= 0) return 100;
|
|
1145
|
-
const scrolled = window.scrollY ?? root.scrollTop ?? 0;
|
|
1146
|
-
return Math.max(0, Math.min(100, Math.round(scrolled / scrollable * 100)));
|
|
1147
|
-
}
|
|
1148
|
-
function startScrollDepth() {
|
|
1149
|
-
let max = null;
|
|
1150
|
-
const sample = () => {
|
|
1151
|
-
const depth = currentDepth();
|
|
1152
|
-
if (depth !== null && (max === null || depth > max)) max = depth;
|
|
1153
|
-
};
|
|
1154
|
-
if (typeof window !== "undefined") {
|
|
1155
|
-
window.addEventListener("scroll", sample, { passive: true });
|
|
1156
|
-
window.addEventListener("resize", sample, { passive: true });
|
|
1157
|
-
}
|
|
1158
|
-
sample();
|
|
1159
|
-
return {
|
|
1160
|
-
read: () => {
|
|
1161
|
-
sample();
|
|
1162
|
-
return max;
|
|
1163
|
-
},
|
|
1164
|
-
reset: () => {
|
|
1165
|
-
max = null;
|
|
1166
|
-
sample();
|
|
1167
|
-
},
|
|
1168
|
-
stop: () => {
|
|
1169
|
-
if (typeof window === "undefined") return;
|
|
1170
|
-
window.removeEventListener("scroll", sample);
|
|
1171
|
-
window.removeEventListener("resize", sample);
|
|
1172
|
-
}
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// src/capture/web-vitals.ts
|
|
1177
|
-
function webVitalProperties(metric, path) {
|
|
1178
|
-
return {
|
|
1179
|
-
metric_name: metric.name,
|
|
1180
|
-
metric_id: metric.id,
|
|
1181
|
-
value: metric.value,
|
|
1182
|
-
...metric.delta !== void 0 ? { delta: metric.delta } : {},
|
|
1183
|
-
...metric.rating ? { rating: metric.rating } : {},
|
|
1184
|
-
...metric.navigationType ? { navigation_type: metric.navigationType } : {},
|
|
1185
|
-
path
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// src/config.ts
|
|
1190
|
-
function resolveCapture(config, pageviewDefault) {
|
|
1191
|
-
return {
|
|
1192
|
-
pageview: config?.pageview ?? pageviewDefault,
|
|
1193
|
-
pageleave: config?.pageleave ?? true,
|
|
1194
|
-
scrollDepth: config?.scrollDepth ?? true,
|
|
1195
|
-
exceptions: config?.exceptions ?? false
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1198
|
-
function resolveTracingHosts(option) {
|
|
1199
|
-
if (!option) return [];
|
|
1200
|
-
if (Array.isArray(option)) return option;
|
|
1201
|
-
if (typeof location === "undefined" || !location.hostname) return [];
|
|
1202
|
-
return [location.hostname];
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// src/identity/device.ts
|
|
1206
|
-
import {
|
|
1207
|
-
consentCookieName,
|
|
1208
|
-
deviceCookieName,
|
|
1209
|
-
isValidIdentityId,
|
|
1210
|
-
parseConsentCookie,
|
|
1211
|
-
parseDeviceCookie,
|
|
1212
|
-
serializeConsentCookie,
|
|
1213
|
-
serializeDeviceCookie
|
|
1214
|
-
} from "@reopt-ai/data-contract/identity";
|
|
1215
|
-
|
|
1216
|
-
// src/identity/cookie-storage.ts
|
|
1217
|
-
import { COOKIE_MAX_AGE_SECONDS } from "@reopt-ai/data-contract/identity";
|
|
1218
|
-
function readCookie(name) {
|
|
1219
|
-
if (typeof document === "undefined") return null;
|
|
1220
|
-
const prefix = `${name}=`;
|
|
1221
|
-
for (const part of document.cookie.split(";")) {
|
|
1222
|
-
const trimmed = part.trim();
|
|
1223
|
-
if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length);
|
|
1224
|
-
}
|
|
1225
|
-
return null;
|
|
1226
|
-
}
|
|
1227
|
-
function writeCookie(name, value, options = {}) {
|
|
1228
|
-
if (typeof document === "undefined") return;
|
|
1229
|
-
const maxAge = options.maxAgeSeconds ?? COOKIE_MAX_AGE_SECONDS;
|
|
1230
|
-
let cookie = `${name}=${value}; Path=/; Max-Age=${maxAge}; SameSite=Lax`;
|
|
1231
|
-
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
1232
|
-
if (typeof location !== "undefined" && location.protocol === "https:") cookie += "; Secure";
|
|
1233
|
-
document.cookie = cookie;
|
|
1234
|
-
}
|
|
1235
|
-
function deleteCookie(name, options = {}) {
|
|
1236
|
-
if (typeof document === "undefined") return;
|
|
1237
|
-
let cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`;
|
|
1238
|
-
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
1239
|
-
document.cookie = cookie;
|
|
1240
|
-
}
|
|
1241
|
-
function cookiesAvailable() {
|
|
1242
|
-
if (typeof document === "undefined") return false;
|
|
1243
|
-
try {
|
|
1244
|
-
const probe = "reopt_probe";
|
|
1245
|
-
document.cookie = `${probe}=1; Path=/; Max-Age=60; SameSite=Lax`;
|
|
1246
|
-
const ok = readCookie(probe) === "1";
|
|
1247
|
-
document.cookie = `${probe}=; Path=/; Max-Age=0; SameSite=Lax`;
|
|
1248
|
-
return ok;
|
|
1249
|
-
} catch {
|
|
1250
|
-
return false;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// src/identity/device.ts
|
|
1255
|
-
var LEGACY_DEVICE_KEY = "device_id";
|
|
1256
|
-
function localStorageBackend() {
|
|
1257
|
-
try {
|
|
1258
|
-
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
1259
|
-
const storage = window.localStorage;
|
|
1260
|
-
const probe = "reopt_probe";
|
|
1261
|
-
storage.setItem(probe, "1");
|
|
1262
|
-
storage.removeItem(probe);
|
|
1263
|
-
return storage;
|
|
1264
|
-
} catch {
|
|
1265
|
-
return null;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
function makeStore(kind, writeKey, backend) {
|
|
1269
|
-
const deviceName = deviceCookieName(writeKey);
|
|
1270
|
-
const consentName = consentCookieName(writeKey);
|
|
1271
|
-
const readConsent = () => parseConsentCookie(backend.getItem(consentName));
|
|
1272
|
-
return {
|
|
1273
|
-
kind,
|
|
1274
|
-
readDevice: () => parseDeviceCookie(backend.getItem(deviceName)),
|
|
1275
|
-
writeDevice: (state) => backend.setItem(deviceName, serializeDeviceCookie(state)),
|
|
1276
|
-
clearDevice: () => backend.removeItem(deviceName),
|
|
1277
|
-
readConsent,
|
|
1278
|
-
writeConsent: (state) => backend.setItem(consentName, serializeConsentCookie(state)),
|
|
1279
|
-
consentBackend: () => ({
|
|
1280
|
-
getItem: () => {
|
|
1281
|
-
const state = readConsent();
|
|
1282
|
-
return state ? JSON.stringify(state) : null;
|
|
1283
|
-
},
|
|
1284
|
-
setItem: (_key, value) => backend.setItem(consentName, encodeURIComponent(value)),
|
|
1285
|
-
removeItem: () => backend.removeItem(consentName)
|
|
1286
|
-
})
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
function cookieBackend(cookie) {
|
|
1290
|
-
return {
|
|
1291
|
-
getItem: readCookie,
|
|
1292
|
-
setItem: (name, value) => writeCookie(name, value, cookie),
|
|
1293
|
-
removeItem: (name) => deleteCookie(name, cookie)
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
function createIdentityStore(writeKey, config) {
|
|
1297
|
-
const kind = config?.storage ?? "auto";
|
|
1298
|
-
const cookie = { domain: config?.cookieDomain, maxAgeSeconds: config?.cookieMaxAgeSeconds };
|
|
1299
|
-
if (kind === "cookie" || kind === "auto" && cookiesAvailable()) {
|
|
1300
|
-
return makeStore("cookie", writeKey, cookieBackend(cookie));
|
|
1301
|
-
}
|
|
1302
|
-
const local = kind === "memory" ? null : localStorageBackend();
|
|
1303
|
-
if (local) return makeStore("localStorage", writeKey, local);
|
|
1304
|
-
return makeStore("memory", writeKey, memoryStorage());
|
|
1305
|
-
}
|
|
1306
|
-
function resolveDeviceId(store, bootstrapDeviceId, storagePrefix) {
|
|
1307
|
-
const stored = store.readDevice();
|
|
1308
|
-
if (stored) return { deviceId: stored.deviceId, source: "store" };
|
|
1309
|
-
if (isValidIdentityId(bootstrapDeviceId)) {
|
|
1310
|
-
store.writeDevice({ deviceId: bootstrapDeviceId });
|
|
1311
|
-
return { deviceId: bootstrapDeviceId, source: "bootstrap" };
|
|
1312
|
-
}
|
|
1313
|
-
const local = localStorageBackend();
|
|
1314
|
-
const legacy = local?.getItem(`${storagePrefix}${LEGACY_DEVICE_KEY}`);
|
|
1315
|
-
if (isValidIdentityId(legacy)) {
|
|
1316
|
-
store.writeDevice({ deviceId: legacy });
|
|
1317
|
-
local?.removeItem(`${storagePrefix}${LEGACY_DEVICE_KEY}`);
|
|
1318
|
-
return { deviceId: legacy, source: "legacy" };
|
|
1319
|
-
}
|
|
1320
|
-
const generated = uuidv4();
|
|
1321
|
-
store.writeDevice({ deviceId: generated });
|
|
1322
|
-
return { deviceId: generated, source: "generated" };
|
|
1323
|
-
}
|
|
1324
|
-
function regenerateDeviceId(store) {
|
|
1325
|
-
const deviceId = uuidv4();
|
|
1326
|
-
store.writeDevice({ deviceId });
|
|
1327
|
-
return deviceId;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// src/client.ts
|
|
1331
|
-
var CLOCK_SKEW_THRESHOLD_MS = 3e4;
|
|
1332
|
-
var MAX_CLOCK_SKEW_MS = 24 * 60 * 60 * 1e3;
|
|
1333
|
-
function routedStorage(consentKey, consent, rest) {
|
|
1334
|
-
const pick = (key) => key === consentKey ? consent : rest;
|
|
1335
|
-
return {
|
|
1336
|
-
getItem: (key) => pick(key).getItem(key),
|
|
1337
|
-
setItem: (key, value) => pick(key).setItem(key, value),
|
|
1338
|
-
removeItem: (key) => pick(key).removeItem(key)
|
|
1339
|
-
};
|
|
1340
|
-
}
|
|
1341
|
-
function normalizedPath(normalize, pathname) {
|
|
1342
|
-
if (!normalize) return { path: pathname };
|
|
1343
|
-
try {
|
|
1344
|
-
const result = normalize(pathname);
|
|
1345
|
-
if (typeof result === "string") return { path: result || pathname };
|
|
1346
|
-
if (result && typeof result.path === "string" && result.path) return result;
|
|
1347
|
-
} catch {
|
|
1348
|
-
}
|
|
1349
|
-
return { path: pathname };
|
|
1350
|
-
}
|
|
1351
|
-
function pageContext(normalize) {
|
|
1352
|
-
const hasWindow = typeof window !== "undefined";
|
|
1353
|
-
const hasDocument = typeof document !== "undefined";
|
|
1354
|
-
const normalized = normalizedPath(normalize, hasWindow ? window.location.pathname : "/");
|
|
1355
|
-
return {
|
|
1356
|
-
path: normalized.path,
|
|
1357
|
-
origin: hasWindow ? window.location.origin : "",
|
|
1358
|
-
title: hasDocument ? document.title : "",
|
|
1359
|
-
referrer: hasDocument ? document.referrer : "",
|
|
1360
|
-
utm: hasWindow ? extractUTMParams(window.location.search) : {},
|
|
1361
|
-
...normalized.properties ? { properties: normalized.properties } : {}
|
|
1362
|
-
};
|
|
1363
|
-
}
|
|
1364
|
-
var DISABLED_BASE_URL = "https://reopt.invalid";
|
|
1365
|
-
var ReoptClient = class extends ReoptCore {
|
|
1366
|
-
writeKey;
|
|
1367
|
-
capture;
|
|
1368
|
-
store;
|
|
1369
|
-
pageLeave;
|
|
1370
|
-
scroll;
|
|
1371
|
-
uninstallers = [];
|
|
1372
|
-
torndown = false;
|
|
1373
|
-
session;
|
|
1374
|
-
optedOut;
|
|
1375
|
-
normalize;
|
|
1376
|
-
/** True when `writeKey` or `baseUrl` was missing: every call is a silent no-op. */
|
|
1377
|
-
disabled;
|
|
1378
|
-
constructor(config, defaults = { pageview: true }) {
|
|
1379
|
-
const disabled = !config.writeKey || !config.baseUrl;
|
|
1380
|
-
if (disabled) {
|
|
1381
|
-
console.warn(`[reopt] ${!config.writeKey ? "writeKey" : "baseUrl"} missing \u2014 analytics disabled`);
|
|
1382
|
-
}
|
|
1383
|
-
const writeKey = config.writeKey || "disabled";
|
|
1384
|
-
const storagePrefix = config.storagePrefix ?? DEFAULT_STORAGE_PREFIX;
|
|
1385
|
-
const rawStore = createIdentityStore(writeKey, { ...config.identity, ...disabled ? { storage: "memory" } : {} });
|
|
1386
|
-
const optedOut = {
|
|
1387
|
-
value: isOptedOut(rawStore.readConsent() ?? config.bootstrap?.consent ?? null) || config.consent?.defaultConsent === false
|
|
1388
|
-
};
|
|
1389
|
-
const store = {
|
|
1390
|
-
...rawStore,
|
|
1391
|
-
writeDevice: (state) => {
|
|
1392
|
-
if (!optedOut.value) rawStore.writeDevice(state);
|
|
1393
|
-
}
|
|
1394
|
-
};
|
|
1395
|
-
const identity = resolveDeviceId(store, config.bootstrap?.deviceId, storagePrefix);
|
|
1396
|
-
const now = correctedClock(config.bootstrap);
|
|
1397
|
-
const queueStorage = disabled ? memoryStorage() : config.queueStorage ?? localStorageBackend() ?? memoryStorage();
|
|
1398
|
-
const transportFetch = config.fetch ?? (typeof window !== "undefined" ? window.fetch.bind(window) : void 0);
|
|
1399
|
-
const box = {};
|
|
1400
|
-
const session = { header: store.readDevice()?.sessionId ?? null };
|
|
1401
|
-
const runtime = {
|
|
1402
|
-
deviceId: identity.deviceId,
|
|
1403
|
-
getSessionId: () => parseSessionHeader(session.header) ? session.header : null,
|
|
1404
|
-
onSession: (credential) => {
|
|
1405
|
-
session.header = formatSessionHeader(credential);
|
|
1406
|
-
const current = store.readDevice();
|
|
1407
|
-
store.writeDevice({ ...current, deviceId: current?.deviceId ?? identity.deviceId, sessionId: session.header });
|
|
1408
|
-
},
|
|
1409
|
-
storage: routedStorage(`${storagePrefix}consent`, store.consentBackend(), queueStorage),
|
|
1410
|
-
now,
|
|
1411
|
-
fetch: transportFetch,
|
|
1412
|
-
getPageContext: () => pageContext(config.normalizePath),
|
|
1413
|
-
regenerateDeviceId: () => regenerateDeviceId(store),
|
|
1414
|
-
onFlushSignal: (flush) => {
|
|
1415
|
-
if (typeof window === "undefined") return () => {
|
|
1416
|
-
};
|
|
1417
|
-
const onHide = () => {
|
|
1418
|
-
box.leave?.();
|
|
1419
|
-
flush();
|
|
1420
|
-
};
|
|
1421
|
-
const onVisibility = () => {
|
|
1422
|
-
if (document.visibilityState === "hidden") onHide();
|
|
1423
|
-
};
|
|
1424
|
-
window.addEventListener("pagehide", onHide);
|
|
1425
|
-
document.addEventListener("visibilitychange", onVisibility);
|
|
1426
|
-
return () => {
|
|
1427
|
-
window.removeEventListener("pagehide", onHide);
|
|
1428
|
-
document.removeEventListener("visibilitychange", onVisibility);
|
|
1429
|
-
};
|
|
1430
|
-
}
|
|
1431
|
-
};
|
|
1432
|
-
super({
|
|
1433
|
-
auth: { writeKey },
|
|
1434
|
-
baseUrl: disabled ? DISABLED_BASE_URL : config.baseUrl,
|
|
1435
|
-
runtime,
|
|
1436
|
-
debug: config.debug,
|
|
1437
|
-
batch: config.batch,
|
|
1438
|
-
maxQueueSize: config.maxQueueSize,
|
|
1439
|
-
retry: config.retry,
|
|
1440
|
-
circuitBreaker: config.circuitBreaker,
|
|
1441
|
-
consent: config.consent,
|
|
1442
|
-
storagePrefix
|
|
1443
|
-
});
|
|
1444
|
-
this.writeKey = config.writeKey;
|
|
1445
|
-
this.disabled = disabled;
|
|
1446
|
-
this.session = session;
|
|
1447
|
-
this.optedOut = optedOut;
|
|
1448
|
-
this.normalize = config.normalizePath;
|
|
1449
|
-
this.store = store;
|
|
1450
|
-
this.capture = resolveCapture(config.capture, defaults.pageview);
|
|
1451
|
-
this.pageLeave = new PageLeaveTracker(now);
|
|
1452
|
-
this.scroll = !disabled && this.capture.scrollDepth && typeof window !== "undefined" ? startScrollDepth() : null;
|
|
1453
|
-
if (disabled) {
|
|
1454
|
-
this.pauseTracking();
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
if (config.properties) this.register(config.properties);
|
|
1458
|
-
box.leave = () => this.emitPageLeave();
|
|
1459
|
-
this.log(`Device ${identity.deviceId} (${identity.source}, ${store.kind})`);
|
|
1460
|
-
if (config.bootstrap?.consent && !store.readConsent() && Object.keys(config.bootstrap.consent).length > 0) {
|
|
1461
|
-
this.replaceConsentState(config.bootstrap.consent);
|
|
1462
|
-
}
|
|
1463
|
-
const hosts = resolveTracingHosts(config.tracingHeaders);
|
|
1464
|
-
if (hosts.length > 0) {
|
|
1465
|
-
void import("./tracing-LC3NZND7.js").then(({ installTracingHeaders }) => {
|
|
1466
|
-
if (!this.torndown)
|
|
1467
|
-
this.uninstallers.push(installTracingHeaders({ hosts, getDeviceId: () => this.getDeviceId() }));
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
1470
|
-
if (this.capture.exceptions) {
|
|
1471
|
-
void import("./exceptions-NQHZUDYO.js").then(({ installExceptionCapture }) => {
|
|
1472
|
-
if (!this.torndown)
|
|
1473
|
-
this.uninstallers.push(installExceptionCapture((properties) => this.trackException(properties)));
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
if (typeof document !== "undefined") {
|
|
1477
|
-
const onVisible = () => {
|
|
1478
|
-
if (document.visibilityState === "visible" && !this.pageLeave.peek()) {
|
|
1479
|
-
const context = pageContext(this.normalize);
|
|
1480
|
-
this.pageLeave.enter(context.path ?? "/", context.origin ?? "", null);
|
|
1481
|
-
this.scroll?.reset();
|
|
1482
|
-
}
|
|
1483
|
-
};
|
|
1484
|
-
document.addEventListener("visibilitychange", onVisible);
|
|
1485
|
-
this.uninstallers.push(() => document.removeEventListener("visibilitychange", onVisible));
|
|
1486
|
-
}
|
|
1487
|
-
if (this.capture.pageview && typeof document !== "undefined") {
|
|
1488
|
-
this.pageView();
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
/** Which store identity ended up in — `cookie`, `localStorage` or `memory`. */
|
|
1492
|
-
get identityStorage() {
|
|
1493
|
-
return this.store.kind;
|
|
1494
|
-
}
|
|
1495
|
-
pageView(options = {}) {
|
|
1496
|
-
const context = pageContext(this.normalize);
|
|
1497
|
-
const path = options.path ?? context.path ?? "/";
|
|
1498
|
-
const origin = options.origin ?? context.origin ?? "";
|
|
1499
|
-
if (this.capture.pageleave) {
|
|
1500
|
-
const left = this.pageLeave.enter(path, origin, this.scroll?.read() ?? null);
|
|
1501
|
-
if (left) this.track({ name: "$pageleave", properties: left });
|
|
1502
|
-
}
|
|
1503
|
-
this.scroll?.reset();
|
|
1504
|
-
return super.pageView(options);
|
|
1505
|
-
}
|
|
1506
|
-
setConsent(category, allowed) {
|
|
1507
|
-
super.setConsent(category, allowed);
|
|
1508
|
-
this.syncOptOut();
|
|
1509
|
-
}
|
|
1510
|
-
setAllConsent(allowed) {
|
|
1511
|
-
super.setAllConsent(allowed);
|
|
1512
|
-
this.syncOptOut();
|
|
1513
|
-
}
|
|
1514
|
-
replaceConsentState(state) {
|
|
1515
|
-
super.replaceConsentState(state);
|
|
1516
|
-
this.syncOptOut();
|
|
1517
|
-
}
|
|
1518
|
-
/**
|
|
1519
|
-
* Refusing `analytics` removes the stored identity (the proxy does the
|
|
1520
|
-
* same on its side); granting it again writes the current device through
|
|
1521
|
-
* so the visit is attributable from that point on.
|
|
1522
|
-
*/
|
|
1523
|
-
syncOptOut() {
|
|
1524
|
-
const now = isOptedOut(this.getConsentState());
|
|
1525
|
-
if (now === this.optedOut.value) return;
|
|
1526
|
-
this.optedOut.value = now;
|
|
1527
|
-
if (now) {
|
|
1528
|
-
this.store.clearDevice();
|
|
1529
|
-
this.session.header = null;
|
|
1530
|
-
} else {
|
|
1531
|
-
this.store.writeDevice({ deviceId: this.getDeviceId() });
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
identify(options) {
|
|
1535
|
-
const result = super.identify(options);
|
|
1536
|
-
if (!options.identity && result.queued) {
|
|
1537
|
-
this.store.writeDevice({ deviceId: this.getDeviceId(), profileId: String(options.profileId) });
|
|
1538
|
-
}
|
|
1539
|
-
return result;
|
|
1540
|
-
}
|
|
1541
|
-
/** Feed a Web Vitals metric (from `next/web-vitals` or the `web-vitals` package). */
|
|
1542
|
-
captureWebVital(metric) {
|
|
1543
|
-
const { path, properties } = this.currentPath();
|
|
1544
|
-
return this.track({ name: "$web_vitals", properties: { ...properties, ...webVitalProperties(metric, path) } });
|
|
1545
|
-
}
|
|
1546
|
-
/** The page's path through `normalizePath`, for events the SDK stamps itself. */
|
|
1547
|
-
currentPath() {
|
|
1548
|
-
return normalizedPath(this.normalize, typeof location !== "undefined" ? location.pathname : "/");
|
|
1549
|
-
}
|
|
1550
|
-
/** Report an error you caught yourself. */
|
|
1551
|
-
captureException(error, properties) {
|
|
1552
|
-
const described = describeError(error, "window.onerror");
|
|
1553
|
-
return this.trackException({ ...described, ...properties, $exception_handled: true });
|
|
1554
|
-
}
|
|
1555
|
-
trackException(properties) {
|
|
1556
|
-
const current = this.currentPath();
|
|
1557
|
-
return this.track({ name: "$exception", properties: { ...current.properties, ...properties, path: current.path } });
|
|
1558
|
-
}
|
|
1559
|
-
emitPageLeave() {
|
|
1560
|
-
if (!this.capture.pageleave) return;
|
|
1561
|
-
const left = this.pageLeave.leave(this.scroll?.read() ?? null);
|
|
1562
|
-
if (left) this.track({ name: "$pageleave", properties: left });
|
|
1563
|
-
}
|
|
1564
|
-
/**
|
|
1565
|
-
* Log out: forget the profile and the queue, and become a new device.
|
|
1566
|
-
* On a shared computer the next person must not inherit this one's
|
|
1567
|
-
* history.
|
|
1568
|
-
*/
|
|
1569
|
-
reset() {
|
|
1570
|
-
super.reset();
|
|
1571
|
-
this.session.header = null;
|
|
1572
|
-
this.store.writeDevice({ deviceId: this.getDeviceId() });
|
|
1573
|
-
}
|
|
1574
|
-
async close() {
|
|
1575
|
-
this.torndown = true;
|
|
1576
|
-
this.emitPageLeave();
|
|
1577
|
-
this.scroll?.stop();
|
|
1578
|
-
for (const uninstall of this.uninstallers.splice(0)) uninstall();
|
|
1579
|
-
return super.close();
|
|
1580
|
-
}
|
|
1581
|
-
};
|
|
1582
|
-
function correctedClock(bootstrap) {
|
|
1583
|
-
if (!bootstrap || !Number.isFinite(bootstrap.serverTimeMs)) return () => Date.now();
|
|
1584
|
-
const skew = bootstrap.serverTimeMs - Date.now();
|
|
1585
|
-
if (Math.abs(skew) < CLOCK_SKEW_THRESHOLD_MS || Math.abs(skew) > MAX_CLOCK_SKEW_MS) return () => Date.now();
|
|
1586
|
-
return () => Date.now() + skew;
|
|
1587
|
-
}
|
|
1588
|
-
function usableBootstrap(bootstrap) {
|
|
1589
|
-
if (!bootstrap) return null;
|
|
1590
|
-
if (bootstrap.deviceId && !isValidIdentityId2(bootstrap.deviceId)) return { ...bootstrap, deviceId: "" };
|
|
1591
|
-
return bootstrap;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
// src/registry.ts
|
|
1595
|
-
function registry() {
|
|
1596
|
-
if (typeof window === "undefined") return null;
|
|
1597
|
-
const holder = window;
|
|
1598
|
-
if (!holder.__reopt) holder.__reopt = { clients: /* @__PURE__ */ new Map() };
|
|
1599
|
-
return holder.__reopt;
|
|
1600
|
-
}
|
|
1601
|
-
function getOrCreateClient(config, defaults) {
|
|
1602
|
-
const store = registry();
|
|
1603
|
-
const existing = store?.clients.get(config.writeKey);
|
|
1604
|
-
if (existing) return existing;
|
|
1605
|
-
const client = new ReoptClient({ ...config, bootstrap: usableBootstrap(config.bootstrap) }, defaults);
|
|
1606
|
-
store?.clients.set(config.writeKey, client);
|
|
1607
|
-
return client;
|
|
1608
|
-
}
|
|
1609
|
-
function getClient(writeKey) {
|
|
1610
|
-
const store = registry();
|
|
1611
|
-
if (!store) return null;
|
|
1612
|
-
if (writeKey) return store.clients.get(writeKey) ?? null;
|
|
1613
|
-
const first = store.clients.values().next();
|
|
1614
|
-
return first.done ? null : first.value;
|
|
1615
|
-
}
|
|
1616
|
-
function forgetClient(client) {
|
|
1617
|
-
registry()?.clients.delete(client.writeKey);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
export {
|
|
1621
|
-
extractUTMParams,
|
|
1622
|
-
ReoptClient,
|
|
1623
|
-
getOrCreateClient,
|
|
1624
|
-
getClient,
|
|
1625
|
-
forgetClient
|
|
1626
|
-
};
|
|
1627
|
-
//# sourceMappingURL=chunk-BJGIHFDA.js.map
|