@faststats/web 0.6.0 → 0.8.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.
@@ -0,0 +1,10 @@
1
+ import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
2
+ //#region src/outbound-links.d.ts
3
+ declare function outboundLinks(): AnalyticsExtension;
4
+ declare class OutboundLinkTracker {
5
+ private lastActivation;
6
+ getHref(event: Event): string | null;
7
+ private isDuplicate;
8
+ }
9
+ //#endregion
10
+ export { OutboundLinkTracker, outboundLinks };
@@ -0,0 +1,62 @@
1
+ import { a as sanitizeUrl } from "./chunks/api-urls-DWk43fu6.js";
2
+ //#region src/outbound-links.ts
3
+ function outboundLinks() {
4
+ return {
5
+ name: "outbound-links",
6
+ setup(context) {
7
+ const tracker = new OutboundLinkTracker();
8
+ const onClick = (event) => {
9
+ const href = tracker.getHref(event);
10
+ if (href) context.capture("outbound_link", { outbound_link: sanitizeUrl(href) });
11
+ };
12
+ document.addEventListener("click", onClick, true);
13
+ document.addEventListener("auxclick", onClick, true);
14
+ return () => {
15
+ document.removeEventListener("click", onClick, true);
16
+ document.removeEventListener("auxclick", onClick, true);
17
+ };
18
+ }
19
+ };
20
+ }
21
+ const DEDUPE_MS = 500;
22
+ var OutboundLinkTracker = class {
23
+ lastActivation = null;
24
+ getHref(event) {
25
+ if (!(event instanceof MouseEvent) || !isEligibleActivation(event)) return null;
26
+ const anchor = findOutboundAnchor(event);
27
+ if (!anchor || this.isDuplicate(event, anchor.href)) return null;
28
+ return anchor.href;
29
+ }
30
+ isDuplicate(event, href) {
31
+ const activation = {
32
+ button: event.button,
33
+ href,
34
+ clientX: event.clientX,
35
+ clientY: event.clientY,
36
+ at: Date.now()
37
+ };
38
+ const previous = this.lastActivation;
39
+ this.lastActivation = activation;
40
+ return !!(previous && previous.href === activation.href && previous.button === activation.button && previous.clientX === activation.clientX && previous.clientY === activation.clientY && activation.at - previous.at <= DEDUPE_MS);
41
+ }
42
+ };
43
+ function isEligibleActivation(event) {
44
+ if (event.defaultPrevented) return false;
45
+ if (event.type === "click") return event.button === 0;
46
+ if (event.type === "auxclick") return event.button === 1;
47
+ return false;
48
+ }
49
+ function findOutboundAnchor(event) {
50
+ for (const node of event.composedPath()) if (isOutboundAnchor(node)) return node;
51
+ let node = event.target;
52
+ while (node) {
53
+ if (isOutboundAnchor(node)) return node;
54
+ node = node.parentNode;
55
+ }
56
+ return null;
57
+ }
58
+ function isOutboundAnchor(node) {
59
+ return node instanceof HTMLAnchorElement && (node.protocol === "http:" || node.protocol === "https:") && node.host !== location.host;
60
+ }
61
+ //#endregion
62
+ export { OutboundLinkTracker, outboundLinks };
package/dist/replay.d.ts CHANGED
@@ -1,2 +1,128 @@
1
- import { n as ReplayTrackerOptions, t as ReplayTracker } from "./chunks/replay-DgodPHzA.js";
2
- export { ReplayTrackerOptions, ReplayTracker as default };
1
+ import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
2
+ import { record } from "@rrweb/record";
3
+ import { LogLevel } from "@rrweb/rrweb-plugin-console-record";
4
+ //#region ../../node_modules/.bun/@rrweb+types@2.1.1/node_modules/@rrweb/types/dist/index.d.ts
5
+ declare global {
6
+ interface Window {
7
+ FontFace: typeof FontFace;
8
+ }
9
+ }
10
+ //#endregion
11
+ //#region src/replay-mutation-throttler.d.ts
12
+ type ReplayMutationThrottleOptions = {
13
+ enabled?: boolean;
14
+ bucketSize?: number;
15
+ refillRate?: number;
16
+ };
17
+ //#endregion
18
+ //#region src/replay.d.ts
19
+ type RecordOptions = NonNullable<Parameters<typeof record>[0]>;
20
+ interface ReplayTrackerOptions {
21
+ siteKey: string;
22
+ baseUrl?: string;
23
+ debug?: boolean;
24
+ trackHash?: boolean;
25
+ cookieless?: boolean;
26
+ compress?: boolean;
27
+ flushInterval?: number;
28
+ maxEvents?: number;
29
+ maxBatchSizeBytes?: number;
30
+ maxLowLatencyBatchSizeBytes?: number;
31
+ maxPendingBatches?: number;
32
+ maxQueueSizeBytes?: number;
33
+ minReplayLengthMs?: number;
34
+ sampling?: RecordOptions["sampling"];
35
+ slimDOMOptions?: RecordOptions["slimDOMOptions"];
36
+ maskAllInputs?: boolean;
37
+ maskInputOptions?: RecordOptions["maskInputOptions"];
38
+ blockClass?: string;
39
+ blockSelector?: string;
40
+ maskTextClass?: string;
41
+ maskTextSelector?: string;
42
+ checkoutEveryNms?: number;
43
+ checkoutEveryNth?: number;
44
+ recordConsole?: boolean;
45
+ consoleLevel?: LogLevel[];
46
+ consoleLengthThreshold?: number;
47
+ consoleStringLengthLimit?: number;
48
+ mutationThrottle?: ReplayMutationThrottleOptions;
49
+ }
50
+ type SessionReplayExtensionOptions = Omit<ReplayTrackerOptions, "siteKey" | "baseUrl" | "debug" | "trackHash" | "cookieless">;
51
+ declare function sessionReplay(options?: SessionReplayExtensionOptions): AnalyticsExtension;
52
+ declare class ReplayTracker {
53
+ private readonly options;
54
+ private readonly endpoint;
55
+ private readonly compressionSupported;
56
+ private readonly flushInterval;
57
+ private readonly maxEvents;
58
+ private readonly maxBatchSizeBytes;
59
+ private readonly maxLowLatencyBatchSizeBytes;
60
+ private readonly maxPendingBatches;
61
+ private readonly maxQueueSizeBytes;
62
+ private readonly minReplayLengthMs;
63
+ private readonly events;
64
+ private readonly pending;
65
+ private readonly minLengthBlockedBatches;
66
+ private queuedEventsSizeBytes;
67
+ private pendingSizeBytes;
68
+ private viewId;
69
+ private viewUrl;
70
+ private chunkStartedAt;
71
+ private started;
72
+ private disposed;
73
+ private startTime;
74
+ private sequence;
75
+ private intervalId;
76
+ private flushTask;
77
+ private retryTask;
78
+ private retryAttempt;
79
+ private minLengthFlushTask;
80
+ private stopRecording?;
81
+ private sending;
82
+ private inFlightBatch;
83
+ private queuedFlush;
84
+ private unsubscribeRotation?;
85
+ private lastCookielessMode;
86
+ private terminalFlushRequested;
87
+ private overflowed;
88
+ private replayEventSequence;
89
+ private readonly mutationThrottler;
90
+ constructor(options: ReplayTrackerOptions);
91
+ private log;
92
+ private clearFlushTimers;
93
+ private clearQueues;
94
+ private dropMinLengthBlockedBatches;
95
+ private unblockSessionBatches;
96
+ private finalizeSessionPending;
97
+ private nextBatchSequence;
98
+ setCookielessMode(cookieless: boolean): void;
99
+ start(): void;
100
+ trackPageChange(url?: string): void;
101
+ onPageHidden(persisted?: boolean, terminal?: boolean): void;
102
+ onPageShow(persisted?: boolean): void;
103
+ private beginRecording;
104
+ stop(discard?: boolean): void;
105
+ private onSessionRotated;
106
+ private subscribeToSessionRotation;
107
+ private applySessionRotation;
108
+ private resetSessionTiming;
109
+ private onEvent;
110
+ private onUnload;
111
+ private requestTerminalFlush;
112
+ private hasReachedMinLength;
113
+ private requestFlush;
114
+ private scheduleMinLengthFlush;
115
+ private enqueueEventsIfReady;
116
+ private takeEventBatches;
117
+ private createBatch;
118
+ private eventSizeBytes;
119
+ private batchSizeBytes;
120
+ private removePending;
121
+ private enqueueBatch;
122
+ private flush;
123
+ private scheduleRetry;
124
+ private retryDelay;
125
+ private sendBatch;
126
+ }
127
+ //#endregion
128
+ export { ReplayTrackerOptions, SessionReplayExtensionOptions, ReplayTracker as default, sessionReplay };
package/dist/replay.js CHANGED
@@ -1,21 +1,91 @@
1
- import { a as resolveBaseUrl, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-CaNa8upY.js";
2
- import { a as onSessionRotated, c as touchActivity, d as setStorageItem, i as isCookielessMode, l as getStorageItem, n as createId, r as getSessionContext, t as sendData } from "./chunks/send-data-DjdbGDDc.js";
3
- import { t as getAnonymousId } from "./chunks/identifiers-cUymVK5H.js";
1
+ import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
2
+ import { i as onSessionRotated, l as setStorageItem, n as createId, o as touchActivity, r as getSessionContext, s as getStorageItem, t as sendData } from "./chunks/send-data-DFZnsaL2.js";
3
+ import { t as getAnonymousId } from "./chunks/identifiers-KgQDX74Q.js";
4
4
  import { record } from "@rrweb/record";
5
+ import { getRecordConsolePlugin } from "@rrweb/rrweb-plugin-console-record";
6
+ //#region src/replay-mutation-throttler.ts
7
+ const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
8
+ const MUTATION_SOURCE_TYPE = 0;
9
+ const MAX_TRACKED_NODES = 1e4;
10
+ /** Bounds pathological per-node attribute churn without breaking the mutation chain. */
11
+ var ReplayMutationThrottler = class {
12
+ buckets = /* @__PURE__ */ new Map();
13
+ bucketSize;
14
+ refillRate;
15
+ constructor(options = {}) {
16
+ this.bucketSize = Math.max(1, options.bucketSize ?? 100);
17
+ this.refillRate = Math.max(1, options.refillRate ?? 10);
18
+ }
19
+ reset() {
20
+ this.buckets.clear();
21
+ }
22
+ throttle(event) {
23
+ if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE) return event;
24
+ const data = event.data;
25
+ if (data.source !== MUTATION_SOURCE_TYPE || !data.attributes?.length) return event;
26
+ const attributes = data.attributes.filter(({ id }) => this.consume(id));
27
+ if (attributes.length === data.attributes.length) return event;
28
+ if (attributes.length === 0 && (data.adds?.length ?? 0) === 0 && (data.removes?.length ?? 0) === 0 && (data.texts?.length ?? 0) === 0) return;
29
+ return {
30
+ ...event,
31
+ data: {
32
+ ...data,
33
+ attributes
34
+ }
35
+ };
36
+ }
37
+ consume(nodeId) {
38
+ const now = Date.now();
39
+ const bucket = this.buckets.get(nodeId) ?? {
40
+ tokens: this.bucketSize,
41
+ updatedAt: now
42
+ };
43
+ const elapsedSeconds = Math.max(0, now - bucket.updatedAt) / 1e3;
44
+ bucket.tokens = Math.min(this.bucketSize, bucket.tokens + elapsedSeconds * this.refillRate);
45
+ bucket.updatedAt = now;
46
+ if (!this.buckets.has(nodeId) && this.buckets.size >= MAX_TRACKED_NODES) this.buckets.delete(this.buckets.keys().next().value);
47
+ this.buckets.set(nodeId, bucket);
48
+ if (bucket.tokens < 1) return false;
49
+ bucket.tokens -= 1;
50
+ return true;
51
+ }
52
+ };
53
+ //#endregion
5
54
  //#region src/replay.ts
6
55
  const MAX_TIMEOUT_MS = 2147483647;
7
- const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2 * 1024 * 1024;
8
- const DEFAULT_MAX_BATCH_SIZE_BYTES = 512 * 1024;
9
- const DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES = 60 * 1024;
56
+ const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2097152;
57
+ const DEFAULT_MAX_BATCH_SIZE_BYTES = 921600;
58
+ const DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES = 61440;
10
59
  const RETRY_BASE_DELAY_MS = 2e3;
11
60
  const RETRY_MAX_ATTEMPTS = 5;
12
- const RETRY_COOLDOWN_MS = 5 * 6e4;
61
+ const RETRY_COOLDOWN_MS = 3e5;
13
62
  const VIEW_META_EVENT_TYPE = 5;
14
63
  const RRWEB_EVENT_FULL_SNAPSHOT = 2;
15
64
  const RRWEB_EVENT_META = 4;
16
65
  const REPLAY_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
17
66
  const textEncoder = new TextEncoder();
18
67
  const batchSizeCache = /* @__PURE__ */ new WeakMap();
68
+ function sessionReplay(options = {}) {
69
+ return {
70
+ name: "session-replay",
71
+ setup(context) {
72
+ const tracker = new ReplayTracker({
73
+ ...options,
74
+ siteKey: context.siteKey,
75
+ baseUrl: context.baseUrl,
76
+ debug: context.debug,
77
+ trackHash: context.trackHash,
78
+ cookieless: context.isCookieless()
79
+ });
80
+ context.on("pageChange", ({ url }) => tracker.trackPageChange(url));
81
+ context.on("pageHide", ({ persisted, terminal }) => tracker.onPageHidden(persisted, terminal));
82
+ context.on("pageShow", ({ persisted }) => tracker.onPageShow(persisted));
83
+ context.on("consentChange", ({ cookieless }) => tracker.setCookielessMode(cookieless));
84
+ tracker.start();
85
+ return ({ discard }) => tracker.stop(discard);
86
+ }
87
+ };
88
+ }
19
89
  var ReplayTracker = class {
20
90
  options;
21
91
  endpoint;
@@ -33,10 +103,10 @@ var ReplayTracker = class {
33
103
  queuedEventsSizeBytes = 0;
34
104
  pendingSizeBytes = 0;
35
105
  viewId = createId();
106
+ viewUrl = "";
36
107
  chunkStartedAt = 0;
37
108
  started = false;
38
109
  disposed = false;
39
- startGeneration = 0;
40
110
  startTime = 0;
41
111
  sequence = 0;
42
112
  intervalId = null;
@@ -49,17 +119,23 @@ var ReplayTracker = class {
49
119
  inFlightBatch = null;
50
120
  queuedFlush = null;
51
121
  unsubscribeRotation;
52
- lastCookielessMode = isCookielessMode();
122
+ lastCookielessMode;
123
+ terminalFlushRequested = false;
124
+ overflowed = false;
125
+ replayEventSequence = 0;
126
+ mutationThrottler;
53
127
  constructor(options) {
54
128
  this.options = options;
129
+ this.lastCookielessMode = options.cookieless ?? false;
55
130
  this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.replay}`;
56
- this.flushInterval = options.flushInterval ?? 5e3;
131
+ this.flushInterval = options.flushInterval ?? 15e3;
57
132
  this.maxEvents = options.maxEvents ?? 1e3;
58
133
  this.maxBatchSizeBytes = options.maxBatchSizeBytes ?? DEFAULT_MAX_BATCH_SIZE_BYTES;
59
134
  this.maxLowLatencyBatchSizeBytes = options.maxLowLatencyBatchSizeBytes ?? DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES;
60
135
  this.maxPendingBatches = options.maxPendingBatches ?? 30;
61
136
  this.maxQueueSizeBytes = options.maxQueueSizeBytes ?? DEFAULT_MAX_QUEUE_SIZE_BYTES;
62
137
  this.minReplayLengthMs = options.minReplayLengthMs ?? 3e3;
138
+ this.mutationThrottler = options.mutationThrottle?.enabled === false ? null : new ReplayMutationThrottler(options.mutationThrottle);
63
139
  }
64
140
  log(...args) {
65
141
  if (this.options.debug) console.log("[Replay]", ...args);
@@ -113,38 +189,33 @@ var ReplayTracker = class {
113
189
  setCookielessMode(cookieless) {
114
190
  if (this.lastCookielessMode === cookieless) return;
115
191
  this.lastCookielessMode = cookieless;
116
- if (cookieless) {
117
- this.clearQueues();
118
- this.clearFlushTimers();
119
- }
120
- this.startTime = getSessionContext(this.options.siteKey).sessionStart;
192
+ if (this.started) this.subscribeToSessionRotation();
193
+ this.clearQueues();
194
+ this.clearFlushTimers();
195
+ this.startTime = getSessionContext(this.options.siteKey, this.lastCookielessMode).sessionStart;
121
196
  this.chunkStartedAt = Date.now();
122
197
  }
123
198
  start() {
124
199
  if (this.started || typeof window === "undefined") return;
125
200
  this.started = true;
126
201
  this.disposed = false;
202
+ this.terminalFlushRequested = false;
203
+ this.overflowed = false;
127
204
  this.viewId = createId();
128
- this.startTime = getSessionContext(this.options.siteKey).sessionStart;
205
+ this.viewUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
206
+ this.startTime = getSessionContext(this.options.siteKey, this.lastCookielessMode).sessionStart;
129
207
  this.chunkStartedAt = Date.now();
130
- this.unsubscribeRotation = onSessionRotated((prev, next) => {
131
- this.onSessionRotated(prev, next);
132
- });
133
- const generation = ++this.startGeneration;
134
- this.beginRecording(generation);
208
+ this.subscribeToSessionRotation();
209
+ this.beginRecording();
135
210
  this.intervalId = setInterval(() => this.requestFlush("interval"), this.flushInterval);
136
211
  window.addEventListener("beforeunload", this.onUnload);
137
212
  this.log("Recording started");
138
213
  }
139
214
  trackPageChange(url) {
140
215
  if (!this.started) return;
141
- this.enqueueEventsIfReady(getSessionContext(this.options.siteKey), {
142
- flushReason: "navigation",
143
- sealBeforeMinLength: true
144
- });
145
216
  this.viewId = createId();
146
- this.chunkStartedAt = Date.now();
147
- const href = sanitizeUrl(url ?? window.location.href);
217
+ const href = sanitizeUrl(url ?? window.location.href, this.options.trackHash ?? false);
218
+ this.viewUrl = href;
148
219
  this.onEvent({
149
220
  type: VIEW_META_EVENT_TYPE,
150
221
  timestamp: Date.now(),
@@ -156,21 +227,22 @@ var ReplayTracker = class {
156
227
  }
157
228
  }
158
229
  }, false);
159
- if (this.pending.length > 0) queueMicrotask(() => void this.flush(false));
160
230
  }
161
- onPageHidden(persisted = false) {
231
+ onPageHidden(persisted = false, terminal = false) {
162
232
  if (persisted) return;
163
- this.flush(true, true, "pageHidden");
233
+ if (terminal) {
234
+ this.requestTerminalFlush("pageHidden");
235
+ return;
236
+ }
237
+ this.flush(false, false, "pageHidden");
164
238
  }
165
239
  onPageShow(persisted = false) {
166
240
  if (!persisted || !this.started) return;
167
241
  if (this.pending.length > 0) this.flush(false, false, "pageShow");
168
242
  }
169
- async beginRecording(generation) {
170
- const [consolePluginModule, sequentialIdPluginModule] = await Promise.all([this.options.recordConsole === true ? import("@rrweb/rrweb-plugin-console-record") : Promise.resolve(null), import("@rrweb/rrweb-plugin-sequential-id-record")]);
171
- if (generation !== this.startGeneration) return;
172
- const plugins = [sequentialIdPluginModule.getRecordSequentialIdPlugin({ key: REPLAY_SEQUENTIAL_ID_KEY })];
173
- if (consolePluginModule) plugins.push(consolePluginModule.getRecordConsolePlugin({
243
+ beginRecording() {
244
+ const plugins = [];
245
+ if (this.options.recordConsole === true) plugins.push(getRecordConsolePlugin({
174
246
  level: this.options.consoleLevel ?? [
175
247
  "error",
176
248
  "warn",
@@ -214,21 +286,16 @@ var ReplayTracker = class {
214
286
  blockSelector: this.options.blockSelector,
215
287
  maskTextClass: this.options.maskTextClass,
216
288
  maskTextSelector: this.options.maskTextSelector,
217
- checkoutEveryNms: this.options.checkoutEveryNms ?? 6e4,
289
+ checkoutEveryNms: this.options.checkoutEveryNms ?? 3e5,
218
290
  checkoutEveryNth: this.options.checkoutEveryNth,
219
291
  plugins
220
292
  });
221
- if (generation !== this.startGeneration) {
222
- stop?.();
223
- return;
224
- }
225
293
  this.stopRecording = stop;
226
294
  }
227
- stop() {
295
+ stop(discard = false) {
228
296
  if (!this.started) return;
229
297
  this.started = false;
230
298
  this.disposed = true;
231
- this.startGeneration++;
232
299
  this.unsubscribeRotation?.();
233
300
  this.unsubscribeRotation = void 0;
234
301
  this.stopRecording?.();
@@ -237,6 +304,11 @@ var ReplayTracker = class {
237
304
  this.clearFlushTimers();
238
305
  this.intervalId = null;
239
306
  window.removeEventListener("beforeunload", this.onUnload);
307
+ if (discard) {
308
+ this.clearQueues();
309
+ this.log("Recording discarded");
310
+ return;
311
+ }
240
312
  if (!this.hasReachedMinLength()) {
241
313
  this.events.length = 0;
242
314
  this.queuedEventsSizeBytes = 0;
@@ -251,6 +323,10 @@ var ReplayTracker = class {
251
323
  this.applySessionRotation(prev, next);
252
324
  if (this.pending.length > 0) queueMicrotask(() => void this.flush(false, false, "sessionRotate"));
253
325
  }
326
+ subscribeToSessionRotation() {
327
+ this.unsubscribeRotation?.();
328
+ this.unsubscribeRotation = onSessionRotated(this.options.siteKey, this.lastCookielessMode, (prev, next) => this.onSessionRotated(prev, next));
329
+ }
254
330
  applySessionRotation(prev, next) {
255
331
  this.enqueueEventsIfReady(prev, {
256
332
  isFinal: true,
@@ -266,7 +342,8 @@ var ReplayTracker = class {
266
342
  this.startTime = next.sessionStart;
267
343
  this.chunkStartedAt = Date.now();
268
344
  }
269
- onEvent = (event, isCheckout) => {
345
+ onEvent = (event, _isCheckout) => {
346
+ if (this.overflowed) return;
270
347
  let capturedEvent = event;
271
348
  if (event.type === RRWEB_EVENT_META) {
272
349
  const data = event.data;
@@ -274,23 +351,39 @@ var ReplayTracker = class {
274
351
  ...event,
275
352
  data: {
276
353
  ...data,
277
- href: sanitizeUrl(data.href)
354
+ href: sanitizeUrl(data.href, this.options.trackHash ?? false)
278
355
  }
279
356
  };
280
357
  }
358
+ if (capturedEvent.type === RRWEB_EVENT_FULL_SNAPSHOT) this.mutationThrottler?.reset();
359
+ else {
360
+ const throttled = this.mutationThrottler?.throttle(capturedEvent);
361
+ if (this.mutationThrottler && !throttled) return;
362
+ if (throttled) capturedEvent = throttled;
363
+ }
364
+ capturedEvent = Object.assign(capturedEvent, { [REPLAY_SEQUENTIAL_ID_KEY]: ++this.replayEventSequence });
281
365
  this.events.push(capturedEvent);
282
366
  this.queuedEventsSizeBytes += this.eventSizeBytes(capturedEvent);
283
367
  let reason;
284
- if (isCheckout) reason = "checkout";
285
- else if (this.events.length >= this.maxEvents) reason = "maxEvents";
368
+ if (this.events.length >= this.maxEvents) reason = "maxEvents";
286
369
  else if (this.queuedEventsSizeBytes >= this.maxBatchSizeBytes) reason = "maxBytes";
287
- else if (capturedEvent.type === RRWEB_EVENT_FULL_SNAPSHOT && this.hasReachedMinLength()) reason = "fullSnapshot";
288
370
  if (reason) this.requestFlush(reason);
289
371
  else this.scheduleMinLengthFlush();
290
372
  };
291
373
  onUnload = () => {
292
- this.flush(true, true, "unload");
374
+ this.requestTerminalFlush("unload");
293
375
  };
376
+ requestTerminalFlush(reason) {
377
+ if (this.terminalFlushRequested) return;
378
+ this.terminalFlushRequested = true;
379
+ if (!this.hasReachedMinLength()) {
380
+ this.events.length = 0;
381
+ this.queuedEventsSizeBytes = 0;
382
+ this.dropMinLengthBlockedBatches();
383
+ return;
384
+ }
385
+ this.flush(true, true, reason);
386
+ }
294
387
  hasReachedMinLength() {
295
388
  return this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
296
389
  }
@@ -347,7 +440,7 @@ var ReplayTracker = class {
347
440
  return batches;
348
441
  }
349
442
  createBatch(events, context, isFinal, flushReason = "manual") {
350
- const identifier = getAnonymousId();
443
+ const identifier = getAnonymousId(this.options.siteKey, this.lastCookielessMode);
351
444
  const sequence = this.nextBatchSequence(context);
352
445
  const chunkStartedAt = this.chunkStartedAt;
353
446
  const now = Date.now();
@@ -364,7 +457,7 @@ var ReplayTracker = class {
364
457
  batchId: `${context.sessionId}-${sequence}-${chunkStartedAt}`,
365
458
  sequence,
366
459
  timestamp: now,
367
- url: sanitizeUrl(window.location.href),
460
+ url: this.viewUrl || sanitizeUrl(window.location.href, this.options.trackHash ?? false),
368
461
  ...isFinal ? { isFinal: true } : {},
369
462
  flushReason,
370
463
  events
@@ -380,10 +473,6 @@ var ReplayTracker = class {
380
473
  batchSizeCache.set(batch, size);
381
474
  return size;
382
475
  }
383
- dropOldestPending(reason) {
384
- const batch = this.pending[0];
385
- if (batch) this.removePending(batch, reason);
386
- }
387
476
  removePending(batch, reason) {
388
477
  const index = this.pending.indexOf(batch);
389
478
  if (index < 0) return false;
@@ -394,18 +483,20 @@ var ReplayTracker = class {
394
483
  }
395
484
  enqueueBatch(batch) {
396
485
  const size = this.batchSizeBytes(batch);
397
- if (size > this.maxQueueSizeBytes) {
398
- this.log(`Replay batch ${batch.sequence} is ${size}B, exceeding ${this.maxQueueSizeBytes}B queue limit; dropping`);
486
+ if (size > this.maxQueueSizeBytes || this.pending.length >= this.maxPendingBatches || this.pendingSizeBytes + size > this.maxQueueSizeBytes) {
487
+ this.log(`Replay queue limit reached at batch ${batch.sequence}; stopping recording at the last complete chain`);
488
+ this.overflowed = true;
489
+ this.stopRecording?.();
490
+ this.stopRecording = void 0;
399
491
  return;
400
492
  }
401
- while ((this.pending.length >= this.maxPendingBatches || this.pendingSizeBytes + size > this.maxQueueSizeBytes) && this.pending.length > 0) this.dropOldestPending("Pending queue limit reached");
402
493
  this.pending.push(batch);
403
494
  this.pendingSizeBytes += size;
404
495
  }
405
496
  async flush(lowLatency, isFinal = false, flushReason = "manual") {
406
- const rotation = touchActivity(this.options.siteKey);
497
+ const rotation = touchActivity(this.options.siteKey, this.lastCookielessMode);
407
498
  if (rotation) this.applySessionRotation(rotation.prev, rotation.next);
408
- const context = getSessionContext(this.options.siteKey);
499
+ const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
409
500
  this.enqueueEventsIfReady(context, {
410
501
  isFinal,
411
502
  flushReason,
@@ -477,9 +568,10 @@ var ReplayTracker = class {
477
568
  }
478
569
  async sendBatch(batch, lowLatency) {
479
570
  const json = JSON.stringify(batch);
571
+ const lowLatencySafe = lowLatency && textEncoder.encode(json).byteLength <= 61440;
480
572
  const sendOptions = {
481
- useBeacon: lowLatency,
482
- keepalive: lowLatency
573
+ useBeacon: lowLatencySafe,
574
+ keepalive: lowLatencySafe
483
575
  };
484
576
  if (!lowLatency && (this.options.compress ?? true) && this.compressionSupported) try {
485
577
  const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
@@ -503,4 +595,4 @@ var ReplayTracker = class {
503
595
  }
504
596
  };
505
597
  //#endregion
506
- export { ReplayTracker as default };
598
+ export { ReplayTracker as default, sessionReplay };
@@ -1,10 +1,15 @@
1
+ import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
1
2
  //#region src/web-vitals.d.ts
2
3
  interface WebVitalsOptions {
3
4
  siteKey: string;
4
5
  baseUrl?: string;
5
6
  debug?: boolean;
6
7
  attribution?: boolean;
8
+ trackHash?: boolean;
9
+ cookieless?: boolean;
7
10
  }
11
+ type WebVitalsExtensionOptions = Pick<WebVitalsOptions, "attribution">;
12
+ declare function webVitals(options?: WebVitalsExtensionOptions): AnalyticsExtension;
8
13
  declare class WebVitalsTracker {
9
14
  private readonly options;
10
15
  private readonly endpoint;
@@ -12,15 +17,15 @@ declare class WebVitalsTracker {
12
17
  private started;
13
18
  private flushing;
14
19
  private initialUrl;
15
- private currentUrl;
20
+ private session;
21
+ private cookieless;
16
22
  constructor(options: WebVitalsOptions);
23
+ setCookielessMode(cookieless: boolean): void;
17
24
  start(): Promise<void>;
18
- stop(): void;
25
+ stop(discard?: boolean): void;
19
26
  onPageHidden(persisted?: boolean): void;
20
- trackPageChange(url?: string): void;
21
27
  private onMetric;
22
- private getMetricUrl;
23
28
  private flush;
24
29
  }
25
30
  //#endregion
26
- export { WebVitalsOptions, WebVitalsTracker as default };
31
+ export { WebVitalsExtensionOptions, WebVitalsOptions, WebVitalsTracker as default, webVitals };