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