@faststats/web 0.10.1 → 0.10.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.
@@ -1,21 +1,18 @@
1
- import { c as removeStorageItem, l as setStorageItem, n as createId, s as getStorageItem } from "./send-data-Dplz7m4c.js";
1
+ import { c as removeStorageItem, l as setStorageItem, n as createId, s as getStorageItem } from "./send-data-DAVUdfNm.js";
2
2
  //#region src/utils/identifiers.ts
3
3
  const ANONYMOUS_ID_KEY = "faststats_anon_id_";
4
- function getOrCreateAnonymousId(siteKey) {
4
+ function getAnonymousId(siteKey, cookieless = false) {
5
+ if (cookieless) return "";
5
6
  const key = `${ANONYMOUS_ID_KEY}${siteKey}`;
6
7
  const existingId = getStorageItem("localStorage", key);
7
8
  if (existingId) return existingId;
8
9
  const newId = createId();
9
10
  return setStorageItem("localStorage", key, newId) ? newId : "";
10
11
  }
11
- function getAnonymousId(siteKey, cookieless = false) {
12
- if (cookieless) return "";
13
- return getOrCreateAnonymousId(siteKey);
14
- }
15
12
  function resetAnonymousId(siteKey, cookieless) {
16
13
  if (cookieless) return "";
17
14
  if (!removeStorageItem("localStorage", `${ANONYMOUS_ID_KEY}${siteKey}`)) return "";
18
- return getOrCreateAnonymousId(siteKey);
15
+ return getAnonymousId(siteKey);
19
16
  }
20
17
  //#endregion
21
18
  export { resetAnonymousId as n, getAnonymousId as t };
@@ -150,7 +150,7 @@ function onSessionRotated(siteKey, cookieless, listener) {
150
150
  //#endregion
151
151
  //#region src/utils/send-data.ts
152
152
  async function sendData(options) {
153
- const { url, data, contentType = "application/json", headers = {}, debug = false, debugPrefix = "[Analytics]", useBeacon = true, keepalive = true, signal } = options;
153
+ const { url, data, contentType = "application/json", headers = {}, debug = false, debugPrefix = "[Analytics]", useBeacon = true, keepalive = true, signal, settleRejected } = options;
154
154
  if (signal?.aborted) return false;
155
155
  if (useBeacon && typeof globalThis.navigator?.sendBeacon === "function") try {
156
156
  const beaconBody = data instanceof Blob || typeof Blob === "undefined" ? data : new Blob([data], { type: contentType });
@@ -160,7 +160,7 @@ async function sendData(options) {
160
160
  }
161
161
  } catch {}
162
162
  try {
163
- const response = await fetch(url, {
163
+ const { ok, status } = await fetch(url, {
164
164
  method: "POST",
165
165
  body: data,
166
166
  headers: {
@@ -171,14 +171,10 @@ async function sendData(options) {
171
171
  credentials: "omit",
172
172
  signal
173
173
  });
174
- const success = response.ok;
175
- if (debug) {
176
- const message = success ? `${debugPrefix} Sent via fetch` : `${debugPrefix} Failed: ${response.status}`;
177
- console[success ? "log" : "warn"](message);
178
- }
179
- return success;
174
+ if (debug) console.log(debugPrefix, ok ? "Sent via fetch" : status);
175
+ return ok || !!settleRejected && status < 500 && status !== 429;
180
176
  } catch {
181
- if (debug) console.warn(`${debugPrefix} Failed to send`);
177
+ if (debug) console.log(debugPrefix, "Failed to send");
182
178
  return false;
183
179
  }
184
180
  }
package/dist/error.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { i as resolveBaseUrl, n as URLS, r as getPageContext, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
2
- import { r as getSessionContext, t as sendData } from "./chunks/send-data-Dplz7m4c.js";
3
- import { t as getAnonymousId } from "./chunks/identifiers-xX7o7oZz.js";
2
+ import { r as getSessionContext, t as sendData } from "./chunks/send-data-DAVUdfNm.js";
3
+ import { t as getAnonymousId } from "./chunks/identifiers-BgGtAQdp.js";
4
4
  //#region src/error.ts
5
5
  const SDK_NAME = "@faststats/web";
6
- const SDK_VERSION = "0.10.1";
6
+ const SDK_VERSION = "0.10.3";
7
7
  function errorTracking(options = {}) {
8
8
  return {
9
9
  name: "error-tracking",
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { i as resolveBaseUrl, n as URLS, r as getPageContext, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
2
- import { a as resetSession, n as createId, o as touchActivity, r as getSessionContext, t as sendData } from "./chunks/send-data-Dplz7m4c.js";
3
- import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-xX7o7oZz.js";
2
+ import { a as resetSession, n as createId, o as touchActivity, r as getSessionContext, t as sendData } from "./chunks/send-data-DAVUdfNm.js";
3
+ import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-BgGtAQdp.js";
4
4
  //#region src/extensions.ts
5
5
  var ExtensionHost = class {
6
6
  runtime;
package/dist/replay.d.ts CHANGED
@@ -31,6 +31,7 @@ export interface ReplayTrackerOptions {
31
31
  maxLowLatencyBatchSizeBytes?: number;
32
32
  maxPendingBatches?: number;
33
33
  maxQueueSizeBytes?: number;
34
+ /** Minimum elapsed time per recording stream before upload. Defaults to 3000. */
34
35
  minReplayLengthMs?: number;
35
36
  sampling?: RecordOptions["sampling"];
36
37
  slimDOMOptions?: RecordOptions["slimDOMOptions"];
@@ -67,7 +68,6 @@ export default class ReplayTracker {
67
68
  private readonly minReplayLengthMs;
68
69
  private readonly events;
69
70
  private readonly pending;
70
- private readonly minLengthBlockedBatches;
71
71
  private queuedEventsSizeBytes;
72
72
  private pendingSizeBytes;
73
73
  private viewId;
@@ -76,7 +76,6 @@ export default class ReplayTracker {
76
76
  private started;
77
77
  private disposed;
78
78
  private startTime;
79
- private sequence;
80
79
  private intervalId;
81
80
  private flushTask;
82
81
  private retryTask;
@@ -90,6 +89,7 @@ export default class ReplayTracker {
90
89
  private lastCookielessMode;
91
90
  private needsFinalization;
92
91
  private initialFlushPending;
92
+ private resumed;
93
93
  private overflowed;
94
94
  private replayEventSequence;
95
95
  private readonly mutationThrottler;
@@ -97,10 +97,7 @@ export default class ReplayTracker {
97
97
  private log;
98
98
  private clearFlushTimers;
99
99
  private clearQueues;
100
- private dropMinLengthBlockedBatches;
101
- private unblockSessionBatches;
102
100
  private finalizeSession;
103
- private nextBatchSequence;
104
101
  setCookielessMode(cookieless: boolean): void;
105
102
  start(): void;
106
103
  trackPageChange(url?: string): void;
@@ -126,6 +123,7 @@ export default class ReplayTracker {
126
123
  private eventSizeBytes;
127
124
  private batchSizeBytes;
128
125
  private removePending;
126
+ private stopCapture;
129
127
  private enqueueBatch;
130
128
  private flush;
131
129
  private sendPending;
package/dist/replay.js CHANGED
@@ -1,6 +1,6 @@
1
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-Dplz7m4c.js";
3
- import { t as getAnonymousId } from "./chunks/identifiers-xX7o7oZz.js";
2
+ import { c as removeStorageItem, 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-DAVUdfNm.js";
3
+ import { t as getAnonymousId } from "./chunks/identifiers-BgGtAQdp.js";
4
4
  import { record } from "@rrweb/record";
5
5
  import { getRecordConsolePlugin } from "@rrweb/rrweb-plugin-console-record";
6
6
  import { getRecordNetworkPlugin } from "@rrweb/rrweb-plugin-network-record";
@@ -52,6 +52,57 @@ var ReplayMutationThrottler = class {
52
52
  }
53
53
  };
54
54
  //#endregion
55
+ //#region src/replay-sequence.ts
56
+ const counters = /* @__PURE__ */ new Map();
57
+ function sameRecording(counter, context) {
58
+ return counter?.sessionId === context.sessionId && counter.windowId === context.windowId;
59
+ }
60
+ function readCounter(value) {
61
+ try {
62
+ const parsed = JSON.parse(value);
63
+ if (parsed && typeof parsed.sessionId === "string" && typeof parsed.windowId === "string" && typeof parsed.next === "number" && Number.isSafeInteger(parsed.next) && parsed.next >= 0 && (parsed.version === 0 || parsed.version === 1)) return parsed;
64
+ } catch {}
65
+ }
66
+ const sequenceKey = (siteKey) => `faststats_replay_sequence_v1_${siteKey}`;
67
+ /** Whether this recording already sent batches, e.g. from an earlier page load. */
68
+ function hasReplaySequence(siteKey, context, cookieless) {
69
+ const raw = cookieless ? null : getStorageItem("sessionStorage", sequenceKey(siteKey));
70
+ return [counters.get(siteKey), raw === null ? void 0 : readCounter(raw)].some((counter) => sameRecording(counter, context) && counter.next > 0);
71
+ }
72
+ function nextReplaySequence(siteKey, context, cookieless) {
73
+ const key = sequenceKey(siteKey);
74
+ const cached = counters.get(siteKey);
75
+ const raw = cookieless ? null : getStorageItem("sessionStorage", key);
76
+ const stored = raw === null ? void 0 : readCounter(raw);
77
+ if (raw !== null && !stored && !sameRecording(cached, context)) return;
78
+ let counter = sameRecording(cached, context) ? cached : void 0;
79
+ if (sameRecording(stored, context) && stored && (!counter || stored.next > counter.next)) counter = stored;
80
+ const legacyKey = `faststats_replay_batch_sequence_${siteKey}_${context.windowId}`;
81
+ if (!counter) {
82
+ const legacy = !cookieless && !stored && !sameRecording(cached, context) ? getStorageItem("sessionStorage", legacyKey) : null;
83
+ const next = legacy === null ? 0 : Number(legacy);
84
+ if (!Number.isSafeInteger(next) || next < 0 || legacy !== null && !/^\d+$/.test(legacy)) return;
85
+ counter = {
86
+ sessionId: context.sessionId,
87
+ windowId: context.windowId,
88
+ next,
89
+ version: legacy === null ? 1 : 0
90
+ };
91
+ }
92
+ if (!Number.isSafeInteger(counter.next + 1)) return;
93
+ const sequence = counter.next;
94
+ const updated = {
95
+ ...counter,
96
+ next: sequence + 1
97
+ };
98
+ counters.set(siteKey, updated);
99
+ if (!cookieless && setStorageItem("sessionStorage", key, JSON.stringify(updated))) removeStorageItem("sessionStorage", legacyKey);
100
+ return {
101
+ sequence,
102
+ ...counter.version === 1 ? { sequenceContractVersion: 1 } : {}
103
+ };
104
+ }
105
+ //#endregion
55
106
  //#region src/replay.ts
56
107
  const MAX_TIMEOUT_MS = 2147483647;
57
108
  const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2097152;
@@ -66,6 +117,10 @@ const RRWEB_EVENT_META = 4;
66
117
  const REPLAY_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
67
118
  const textEncoder = new TextEncoder();
68
119
  const batchSizeCache = /* @__PURE__ */ new WeakMap();
120
+ function boundedInteger(value, fallback, min, max) {
121
+ if (value === void 0 || !Number.isFinite(value) || value < min) return fallback;
122
+ return Math.min(Math.trunc(value), max);
123
+ }
69
124
  function sessionReplay(options = {}) {
70
125
  return {
71
126
  name: "session-replay",
@@ -100,7 +155,6 @@ var ReplayTracker = class {
100
155
  minReplayLengthMs;
101
156
  events = [];
102
157
  pending = [];
103
- minLengthBlockedBatches = /* @__PURE__ */ new WeakSet();
104
158
  queuedEventsSizeBytes = 0;
105
159
  pendingSizeBytes = 0;
106
160
  viewId = createId();
@@ -109,7 +163,6 @@ var ReplayTracker = class {
109
163
  started = false;
110
164
  disposed = false;
111
165
  startTime = 0;
112
- sequence = 0;
113
166
  intervalId = null;
114
167
  flushTask = null;
115
168
  retryTask = null;
@@ -123,6 +176,7 @@ var ReplayTracker = class {
123
176
  lastCookielessMode;
124
177
  needsFinalization = false;
125
178
  initialFlushPending = true;
179
+ resumed = false;
126
180
  overflowed = false;
127
181
  replayEventSequence = 0;
128
182
  mutationThrottler;
@@ -130,13 +184,13 @@ var ReplayTracker = class {
130
184
  this.options = options;
131
185
  this.lastCookielessMode = options.cookieless ?? false;
132
186
  this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.replay}`;
133
- this.flushInterval = options.flushInterval ?? 15e3;
134
- this.maxEvents = options.maxEvents ?? 1e3;
135
- this.maxBatchSizeBytes = options.maxBatchSizeBytes ?? DEFAULT_MAX_BATCH_SIZE_BYTES;
136
- this.maxLowLatencyBatchSizeBytes = options.maxLowLatencyBatchSizeBytes ?? DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES;
137
- this.maxPendingBatches = options.maxPendingBatches ?? 30;
138
- this.maxQueueSizeBytes = options.maxQueueSizeBytes ?? DEFAULT_MAX_QUEUE_SIZE_BYTES;
139
- this.minReplayLengthMs = options.minReplayLengthMs ?? 3e3;
187
+ this.flushInterval = boundedInteger(options.flushInterval, 15e3, 1, MAX_TIMEOUT_MS);
188
+ this.maxEvents = boundedInteger(options.maxEvents, 1e3, 1, 1e5);
189
+ this.maxBatchSizeBytes = boundedInteger(options.maxBatchSizeBytes, DEFAULT_MAX_BATCH_SIZE_BYTES, 1, 33554432);
190
+ this.maxLowLatencyBatchSizeBytes = boundedInteger(options.maxLowLatencyBatchSizeBytes, DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES, 1, DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES);
191
+ this.maxPendingBatches = boundedInteger(options.maxPendingBatches, 30, 1, 1e3);
192
+ this.maxQueueSizeBytes = boundedInteger(options.maxQueueSizeBytes, DEFAULT_MAX_QUEUE_SIZE_BYTES, 1, 33554432);
193
+ this.minReplayLengthMs = boundedInteger(options.minReplayLengthMs, 3e3, 0, MAX_TIMEOUT_MS);
140
194
  this.mutationThrottler = options.mutationThrottle?.enabled === false ? null : new ReplayMutationThrottler(options.mutationThrottle);
141
195
  }
142
196
  log(...args) {
@@ -158,28 +212,10 @@ var ReplayTracker = class {
158
212
  this.pending.length = 0;
159
213
  this.pendingSizeBytes = 0;
160
214
  }
161
- dropMinLengthBlockedBatches() {
162
- this.needsFinalization = false;
163
- for (let index = this.pending.length - 1; index >= 0; index--) {
164
- const batch = this.pending[index];
165
- if (batch && this.minLengthBlockedBatches.has(batch)) this.removePending(batch);
166
- }
167
- }
168
- unblockSessionBatches(sessionId) {
169
- for (const batch of this.pending) if (batch.sessionId === sessionId) this.minLengthBlockedBatches.delete(batch);
170
- }
171
215
  finalizeSession(context, flushReason) {
172
216
  if (!this.needsFinalization) return;
173
- this.enqueueBatch(this.createBatch([], context, true, flushReason));
174
- }
175
- nextBatchSequence(context) {
176
- const key = `faststats_replay_batch_sequence_${this.options.siteKey}_${context.windowId}`;
177
- let sequence = this.sequence;
178
- const parsed = Number.parseInt(getStorageItem("sessionStorage", key) ?? "", 10);
179
- if (Number.isFinite(parsed) && parsed >= 0) sequence = parsed;
180
- setStorageItem("sessionStorage", key, String(sequence + 1));
181
- this.sequence = sequence + 1;
182
- return sequence;
217
+ const batch = this.createBatch([], context, true, flushReason);
218
+ if (batch) this.enqueueBatch(batch);
183
219
  }
184
220
  setCookielessMode(cookieless) {
185
221
  if (this.lastCookielessMode === cookieless) return;
@@ -189,8 +225,7 @@ var ReplayTracker = class {
189
225
  this.clearFlushTimers();
190
226
  const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
191
227
  this.activeSessionId = context.sessionId;
192
- this.startTime = context.sessionStart;
193
- this.chunkStartedAt = Date.now();
228
+ this.resetSessionTiming(context);
194
229
  this.beginReplayStream();
195
230
  }
196
231
  start() {
@@ -204,8 +239,7 @@ var ReplayTracker = class {
204
239
  this.viewUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
205
240
  const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
206
241
  this.activeSessionId = context.sessionId;
207
- this.startTime = context.sessionStart;
208
- this.chunkStartedAt = Date.now();
242
+ this.resetSessionTiming(context);
209
243
  this.subscribeToSessionRotation();
210
244
  this.beginRecording();
211
245
  this.intervalId = setInterval(() => this.requestFlush("interval"), this.flushInterval);
@@ -227,7 +261,7 @@ var ReplayTracker = class {
227
261
  viewId: this.viewId
228
262
  }
229
263
  }
230
- }, false);
264
+ });
231
265
  }
232
266
  onPageHidden(persisted = false, terminal = false) {
233
267
  if (persisted) return;
@@ -276,7 +310,7 @@ var ReplayTracker = class {
276
310
  return plugins;
277
311
  }
278
312
  beginRecording() {
279
- const stop = record({
313
+ this.stopRecording = record({
280
314
  emit: this.onEvent,
281
315
  sampling: this.options.sampling ?? {
282
316
  mousemove: 50,
@@ -310,7 +344,6 @@ var ReplayTracker = class {
310
344
  checkoutEveryNth: this.options.checkoutEveryNth,
311
345
  plugins: this.recordingPlugins()
312
346
  });
313
- this.stopRecording = stop;
314
347
  }
315
348
  stop(discard = false) {
316
349
  if (!this.started) return;
@@ -332,8 +365,8 @@ var ReplayTracker = class {
332
365
  if (!this.hasReachedMinLength()) {
333
366
  this.events.length = 0;
334
367
  this.queuedEventsSizeBytes = 0;
335
- this.dropMinLengthBlockedBatches();
336
- this.log(`Session too short (${Date.now() - this.startTime}ms), discarding events`);
368
+ this.needsFinalization = false;
369
+ this.log(`Recording too short (${Date.now() - this.startTime}ms), discarding events`);
337
370
  }
338
371
  this.flush(true, true, "stop");
339
372
  this.log("Recording stopped");
@@ -349,12 +382,12 @@ var ReplayTracker = class {
349
382
  }
350
383
  applySessionRotation(prev, next) {
351
384
  if (this.activeSessionId === next.sessionId) return;
352
- this.enqueueEventsIfReady(prev, {
353
- isFinal: true,
354
- flushReason: "sessionRotate"
355
- });
385
+ if (this.hasReachedMinLength()) this.enqueueEventsIfReady(prev, "sessionRotate");
386
+ else {
387
+ this.events.length = 0;
388
+ this.queuedEventsSizeBytes = 0;
389
+ }
356
390
  this.finalizeSession(prev, "sessionRotate");
357
- this.unblockSessionBatches(prev.sessionId);
358
391
  this.needsFinalization = false;
359
392
  this.activeSessionId = next.sessionId;
360
393
  this.resetSessionTiming(next);
@@ -383,11 +416,12 @@ var ReplayTracker = class {
383
416
  resetSessionTiming(next) {
384
417
  if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
385
418
  this.minLengthFlushTask = null;
386
- this.startTime = next.sessionStart;
419
+ this.resumed = hasReplaySequence(this.options.siteKey, next, this.lastCookielessMode);
420
+ this.startTime = Date.now();
387
421
  this.chunkStartedAt = Date.now();
388
422
  this.initialFlushPending = true;
389
423
  }
390
- onEvent = (event, _isCheckout) => {
424
+ onEvent = (event) => {
391
425
  if (this.overflowed) return;
392
426
  let capturedEvent = event;
393
427
  if (event.type === RRWEB_EVENT_META) {
@@ -406,9 +440,17 @@ var ReplayTracker = class {
406
440
  if (this.mutationThrottler && !throttled) return;
407
441
  if (throttled) capturedEvent = throttled;
408
442
  }
409
- capturedEvent = Object.assign(capturedEvent, { [REPLAY_SEQUENTIAL_ID_KEY]: ++this.replayEventSequence });
410
- this.events.push(capturedEvent);
411
- this.queuedEventsSizeBytes += this.eventSizeBytes(capturedEvent);
443
+ const sequencedEvent = {
444
+ ...capturedEvent,
445
+ [REPLAY_SEQUENTIAL_ID_KEY]: ++this.replayEventSequence
446
+ };
447
+ const eventBytes = this.eventSizeBytes(sequencedEvent);
448
+ if (this.pendingSizeBytes + this.queuedEventsSizeBytes + eventBytes > this.maxQueueSizeBytes) {
449
+ this.stopCapture("Replay buffer limit reached");
450
+ return;
451
+ }
452
+ this.events.push(sequencedEvent);
453
+ this.queuedEventsSizeBytes += eventBytes;
412
454
  let reason;
413
455
  if (this.events.length >= this.maxEvents) reason = "maxEvents";
414
456
  else if (this.queuedEventsSizeBytes >= this.maxBatchSizeBytes) reason = "maxBytes";
@@ -422,13 +464,13 @@ var ReplayTracker = class {
422
464
  if (!this.hasReachedMinLength()) {
423
465
  this.events.length = 0;
424
466
  this.queuedEventsSizeBytes = 0;
425
- this.dropMinLengthBlockedBatches();
467
+ this.needsFinalization = false;
426
468
  return;
427
469
  }
428
470
  this.flush(true, true, reason);
429
471
  }
430
472
  hasReachedMinLength() {
431
- return this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
473
+ return this.resumed || this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
432
474
  }
433
475
  requestFlush = (reason = "manual") => {
434
476
  if (this.flushTask || this.events.length === 0) return;
@@ -456,18 +498,19 @@ var ReplayTracker = class {
456
498
  this.requestFlush("minLength");
457
499
  }, Math.min(remainingMs, MAX_TIMEOUT_MS));
458
500
  }
459
- enqueueEventsIfReady(context, options = {}) {
460
- const { isFinal = false, flushReason = "manual", maxBatchSizeBytes = this.maxBatchSizeBytes, sealBeforeMinLength = false } = options;
501
+ enqueueEventsIfReady(context, flushReason = "manual", maxBatchSizeBytes = this.maxBatchSizeBytes) {
461
502
  if (this.events.length === 0) return;
462
- if (!isFinal && !sealBeforeMinLength && !this.hasReachedMinLength()) {
503
+ if (!this.hasReachedMinLength()) {
463
504
  this.scheduleMinLengthFlush();
464
505
  return;
465
506
  }
466
- const batches = this.takeEventBatches(maxBatchSizeBytes);
467
- const blockedByMinLength = sealBeforeMinLength && !this.hasReachedMinLength();
468
- for (const [index, events] of batches.entries()) {
469
- const batch = this.createBatch(events, context, isFinal && index === batches.length - 1, flushReason);
470
- if (blockedByMinLength) this.minLengthBlockedBatches.add(batch);
507
+ for (const events of this.takeEventBatches(maxBatchSizeBytes)) {
508
+ if (this.pending.length >= this.maxPendingBatches) {
509
+ this.stopCapture("Replay queue limit reached");
510
+ break;
511
+ }
512
+ const batch = this.createBatch(events, context, false, flushReason);
513
+ if (!batch) break;
471
514
  this.enqueueBatch(batch);
472
515
  }
473
516
  this.queuedEventsSizeBytes = 0;
@@ -491,7 +534,11 @@ var ReplayTracker = class {
491
534
  }
492
535
  createBatch(events, context, isFinal, flushReason = "manual") {
493
536
  const identifier = getAnonymousId(this.options.siteKey, this.lastCookielessMode);
494
- const sequence = this.nextBatchSequence(context);
537
+ const sequence = nextReplaySequence(this.options.siteKey, context, this.lastCookielessMode);
538
+ if (!sequence) {
539
+ this.stopCapture("Replay sequence state is invalid or exhausted");
540
+ return;
541
+ }
495
542
  const chunkStartedAt = this.chunkStartedAt;
496
543
  const now = Date.now();
497
544
  this.chunkStartedAt = now;
@@ -504,8 +551,8 @@ var ReplayTracker = class {
504
551
  chunkStartedAt,
505
552
  chunkEndedAt: now,
506
553
  ...identifier ? { identifier } : {},
507
- batchId: `${context.sessionId}-${sequence}-${chunkStartedAt}`,
508
- sequence,
554
+ batchId: `${context.sessionId}-${sequence.sequence}-${chunkStartedAt}`,
555
+ ...sequence,
509
556
  timestamp: now,
510
557
  url: this.viewUrl || sanitizeUrl(window.location.href, this.options.trackHash ?? false),
511
558
  ...isFinal ? { isFinal: true } : {},
@@ -523,76 +570,62 @@ var ReplayTracker = class {
523
570
  batchSizeCache.set(batch, size);
524
571
  return size;
525
572
  }
526
- removePending(batch, reason) {
573
+ removePending(batch) {
527
574
  const index = this.pending.indexOf(batch);
528
- if (index < 0) return false;
575
+ if (index < 0) return;
529
576
  this.pending.splice(index, 1);
530
577
  this.pendingSizeBytes = Math.max(0, this.pendingSizeBytes - this.batchSizeBytes(batch));
531
- if (reason) this.log(`${reason}, dropping batch ${batch.sequence}`);
532
- return true;
578
+ this.log(`Sent replay batch ${batch.sequence}`);
579
+ }
580
+ stopCapture(reason) {
581
+ this.log(reason);
582
+ this.overflowed = true;
583
+ this.stopRecording?.();
584
+ this.stopRecording = void 0;
533
585
  }
534
586
  enqueueBatch(batch) {
535
- const size = this.batchSizeBytes(batch);
536
- if (size > this.maxQueueSizeBytes || this.pending.length >= this.maxPendingBatches || this.pendingSizeBytes + size > this.maxQueueSizeBytes) {
537
- this.log(`Replay queue limit reached at batch ${batch.sequence}; stopping recording at the last complete chain`);
538
- this.overflowed = true;
539
- this.stopRecording?.();
540
- this.stopRecording = void 0;
541
- return;
542
- }
543
587
  this.needsFinalization = !batch.isFinal;
544
588
  this.pending.push(batch);
545
- this.pendingSizeBytes += size;
589
+ this.pendingSizeBytes += this.batchSizeBytes(batch);
546
590
  }
547
591
  async flush(lowLatency, isFinal = false, flushReason = "manual") {
592
+ if (this.events.length === 0 && !isFinal) {
593
+ await this.sendPending(lowLatency);
594
+ return;
595
+ }
548
596
  const rotation = touchActivity(this.options.siteKey, this.lastCookielessMode);
549
597
  if (rotation) this.applySessionRotation(rotation.prev, rotation.next);
550
598
  const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
551
- this.enqueueEventsIfReady(context, {
552
- isFinal,
553
- flushReason,
554
- maxBatchSizeBytes: lowLatency ? this.maxLowLatencyBatchSizeBytes : this.maxBatchSizeBytes
555
- });
556
- if (isFinal) this.finalizeSession(context, flushReason);
557
- if (this.pending.length === 0) return;
558
- if (isFinal) for (const batch of this.pending) this.minLengthBlockedBatches.delete(batch);
559
- else if (!rotation && flushReason !== "sessionRotate" && this.hasReachedMinLength()) this.unblockSessionBatches(context.sessionId);
560
- await this.sendPending(lowLatency, lowLatency && isFinal);
561
- }
562
- async sendPending(lowLatency, bypassRetryDelay) {
563
- if (this.retryTask) {
564
- if (!bypassRetryDelay) return;
565
- clearTimeout(this.retryTask);
566
- this.retryTask = null;
567
- }
599
+ this.enqueueEventsIfReady(context, flushReason, lowLatency ? this.maxLowLatencyBatchSizeBytes : this.maxBatchSizeBytes);
600
+ if (!isFinal) return this.sendPending(lowLatency);
601
+ this.finalizeSession(context, flushReason);
602
+ for (const batch of [...this.pending].reverse()) this.sendBatch(batch, true).then((sent) => sent && this.removePending(batch));
603
+ }
604
+ async sendPending(lowLatency) {
605
+ if (this.retryTask) return;
568
606
  if (this.sending) {
569
- const queued = this.queuedFlush;
570
- this.queuedFlush = {
571
- lowLatency: (queued?.lowLatency ?? false) || lowLatency,
572
- bypassRetryDelay: (queued?.bypassRetryDelay ?? false) || bypassRetryDelay
573
- };
607
+ this.queuedFlush = !!this.queuedFlush || lowLatency;
574
608
  return;
575
609
  }
576
610
  this.sending = true;
577
611
  try {
578
612
  while (this.pending.length > 0) {
579
613
  const batch = this.pending[0];
580
- if (this.minLengthBlockedBatches.has(batch)) break;
581
614
  if (!await this.sendBatch(batch, lowLatency)) {
582
615
  this.log(`Failed to send replay batch ${batch.sequence}`);
583
616
  if (!this.disposed) this.scheduleRetry();
584
617
  break;
585
618
  }
586
619
  this.retryAttempt = 0;
587
- this.removePending(batch, "Sent replay batch");
588
- if (this.queuedFlush?.lowLatency) break;
620
+ this.removePending(batch);
621
+ if (this.queuedFlush) break;
589
622
  }
590
623
  } finally {
591
624
  this.sending = false;
592
- if (this.queuedFlush) {
593
- const queued = this.queuedFlush;
625
+ if (this.queuedFlush !== null) {
626
+ const lowLatency = this.queuedFlush;
594
627
  this.queuedFlush = null;
595
- this.sendPending(queued.lowLatency, queued.bypassRetryDelay);
628
+ this.sendPending(lowLatency);
596
629
  }
597
630
  }
598
631
  }
@@ -604,7 +637,7 @@ var ReplayTracker = class {
604
637
  this.retryTask = setTimeout(() => {
605
638
  this.retryTask = null;
606
639
  if (this.retryAttempt === RETRY_MAX_ATTEMPTS) this.retryAttempt = 0;
607
- this.flush(false);
640
+ this.sendPending(false);
608
641
  }, delay);
609
642
  }
610
643
  retryDelay(attempt) {
@@ -619,7 +652,8 @@ var ReplayTracker = class {
619
652
  debug: this.options.debug,
620
653
  debugPrefix: "[Replay]",
621
654
  useBeacon: lowLatencySafe,
622
- keepalive: lowLatencySafe
655
+ keepalive: lowLatencySafe,
656
+ settleRejected: true
623
657
  };
624
658
  if (!lowLatency && (this.options.compress ?? true) && this.compressionSupported) try {
625
659
  const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
@@ -1,5 +1,5 @@
1
1
  import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
2
- import { r as getSessionContext, t as sendData } from "./chunks/send-data-Dplz7m4c.js";
2
+ import { r as getSessionContext, t as sendData } from "./chunks/send-data-DAVUdfNm.js";
3
3
  import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals/attribution";
4
4
  //#region src/utils/web-vitals-collector.ts
5
5
  const collectors = /* @__PURE__ */ new WeakMap();
package/package.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "version": "0.10.1",
45
+ "version": "0.10.3",
46
46
  "scripts": {
47
47
  "build": "tsdown && bun run check-size",
48
48
  "dev": "bun run build",
@@ -60,6 +60,6 @@
60
60
  "@rrweb/record": "^2.1.6",
61
61
  "@rrweb/rrweb-plugin-console-record": "^2.1.6",
62
62
  "@rrweb/rrweb-plugin-network-record": "^2.1.6",
63
- "web-vitals": "^6.2.1"
63
+ "web-vitals": "^6.2.2"
64
64
  }
65
65
  }