@faststats/web 0.10.3 → 0.12.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.
package/dist/replay.js CHANGED
@@ -1,6 +1,6 @@
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-DAVUdfNm.js";
3
- import { t as getAnonymousId } from "./chunks/identifiers-BgGtAQdp.js";
1
+ import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as sendData } from "./chunks/send-data-DYZZjeM-.js";
2
+ import { a as touchActivity, c as setStorageItem, n as getSessionContext, o as getStorageItem, r as onSessionRotated, t as createId } from "./chunks/session-manager-CFQEWbgo.js";
3
+ import { t as getAnonymousId } from "./chunks/identifiers-CpmDmpDv.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";
@@ -57,57 +57,47 @@ const counters = /* @__PURE__ */ new Map();
57
57
  function sameRecording(counter, context) {
58
58
  return counter?.sessionId === context.sessionId && counter.windowId === context.windowId;
59
59
  }
60
- function readCounter(value) {
60
+ function readCounter(siteKey, cookieless) {
61
+ const raw = cookieless ? null : getStorageItem("sessionStorage", sequenceKey(siteKey));
62
+ if (raw === null) return { corrupt: false };
61
63
  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
+ const parsed = JSON.parse(raw);
65
+ if (parsed && typeof parsed.sessionId === "string" && typeof parsed.windowId === "string" && typeof parsed.next === "number" && Number.isSafeInteger(parsed.next) && parsed.next >= 0) return {
66
+ corrupt: false,
67
+ counter: parsed
68
+ };
64
69
  } catch {}
70
+ return { corrupt: true };
65
71
  }
66
72
  const sequenceKey = (siteKey) => `faststats_replay_sequence_v1_${siteKey}`;
67
73
  /** Whether this recording already sent batches, e.g. from an earlier page load. */
68
74
  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);
75
+ return [counters.get(siteKey), readCounter(siteKey, cookieless).counter].some((counter) => sameRecording(counter, context) && counter.next > 0);
71
76
  }
77
+ /** Next batch sequence for this recording, or undefined when it cannot be trusted. */
72
78
  function nextReplaySequence(siteKey, context, cookieless) {
73
- const key = sequenceKey(siteKey);
74
79
  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,
80
+ const stored = readCounter(siteKey, cookieless);
81
+ if (stored.corrupt && !sameRecording(cached, context)) return;
82
+ let sequence = sameRecording(cached, context) ? cached.next : 0;
83
+ if (sameRecording(stored.counter, context)) sequence = Math.max(sequence, stored.counter.next);
84
+ if (!Number.isSafeInteger(sequence + 1)) return;
85
+ const counter = {
86
+ sessionId: context.sessionId,
87
+ windowId: context.windowId,
96
88
  next: sequence + 1
97
89
  };
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
- };
90
+ counters.set(siteKey, counter);
91
+ if (!cookieless) setStorageItem("sessionStorage", sequenceKey(siteKey), JSON.stringify(counter));
92
+ return sequence;
104
93
  }
105
94
  //#endregion
106
95
  //#region src/replay.ts
107
96
  const MAX_TIMEOUT_MS = 2147483647;
108
97
  const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2097152;
109
98
  const DEFAULT_MAX_BATCH_SIZE_BYTES = 921600;
110
- const DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES = 61440;
99
+ const KEEPALIVE_LIMIT_BYTES = 61440;
100
+ const DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES = 57344;
111
101
  const RETRY_BASE_DELAY_MS = 2e3;
112
102
  const RETRY_MAX_ATTEMPTS = 5;
113
103
  const RETRY_COOLDOWN_MS = 3e5;
@@ -116,7 +106,12 @@ const RRWEB_EVENT_FULL_SNAPSHOT = 2;
116
106
  const RRWEB_EVENT_META = 4;
117
107
  const REPLAY_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
118
108
  const textEncoder = new TextEncoder();
109
+ const eventSizeCache = /* @__PURE__ */ new WeakMap();
119
110
  const batchSizeCache = /* @__PURE__ */ new WeakMap();
111
+ async function gzip(json) {
112
+ const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
113
+ return new Uint8Array(await new Response(stream).arrayBuffer());
114
+ }
120
115
  function boundedInteger(value, fallback, min, max) {
121
116
  if (value === void 0 || !Number.isFinite(value) || value < min) return fallback;
122
117
  return Math.min(Math.trunc(value), max);
@@ -145,7 +140,7 @@ function sessionReplay(options = {}) {
145
140
  var ReplayTracker = class {
146
141
  options;
147
142
  endpoint;
148
- compressionSupported = typeof window !== "undefined" && "CompressionStream" in window;
143
+ compress;
149
144
  flushInterval;
150
145
  maxEvents;
151
146
  maxBatchSizeBytes;
@@ -183,8 +178,9 @@ var ReplayTracker = class {
183
178
  constructor(options) {
184
179
  this.options = options;
185
180
  this.lastCookielessMode = options.cookieless ?? false;
186
- this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.replay}`;
187
- this.flushInterval = boundedInteger(options.flushInterval, 15e3, 1, MAX_TIMEOUT_MS);
181
+ this.endpoint = `${resolveBaseUrl(options.baseUrl)}${URLS.replay}`;
182
+ this.compress = (options.compress ?? true) && typeof window !== "undefined" && "CompressionStream" in window;
183
+ this.flushInterval = boundedInteger(options.flushInterval, 5e3, 1, MAX_TIMEOUT_MS);
188
184
  this.maxEvents = boundedInteger(options.maxEvents, 1e3, 1, 1e5);
189
185
  this.maxBatchSizeBytes = boundedInteger(options.maxBatchSizeBytes, DEFAULT_MAX_BATCH_SIZE_BYTES, 1, 33554432);
190
186
  this.maxLowLatencyBatchSizeBytes = boundedInteger(options.maxLowLatencyBatchSizeBytes, DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES, 1, DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES);
@@ -196,6 +192,12 @@ var ReplayTracker = class {
196
192
  log(...args) {
197
193
  if (this.options.debug) console.log("[Replay]", ...args);
198
194
  }
195
+ sessionContext() {
196
+ return getSessionContext(this.options.siteKey, this.lastCookielessMode);
197
+ }
198
+ sanitize(url) {
199
+ return sanitizeUrl(url, this.options.trackHash ?? false);
200
+ }
199
201
  clearFlushTimers() {
200
202
  if (this.flushTask) clearTimeout(this.flushTask);
201
203
  if (this.retryTask) clearTimeout(this.retryTask);
@@ -205,10 +207,13 @@ var ReplayTracker = class {
205
207
  this.retryAttempt = 0;
206
208
  this.minLengthFlushTask = null;
207
209
  }
208
- clearQueues() {
209
- this.needsFinalization = false;
210
+ dropEvents() {
210
211
  this.events.length = 0;
211
212
  this.queuedEventsSizeBytes = 0;
213
+ }
214
+ clearQueues() {
215
+ this.needsFinalization = false;
216
+ this.dropEvents();
212
217
  this.pending.length = 0;
213
218
  this.pendingSizeBytes = 0;
214
219
  }
@@ -223,7 +228,7 @@ var ReplayTracker = class {
223
228
  if (this.started) this.subscribeToSessionRotation();
224
229
  this.clearQueues();
225
230
  this.clearFlushTimers();
226
- const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
231
+ const context = this.sessionContext();
227
232
  this.activeSessionId = context.sessionId;
228
233
  this.resetSessionTiming(context);
229
234
  this.beginReplayStream();
@@ -236,20 +241,19 @@ var ReplayTracker = class {
236
241
  this.overflowed = false;
237
242
  this.replayEventSequence = 0;
238
243
  this.viewId = createId();
239
- this.viewUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
240
- const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
244
+ this.viewUrl = this.sanitize(window.location.href);
245
+ const context = this.sessionContext();
241
246
  this.activeSessionId = context.sessionId;
242
247
  this.resetSessionTiming(context);
243
248
  this.subscribeToSessionRotation();
244
249
  this.beginRecording();
245
250
  this.intervalId = setInterval(() => this.requestFlush("interval"), this.flushInterval);
246
- window.addEventListener("beforeunload", this.onUnload);
247
251
  this.log("Recording started");
248
252
  }
249
253
  trackPageChange(url) {
250
254
  if (!this.started) return;
251
255
  this.viewId = createId();
252
- const href = sanitizeUrl(url ?? window.location.href, this.options.trackHash ?? false);
256
+ const href = this.sanitize(url ?? window.location.href);
253
257
  this.viewUrl = href;
254
258
  this.onEvent({
255
259
  type: VIEW_META_EVENT_TYPE,
@@ -293,7 +297,7 @@ var ReplayTracker = class {
293
297
  }));
294
298
  if (this.options.recordNetwork === true) {
295
299
  const options = this.options.networkOptions;
296
- const base = resolveBaseUrl(this.options.baseUrl, ANALYTICS_BASE);
300
+ const base = resolveBaseUrl(this.options.baseUrl);
297
301
  const endpoints = new Set(Object.values(URLS).map((path) => {
298
302
  const url = new URL(`${base}${path}`, window.location.href);
299
303
  return `${url.origin}${url.pathname}`;
@@ -356,19 +360,12 @@ var ReplayTracker = class {
356
360
  if (this.intervalId) clearInterval(this.intervalId);
357
361
  this.clearFlushTimers();
358
362
  this.intervalId = null;
359
- window.removeEventListener("beforeunload", this.onUnload);
360
363
  if (discard) {
361
364
  this.clearQueues();
362
365
  this.log("Recording discarded");
363
366
  return;
364
367
  }
365
- if (!this.hasReachedMinLength()) {
366
- this.events.length = 0;
367
- this.queuedEventsSizeBytes = 0;
368
- this.needsFinalization = false;
369
- this.log(`Recording too short (${Date.now() - this.startTime}ms), discarding events`);
370
- }
371
- this.flush(true, true, "stop");
368
+ this.requestTerminalFlush("stop");
372
369
  this.log("Recording stopped");
373
370
  }
374
371
  onSessionRotated(prev, next) {
@@ -383,10 +380,7 @@ var ReplayTracker = class {
383
380
  applySessionRotation(prev, next) {
384
381
  if (this.activeSessionId === next.sessionId) return;
385
382
  if (this.hasReachedMinLength()) this.enqueueEventsIfReady(prev, "sessionRotate");
386
- else {
387
- this.events.length = 0;
388
- this.queuedEventsSizeBytes = 0;
389
- }
383
+ else this.dropEvents();
390
384
  this.finalizeSession(prev, "sessionRotate");
391
385
  this.needsFinalization = false;
392
386
  this.activeSessionId = next.sessionId;
@@ -430,7 +424,7 @@ var ReplayTracker = class {
430
424
  ...event,
431
425
  data: {
432
426
  ...data,
433
- href: sanitizeUrl(data.href, this.options.trackHash ?? false)
427
+ href: this.sanitize(data.href)
434
428
  }
435
429
  };
436
430
  }
@@ -457,22 +451,19 @@ var ReplayTracker = class {
457
451
  if (reason) this.requestFlush(reason);
458
452
  else this.scheduleMinLengthFlush();
459
453
  };
460
- onUnload = () => {
461
- this.flush(true, false, "unload");
462
- };
463
454
  requestTerminalFlush(reason) {
464
455
  if (!this.hasReachedMinLength()) {
465
- this.events.length = 0;
466
- this.queuedEventsSizeBytes = 0;
456
+ this.dropEvents();
467
457
  this.needsFinalization = false;
468
- return;
458
+ this.log(`Recording too short (${Date.now() - this.startTime}ms), discarding events`);
459
+ if (this.pending.length === 0) return;
469
460
  }
470
461
  this.flush(true, true, reason);
471
462
  }
472
463
  hasReachedMinLength() {
473
464
  return this.resumed || this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
474
465
  }
475
- requestFlush = (reason = "manual") => {
466
+ requestFlush(reason) {
476
467
  if (this.flushTask || this.events.length === 0) return;
477
468
  if (this.minLengthFlushTask) {
478
469
  clearTimeout(this.minLengthFlushTask);
@@ -482,7 +473,7 @@ var ReplayTracker = class {
482
473
  this.flushTask = null;
483
474
  this.flush(false, false, reason);
484
475
  }, 0);
485
- };
476
+ }
486
477
  scheduleMinLengthFlush() {
487
478
  if (!this.initialFlushPending || this.events.length === 0 || this.minReplayLengthMs <= 0) return;
488
479
  if (this.hasReachedMinLength()) {
@@ -498,7 +489,7 @@ var ReplayTracker = class {
498
489
  this.requestFlush("minLength");
499
490
  }, Math.min(remainingMs, MAX_TIMEOUT_MS));
500
491
  }
501
- enqueueEventsIfReady(context, flushReason = "manual", maxBatchSizeBytes = this.maxBatchSizeBytes) {
492
+ enqueueEventsIfReady(context, flushReason, maxBatchSizeBytes = this.maxBatchSizeBytes) {
502
493
  if (this.events.length === 0) return;
503
494
  if (!this.hasReachedMinLength()) {
504
495
  this.scheduleMinLengthFlush();
@@ -532,10 +523,10 @@ var ReplayTracker = class {
532
523
  if (current.length > 0) batches.push(current);
533
524
  return batches;
534
525
  }
535
- createBatch(events, context, isFinal, flushReason = "manual") {
526
+ createBatch(events, context, isFinal, flushReason) {
536
527
  const identifier = getAnonymousId(this.options.siteKey, this.lastCookielessMode);
537
528
  const sequence = nextReplaySequence(this.options.siteKey, context, this.lastCookielessMode);
538
- if (!sequence) {
529
+ if (sequence === void 0) {
539
530
  this.stopCapture("Replay sequence state is invalid or exhausted");
540
531
  return;
541
532
  }
@@ -551,17 +542,23 @@ var ReplayTracker = class {
551
542
  chunkStartedAt,
552
543
  chunkEndedAt: now,
553
544
  ...identifier ? { identifier } : {},
554
- batchId: `${context.sessionId}-${sequence.sequence}-${chunkStartedAt}`,
555
- ...sequence,
545
+ batchId: `${context.sessionId}-${sequence}-${chunkStartedAt}`,
546
+ sequence,
547
+ sequenceContractVersion: 1,
556
548
  timestamp: now,
557
- url: this.viewUrl || sanitizeUrl(window.location.href, this.options.trackHash ?? false),
549
+ url: this.viewUrl || this.sanitize(window.location.href),
558
550
  ...isFinal ? { isFinal: true } : {},
559
551
  flushReason,
560
552
  events
561
553
  };
562
554
  }
563
555
  eventSizeBytes(event) {
564
- return textEncoder.encode(JSON.stringify(event)).byteLength + 1;
556
+ let size = eventSizeCache.get(event);
557
+ if (size === void 0) {
558
+ size = textEncoder.encode(JSON.stringify(event)).byteLength + 1;
559
+ eventSizeCache.set(event, size);
560
+ }
561
+ return size;
565
562
  }
566
563
  batchSizeBytes(batch) {
567
564
  const cached = batchSizeCache.get(batch);
@@ -595,7 +592,7 @@ var ReplayTracker = class {
595
592
  }
596
593
  const rotation = touchActivity(this.options.siteKey, this.lastCookielessMode);
597
594
  if (rotation) this.applySessionRotation(rotation.prev, rotation.next);
598
- const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
595
+ const context = this.sessionContext();
599
596
  this.enqueueEventsIfReady(context, flushReason, lowLatency ? this.maxLowLatencyBatchSizeBytes : this.maxBatchSizeBytes);
600
597
  if (!isFinal) return this.sendPending(lowLatency);
601
598
  this.finalizeSession(context, flushReason);
@@ -647,33 +644,25 @@ var ReplayTracker = class {
647
644
  }
648
645
  async sendBatch(batch, lowLatency) {
649
646
  const json = JSON.stringify(batch);
650
- const lowLatencySafe = lowLatency && textEncoder.encode(json).byteLength <= 61440;
651
- const sendOptions = {
647
+ const keepalive = lowLatency && this.batchSizeBytes(batch) <= KEEPALIVE_LIMIT_BYTES;
648
+ const send = (data, contentType, query = "") => sendData({
649
+ url: `${this.endpoint}${query}`,
650
+ data,
651
+ contentType,
652
652
  debug: this.options.debug,
653
653
  debugPrefix: "[Replay]",
654
- useBeacon: lowLatencySafe,
655
- keepalive: lowLatencySafe,
654
+ useBeacon: keepalive,
655
+ keepalive,
656
656
  settleRejected: true
657
- };
658
- if (!lowLatency && (this.options.compress ?? true) && this.compressionSupported) try {
659
- const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
660
- const compressed = new Uint8Array(await new Response(stream).arrayBuffer());
657
+ });
658
+ if (!lowLatency && this.compress) try {
659
+ const compressed = await gzip(json);
661
660
  this.log(`Compressed ${json.length}B -> ${compressed.byteLength}B (${Math.round(compressed.byteLength / json.length * 100)}%)`);
662
- return sendData({
663
- url: `${this.endpoint}?encoding=gzip`,
664
- data: compressed,
665
- contentType: "application/octet-stream",
666
- ...sendOptions
667
- });
661
+ return send(compressed, "application/octet-stream", "?encoding=gzip");
668
662
  } catch {
669
663
  this.log("Compression failed, using uncompressed");
670
664
  }
671
- return sendData({
672
- url: this.endpoint,
673
- data: json,
674
- contentType: "application/json",
675
- ...sendOptions
676
- });
665
+ return send(json, "application/json");
677
666
  }
678
667
  };
679
668
  //#endregion
@@ -1,4 +1,4 @@
1
- import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
1
+ import { t as AnalyticsExtension } from "./chunks/extensions-BfubKIzk.js";
2
2
  //#region src/web-vitals.d.ts
3
3
  export interface WebVitalsOptions {
4
4
  siteKey: string;
@@ -1,5 +1,5 @@
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-DAVUdfNm.js";
1
+ import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as sendData } from "./chunks/send-data-DYZZjeM-.js";
2
+ import { n as getSessionContext } from "./chunks/session-manager-CFQEWbgo.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();
@@ -72,7 +72,7 @@ var WebVitalsTracker = class {
72
72
  cookieless;
73
73
  constructor(options) {
74
74
  this.options = options;
75
- this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.vitals}`;
75
+ this.endpoint = `${resolveBaseUrl(options.baseUrl)}${URLS.vitals}`;
76
76
  this.cookieless = options.cookieless ?? false;
77
77
  }
78
78
  setCookielessMode(cookieless) {
@@ -104,10 +104,8 @@ var WebVitalsTracker = class {
104
104
  });
105
105
  }
106
106
  onMetric = (metric) => {
107
- if (!this.started) return;
108
- const name = metric.name;
109
- if (!METRIC_NAMES.has(name) || !Number.isFinite(metric.value) || metric.value < 0) return;
110
- this.metrics.set(name, metric);
107
+ if (!this.started || !METRIC_NAMES.has(metric.name) || !Number.isFinite(metric.value) || metric.value < 0) return;
108
+ this.metrics.set(metric.name, metric);
111
109
  };
112
110
  discard() {
113
111
  this.generation++;
package/package.json CHANGED
@@ -13,11 +13,6 @@
13
13
  "import": "./dist/index.js",
14
14
  "default": "./dist/index.js"
15
15
  },
16
- "./outbound-links": {
17
- "types": "./dist/outbound-links.d.ts",
18
- "import": "./dist/outbound-links.js",
19
- "default": "./dist/outbound-links.js"
20
- },
21
16
  "./replay": {
22
17
  "types": "./dist/replay.d.ts",
23
18
  "import": "./dist/replay.js",
@@ -28,6 +23,11 @@
28
23
  "import": "./dist/error.js",
29
24
  "default": "./dist/error.js"
30
25
  },
26
+ "./flags": {
27
+ "types": "./dist/flags.d.ts",
28
+ "import": "./dist/flags.js",
29
+ "default": "./dist/flags.js"
30
+ },
31
31
  "./web-vitals": {
32
32
  "types": "./dist/web-vitals.d.ts",
33
33
  "import": "./dist/web-vitals.js",
@@ -42,7 +42,7 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "version": "0.10.3",
45
+ "version": "0.12.0",
46
46
  "scripts": {
47
47
  "build": "tsdown && bun run check-size",
48
48
  "dev": "bun run build",
@@ -51,15 +51,15 @@
51
51
  "check-size": "node scripts/check-bundle-size.mjs"
52
52
  },
53
53
  "devDependencies": {
54
- "@rrweb/types": "^2.1.6",
54
+ "@rrweb/types": "^2.1.7",
55
55
  "@types/bun": "^1.4.2",
56
56
  "tsdown": "^0.23.0",
57
57
  "typescript": "^7.0.2"
58
58
  },
59
59
  "dependencies": {
60
- "@rrweb/record": "^2.1.6",
61
- "@rrweb/rrweb-plugin-console-record": "^2.1.6",
62
- "@rrweb/rrweb-plugin-network-record": "^2.1.6",
63
- "web-vitals": "^6.2.2"
60
+ "@rrweb/record": "^2.1.7",
61
+ "@rrweb/rrweb-plugin-console-record": "^2.1.7",
62
+ "@rrweb/rrweb-plugin-network-record": "^2.1.7",
63
+ "web-vitals": "^6.2.3"
64
64
  }
65
65
  }
@@ -1,53 +0,0 @@
1
- //#region src/utils/api-urls.ts
2
- const ANALYTICS_BASE = "https://metrics.faststats.dev";
3
- function resolveBaseUrl(input, fallback) {
4
- return input?.replace(/\/+$/, "") || fallback;
5
- }
6
- const URLS = {
7
- events: "/v1/web",
8
- identify: "/v1/identify",
9
- replay: "/v1/replay",
10
- vitals: "/v1/vitals"
11
- };
12
- function sanitizeUrl(url, includeHash = false) {
13
- try {
14
- const parsed = new URL(url, location.href);
15
- return `${parsed.origin}${parsed.pathname}${includeHash ? parsed.hash : ""}`;
16
- } catch {
17
- return url.split(includeHash ? "?" : /[?#]/, 1)[0] ?? "";
18
- }
19
- }
20
- const UTM_KEYS = [
21
- "utm_source",
22
- "utm_medium",
23
- "utm_campaign",
24
- "utm_term",
25
- "utm_content"
26
- ];
27
- function getReferrer() {
28
- const raw = document.referrer;
29
- if (!raw) return null;
30
- try {
31
- const pageOrigin = new URL(location.href).origin;
32
- if (new URL(raw, location.href).origin === pageOrigin) return null;
33
- } catch {}
34
- return sanitizeUrl(raw);
35
- }
36
- function getPageContext(includeHash = false) {
37
- const hash = includeHash ? location.hash : "";
38
- const ctx = {
39
- page: `${location.pathname}${hash}`,
40
- url: sanitizeUrl(location.href, includeHash),
41
- referrer: getReferrer(),
42
- title: document.title || ""
43
- };
44
- if (!location.search) return ctx;
45
- const sp = new URLSearchParams(location.search);
46
- for (const key of UTM_KEYS) {
47
- const value = sp.get(key);
48
- if (value) ctx[key] = value;
49
- }
50
- return ctx;
51
- }
52
- //#endregion
53
- export { sanitizeUrl as a, resolveBaseUrl as i, URLS as n, getPageContext as r, ANALYTICS_BASE as t };
@@ -1,9 +0,0 @@
1
- import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
2
- //#region src/outbound-links.d.ts
3
- export declare function outboundLinks(): AnalyticsExtension;
4
- export declare class OutboundLinkTracker {
5
- private lastActivation;
6
- getHref(event: Event): string | null;
7
- private isDuplicate;
8
- }
9
- //#endregion
@@ -1,62 +0,0 @@
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 };