@reopt-ai/data-sdk-client 0.1.1 → 0.1.3

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.
@@ -0,0 +1,571 @@
1
+ import { ReoptBootstrap, DeviceCookieState, ConsentCookieState } from '@reopt-ai/data-contract/identity';
2
+
3
+ /**
4
+ * Types shared by every runtime. Nothing here refers to `window`, `document`
5
+ * or a Node built-in — the runtime-specific pieces are injected through
6
+ * {@link ReoptRuntime}.
7
+ */
8
+ /** Consent categories. `analytics` is the one whose refusal stops everything. */
9
+ type ConsentCategory = "analytics" | "marketing" | "functional" | "performance";
10
+ interface ConsentConfig {
11
+ /** Default decision for every configured category. Default: `true` (opt-out model). */
12
+ defaultConsent?: boolean;
13
+ /** Categories the integration recognises. Default: `["analytics"]`. */
14
+ categories?: ConsentCategory[];
15
+ /** Persist decisions through the runtime's storage. Default: `true`. */
16
+ persist?: boolean;
17
+ }
18
+ /** Minimal synchronous key/value store — `localStorage`-shaped on purpose. */
19
+ interface StorageBackend {
20
+ getItem(key: string): string | null;
21
+ setItem(key: string, value: string): void;
22
+ removeItem(key: string): void;
23
+ }
24
+ interface RetryConfig {
25
+ /** Attempts after the first. Default: 3. */
26
+ maxRetries?: number;
27
+ /** First back-off, milliseconds. Default: 1000. */
28
+ baseDelay?: number;
29
+ /** Back-off ceiling, milliseconds. Default: 30000. */
30
+ maxDelay?: number;
31
+ /** ± fraction of the delay. Default: 0.1. */
32
+ jitter?: number;
33
+ }
34
+ interface CircuitBreakerConfig {
35
+ /** Consecutive transient failures before the circuit opens. Default: 5. */
36
+ failureThreshold?: number;
37
+ /** How long the circuit stays open before one probe is allowed. Default: 60000. */
38
+ recoveryTimeout?: number;
39
+ }
40
+ interface BatchConfig {
41
+ /** Events per request. Default: 100. */
42
+ size?: number;
43
+ /** Milliseconds to wait for a batch to fill before sending. Default: 1000. */
44
+ intervalMs?: number;
45
+ /**
46
+ * Serialized bytes per request. Default: 400 000, under the server's
47
+ * 512 000 hard cap with room for the JSON envelope. A 413 is a 4xx, and 4xx
48
+ * drops the batch — so overshooting the cap loses every event in it.
49
+ */
50
+ maxBytes?: number;
51
+ }
52
+ /** Browser credentials — public, ships in the page. */
53
+ interface WriteKeyAuth {
54
+ writeKey: string;
55
+ }
56
+ /** Server credentials — must never reach a client bundle. */
57
+ interface ClientCredentialsAuth {
58
+ clientId: string;
59
+ clientSecret: string;
60
+ }
61
+ type ReoptAuth = WriteKeyAuth | ClientCredentialsAuth;
62
+ type FetchLike = (input: string, init: FetchInit) => Promise<FetchResponseLike>;
63
+ interface FetchInit {
64
+ method: string;
65
+ headers: Record<string, string>;
66
+ body: string;
67
+ keepalive?: boolean;
68
+ }
69
+ interface FetchResponseLike {
70
+ ok: boolean;
71
+ status: number;
72
+ headers: {
73
+ get(name: string): string | null;
74
+ };
75
+ text(): Promise<string>;
76
+ }
77
+ /** Context the runtime fills in for a `$pageview` when the caller does not. */
78
+ interface PageContext {
79
+ path?: string;
80
+ origin?: string;
81
+ title?: string;
82
+ referrer?: string;
83
+ utm?: UTMParams;
84
+ /** Extra properties the runtime derived from the page, e.g. a `normalizePath` hook's output. */
85
+ properties?: Record<string, unknown>;
86
+ }
87
+ /**
88
+ * Everything the engine needs from the world around it. The browser package
89
+ * fills this from cookies, `localStorage` and `window`; the server package
90
+ * from request cookies and headers; tests from plain objects.
91
+ */
92
+ interface ReoptRuntime {
93
+ /** Resolved device id. Identity resolution is the runtime's job, not the engine's. */
94
+ deviceId: string;
95
+ /**
96
+ * Value for the `reopt-session-id` header — the `"<id>.<token>"` credential
97
+ * ingest handed back, if the runtime kept it. Unsigned values are ignored
98
+ * by ingest, so only send what came from `onSession`.
99
+ */
100
+ getSessionId?: () => string | null | undefined;
101
+ /** Ingest assigned (or confirmed) a session for this batch. The runtime persists it. */
102
+ onSession?: (credential: {
103
+ id: string;
104
+ token: string;
105
+ }) => void;
106
+ /** Durable store for the offline queue and consent. Absent = memory only. */
107
+ storage?: StorageBackend;
108
+ /** Clock. Defaults to `Date.now`; the browser package corrects for server skew. */
109
+ now?: () => number;
110
+ /** Id generator. Defaults to monotonic UUIDv7. */
111
+ createId?: () => string;
112
+ /** `fetch` implementation. Defaults to `globalThis.fetch`. */
113
+ fetch?: FetchLike;
114
+ /**
115
+ * Subscribe to "the process is about to go away" signals (pagehide,
116
+ * visibilitychange, SIGTERM). The engine flushes on each signal. Returns
117
+ * the unsubscribe function.
118
+ */
119
+ onFlushSignal?: (flush: () => void) => () => void;
120
+ /**
121
+ * Last-chance delivery used when a flush signal fires, e.g. `sendBeacon`.
122
+ * Return `true` if the runtime accepted the payload; `false` falls back to
123
+ * keepalive fetch.
124
+ */
125
+ sendLastChance?: (url: string, body: string, headers: Record<string, string>) => boolean;
126
+ /** Defaults for `$pageview` properties. */
127
+ getPageContext?: () => PageContext;
128
+ /**
129
+ * Mint and persist a fresh device id. Called by `reset()`; without it a
130
+ * reset keeps the old device, which is wrong on a shared machine.
131
+ */
132
+ regenerateDeviceId?: () => string;
133
+ }
134
+ interface ReoptCoreConfig {
135
+ auth: ReoptAuth;
136
+ /** Origin of the reopt-data deployment, or a same-origin proxy prefix like `/ingest`. */
137
+ baseUrl: string;
138
+ runtime: ReoptRuntime;
139
+ debug?: boolean;
140
+ batch?: BatchConfig;
141
+ /** Upper bound on queued events; the oldest are dropped past it. Default: 10000. */
142
+ maxQueueSize?: number;
143
+ retry?: RetryConfig;
144
+ circuitBreaker?: CircuitBreakerConfig;
145
+ consent?: ConsentConfig;
146
+ /** Prefix for storage keys. Default: `reopt_`. */
147
+ storagePrefix?: string;
148
+ /** Persist the queue through `runtime.storage`. Default: `true` when storage exists. */
149
+ enableOfflineBuffer?: boolean;
150
+ /** Flush on runtime flush signals. Default: `true` when the runtime provides them. */
151
+ autoFlushOnUnload?: boolean;
152
+ }
153
+ /**
154
+ * Identity a single event was recorded under, when it differs from the
155
+ * runtime's. The server SDK uses this to file events from many requests
156
+ * through one engine: each request's device travels with its events and
157
+ * becomes that batch's `reopt-device-id`.
158
+ */
159
+ interface EventIdentity {
160
+ deviceId?: string;
161
+ sessionId?: string | null;
162
+ }
163
+ interface TrackEventOptions {
164
+ /** File this event under another visitor than the runtime's. Server SDK use. */
165
+ identity?: EventIdentity;
166
+ name: string;
167
+ properties?: Record<string, unknown>;
168
+ profileId?: string | number;
169
+ /** Consent category this event belongs to. Default: `analytics`. */
170
+ consentCategory?: ConsentCategory;
171
+ }
172
+ interface IdentifyOptions {
173
+ /** File this event under another visitor than the runtime's. Server SDK use. */
174
+ identity?: EventIdentity;
175
+ profileId: string | number;
176
+ firstName?: string;
177
+ lastName?: string;
178
+ email?: string;
179
+ avatar?: string;
180
+ properties?: Record<string, unknown>;
181
+ consentCategory?: ConsentCategory;
182
+ }
183
+ interface IncrementOptions {
184
+ /** File this event under another visitor than the runtime's. Server SDK use. */
185
+ identity?: EventIdentity;
186
+ profileId: string | number;
187
+ property: string;
188
+ value?: number;
189
+ consentCategory?: ConsentCategory;
190
+ }
191
+ interface DecrementOptions {
192
+ /** File this event under another visitor than the runtime's. Server SDK use. */
193
+ identity?: EventIdentity;
194
+ profileId: string | number;
195
+ property: string;
196
+ value?: number;
197
+ consentCategory?: ConsentCategory;
198
+ }
199
+ interface UTMParams {
200
+ utm_source?: string;
201
+ utm_medium?: string;
202
+ utm_campaign?: string;
203
+ utm_term?: string;
204
+ utm_content?: string;
205
+ }
206
+ interface PageViewOptions {
207
+ /** File this event under another visitor than the runtime's. Server SDK use. */
208
+ identity?: EventIdentity;
209
+ path?: string;
210
+ /** Scheme + host. */
211
+ origin?: string;
212
+ title?: string;
213
+ referrer?: string;
214
+ properties?: Record<string, unknown>;
215
+ utm?: UTMParams;
216
+ consentCategory?: ConsentCategory;
217
+ }
218
+ type EventPayload = {
219
+ type: "track";
220
+ eventId: string;
221
+ timestamp: number;
222
+ payload: {
223
+ name: string;
224
+ properties?: Record<string, unknown>;
225
+ profileId?: string | number;
226
+ };
227
+ } | {
228
+ type: "identify";
229
+ eventId: string;
230
+ timestamp: number;
231
+ payload: {
232
+ profileId: string | number;
233
+ firstName?: string;
234
+ lastName?: string;
235
+ email?: string;
236
+ avatar?: string;
237
+ properties?: Record<string, unknown>;
238
+ };
239
+ } | {
240
+ type: "increment";
241
+ eventId: string;
242
+ timestamp: number;
243
+ payload: {
244
+ profileId: string | number;
245
+ property: string;
246
+ value?: number;
247
+ };
248
+ } | {
249
+ type: "decrement";
250
+ eventId: string;
251
+ timestamp: number;
252
+ payload: {
253
+ profileId: string | number;
254
+ property: string;
255
+ value?: number;
256
+ };
257
+ };
258
+ type QueueDropReason = "tracking_paused" | "consent_denied" | "validation_failed" | "payload_too_large";
259
+ interface QueueResult {
260
+ eventId: string;
261
+ queued: boolean;
262
+ reason?: QueueDropReason;
263
+ errors?: Array<{
264
+ field: string;
265
+ message: string;
266
+ }>;
267
+ }
268
+ interface FlushResult {
269
+ status: "idle" | "success" | "failed" | "skipped";
270
+ sent: number;
271
+ failed: number;
272
+ pending: number;
273
+ }
274
+ interface FlushOptions {
275
+ /** Ask the transport to outlive the page (keepalive fetch / beacon). */
276
+ keepalive?: boolean;
277
+ /** Keep sending until the queue is empty or no progress is made. */
278
+ drain?: boolean;
279
+ }
280
+
281
+ type ConsentState = Record<string, boolean>;
282
+
283
+ interface BatchDelivery {
284
+ sent: number;
285
+ failed: number;
286
+ /** The signed session credential ingest returned, when it did. */
287
+ session?: {
288
+ id: string;
289
+ token: string;
290
+ };
291
+ }
292
+ interface SendOptions {
293
+ keepalive?: boolean;
294
+ /** Overrides the runtime's device/session for this request only. */
295
+ identity?: EventIdentity;
296
+ }
297
+ interface Transport {
298
+ readonly url: string;
299
+ headers(identity?: EventIdentity): Record<string, string>;
300
+ send(batch: EventPayload[], options?: SendOptions): Promise<BatchDelivery>;
301
+ }
302
+
303
+ /**
304
+ * The engine: queue → batch → transport, with retry, a circuit breaker and
305
+ * consent gates. Everything that differs between browser and server comes in
306
+ * through {@link ReoptRuntime}; the engine itself never touches `window`,
307
+ * `document`, `process` or a Node built-in.
308
+ */
309
+ declare class ReoptCore {
310
+ protected readonly runtime: ReoptRuntime;
311
+ protected readonly transport: Transport;
312
+ private readonly queue;
313
+ private readonly consent;
314
+ private readonly breaker;
315
+ private readonly retry;
316
+ private readonly batch;
317
+ private readonly factory;
318
+ private readonly debug;
319
+ private deviceId;
320
+ private profileId;
321
+ private globalProperties;
322
+ private activeFlush;
323
+ private flushTimeout;
324
+ private removeFlushSignal;
325
+ private closed;
326
+ /** Bumped by `reset()`; a batch from an older generation is never requeued. */
327
+ private generation;
328
+ private lastFlushRequeued;
329
+ constructor(config: ReoptCoreConfig);
330
+ protected log(...args: unknown[]): void;
331
+ getDeviceId(): string;
332
+ /** Swap the device id, e.g. after a server bootstrap or a `reset()`. */
333
+ setDeviceId(deviceId: string): void;
334
+ setProfileId(profileId: string | number | null): void;
335
+ getProfileId(): string | number | null;
336
+ setConsent(category: ConsentCategory, allowed: boolean): void;
337
+ getConsent(category: ConsentCategory): boolean;
338
+ setAllConsent(allowed: boolean): void;
339
+ getConsentState(): ConsentState;
340
+ replaceConsentState(state: ConsentState): void;
341
+ pauseTracking(): void;
342
+ resumeTracking(): void;
343
+ /**
344
+ * Properties attached to every `track` event (and so every automatic
345
+ * event) from now on. An event's own properties win on conflict. This is
346
+ * how a host adds its breakdown axis — a `page_id`, a tenant — to
347
+ * `$web_vitals` and `$pageleave`, which the SDK otherwise stamps with
348
+ * only a path.
349
+ */
350
+ register(properties: Record<string, unknown>): void;
351
+ unregister(...keys: string[]): void;
352
+ getGlobalProperties(): Record<string, unknown>;
353
+ /**
354
+ * `options.identity` files the event under another visitor than the
355
+ * runtime's — the server SDK's way of serving many requests from one
356
+ * engine. Such an event never reads or writes the engine's own profile:
357
+ * that belongs to the runtime's visitor, not the request's.
358
+ */
359
+ track(options: TrackEventOptions): QueueResult;
360
+ identify(options: IdentifyOptions): QueueResult;
361
+ increment(options: IncrementOptions): QueueResult;
362
+ decrement(options: DecrementOptions): QueueResult;
363
+ pageView(options?: PageViewOptions): QueueResult;
364
+ screenView(screenName: string, properties?: Record<string, unknown>, identity?: EventIdentity): QueueResult;
365
+ get pending(): number;
366
+ private enqueue;
367
+ private scheduleFlush;
368
+ private clearScheduledFlush;
369
+ flush(options?: FlushOptions): Promise<FlushResult>;
370
+ /**
371
+ * The page (or process) is going away. Persist first so nothing is lost if
372
+ * the request never completes, then send what can be sent with keepalive.
373
+ */
374
+ private flushOnSignal;
375
+ /**
376
+ * The identity a batch is sent under, fixed when it leaves the queue. A
377
+ * `reset()` during a retry must not re-address the previous visitor's
378
+ * events to the new device.
379
+ */
380
+ private identityFor;
381
+ private flushExclusive;
382
+ private flushAll;
383
+ private flushOnce;
384
+ private continueOrSettle;
385
+ /**
386
+ * Forget who this is: profile, queued events and — when the runtime can
387
+ * mint one — the device id. A shared computer that logs out must not keep
388
+ * attributing the next person to the previous one.
389
+ */
390
+ reset(): void;
391
+ close(): Promise<FlushResult>;
392
+ }
393
+
394
+ /**
395
+ * The shape `next/web-vitals`' `useReportWebVitals` and the `web-vitals`
396
+ * package both produce. Declared here so the vanilla entry does not have to
397
+ * import either.
398
+ */
399
+ interface WebVitalMetric {
400
+ id: string;
401
+ name: string;
402
+ value: number;
403
+ delta?: number;
404
+ rating?: "good" | "needs-improvement" | "poor";
405
+ navigationType?: string;
406
+ }
407
+ type WebVitalProperties = {
408
+ metric_name: string;
409
+ metric_id: string;
410
+ value: number;
411
+ delta?: number;
412
+ rating?: string;
413
+ navigation_type?: string;
414
+ path: string;
415
+ };
416
+
417
+ /**
418
+ * Rewrites a pathname before it is attached to an event, and may lift the
419
+ * parts it removed into properties so they stay available for breakdowns:
420
+ * `/workspace/8f3…/crm/customers/2a1…` → `{ path: "/workspace/:id/crm/customers/:id",
421
+ * properties: { workspace_id: "8f3…" } }`.
422
+ *
423
+ * Applied wherever the SDK fills in a path itself — the default `$pageview`
424
+ * path, `$pageleave`, `$web_vitals`, `$exception`. A `path` you pass to
425
+ * `pageView()` explicitly is used as-is, so run the same function on it
426
+ * yourself; two different rules would give one visit two different paths.
427
+ * Must be synchronous and pure. If it throws, the raw pathname is used.
428
+ */
429
+ type NormalizePath = (pathname: string) => string | {
430
+ path: string;
431
+ properties?: Record<string, unknown>;
432
+ };
433
+
434
+ type IdentityStorageKind = "auto" | "cookie" | "localStorage" | "memory";
435
+ interface IdentityConfig {
436
+ /**
437
+ * Where the device id lives. `auto` (default) prefers a cookie: Safari
438
+ * expires script-written `localStorage` after seven days of inactivity,
439
+ * which turns every returning visitor into a new device. A cookie the
440
+ * server also reads survives that, and lets server-side events join the
441
+ * same device.
442
+ */
443
+ storage?: IdentityStorageKind;
444
+ /** Cookie `Domain`; omit for host-only. Set to share across subdomains. */
445
+ cookieDomain?: string;
446
+ /** Cookie lifetime. Default 400 days (the longest Chrome honours). */
447
+ cookieMaxAgeSeconds?: number;
448
+ }
449
+ interface CaptureConfig {
450
+ /**
451
+ * Send `$pageview` on `init()`. Default `true` for the vanilla entry.
452
+ * The Next entry sets it to `false` because `<ReoptPageView />` owns page
453
+ * views there — including the first one.
454
+ */
455
+ pageview?: boolean;
456
+ /** Send `$pageleave` with time-on-page when the page is hidden or the route changes. Default `true`. */
457
+ pageleave?: boolean;
458
+ /** Attach max scroll depth to `$pageleave`. Default `true`. */
459
+ scrollDepth?: boolean;
460
+ /** Send `$exception` for uncaught errors and unhandled rejections. Default `false`. */
461
+ exceptions?: boolean;
462
+ }
463
+ interface ReoptClientConfig {
464
+ writeKey: string;
465
+ /** reopt-data origin, or the same-origin proxy prefix (e.g. `/ingest`) the Next.js proxy rewrites. */
466
+ baseUrl: string;
467
+ /** What the server already knows about this visitor. `null` = nothing. */
468
+ bootstrap?: ReoptBootstrap | null;
469
+ identity?: IdentityConfig;
470
+ capture?: CaptureConfig;
471
+ /**
472
+ * Hosts that receive the `reopt-device-id` header on outgoing `fetch`/XHR,
473
+ * so a server-side `track()` lands on the same device. `true` = the page's
474
+ * own hostname. **Default: off** — the trusted path is passing
475
+ * `getDeviceId()` to your server call explicitly; this patches `fetch`
476
+ * and loads as a separate chunk only when enabled.
477
+ */
478
+ tracingHeaders?: boolean | string[];
479
+ /** See {@link NormalizePath}. */
480
+ normalizePath?: NormalizePath;
481
+ /**
482
+ * Properties attached to every event from the very first one. Same as
483
+ * calling `register()` right after `init()`, except that nothing can
484
+ * slip out in between: web vitals arrive from a buffered
485
+ * `PerformanceObserver` and can fire the instant the client exists.
486
+ * Pass the page context the server already knows here; update it on
487
+ * client-side navigation with `register()`.
488
+ */
489
+ properties?: Record<string, unknown>;
490
+ consent?: ConsentConfig;
491
+ batch?: BatchConfig;
492
+ retry?: RetryConfig;
493
+ circuitBreaker?: CircuitBreakerConfig;
494
+ /** Upper bound on queued events. Default 10 000. */
495
+ maxQueueSize?: number;
496
+ /** Overrides the store for the offline queue. Default `localStorage`. */
497
+ queueStorage?: StorageBackend;
498
+ /** Prefix for storage keys. Default `reopt_`. */
499
+ storagePrefix?: string;
500
+ /**
501
+ * Transport override. Tests hand in a recording function so a Playwright
502
+ * spec can assert on the exact payload the SDK built, without intercepting
503
+ * the network. Defaults to the page's `fetch` (captured at init, before the
504
+ * tracing patch, so the SDK's own requests are never tagged twice).
505
+ */
506
+ fetch?: FetchLike;
507
+ debug?: boolean;
508
+ }
509
+ interface ResolvedCaptureConfig {
510
+ pageview: boolean;
511
+ pageleave: boolean;
512
+ scrollDepth: boolean;
513
+ exceptions: boolean;
514
+ }
515
+
516
+ interface IdentityStore {
517
+ readonly kind: Exclude<IdentityStorageKind, "auto">;
518
+ readDevice(): DeviceCookieState | null;
519
+ writeDevice(state: DeviceCookieState): void;
520
+ clearDevice(): void;
521
+ readConsent(): ConsentCookieState | null;
522
+ writeConsent(state: ConsentCookieState): void;
523
+ /** Engine-facing backend for the consent key, so consent decisions land in the same place. */
524
+ consentBackend(): StorageBackend;
525
+ }
526
+
527
+ interface ReoptClientDefaults {
528
+ /** Whether `init()` sends the first `$pageview`. The Next entry says no. */
529
+ pageview: boolean;
530
+ }
531
+ /**
532
+ * The browser client: the shared engine plus everything that only makes
533
+ * sense in a document — cookie identity, tracing headers, page leave,
534
+ * scroll depth, exception capture, and a clock corrected against the
535
+ * server's.
536
+ */
537
+ declare class ReoptClient extends ReoptCore {
538
+ readonly writeKey: string;
539
+ readonly capture: ResolvedCaptureConfig;
540
+ private readonly store;
541
+ private readonly pageLeave;
542
+ private readonly scroll;
543
+ private readonly uninstallers;
544
+ private torndown;
545
+ private readonly session;
546
+ private readonly normalize;
547
+ /** True when `writeKey` or `baseUrl` was missing: every call is a silent no-op. */
548
+ readonly disabled: boolean;
549
+ constructor(config: ReoptClientConfig, defaults?: ReoptClientDefaults);
550
+ /** Which store identity ended up in — `cookie`, `localStorage` or `memory`. */
551
+ get identityStorage(): IdentityStore["kind"];
552
+ pageView(options?: PageViewOptions): QueueResult;
553
+ identify(options: IdentifyOptions): QueueResult;
554
+ /** Feed a Web Vitals metric (from `next/web-vitals` or the `web-vitals` package). */
555
+ captureWebVital(metric: WebVitalMetric): QueueResult;
556
+ /** The page's path through `normalizePath`, for events the SDK stamps itself. */
557
+ private currentPath;
558
+ /** Report an error you caught yourself. */
559
+ captureException(error: unknown, properties?: Record<string, unknown>): QueueResult;
560
+ private trackException;
561
+ private emitPageLeave;
562
+ /**
563
+ * Log out: forget the profile and the queue, and become a new device.
564
+ * On a shared computer the next person must not inherit this one's
565
+ * history.
566
+ */
567
+ reset(): void;
568
+ close(): Promise<FlushResult>;
569
+ }
570
+
571
+ export { type BatchConfig as B, type ConsentCategory as C, type DecrementOptions as D, type EventIdentity as E, type FlushResult as F, type IdentifyOptions as I, type NormalizePath as N, type PageViewOptions as P, type QueueResult as Q, ReoptClient as R, type StorageBackend as S, type TrackEventOptions as T, type UTMParams as U, type WebVitalMetric as W, type ReoptClientConfig as a, type ReoptClientDefaults as b, type IncrementOptions as c, type CaptureConfig as d, type CircuitBreakerConfig as e, type ConsentConfig as f, type EventPayload as g, type FetchInit as h, type FetchLike as i, type FetchResponseLike as j, type IdentityConfig as k, type IdentityStorageKind as l, type QueueDropReason as m, type RetryConfig as n, type WebVitalProperties as o };
package/dist/index.cjs CHANGED
@@ -822,10 +822,13 @@ function readIngestCounts(rawBody) {
822
822
  if (!record || record.status !== "ok" && record.status !== "accepted" || !count(record.accepted) || !count(record.duplicates) || !Array.isArray(record.rejected)) {
823
823
  throw new TransportError("Track response did not match contract: unexpected shape", 409, true);
824
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;
825
827
  return {
826
828
  accepted: record.accepted,
827
829
  duplicates: record.duplicates,
828
- rejected: record.rejected.length
830
+ rejected: record.rejected.length,
831
+ ...credential ? { session: credential } : {}
829
832
  };
830
833
  }
831
834
  function resolveBaseUrl(value) {
@@ -883,7 +886,11 @@ function createTransport(options) {
883
886
  if (counts.accepted + counts.duplicates + counts.rejected !== batch.length) {
884
887
  throw new TransportError("Track response counts do not reconcile with the submitted batch", 409, true);
885
888
  }
886
- return { sent: counts.accepted + counts.duplicates, failed: counts.rejected };
889
+ return {
890
+ sent: counts.accepted + counts.duplicates,
891
+ failed: counts.rejected,
892
+ ...counts.session ? { session: counts.session } : {}
893
+ };
887
894
  };
888
895
  return { url, headers, send };
889
896
  }
@@ -1193,6 +1200,9 @@ var ReoptCore = class {
1193
1200
  onRetry: (attempt, waitMs) => this.log("retry", attempt, Math.round(waitMs))
1194
1201
  });
1195
1202
  this.log("flushed", delivery.sent, delivery.failed);
1203
+ if (delivery.session && !batch[0]?.identity) {
1204
+ this.runtime.onSession?.(delivery.session);
1205
+ }
1196
1206
  if (this.breaker.getState() === "half-open") this.log("breaker closed");
1197
1207
  this.breaker.recordSuccess();
1198
1208
  this.queue.settle();
@@ -1436,13 +1446,14 @@ function cookiesAvailable() {
1436
1446
 
1437
1447
  // src/identity/device.ts
1438
1448
  var LEGACY_DEVICE_KEY = "device_id";
1439
- function localStorageOrNull() {
1449
+ function localStorageBackend() {
1440
1450
  try {
1441
1451
  if (typeof window === "undefined" || !window.localStorage) return null;
1452
+ const storage = window.localStorage;
1442
1453
  const probe = "reopt_probe";
1443
- window.localStorage.setItem(probe, "1");
1444
- window.localStorage.removeItem(probe);
1445
- return window.localStorage;
1454
+ storage.setItem(probe, "1");
1455
+ storage.removeItem(probe);
1456
+ return storage;
1446
1457
  } catch {
1447
1458
  return null;
1448
1459
  }
@@ -1481,7 +1492,7 @@ function createIdentityStore(writeKey, config) {
1481
1492
  if (kind === "cookie" || kind === "auto" && cookiesAvailable()) {
1482
1493
  return makeStore("cookie", writeKey, cookieBackend(cookie));
1483
1494
  }
1484
- const local = kind === "memory" ? null : localStorageOrNull();
1495
+ const local = kind === "memory" ? null : localStorageBackend();
1485
1496
  if (local) return makeStore("localStorage", writeKey, local);
1486
1497
  return makeStore("memory", writeKey, memoryStorage());
1487
1498
  }
@@ -1492,7 +1503,7 @@ function resolveDeviceId(store, bootstrapDeviceId, storagePrefix) {
1492
1503
  store.writeDevice({ deviceId: bootstrapDeviceId });
1493
1504
  return { deviceId: bootstrapDeviceId, source: "bootstrap" };
1494
1505
  }
1495
- const local = localStorageOrNull();
1506
+ const local = localStorageBackend();
1496
1507
  const legacy = local?.getItem(`${storagePrefix}${LEGACY_DEVICE_KEY}`);
1497
1508
  if ((0, import_identity3.isValidIdentityId)(legacy)) {
1498
1509
  store.writeDevice({ deviceId: legacy });
@@ -1512,19 +1523,6 @@ function regenerateDeviceId(store) {
1512
1523
  // src/client.ts
1513
1524
  var CLOCK_SKEW_THRESHOLD_MS = 3e4;
1514
1525
  var MAX_CLOCK_SKEW_MS = 24 * 60 * 60 * 1e3;
1515
- function localStorageBackend() {
1516
- try {
1517
- if (typeof window === "undefined" || !window.localStorage) return null;
1518
- const storage = window.localStorage;
1519
- return {
1520
- getItem: (key) => storage.getItem(key),
1521
- setItem: (key, value) => storage.setItem(key, value),
1522
- removeItem: (key) => storage.removeItem(key)
1523
- };
1524
- } catch {
1525
- return null;
1526
- }
1527
- }
1528
1526
  function routedStorage(consentKey, consent, rest) {
1529
1527
  const pick = (key) => key === consentKey ? consent : rest;
1530
1528
  return {
@@ -1565,6 +1563,7 @@ var ReoptClient = class extends ReoptCore {
1565
1563
  scroll;
1566
1564
  uninstallers = [];
1567
1565
  torndown = false;
1566
+ session;
1568
1567
  normalize;
1569
1568
  /** True when `writeKey` or `baseUrl` was missing: every call is a silent no-op. */
1570
1569
  disabled;
@@ -1581,8 +1580,15 @@ var ReoptClient = class extends ReoptCore {
1581
1580
  const queueStorage = disabled ? memoryStorage() : config.queueStorage ?? localStorageBackend() ?? memoryStorage();
1582
1581
  const transportFetch = config.fetch ?? (typeof window !== "undefined" ? window.fetch.bind(window) : void 0);
1583
1582
  const box = {};
1583
+ const session = { header: store.readDevice()?.sessionId ?? null };
1584
1584
  const runtime = {
1585
1585
  deviceId: identity.deviceId,
1586
+ getSessionId: () => (0, import_identity5.parseSessionHeader)(session.header) ? session.header : null,
1587
+ onSession: (credential) => {
1588
+ session.header = (0, import_identity5.formatSessionHeader)(credential);
1589
+ const current2 = store.readDevice();
1590
+ store.writeDevice({ ...current2, deviceId: current2?.deviceId ?? identity.deviceId, sessionId: session.header });
1591
+ },
1586
1592
  storage: routedStorage(`${storagePrefix}consent`, store.consentBackend(), queueStorage),
1587
1593
  now,
1588
1594
  fetch: transportFetch,
@@ -1620,6 +1626,7 @@ var ReoptClient = class extends ReoptCore {
1620
1626
  });
1621
1627
  this.writeKey = config.writeKey;
1622
1628
  this.disabled = disabled;
1629
+ this.session = session;
1623
1630
  this.normalize = config.normalizePath;
1624
1631
  this.store = store;
1625
1632
  this.capture = resolveCapture(config.capture, defaults.pageview);
@@ -1715,6 +1722,7 @@ var ReoptClient = class extends ReoptCore {
1715
1722
  */
1716
1723
  reset() {
1717
1724
  super.reset();
1725
+ this.session.header = null;
1718
1726
  this.store.writeDevice({ deviceId: this.getDeviceId() });
1719
1727
  }
1720
1728
  async close() {