@faststats/web 0.10.2 → 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,4 +1,4 @@
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
4
  function getAnonymousId(siteKey, cookieless = false) {
@@ -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-I5UnTXGR.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.2";
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-I5UnTXGR.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
@@ -89,6 +89,7 @@ export default class ReplayTracker {
89
89
  private lastCookielessMode;
90
90
  private needsFinalization;
91
91
  private initialFlushPending;
92
+ private resumed;
92
93
  private overflowed;
93
94
  private replayEventSequence;
94
95
  private readonly mutationThrottler;
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 { 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-Dplz7m4c.js";
3
- import { t as getAnonymousId } from "./chunks/identifiers-I5UnTXGR.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";
@@ -63,8 +63,14 @@ function readCounter(value) {
63
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
64
  } catch {}
65
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
+ }
66
72
  function nextReplaySequence(siteKey, context, cookieless) {
67
- const key = `faststats_replay_sequence_v1_${siteKey}`;
73
+ const key = sequenceKey(siteKey);
68
74
  const cached = counters.get(siteKey);
69
75
  const raw = cookieless ? null : getStorageItem("sessionStorage", key);
70
76
  const stored = raw === null ? void 0 : readCounter(raw);
@@ -170,6 +176,7 @@ var ReplayTracker = class {
170
176
  lastCookielessMode;
171
177
  needsFinalization = false;
172
178
  initialFlushPending = true;
179
+ resumed = false;
173
180
  overflowed = false;
174
181
  replayEventSequence = 0;
175
182
  mutationThrottler;
@@ -218,8 +225,7 @@ var ReplayTracker = class {
218
225
  this.clearFlushTimers();
219
226
  const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
220
227
  this.activeSessionId = context.sessionId;
221
- this.startTime = Date.now();
222
- this.chunkStartedAt = Date.now();
228
+ this.resetSessionTiming(context);
223
229
  this.beginReplayStream();
224
230
  }
225
231
  start() {
@@ -233,8 +239,7 @@ var ReplayTracker = class {
233
239
  this.viewUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
234
240
  const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
235
241
  this.activeSessionId = context.sessionId;
236
- this.startTime = Date.now();
237
- this.chunkStartedAt = Date.now();
242
+ this.resetSessionTiming(context);
238
243
  this.subscribeToSessionRotation();
239
244
  this.beginRecording();
240
245
  this.intervalId = setInterval(() => this.requestFlush("interval"), this.flushInterval);
@@ -377,10 +382,7 @@ var ReplayTracker = class {
377
382
  }
378
383
  applySessionRotation(prev, next) {
379
384
  if (this.activeSessionId === next.sessionId) return;
380
- if (this.hasReachedMinLength()) this.enqueueEventsIfReady(prev, {
381
- isFinal: true,
382
- flushReason: "sessionRotate"
383
- });
385
+ if (this.hasReachedMinLength()) this.enqueueEventsIfReady(prev, "sessionRotate");
384
386
  else {
385
387
  this.events.length = 0;
386
388
  this.queuedEventsSizeBytes = 0;
@@ -411,9 +413,10 @@ var ReplayTracker = class {
411
413
  }
412
414
  }
413
415
  }
414
- resetSessionTiming(_next) {
416
+ resetSessionTiming(next) {
415
417
  if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
416
418
  this.minLengthFlushTask = null;
419
+ this.resumed = hasReplaySequence(this.options.siteKey, next, this.lastCookielessMode);
417
420
  this.startTime = Date.now();
418
421
  this.chunkStartedAt = Date.now();
419
422
  this.initialFlushPending = true;
@@ -467,7 +470,7 @@ var ReplayTracker = class {
467
470
  this.flush(true, true, reason);
468
471
  }
469
472
  hasReachedMinLength() {
470
- return this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
473
+ return this.resumed || this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
471
474
  }
472
475
  requestFlush = (reason = "manual") => {
473
476
  if (this.flushTask || this.events.length === 0) return;
@@ -495,17 +498,20 @@ var ReplayTracker = class {
495
498
  this.requestFlush("minLength");
496
499
  }, Math.min(remainingMs, MAX_TIMEOUT_MS));
497
500
  }
498
- enqueueEventsIfReady(context, options = {}) {
499
- const { isFinal = false, flushReason = "manual", maxBatchSizeBytes = this.maxBatchSizeBytes } = options;
501
+ enqueueEventsIfReady(context, flushReason = "manual", maxBatchSizeBytes = this.maxBatchSizeBytes) {
500
502
  if (this.events.length === 0) return;
501
- if (!isFinal && !this.hasReachedMinLength()) {
503
+ if (!this.hasReachedMinLength()) {
502
504
  this.scheduleMinLengthFlush();
503
505
  return;
504
506
  }
505
- const batches = this.takeEventBatches(maxBatchSizeBytes);
506
- for (const [index, events] of batches.entries()) {
507
- const batch = this.createBatch(events, context, isFinal && index === batches.length - 1, flushReason);
508
- if (!batch || !this.enqueueBatch(batch)) break;
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;
514
+ this.enqueueBatch(batch);
509
515
  }
510
516
  this.queuedEventsSizeBytes = 0;
511
517
  }
@@ -578,45 +584,27 @@ var ReplayTracker = class {
578
584
  this.stopRecording = void 0;
579
585
  }
580
586
  enqueueBatch(batch) {
581
- const size = this.batchSizeBytes(batch);
582
- if (size > this.maxQueueSizeBytes || this.pending.length >= this.maxPendingBatches || this.pendingSizeBytes + size > this.maxQueueSizeBytes) {
583
- this.stopCapture(`Replay queue limit reached at batch ${batch.sequence}`);
584
- return false;
585
- }
586
587
  this.needsFinalization = !batch.isFinal;
587
588
  this.pending.push(batch);
588
- this.pendingSizeBytes += size;
589
- return true;
589
+ this.pendingSizeBytes += this.batchSizeBytes(batch);
590
590
  }
591
591
  async flush(lowLatency, isFinal = false, flushReason = "manual") {
592
592
  if (this.events.length === 0 && !isFinal) {
593
- await this.sendPending(lowLatency, false);
593
+ await this.sendPending(lowLatency);
594
594
  return;
595
595
  }
596
596
  const rotation = touchActivity(this.options.siteKey, this.lastCookielessMode);
597
597
  if (rotation) this.applySessionRotation(rotation.prev, rotation.next);
598
598
  const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
599
- this.enqueueEventsIfReady(context, {
600
- isFinal,
601
- flushReason,
602
- maxBatchSizeBytes: lowLatency ? this.maxLowLatencyBatchSizeBytes : this.maxBatchSizeBytes
603
- });
604
- if (isFinal) this.finalizeSession(context, flushReason);
605
- if (this.pending.length === 0) return;
606
- await this.sendPending(lowLatency, lowLatency && isFinal);
607
- }
608
- async sendPending(lowLatency, bypassRetryDelay) {
609
- if (this.retryTask) {
610
- if (!bypassRetryDelay) return;
611
- clearTimeout(this.retryTask);
612
- this.retryTask = null;
613
- }
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;
614
606
  if (this.sending) {
615
- const queued = this.queuedFlush;
616
- this.queuedFlush = {
617
- lowLatency: (queued?.lowLatency ?? false) || lowLatency,
618
- bypassRetryDelay: (queued?.bypassRetryDelay ?? false) || bypassRetryDelay
619
- };
607
+ this.queuedFlush = !!this.queuedFlush || lowLatency;
620
608
  return;
621
609
  }
622
610
  this.sending = true;
@@ -630,14 +618,14 @@ var ReplayTracker = class {
630
618
  }
631
619
  this.retryAttempt = 0;
632
620
  this.removePending(batch);
633
- if (this.queuedFlush?.lowLatency) break;
621
+ if (this.queuedFlush) break;
634
622
  }
635
623
  } finally {
636
624
  this.sending = false;
637
- if (this.queuedFlush) {
638
- const queued = this.queuedFlush;
625
+ if (this.queuedFlush !== null) {
626
+ const lowLatency = this.queuedFlush;
639
627
  this.queuedFlush = null;
640
- this.sendPending(queued.lowLatency, queued.bypassRetryDelay);
628
+ this.sendPending(lowLatency);
641
629
  }
642
630
  }
643
631
  }
@@ -649,7 +637,7 @@ var ReplayTracker = class {
649
637
  this.retryTask = setTimeout(() => {
650
638
  this.retryTask = null;
651
639
  if (this.retryAttempt === RETRY_MAX_ATTEMPTS) this.retryAttempt = 0;
652
- this.sendPending(false, false);
640
+ this.sendPending(false);
653
641
  }, delay);
654
642
  }
655
643
  retryDelay(attempt) {
@@ -664,7 +652,8 @@ var ReplayTracker = class {
664
652
  debug: this.options.debug,
665
653
  debugPrefix: "[Replay]",
666
654
  useBeacon: lowLatencySafe,
667
- keepalive: lowLatencySafe
655
+ keepalive: lowLatencySafe,
656
+ settleRejected: true
668
657
  };
669
658
  if (!lowLatency && (this.options.compress ?? true) && this.compressionSupported) try {
670
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.2",
45
+ "version": "0.10.3",
46
46
  "scripts": {
47
47
  "build": "tsdown && bun run check-size",
48
48
  "dev": "bun run build",