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