@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 +8 -5
- package/dist/chunks/{identifiers-CpmDmpDv.js → identifiers-DOtve3E5.js} +1 -1
- package/dist/chunks/{send-data-DYZZjeM-.js → send-data-gBIF6DTb.js} +4 -5
- package/dist/chunks/{session-manager-CFQEWbgo.js → session-manager-DUpF3mi3.js} +4 -7
- package/dist/error.js +4 -4
- package/dist/flags.js +3 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +40 -16
- package/dist/replay.d.ts +2 -1
- package/dist/replay.js +5 -6
- package/dist/web-vitals.d.ts +3 -1
- package/dist/web-vitals.js +66 -51
- package/package.json +1 -1
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.
|
|
56
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
57
|
-
if (globalThis.navigator.sendBeacon(url,
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
148
|
-
if (listeners
|
|
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-
|
|
2
|
-
import { n as getSessionContext } from "./chunks/session-manager-
|
|
3
|
-
import { t as getAnonymousId } from "./chunks/identifiers-
|
|
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.
|
|
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-
|
|
2
|
-
import { t as getAnonymousId } from "./chunks/identifiers-
|
|
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-
|
|
2
|
-
import { a as touchActivity, i as resetSession, n as getSessionContext, t as createId } from "./chunks/session-manager-
|
|
3
|
-
import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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,
|
|
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
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-
|
|
2
|
-
import { a as touchActivity, c as setStorageItem, n as getSessionContext, o as getStorageItem, r as onSessionRotated, t as createId } from "./chunks/session-manager-
|
|
3
|
-
import { t as getAnonymousId } from "./chunks/identifiers-
|
|
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
|
|
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
|
-
},
|
|
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;
|
package/dist/web-vitals.d.ts
CHANGED
|
@@ -16,7 +16,8 @@ export default class WebVitalsTracker {
|
|
|
16
16
|
private sending?;
|
|
17
17
|
private generation;
|
|
18
18
|
private unsubscribe?;
|
|
19
|
-
private
|
|
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;
|
package/dist/web-vitals.js
CHANGED
|
@@ -1,36 +1,49 @@
|
|
|
1
|
-
import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as sendData } from "./chunks/send-data-
|
|
2
|
-
import { n as getSessionContext } from "./chunks/session-manager-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 || !
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
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:
|
|
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 (
|
|
151
|
-
|
|
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
|