@faststats/web 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,8 +52,10 @@ Consent remains client-local: use `"anonymous"` for cookieless collection,
52
52
  that the page was visible and its document had focus. It pauses when the tab is
53
53
  hidden, the window loses focus, or the page enters the back-forward cache, and
54
54
  resets on navigation, logout, or session rotation. A monotonic clock keeps
55
- system-clock changes from affecting this duration. Reading without interacting still counts while the page has focus;
56
- this metric does not infer whether a person is idle.
55
+ system-clock changes from affecting this duration. It also pauses after 60
56
+ seconds without pointer, keyboard, or scroll input, unless a `<video>` or
57
+ `<audio>` element on the page is playing, and resumes on the next input. Time
58
+ the device spent asleep or the tab frozen is not counted.
57
59
 
58
60
  Pageviews wait until the page becomes visible. Pages replaced or closed before
59
61
  being shown do not emit pageviews or page-leave events.
@@ -68,9 +70,10 @@ Each visit retains its original identity, session, and page metadata. A session
68
70
  or identity change starts a new visit, even on the same route. Both persistent
69
71
  and cookieless sessions expire after 30 minutes of inactivity or 24 hours of age.
70
72
 
71
- `session_duration` is elapsed wall-clock time since the session started, including
72
- time away. It is separate from active time on the page. Paused visits do not keep
73
- sending larger session durations while the user is away.
73
+ `session_duration` is elapsed wall-clock time from the session start to the last
74
+ counted activity. It includes time away between two active periods of the same
75
+ session, so it is separate from active time on the page. An idle visitor stops
76
+ extending the session, which then expires after 30 minutes like any other.
74
77
 
75
78
  Delivery is best effort: a sudden process kill or network failure can lose time
76
79
  since the last delivered checkpoint. The collector must accept successive events
@@ -1,4 +1,4 @@
1
- import { c as setStorageItem, o as getStorageItem, s as removeStorageItem, t as createId } from "./session-manager-CFQEWbgo.js";
1
+ import { c as setStorageItem, o as getStorageItem, s as removeStorageItem, t as createId } from "./session-manager-DUpF3mi3.js";
2
2
  //#region src/utils/identifiers.ts
3
3
  const ANONYMOUS_ID_KEY = "faststats_anon_id_";
4
4
  function getAnonymousId(siteKey, cookieless = false) {
@@ -27,8 +27,7 @@ function getReferrer() {
27
27
  const raw = document.referrer;
28
28
  if (!raw) return null;
29
29
  try {
30
- const pageOrigin = new URL(location.href).origin;
31
- if (new URL(raw, location.href).origin === pageOrigin) return null;
30
+ if (new URL(raw, location.href).origin === location.origin) return null;
32
31
  } catch {}
33
32
  return sanitizeUrl(raw);
34
33
  }
@@ -38,7 +37,7 @@ function getPageContext(includeHash = false) {
38
37
  page: `${location.pathname}${hash}`,
39
38
  url: sanitizeUrl(location.href, includeHash),
40
39
  referrer: getReferrer(),
41
- title: document.title || ""
40
+ title: document.title
42
41
  };
43
42
  if (!location.search) return ctx;
44
43
  const sp = new URLSearchParams(location.search);
@@ -53,8 +52,8 @@ function getPageContext(includeHash = false) {
53
52
  async function sendData(options) {
54
53
  const { url, data, contentType = "application/json", debug = false, debugPrefix = "[Analytics]", useBeacon = true, keepalive = true, settleRejected } = options;
55
54
  if (useBeacon && typeof globalThis.navigator?.sendBeacon === "function") try {
56
- const beaconBody = data instanceof Blob || typeof Blob === "undefined" ? data : new Blob([data], { type: contentType });
57
- if (globalThis.navigator.sendBeacon(url, beaconBody)) {
55
+ const body = data instanceof Blob ? data : new Blob([data], { type: contentType });
56
+ if (globalThis.navigator.sendBeacon(url, body)) {
58
57
  if (debug) console.log(`${debugPrefix} Sent via beacon`);
59
58
  return true;
60
59
  }
@@ -137,15 +137,12 @@ function resetSession(siteKey, cookieless = false) {
137
137
  }
138
138
  function onSessionRotated(siteKey, cookieless, listener) {
139
139
  const key = memoryKey(siteKey, cookieless);
140
- let listeners = rotationListeners.get(key);
141
- if (!listeners) {
142
- listeners = /* @__PURE__ */ new Set();
143
- rotationListeners.set(key, listeners);
144
- }
140
+ const listeners = rotationListeners.get(key) ?? /* @__PURE__ */ new Set();
141
+ rotationListeners.set(key, listeners);
145
142
  listeners.add(listener);
146
143
  return () => {
147
- listeners?.delete(listener);
148
- if (listeners?.size === 0) rotationListeners.delete(key);
144
+ listeners.delete(listener);
145
+ if (listeners.size === 0) rotationListeners.delete(key);
149
146
  };
150
147
  }
151
148
  //#endregion
package/dist/error.js CHANGED
@@ -1,9 +1,9 @@
1
- import { i as resolveBaseUrl, n as URLS, r as getPageContext, t as sendData } from "./chunks/send-data-DYZZjeM-.js";
2
- import { n as getSessionContext } from "./chunks/session-manager-CFQEWbgo.js";
3
- import { t as getAnonymousId } from "./chunks/identifiers-CpmDmpDv.js";
1
+ import { i as resolveBaseUrl, n as URLS, r as getPageContext, t as sendData } from "./chunks/send-data-gBIF6DTb.js";
2
+ import { n as getSessionContext } from "./chunks/session-manager-DUpF3mi3.js";
3
+ import { t as getAnonymousId } from "./chunks/identifiers-DOtve3E5.js";
4
4
  //#region src/error.ts
5
5
  const SDK_NAME = "@faststats/web";
6
- const SDK_VERSION = "0.12.0";
6
+ const SDK_VERSION = "0.12.1";
7
7
  function errorTracking(options = {}) {
8
8
  return {
9
9
  name: "error-tracking",
package/dist/flags.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as getSessionContext } from "./chunks/session-manager-CFQEWbgo.js";
2
- import { t as getAnonymousId } from "./chunks/identifiers-CpmDmpDv.js";
1
+ import { n as getSessionContext } from "./chunks/session-manager-DUpF3mi3.js";
2
+ import { t as getAnonymousId } from "./chunks/identifiers-DOtve3E5.js";
3
3
  //#region src/flags.ts
4
4
  const DEFAULT_ENDPOINT = "https://flags.faststats.dev";
5
5
  const DEFAULT_TIMEOUT_MS = 3e3;
@@ -138,7 +138,7 @@ var FeatureFlags = class {
138
138
  }
139
139
  }
140
140
  };
141
- function featureFlags(options = {}) {
141
+ function featureFlags(options) {
142
142
  return new FeatureFlags(options);
143
143
  }
144
144
  //#endregion
package/dist/index.d.ts CHANGED
@@ -38,6 +38,7 @@ export declare class WebAnalytics {
38
38
  private pageKey;
39
39
  private visit;
40
40
  private pendingPageview;
41
+ private lastInputAt;
41
42
  private consentMode;
42
43
  constructor(options: WebAnalyticsOptions);
43
44
  start(): void;
@@ -65,6 +66,8 @@ export declare class WebAnalytics {
65
66
  private readonly onPageHide;
66
67
  private readonly onPageShow;
67
68
  private readonly onResume;
69
+ private readonly onActivity;
70
+ private isIdle;
68
71
  private leavePage;
69
72
  private checkpoint;
70
73
  private pauseVisit;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, r as getPageContext, t as sendData } from "./chunks/send-data-DYZZjeM-.js";
2
- import { a as touchActivity, i as resetSession, n as getSessionContext, t as createId } from "./chunks/session-manager-CFQEWbgo.js";
3
- import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-CpmDmpDv.js";
1
+ import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, r as getPageContext, t as sendData } from "./chunks/send-data-gBIF6DTb.js";
2
+ import { a as touchActivity, i as resetSession, n as getSessionContext, t as createId } from "./chunks/session-manager-DUpF3mi3.js";
3
+ import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-DOtve3E5.js";
4
4
  //#region src/extensions.ts
5
5
  var ExtensionHost = class {
6
6
  context;
@@ -40,11 +40,8 @@ var ExtensionHost = class {
40
40
  ...this.context,
41
41
  on: (hook, handler) => {
42
42
  if (this.stopped) return () => {};
43
- let handlers = this.handlers.get(hook);
44
- if (!handlers) {
45
- handlers = /* @__PURE__ */ new Set();
46
- this.handlers.set(hook, handlers);
47
- }
43
+ const handlers = this.handlers.get(hook) ?? /* @__PURE__ */ new Set();
44
+ this.handlers.set(hook, handlers);
48
45
  handlers.add(handler);
49
46
  const off = () => handlers.delete(handler);
50
47
  subscriptions.push(off);
@@ -132,6 +129,7 @@ const RESERVED_EVENTS = /* @__PURE__ */ new Set([
132
129
  "error",
133
130
  "outbound_link"
134
131
  ]);
132
+ const IDLE_MS = 6e4;
135
133
  var WebAnalytics = class {
136
134
  options;
137
135
  baseUrl;
@@ -143,6 +141,7 @@ var WebAnalytics = class {
143
141
  pageKey = "";
144
142
  visit = null;
145
143
  pendingPageview = null;
144
+ lastInputAt = 0;
146
145
  consentMode;
147
146
  constructor(options) {
148
147
  this.options = options;
@@ -170,7 +169,7 @@ var WebAnalytics = class {
170
169
  const key = location.pathname + location.search + (this.options.trackHash ? location.hash : "");
171
170
  if (key !== this.pageKey) {
172
171
  this.leavePage();
173
- if (this.pageKey) this.extensions?.emit("pageChange", { url: getPageContext(this.options.trackHash).url ?? "" });
172
+ if (this.pageKey) this.extensions?.emit("pageChange", { url: getPageContext(this.options.trackHash).url });
174
173
  this.pageKey = key;
175
174
  this.pendingPageview = null;
176
175
  }
@@ -190,6 +189,7 @@ var WebAnalytics = class {
190
189
  context,
191
190
  duration: 0,
192
191
  startedAt: null,
192
+ checkedAt: Date.now(),
193
193
  reportedDuration: -1
194
194
  };
195
195
  this.send("pageview", {
@@ -326,12 +326,18 @@ var WebAnalytics = class {
326
326
  this.bind(window, "focus", this.onResume);
327
327
  this.bind(window, "blur", () => this.pauseVisit());
328
328
  this.bind(window, "popstate", this.onNavigate);
329
+ for (const type of [
330
+ "pointerdown",
331
+ "pointermove",
332
+ "keydown",
333
+ "scroll"
334
+ ]) this.bind(window, type, this.onActivity, true);
329
335
  if (this.options.trackHash) this.bind(window, "hashchange", this.onNavigate);
330
336
  this.cleanup.push(observeHistory(this.onNavigate), trackOutboundLinks((url) => this.send("outbound_link", { outbound_link: url })));
331
337
  }
332
- bind(target, type, listener) {
333
- target.addEventListener(type, listener);
334
- this.cleanup.push(() => target.removeEventListener(type, listener));
338
+ bind(target, type, listener, capture = false) {
339
+ target.addEventListener(type, listener, capture);
340
+ this.cleanup.push(() => target.removeEventListener(type, listener, capture));
335
341
  }
336
342
  onNavigate = () => this.pageview({ trigger: "navigation" });
337
343
  onVisibilityChange = () => {
@@ -365,6 +371,17 @@ var WebAnalytics = class {
365
371
  this.pageview(this.pendingPageview ?? { trigger: "session" });
366
372
  this.resumeVisit();
367
373
  };
374
+ onActivity = () => {
375
+ this.lastInputAt = performance.now();
376
+ if (this.visit?.startedAt === null && document.hasFocus()) this.onResume();
377
+ };
378
+ isIdle() {
379
+ const now = performance.now();
380
+ if (now - this.lastInputAt < IDLE_MS) return false;
381
+ const playing = [...document.querySelectorAll("video,audio")].some((media) => !media.paused && !media.ended);
382
+ if (playing) this.lastInputAt = now;
383
+ return !playing;
384
+ }
368
385
  leavePage() {
369
386
  this.pauseVisit();
370
387
  this.visit = null;
@@ -372,10 +389,16 @@ var WebAnalytics = class {
372
389
  checkpoint() {
373
390
  const visit = this.visit;
374
391
  if (!visit || !this.isActive()) return;
392
+ let activeAt = visit.checkedAt;
375
393
  if (visit.startedAt !== null) {
376
394
  const now = performance.now();
377
- visit.duration += now - visit.startedAt;
395
+ if (now - visit.startedAt < IDLE_MS) {
396
+ const active = Math.min(now, this.lastInputAt + IDLE_MS) - visit.startedAt;
397
+ visit.duration += active;
398
+ activeAt += active;
399
+ }
378
400
  visit.startedAt = now;
401
+ visit.checkedAt = Date.now();
379
402
  }
380
403
  const duration = Math.round(visit.duration);
381
404
  if (duration === visit.reportedDuration) return;
@@ -383,7 +406,7 @@ var WebAnalytics = class {
383
406
  this.send("page_leave", {
384
407
  visit_id: visit.id,
385
408
  time_on_page: duration,
386
- session_duration: Math.max(0, Date.now() - visit.session.sessionStart)
409
+ session_duration: Math.max(0, activeAt - visit.session.sessionStart)
387
410
  }, visit.context);
388
411
  }
389
412
  pauseVisit() {
@@ -394,9 +417,10 @@ var WebAnalytics = class {
394
417
  }
395
418
  resumeVisit() {
396
419
  if (!this.isActive() || !this.visit || this.visit.startedAt !== null || document.visibilityState !== "visible" || !document.hasFocus()) return;
397
- this.visit.startedAt = performance.now();
420
+ this.visit.startedAt = this.lastInputAt = performance.now();
421
+ this.visit.checkedAt = Date.now();
398
422
  this.heartbeatTimer = setInterval(() => {
399
- if (document.visibilityState !== "visible" || !document.hasFocus()) {
423
+ if (document.visibilityState !== "visible" || !document.hasFocus() || this.isIdle()) {
400
424
  this.pauseVisit();
401
425
  return;
402
426
  }
package/dist/replay.d.ts CHANGED
@@ -133,4 +133,5 @@ export default class ReplayTracker {
133
133
  private retryDelay;
134
134
  private sendBatch;
135
135
  }
136
- //#endregion
136
+ //#endregion
137
+ export {};
package/dist/replay.js CHANGED
@@ -1,6 +1,6 @@
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";
1
+ import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as sendData } from "./chunks/send-data-gBIF6DTb.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-DUpF3mi3.js";
3
+ import { t as getAnonymousId } from "./chunks/identifiers-DOtve3E5.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";
@@ -26,7 +26,7 @@ var ReplayMutationThrottler = class {
26
26
  if (data.source !== MUTATION_SOURCE_TYPE || !data.attributes?.length) return event;
27
27
  const attributes = data.attributes.filter(({ id }) => this.consume(id));
28
28
  if (attributes.length === data.attributes.length) return event;
29
- if (attributes.length === 0 && (data.adds?.length ?? 0) === 0 && (data.removes?.length ?? 0) === 0 && (data.texts?.length ?? 0) === 0) return;
29
+ if (!attributes.length && !data.adds?.length && !data.removes?.length && !data.texts?.length) return;
30
30
  return {
31
31
  ...event,
32
32
  data: {
@@ -482,12 +482,11 @@ var ReplayTracker = class {
482
482
  return;
483
483
  }
484
484
  if (this.minLengthFlushTask) return;
485
- const remainingMs = Math.max(0, this.minReplayLengthMs - (Date.now() - this.startTime));
486
485
  this.minLengthFlushTask = setTimeout(() => {
487
486
  this.minLengthFlushTask = null;
488
487
  this.initialFlushPending = false;
489
488
  this.requestFlush("minLength");
490
- }, Math.min(remainingMs, MAX_TIMEOUT_MS));
489
+ }, this.minReplayLengthMs - (Date.now() - this.startTime));
491
490
  }
492
491
  enqueueEventsIfReady(context, flushReason, maxBatchSizeBytes = this.maxBatchSizeBytes) {
493
492
  if (this.events.length === 0) return;
@@ -16,7 +16,8 @@ export default class WebVitalsTracker {
16
16
  private sending?;
17
17
  private generation;
18
18
  private unsubscribe?;
19
- private readonly sentMetrics;
19
+ private unfollowSession?;
20
+ private sentMetrics;
20
21
  private initialUrl;
21
22
  private session;
22
23
  private cookieless;
@@ -25,6 +26,7 @@ export default class WebVitalsTracker {
25
26
  start(): void;
26
27
  stop(discard?: boolean): void;
27
28
  onPageHidden(persisted?: boolean): void;
29
+ private followSession;
28
30
  private onMetric;
29
31
  private discard;
30
32
  private flush;
@@ -1,36 +1,49 @@
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";
1
+ import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as sendData } from "./chunks/send-data-gBIF6DTb.js";
2
+ import { n as getSessionContext, r as onSessionRotated } from "./chunks/session-manager-DUpF3mi3.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();
6
+ function getCollector() {
7
+ const existing = collectors.get(document);
8
+ if (existing) return existing;
9
+ const collector = {
10
+ subscribers: /* @__PURE__ */ new Set(),
11
+ latest: /* @__PURE__ */ new Map(),
12
+ sent: /* @__PURE__ */ new Map()
13
+ };
14
+ collectors.set(document, collector);
15
+ const report = (metric) => {
16
+ const snapshot = { ...metric };
17
+ snapshot.attribution = { ...metric.attribution };
18
+ collector.latest.set(metric.name, snapshot);
19
+ for (const listener of collector.subscribers) listener(snapshot);
20
+ };
21
+ const changes = { reportAllChanges: true };
22
+ onCLS(report, changes);
23
+ onINP(report, changes);
24
+ onLCP(report, changes);
25
+ onFCP(report);
26
+ onTTFB(report);
27
+ return collector;
28
+ }
6
29
  function subscribeToVitals(subscriber) {
7
- let collector = collectors.get(document);
8
- if (!collector) {
9
- collector = {
10
- subscribers: /* @__PURE__ */ new Set(),
11
- latest: /* @__PURE__ */ new Map()
12
- };
13
- collectors.set(document, collector);
14
- const { latest, subscribers } = collector;
15
- const report = (metric) => {
16
- const snapshot = { ...metric };
17
- snapshot.attribution = { ...metric.attribution };
18
- latest.set(metric.name, snapshot);
19
- for (const listener of subscribers) listener(snapshot);
20
- };
21
- const changes = { reportAllChanges: true };
22
- onCLS(report, changes);
23
- onINP(report, changes);
24
- onLCP(report, changes);
25
- onFCP(report);
26
- onTTFB(report);
27
- }
28
- collector.subscribers.add(subscriber);
29
- for (const metric of collector.latest.values()) subscriber(metric);
30
+ const { subscribers, latest } = getCollector();
31
+ subscribers.add(subscriber);
32
+ for (const metric of latest.values()) subscriber(metric);
30
33
  return () => {
31
- collector.subscribers.delete(subscriber);
34
+ subscribers.delete(subscriber);
32
35
  };
33
36
  }
37
+ /**
38
+ * What `destination` already received from this document. Shared so a
39
+ * re-created tracker does not resend the metrics replayed to it.
40
+ */
41
+ function sentVitals(destination) {
42
+ const { sent } = getCollector();
43
+ const vitals = sent.get(destination) ?? /* @__PURE__ */ new Map();
44
+ sent.set(destination, vitals);
45
+ return vitals;
46
+ }
34
47
  //#endregion
35
48
  //#region src/web-vitals.ts
36
49
  function webVitals() {
@@ -51,13 +64,6 @@ function webVitals() {
51
64
  }
52
65
  };
53
66
  }
54
- const METRIC_NAMES = /* @__PURE__ */ new Set([
55
- "CLS",
56
- "INP",
57
- "LCP",
58
- "FCP",
59
- "TTFB"
60
- ]);
61
67
  var WebVitalsTracker = class {
62
68
  options;
63
69
  endpoint;
@@ -66,6 +72,7 @@ var WebVitalsTracker = class {
66
72
  sending;
67
73
  generation = 0;
68
74
  unsubscribe;
75
+ unfollowSession;
69
76
  sentMetrics = /* @__PURE__ */ new Map();
70
77
  initialUrl = "";
71
78
  session = null;
@@ -78,21 +85,21 @@ var WebVitalsTracker = class {
78
85
  setCookielessMode(cookieless) {
79
86
  if (this.cookieless === cookieless) return;
80
87
  this.cookieless = cookieless;
81
- this.discard();
82
- this.session = this.started ? getSessionContext(this.options.siteKey, cookieless) : null;
88
+ if (this.started) this.followSession();
83
89
  }
84
90
  start() {
85
91
  if (this.started || typeof window === "undefined") return;
86
92
  this.started = true;
87
93
  this.initialUrl = sanitizeUrl(window.performance?.getEntriesByType("navigation")[0]?.name || window.location.href, this.options.trackHash ?? false);
88
- this.session = getSessionContext(this.options.siteKey, this.cookieless);
94
+ this.followSession();
95
+ this.sentMetrics = sentVitals(`${this.endpoint}\0${this.options.siteKey}`);
89
96
  this.unsubscribe = subscribeToVitals(this.onMetric);
90
97
  }
91
98
  stop(discard = false) {
92
99
  if (!this.started) return;
93
100
  this.started = false;
94
101
  this.unsubscribe?.();
95
- this.unsubscribe = void 0;
102
+ this.unfollowSession?.();
96
103
  if (discard) this.discard();
97
104
  else this.flush();
98
105
  }
@@ -103,26 +110,39 @@ var WebVitalsTracker = class {
103
110
  if (generation === this.generation) this.flush();
104
111
  });
105
112
  }
113
+ followSession() {
114
+ const { siteKey } = this.options;
115
+ this.unfollowSession?.();
116
+ this.session = getSessionContext(siteKey, this.cookieless);
117
+ this.unfollowSession = onSessionRotated(siteKey, this.cookieless, (_prev, next) => {
118
+ this.session = next;
119
+ });
120
+ }
106
121
  onMetric = (metric) => {
107
- if (!this.started || !METRIC_NAMES.has(metric.name) || !Number.isFinite(metric.value) || metric.value < 0) return;
122
+ if (!this.started || !Number.isFinite(metric.value) || metric.value < 0) return;
108
123
  this.metrics.set(metric.name, metric);
109
124
  };
110
125
  discard() {
111
126
  this.generation++;
112
127
  this.metrics.clear();
113
- this.sentMetrics.clear();
114
128
  }
115
129
  async flush() {
116
130
  const generation = this.generation;
117
131
  while (this.sending) await this.sending;
118
- if (generation !== this.generation || !this.metrics.size) return;
119
- const batch = this.metrics;
120
- const session = this.session ?? getSessionContext(this.options.siteKey, this.cookieless);
132
+ const session = this.session;
133
+ if (generation !== this.generation || !session) return;
134
+ const batch = [...this.metrics.values()].filter(({ name, id, value }) => {
135
+ const sent = this.sentMetrics.get(name);
136
+ return sent?.id !== id || sent.value !== value;
137
+ });
138
+ this.metrics = /* @__PURE__ */ new Map();
139
+ if (!batch.length) return;
140
+ const trackHash = this.options.trackHash ?? false;
121
141
  const data = JSON.stringify({
122
142
  token: this.options.siteKey,
123
143
  sessionId: session.sessionId,
124
144
  windowId: session.windowId,
125
- vitals: [...batch.values()].map(({ name, value, id, rating, navigationType, attribution }) => {
145
+ vitals: batch.map(({ name, value, id, rating, navigationType, attribution }) => {
126
146
  const previous = this.sentMetrics.get(name);
127
147
  return {
128
148
  metric: name,
@@ -137,8 +157,7 @@ var WebVitalsTracker = class {
137
157
  };
138
158
  }),
139
159
  metadata: { url: this.initialUrl }
140
- });
141
- this.metrics = /* @__PURE__ */ new Map();
160
+ }, (_key, value) => typeof value === "string" && /^https?:\/\//.test(value) ? sanitizeUrl(value, trackHash) : value);
142
161
  this.sending = sendData({
143
162
  url: this.endpoint,
144
163
  data,
@@ -147,12 +166,8 @@ var WebVitalsTracker = class {
147
166
  });
148
167
  const sent = await this.sending;
149
168
  this.sending = void 0;
150
- if (generation !== this.generation) return;
151
- for (const [name, vital] of batch) if (sent) this.sentMetrics.set(name, {
152
- id: vital.id,
153
- value: vital.value
154
- });
155
- else if (!this.metrics.has(name)) this.metrics.set(name, vital);
169
+ for (const vital of batch) if (sent) this.sentMetrics.set(vital.name, vital);
170
+ else if (generation === this.generation && !this.metrics.has(vital.name)) this.metrics.set(vital.name, vital);
156
171
  }
157
172
  };
158
173
  //#endregion
package/package.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "version": "0.12.0",
45
+ "version": "0.12.1",
46
46
  "scripts": {
47
47
  "build": "tsdown && bun run check-size",
48
48
  "dev": "bun run build",