@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.
- package/README.md +2 -2
- package/dist/{chunk-47EC5ATF.js → chunk-JNIOYR44.js} +2 -2
- package/dist/{chunk-BIR7OFR3.js → chunk-SRHWNG2G.js} +35 -23
- package/dist/chunk-SRHWNG2G.js.map +1 -0
- package/dist/client-CW9DfTlm.d.ts +571 -0
- package/dist/index.cjs +29 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +29 -21
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.ts +1 -2
- package/dist/next.js +2 -2
- package/dist/react.cjs +29 -21
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +2 -4
- package/dist/react.js +2 -2
- package/package.json +4 -4
- package/dist/chunk-BIR7OFR3.js.map +0 -1
- package/dist/client-g75DV57n.d.ts +0 -180
- /package/dist/{chunk-47EC5ATF.js.map → chunk-JNIOYR44.js.map} +0 -0
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
1444
|
-
|
|
1445
|
-
return
|
|
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 :
|
|
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 =
|
|
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() {
|