@faststats/web 0.0.2 → 0.0.4
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/CHANGELOG.md +14 -0
- package/dist/module.d.ts +0 -3
- package/dist/module.js +3 -408
- package/package.json +3 -4
- package/src/analytics.ts +18 -45
- package/src/error.ts +0 -6
- package/src/replay.ts +0 -6
- package/src/web-vitals.ts +0 -7
- package/tsdown.config.ts +8 -47
- package/dist/error.iife.js +0 -3
- package/dist/error.js +0 -3
- package/dist/replay.iife.js +0 -29
- package/dist/replay.js +0 -29
- package/dist/script.iife.js +0 -1
- package/dist/script.js +0 -1
- package/dist/web-vitals.iife.js +0 -1
- package/dist/web-vitals.js +0 -1
- package/src/index.ts +0 -95
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @faststats/web
|
|
2
2
|
|
|
3
|
+
## 0.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 93393c0: Remove redundant isTrackingDisabled checks from sub-trackers (ErrorTracker, WebVitalsTracker, ReplayTracker).
|
|
8
|
+
Simplify web vitals config - single source of truth (trackWebVitals ?? webVitals?.enabled ?? hasWebVitalsSampling).
|
|
9
|
+
Fix custom events via trackEvent: set window.\_\_FA_webAnalyticsInstance in init() so it works before async start() completes.
|
|
10
|
+
|
|
11
|
+
## 0.0.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- bb3dcd1: ESM only for now
|
|
16
|
+
|
|
3
17
|
## 0.0.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/module.d.ts
CHANGED
|
@@ -429,7 +429,6 @@ interface IdentifyOptions {
|
|
|
429
429
|
interface WebAnalyticsOptions {
|
|
430
430
|
siteKey: string;
|
|
431
431
|
endpoint?: string;
|
|
432
|
-
baseUrl?: string;
|
|
433
432
|
debug?: boolean;
|
|
434
433
|
autoTrack?: boolean;
|
|
435
434
|
trackHash?: boolean;
|
|
@@ -463,7 +462,6 @@ declare class WebAnalytics {
|
|
|
463
462
|
private readonly options;
|
|
464
463
|
private readonly endpoint;
|
|
465
464
|
private readonly debug;
|
|
466
|
-
private readonly baseUrl;
|
|
467
465
|
private started;
|
|
468
466
|
private pageKey;
|
|
469
467
|
private navTimer;
|
|
@@ -481,7 +479,6 @@ declare class WebAnalytics {
|
|
|
481
479
|
private log;
|
|
482
480
|
private init;
|
|
483
481
|
start(): void;
|
|
484
|
-
private load;
|
|
485
482
|
pageview(extra?: Dict): void;
|
|
486
483
|
track(name: string, extra?: Dict): void;
|
|
487
484
|
identify(externalId: string, email: string, options?: IdentifyOptions): void;
|
package/dist/module.js
CHANGED
|
@@ -1,408 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
function getAnonymousId(cookieless) {
|
|
4
|
-
if (cookieless) return "";
|
|
5
|
-
return getOrCreateAnonymousId();
|
|
6
|
-
}
|
|
7
|
-
function getOrCreateAnonymousId() {
|
|
8
|
-
if (typeof localStorage === "undefined") return "";
|
|
9
|
-
const existingId = localStorage.getItem("faststats_anon_id");
|
|
10
|
-
if (existingId) return existingId;
|
|
11
|
-
const newId = crypto.randomUUID();
|
|
12
|
-
localStorage.setItem("faststats_anon_id", newId);
|
|
13
|
-
return newId;
|
|
14
|
-
}
|
|
15
|
-
function resetAnonymousId(cookieless) {
|
|
16
|
-
if (cookieless || typeof localStorage === "undefined") return "";
|
|
17
|
-
localStorage.removeItem("faststats_anon_id");
|
|
18
|
-
return getOrCreateAnonymousId();
|
|
19
|
-
}
|
|
20
|
-
function getOrCreateSessionId() {
|
|
21
|
-
if (typeof sessionStorage === "undefined") return "";
|
|
22
|
-
const existingId = sessionStorage.getItem("session_id");
|
|
23
|
-
const sessionTimestamp = sessionStorage.getItem("session_timestamp");
|
|
24
|
-
if (existingId && sessionTimestamp) {
|
|
25
|
-
if (Date.now() - Number.parseInt(sessionTimestamp, 10) < SESSION_TIMEOUT) {
|
|
26
|
-
sessionStorage.setItem("session_timestamp", Date.now().toString());
|
|
27
|
-
return existingId;
|
|
28
|
-
}
|
|
29
|
-
sessionStorage.removeItem("session_id");
|
|
30
|
-
sessionStorage.removeItem("session_timestamp");
|
|
31
|
-
sessionStorage.removeItem("session_start");
|
|
32
|
-
}
|
|
33
|
-
const now = Date.now().toString();
|
|
34
|
-
const newId = crypto.randomUUID();
|
|
35
|
-
sessionStorage.setItem("session_id", newId);
|
|
36
|
-
sessionStorage.setItem("session_timestamp", now);
|
|
37
|
-
sessionStorage.setItem("session_start", now);
|
|
38
|
-
return newId;
|
|
39
|
-
}
|
|
40
|
-
function resetSessionId() {
|
|
41
|
-
if (typeof sessionStorage === "undefined") return "";
|
|
42
|
-
sessionStorage.removeItem("session_id");
|
|
43
|
-
sessionStorage.removeItem("session_timestamp");
|
|
44
|
-
sessionStorage.removeItem("session_start");
|
|
45
|
-
return getOrCreateSessionId();
|
|
46
|
-
}
|
|
47
|
-
function refreshSessionTimestamp() {
|
|
48
|
-
if (typeof sessionStorage === "undefined") return;
|
|
49
|
-
if (sessionStorage.getItem("session_id")) sessionStorage.setItem("session_timestamp", Date.now().toString());
|
|
50
|
-
}
|
|
51
|
-
function getSessionStart() {
|
|
52
|
-
if (typeof sessionStorage === "undefined") return Date.now();
|
|
53
|
-
const start = sessionStorage.getItem("session_start");
|
|
54
|
-
if (start) return Number.parseInt(start, 10);
|
|
55
|
-
const ts = sessionStorage.getItem("session_timestamp");
|
|
56
|
-
return ts ? Number.parseInt(ts, 10) : Date.now();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region src/utils/types.ts
|
|
61
|
-
function normalizeSamplingPercentage(value) {
|
|
62
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 100;
|
|
63
|
-
return Math.max(0, Math.min(100, value));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
//#endregion
|
|
67
|
-
//#region src/analytics.ts
|
|
68
|
-
function trackEvent(eventName, properties) {
|
|
69
|
-
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
70
|
-
window.__FA_webAnalyticsInstance?.track(eventName, properties ?? {});
|
|
71
|
-
}
|
|
72
|
-
function isTrackingDisabled() {
|
|
73
|
-
if (typeof localStorage === "undefined") return false;
|
|
74
|
-
const value = localStorage.getItem("disable-faststats");
|
|
75
|
-
return value === "true" || value === "1";
|
|
76
|
-
}
|
|
77
|
-
async function sendData(options) {
|
|
78
|
-
const { url, data, contentType = "application/json", headers = {}, debug = false, debugPrefix = "[Analytics]" } = options;
|
|
79
|
-
const blob = data instanceof Blob ? data : new Blob([data], { type: contentType });
|
|
80
|
-
if (navigator.sendBeacon?.(url, blob)) {
|
|
81
|
-
if (debug) console.log(`${debugPrefix} ✓ Sent via beacon`);
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
const response = await fetch(url, {
|
|
86
|
-
method: "POST",
|
|
87
|
-
body: data,
|
|
88
|
-
headers: {
|
|
89
|
-
"Content-Type": contentType,
|
|
90
|
-
...headers
|
|
91
|
-
},
|
|
92
|
-
keepalive: true
|
|
93
|
-
});
|
|
94
|
-
const success = response.ok;
|
|
95
|
-
if (debug) if (success) console.log(`${debugPrefix} ✓ Sent via fetch`);
|
|
96
|
-
else console.warn(`${debugPrefix} ✗ Failed: ${response.status}`);
|
|
97
|
-
return success;
|
|
98
|
-
} catch {
|
|
99
|
-
if (debug) console.warn(`${debugPrefix} ✗ Failed to send`);
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function getLinkEl(el) {
|
|
104
|
-
while (el && !(el.tagName === "A" && el.href)) el = el.parentNode;
|
|
105
|
-
return el;
|
|
106
|
-
}
|
|
107
|
-
function getUTM() {
|
|
108
|
-
const params = {};
|
|
109
|
-
if (!location.search) return params;
|
|
110
|
-
const sp = new URLSearchParams(location.search);
|
|
111
|
-
for (const k of [
|
|
112
|
-
"utm_source",
|
|
113
|
-
"utm_medium",
|
|
114
|
-
"utm_campaign",
|
|
115
|
-
"utm_term",
|
|
116
|
-
"utm_content"
|
|
117
|
-
]) {
|
|
118
|
-
const v = sp.get(k);
|
|
119
|
-
if (v) params[k] = v;
|
|
120
|
-
}
|
|
121
|
-
return params;
|
|
122
|
-
}
|
|
123
|
-
var WebAnalytics = class {
|
|
124
|
-
endpoint;
|
|
125
|
-
debug;
|
|
126
|
-
baseUrl;
|
|
127
|
-
started = false;
|
|
128
|
-
pageKey = "";
|
|
129
|
-
navTimer = null;
|
|
130
|
-
heartbeatTimer = null;
|
|
131
|
-
scrollDepth = 0;
|
|
132
|
-
pageEntryTime = 0;
|
|
133
|
-
pagePath = "";
|
|
134
|
-
pageUrl = "";
|
|
135
|
-
pageHash = "";
|
|
136
|
-
hasLeftCurrentPage = false;
|
|
137
|
-
scrollHandler = null;
|
|
138
|
-
consentMode;
|
|
139
|
-
cookielessWhilePending;
|
|
140
|
-
constructor(options) {
|
|
141
|
-
this.options = options;
|
|
142
|
-
this.endpoint = options.endpoint ?? "https://metrics.faststats.dev/v1/web";
|
|
143
|
-
this.debug = options.debug ?? false;
|
|
144
|
-
this.consentMode = options.consent?.mode ?? "granted";
|
|
145
|
-
this.cookielessWhilePending = options.consent?.cookielessWhilePending ?? true;
|
|
146
|
-
const script = typeof document !== "undefined" ? document.currentScript : null;
|
|
147
|
-
const scriptBase = script?.src ? script.src.substring(0, script.src.lastIndexOf("/")) : "";
|
|
148
|
-
this.baseUrl = (options.baseUrl ?? scriptBase) || (typeof window !== "undefined" ? window.location.origin : "");
|
|
149
|
-
if (options.autoTrack ?? true) this.init();
|
|
150
|
-
}
|
|
151
|
-
log(msg) {
|
|
152
|
-
if (this.debug) console.log(`[Analytics] ${msg}`);
|
|
153
|
-
}
|
|
154
|
-
init() {
|
|
155
|
-
if (typeof window === "undefined") return;
|
|
156
|
-
if (isTrackingDisabled()) {
|
|
157
|
-
this.log("disabled");
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if ("requestIdleCallback" in window) window.requestIdleCallback(() => this.start());
|
|
161
|
-
else setTimeout(() => this.start(), 1);
|
|
162
|
-
}
|
|
163
|
-
start() {
|
|
164
|
-
if (this.started || typeof window === "undefined") return;
|
|
165
|
-
if (window.__FA_webAnalyticsInstance && window.__FA_webAnalyticsInstance !== this) {
|
|
166
|
-
this.log("already started by another instance");
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (isTrackingDisabled()) {
|
|
170
|
-
this.log("disabled");
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
this.started = true;
|
|
174
|
-
window.__FA_webAnalyticsInstance = this;
|
|
175
|
-
window.__FA_getAnonymousId = () => getAnonymousId(this.isCookielessMode());
|
|
176
|
-
window.__FA_getSessionId = getOrCreateSessionId;
|
|
177
|
-
window.__FA_sendData = sendData;
|
|
178
|
-
window.__FA_identify = (externalId, email, options) => this.identify(externalId, email, options);
|
|
179
|
-
window.__FA_logout = (resetAnonymousIdentity) => this.logout(resetAnonymousIdentity);
|
|
180
|
-
window.__FA_setConsentMode = (mode) => this.setConsentMode(mode);
|
|
181
|
-
window.__FA_optIn = () => this.optIn();
|
|
182
|
-
window.__FA_optOut = () => this.optOut();
|
|
183
|
-
window.__FA_trackEvent = (eventName, props) => this.track(eventName, props ?? {});
|
|
184
|
-
window.__FA_isTrackingDisabled = isTrackingDisabled;
|
|
185
|
-
const opts = this.options;
|
|
186
|
-
const hasWebVitalsSampling = opts.webVitals?.sampling?.percentage !== void 0;
|
|
187
|
-
const hasReplaySampling = opts.replayOptions?.samplingPercentage !== void 0 || opts.sessionReplays?.sampling?.percentage !== void 0;
|
|
188
|
-
if (opts.errorTracking?.enabled ?? opts.trackErrors) this.load("error", "__FA_ErrorTracker", {
|
|
189
|
-
siteKey: opts.siteKey,
|
|
190
|
-
endpoint: this.endpoint,
|
|
191
|
-
debug: this.debug,
|
|
192
|
-
getCommonData: () => ({
|
|
193
|
-
url: location.href,
|
|
194
|
-
page: location.pathname,
|
|
195
|
-
referrer: document.referrer || null
|
|
196
|
-
})
|
|
197
|
-
});
|
|
198
|
-
if (opts.webVitals?.enabled ?? opts.trackWebVitals ?? hasWebVitalsSampling) this.load("web-vitals", "__FA_WebVitalsTracker", {
|
|
199
|
-
siteKey: opts.siteKey,
|
|
200
|
-
endpoint: this.endpoint,
|
|
201
|
-
debug: this.debug,
|
|
202
|
-
samplingPercentage: normalizeSamplingPercentage(opts.webVitals?.sampling?.percentage)
|
|
203
|
-
});
|
|
204
|
-
if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
|
|
205
|
-
const replayOpts = opts.replayOptions ?? {};
|
|
206
|
-
this.load("replay", "__FA_ReplayTracker", {
|
|
207
|
-
siteKey: opts.siteKey,
|
|
208
|
-
endpoint: this.endpoint,
|
|
209
|
-
debug: this.debug,
|
|
210
|
-
...replayOpts,
|
|
211
|
-
samplingPercentage: normalizeSamplingPercentage(replayOpts.samplingPercentage ?? opts.sessionReplays?.sampling?.percentage)
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
this.enterPage();
|
|
215
|
-
this.pageview({ trigger: "load" });
|
|
216
|
-
this.links();
|
|
217
|
-
this.trackScroll();
|
|
218
|
-
this.startHeartbeat();
|
|
219
|
-
document.addEventListener("visibilitychange", () => {
|
|
220
|
-
if (document.visibilityState === "hidden") this.leavePage();
|
|
221
|
-
else this.startHeartbeat();
|
|
222
|
-
});
|
|
223
|
-
window.addEventListener("pagehide", () => this.leavePage());
|
|
224
|
-
window.addEventListener("popstate", () => this.navigate());
|
|
225
|
-
if (opts.trackHash) window.addEventListener("hashchange", () => this.navigate());
|
|
226
|
-
this.patch();
|
|
227
|
-
}
|
|
228
|
-
load(script, key, trackerOpts) {
|
|
229
|
-
const el = document.createElement("script");
|
|
230
|
-
el.src = `${this.baseUrl}/${script}.js`;
|
|
231
|
-
el.async = true;
|
|
232
|
-
el.onload = () => {
|
|
233
|
-
const Ctor = window[key];
|
|
234
|
-
if (Ctor) {
|
|
235
|
-
new Ctor(trackerOpts).start();
|
|
236
|
-
this.log(`${script} loaded`);
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
el.onerror = () => {
|
|
240
|
-
if (this.debug) console.warn(`[Analytics] ${script} failed`);
|
|
241
|
-
};
|
|
242
|
-
document.head.appendChild(el);
|
|
243
|
-
}
|
|
244
|
-
pageview(extra = {}) {
|
|
245
|
-
const key = `${location.pathname}|${this.options.trackHash ?? false ? location.hash : ""}`;
|
|
246
|
-
if (key === this.pageKey) return;
|
|
247
|
-
this.pageKey = key;
|
|
248
|
-
this.send("pageview", extra);
|
|
249
|
-
}
|
|
250
|
-
track(name, extra = {}) {
|
|
251
|
-
this.send(name, extra);
|
|
252
|
-
}
|
|
253
|
-
identify(externalId, email, options = {}) {
|
|
254
|
-
if (isTrackingDisabled()) return;
|
|
255
|
-
if (this.isCookielessMode()) return;
|
|
256
|
-
const trimmedExternalId = externalId.trim();
|
|
257
|
-
const trimmedEmail = email.trim();
|
|
258
|
-
if (!trimmedExternalId || !trimmedEmail) return;
|
|
259
|
-
sendData({
|
|
260
|
-
url: this.endpoint.replace(/\/v1\/web$/, "/v1/identify"),
|
|
261
|
-
data: JSON.stringify({
|
|
262
|
-
token: this.options.siteKey,
|
|
263
|
-
identifier: getAnonymousId(false),
|
|
264
|
-
externalId: trimmedExternalId,
|
|
265
|
-
email: trimmedEmail,
|
|
266
|
-
name: options.name?.trim() || void 0,
|
|
267
|
-
phone: options.phone?.trim() || void 0,
|
|
268
|
-
avatarUrl: options.avatarUrl?.trim() || void 0,
|
|
269
|
-
traits: options.traits ?? {}
|
|
270
|
-
}),
|
|
271
|
-
contentType: "text/plain",
|
|
272
|
-
debug: this.debug,
|
|
273
|
-
debugPrefix: "[Analytics] identify"
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
logout(resetAnonymousIdentity = true) {
|
|
277
|
-
if (isTrackingDisabled()) return;
|
|
278
|
-
if (resetAnonymousIdentity) resetAnonymousId(this.isCookielessMode());
|
|
279
|
-
resetSessionId();
|
|
280
|
-
}
|
|
281
|
-
setConsentMode(mode) {
|
|
282
|
-
this.consentMode = mode;
|
|
283
|
-
}
|
|
284
|
-
optIn() {
|
|
285
|
-
this.setConsentMode("granted");
|
|
286
|
-
}
|
|
287
|
-
optOut() {
|
|
288
|
-
this.setConsentMode("denied");
|
|
289
|
-
}
|
|
290
|
-
getConsentMode() {
|
|
291
|
-
return this.consentMode;
|
|
292
|
-
}
|
|
293
|
-
isCookielessMode() {
|
|
294
|
-
if (this.options.cookieless) return true;
|
|
295
|
-
if (this.consentMode === "denied") return true;
|
|
296
|
-
if (this.consentMode === "pending") return this.cookielessWhilePending;
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
send(event, extra = {}) {
|
|
300
|
-
const identifier = getAnonymousId(this.isCookielessMode());
|
|
301
|
-
const payload = JSON.stringify({
|
|
302
|
-
token: this.options.siteKey,
|
|
303
|
-
...identifier ? { userId: identifier } : {},
|
|
304
|
-
sessionId: getOrCreateSessionId(),
|
|
305
|
-
data: {
|
|
306
|
-
event,
|
|
307
|
-
page: location.pathname,
|
|
308
|
-
referrer: document.referrer || null,
|
|
309
|
-
title: document.title || "",
|
|
310
|
-
url: location.href,
|
|
311
|
-
...getUTM(),
|
|
312
|
-
...extra
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
this.log(event);
|
|
316
|
-
sendData({
|
|
317
|
-
url: this.endpoint,
|
|
318
|
-
data: payload,
|
|
319
|
-
contentType: "text/plain",
|
|
320
|
-
debug: this.debug,
|
|
321
|
-
debugPrefix: `[Analytics] ${event}`
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
enterPage() {
|
|
325
|
-
this.pageEntryTime = Date.now();
|
|
326
|
-
this.pagePath = location.pathname;
|
|
327
|
-
this.pageUrl = location.href;
|
|
328
|
-
this.pageHash = location.hash;
|
|
329
|
-
this.scrollDepth = 0;
|
|
330
|
-
this.hasLeftCurrentPage = false;
|
|
331
|
-
}
|
|
332
|
-
leavePage() {
|
|
333
|
-
if (this.hasLeftCurrentPage) return;
|
|
334
|
-
this.hasLeftCurrentPage = true;
|
|
335
|
-
const now = Date.now();
|
|
336
|
-
this.send("page_leave", {
|
|
337
|
-
page: this.pagePath,
|
|
338
|
-
url: this.pageUrl,
|
|
339
|
-
time_on_page: now - this.pageEntryTime,
|
|
340
|
-
scroll_depth: this.scrollDepth,
|
|
341
|
-
session_duration: now - getSessionStart()
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
trackScroll() {
|
|
345
|
-
if (this.scrollHandler) window.removeEventListener("scroll", this.scrollHandler);
|
|
346
|
-
const update = () => {
|
|
347
|
-
const doc = document.documentElement;
|
|
348
|
-
const body = document.body;
|
|
349
|
-
const viewportH = window.innerHeight;
|
|
350
|
-
const docH = Math.max(doc.scrollHeight, body.scrollHeight);
|
|
351
|
-
if (docH <= viewportH) {
|
|
352
|
-
this.scrollDepth = 100;
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const depth = Math.min(100, Math.round(((window.scrollY || doc.scrollTop) + viewportH) / docH * 100));
|
|
356
|
-
if (depth > this.scrollDepth) this.scrollDepth = depth;
|
|
357
|
-
};
|
|
358
|
-
this.scrollHandler = update;
|
|
359
|
-
update();
|
|
360
|
-
window.addEventListener("scroll", update, { passive: true });
|
|
361
|
-
}
|
|
362
|
-
startHeartbeat() {
|
|
363
|
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
364
|
-
this.heartbeatTimer = setInterval(() => {
|
|
365
|
-
if (document.visibilityState === "hidden") {
|
|
366
|
-
clearInterval(this.heartbeatTimer);
|
|
367
|
-
this.heartbeatTimer = null;
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
refreshSessionTimestamp();
|
|
371
|
-
}, 300 * 1e3);
|
|
372
|
-
}
|
|
373
|
-
navigate() {
|
|
374
|
-
if (this.navTimer) clearTimeout(this.navTimer);
|
|
375
|
-
this.navTimer = setTimeout(() => {
|
|
376
|
-
this.navTimer = null;
|
|
377
|
-
const pathChanged = location.pathname !== this.pagePath;
|
|
378
|
-
const hashChanged = (this.options.trackHash ?? false) && location.hash !== this.pageHash;
|
|
379
|
-
if (!pathChanged && !hashChanged) return;
|
|
380
|
-
this.leavePage();
|
|
381
|
-
this.enterPage();
|
|
382
|
-
this.trackScroll();
|
|
383
|
-
this.pageview({ trigger: "navigation" });
|
|
384
|
-
}, 300);
|
|
385
|
-
}
|
|
386
|
-
patch() {
|
|
387
|
-
const fire = () => this.navigate();
|
|
388
|
-
for (const method of ["pushState", "replaceState"]) {
|
|
389
|
-
const orig = history[method];
|
|
390
|
-
history[method] = function(...args) {
|
|
391
|
-
const result = orig.apply(this, args);
|
|
392
|
-
fire();
|
|
393
|
-
return result;
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
links() {
|
|
398
|
-
const handler = (event) => {
|
|
399
|
-
const link = getLinkEl(event.target);
|
|
400
|
-
if (link && link.host !== location.host) this.track("outbound_link", { outbound_link: link.href });
|
|
401
|
-
};
|
|
402
|
-
document.addEventListener("click", handler);
|
|
403
|
-
document.addEventListener("auxclick", handler);
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
//#endregion
|
|
408
|
-
export { WebAnalytics, trackEvent };
|
|
1
|
+
import{getRecordConsolePlugin as e}from"@rrweb/rrweb-plugin-console-record";import{record as t}from"rrweb";import{onCLS as n,onFCP as r,onINP as i,onLCP as a,onTTFB as o}from"web-vitals/attribution";var s=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;getCommonData;handledErrors=new WeakSet;errorCounts=new Map;flushTimer=null;started=!1;constructor(e){this.siteKey=e.siteKey,this.endpoint=e.endpoint??`https://metrics.faststats.dev/v1/web`,this.debug=e.debug??!1,this.flushInterval=e.flushInterval??5e3,this.maxQueueSize=e.maxQueueSize??50,this.getCommonData=e.getCommonData??(()=>({}))}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.handleErrorEvent),window.addEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer=setInterval(()=>this.flush(),this.flushInterval),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.flush()}),window.addEventListener(`pagehide`,()=>this.flush()),this.debug&&console.log(`[ErrorTracker] Started listening for errors`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.handleErrorEvent),window.removeEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush(),this.debug&&console.log(`[ErrorTracker] Stopped listening for errors`))}handleErrorEvent=e=>{let t=e.error;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate error:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:e.message||(t?.message??`Unknown error`),filename:e.filename||void 0,lineno:e.lineno||void 0,colno:e.colno||void 0,stack:t?.stack||void 0,type:`error`})};handleRejection=e=>{let t=e.reason;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate rejection:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:t instanceof Error?t.message:typeof t==`string`?t:`Unhandled promise rejection`,stack:t instanceof Error?t.stack:void 0,type:`unhandledrejection`})};isExtensionError(e){return!!(e.filename?.startsWith(`chrome-extension://`)||e.stack&&e.stack.split(`
|
|
2
|
+
`).find(e=>e.trim().startsWith(`at `))?.includes(`chrome-extension://`))}parseStack(e){if(e)return e.split(`
|
|
3
|
+
`).map(e=>e.trim()).filter(e=>e.length>0)}async generateErrorHash(e){let t=[e.type,e.message,e.filename??``,e.lineno??``].join(`:`),n=new TextEncoder().encode(t),r=await crypto.subtle.digest(`SHA-256`,n);return`err_${Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}`}async queueError(e){if(this.isExtensionError(e))return;this.debug&&console.log(`[ErrorTracker] Captured error:`,e);let t=await this.generateErrorHash(e),n=this.errorCounts.get(t);if(n)n.count++,this.debug&&console.log(`[ErrorTracker] Incremented count for ${t} to ${n.count}`);else{let n={error:e.type===`unhandledrejection`?`UnhandledRejection`:`Error`,message:e.message,stack:this.parseStack(e.stack)};this.errorCounts.set(t,{entry:n,hash:t,count:1}),this.debug&&console.log(`[ErrorTracker] Queued new error: ${t}`)}this.errorCounts.size>=this.maxQueueSize&&this.flush()}captureError(e){if(this.handledErrors.has(e)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate manual capture:`,e.message);return}this.handledErrors.add(e),this.queueError({message:e.message,stack:e.stack,type:`error`})}flush(){if(this.errorCounts.size===0)return;let e=[];for(let{entry:t,hash:n,count:r}of this.errorCounts.values())e.push({...t,hash:n,count:r});this.errorCounts.clear(),this.debug&&console.log(`[ErrorTracker] Flushing errors:`,e);let t=this.getCommonData(),n=window.__FA_getAnonymousId?.(),r=globalThis.__SOURCEMAPS_BUILD__,i=typeof r?.buildId==`string`&&r.buildId.trim().length>0?r.buildId:void 0,a={token:this.siteKey,...n?{userId:n}:{},sessionId:window.__FA_getSessionId?.(),...i?{buildId:i}:{},data:{url:t.url,page:t.page,referrer:t.referrer,title:typeof document<`u`?document.title:``},errors:e},o=JSON.stringify(a);this.debug&&console.log(`[ErrorTracker] Payload:`,o),window.__FA_sendData?.({url:this.endpoint,data:o,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};typeof window<`u`&&(window.__FA_ErrorTracker=s);function c(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}var l=class{endpoint;siteKey;debug;flushInterval;maxEvents;sampling;slimDOMOptions;maskAllInputs;maskInputOptions;blockClass;blockSelector;maskTextClass;maskTextSelector;checkoutEveryNms;checkoutEveryNth;samplingPercentage;recordConsole;events=[];flushTimer=null;stopRecording=void 0;started=!1;startTime=0;sequenceNumber=0;pendingBatches=[];isFlushing=!1;compressionSupported=!1;sessionSamplingSeed;minReplayLengthMs=3e3;constructor(e){this.siteKey=e.siteKey,this.endpoint=e.endpoint?.replace(/\/v1\/web$/,`/v1/replay`)??`https://metrics.faststats.dev/v1/replay`,this.debug=e.debug??!1,this.samplingPercentage=c(e.samplingPercentage),this.flushInterval=e.flushInterval??1e4,this.maxEvents=e.maxEvents??500,this.sampling=e.sampling??{mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},this.slimDOMOptions=e.slimDOMOptions??{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0},this.maskAllInputs=e.maskAllInputs??!0,this.maskInputOptions=e.maskInputOptions??{password:!0,email:!0,tel:!0},this.blockClass=e.blockClass,this.blockSelector=e.blockSelector,this.maskTextClass=e.maskTextClass,this.maskTextSelector=e.maskTextSelector,this.checkoutEveryNms=e.checkoutEveryNms??6e4,this.checkoutEveryNth=e.checkoutEveryNth,this.recordConsole=e.recordConsole??!0,this.sessionSamplingSeed=Math.random()*100,typeof window<`u`&&(this.compressionSupported=`CompressionStream`in window)}start(){if(this.started||typeof window>`u`||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;this.started=!0,this.startTime=Date.now(),this.debug&&console.log(`[Replay] Recording started`);let n={emit:(e,t)=>this.handleEvent(e,t),sampling:this.sampling,slimDOMOptions:this.slimDOMOptions,maskAllInputs:this.maskAllInputs,checkoutEveryNms:this.checkoutEveryNms};this.maskInputOptions&&(n.maskInputOptions=this.maskInputOptions),this.blockClass&&(n.blockClass=this.blockClass),this.blockSelector&&(n.blockSelector=this.blockSelector),this.maskTextClass&&(n.maskTextClass=this.maskTextClass),this.maskTextSelector&&(n.maskTextSelector=this.maskTextSelector),this.checkoutEveryNth&&(n.checkoutEveryNth=this.checkoutEveryNth),this.recordConsole&&(n.plugins=[e()]),this.stopRecording=t(n),this.flushTimer=setInterval(()=>{this.scheduleFlush()},this.flushInterval),window.addEventListener(`beforeunload`,this.handleUnload),window.addEventListener(`pagehide`,this.handleUnload),document.addEventListener(`visibilitychange`,this.handleVisibilityChange)}stop(){if(!this.started)return;this.started=!1,this.debug&&console.log(`[Replay] Recording stopped`),this.stopRecording?.(),this.stopRecording=void 0,this.flushTimer&&=(clearInterval(this.flushTimer),null),window.removeEventListener(`beforeunload`,this.handleUnload),window.removeEventListener(`pagehide`,this.handleUnload),document.removeEventListener(`visibilitychange`,this.handleVisibilityChange);let e=Date.now()-this.startTime;if(e<this.minReplayLengthMs){this.events=[],this.debug&&console.log(`[Replay] Session too short (${e}ms), discarding events`);return}this.flush()}handleEvent(e,t){this.events.push(e),(t||this.events.length>=this.maxEvents)&&this.scheduleFlush()}scheduleFlush(){this.isFlushing||this.events.length===0||(typeof window<`u`&&`requestIdleCallback`in window?window.requestIdleCallback(()=>this.flush(),{timeout:2e3}):setTimeout(()=>this.flush(),0))}handleUnload=()=>{this.flush()};handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush()):document.visibilityState===`visible`&&this.started&&(this.flushTimer||=setInterval(()=>{this.scheduleFlush()},this.flushInterval))};async flush(){if(this.events.length===0){this.processPendingBatches();return}let e=Date.now()-this.startTime;if(e<this.minReplayLengthMs){this.debug&&console.log(`[Replay] Too short (${e}ms), skipping`),this.processPendingBatches();return}if(this.isFlushing)return;this.isFlushing=!0;let t=this.events;this.events=[];let n=window.__FA_getAnonymousId?.(),r={token:this.siteKey,sessionId:window.__FA_getSessionId?.(),...n?{identifier:n}:{},sequence:this.sequenceNumber++,timestamp:Date.now(),url:window.location.href,events:t};this.debug&&console.log(`[Replay] Sending ${t.length} events (seq: ${r.sequence})`);try{let e,t=!1;if(this.compressionSupported)try{e=await this.compress(JSON.stringify(r)),t=!0}catch{this.debug&&console.warn(`[Replay] Compression failed, using uncompressed`),e=new Blob([JSON.stringify(r)],{type:`application/json`})}else e=new Blob([JSON.stringify(r)],{type:`application/json`});await this.send(e,t)||this.pendingBatches.push({batch:r,isCompressed:t,retries:0})}catch(e){this.debug&&console.warn(`[Replay] Flush error:`,e),this.pendingBatches.push({batch:r,isCompressed:!1,retries:0})}finally{this.isFlushing=!1,this.processPendingBatches()}}async processPendingBatches(){if(this.pendingBatches.length===0)return;let e=this.pendingBatches.shift();if(e){if(e.retries>=3){this.debug&&console.warn(`[Replay] Max retries reached, dropping batch ${e.batch.sequence}`),this.processPendingBatches();return}e.retries++;try{let t;if(e.isCompressed&&this.compressionSupported)try{t=await this.compress(JSON.stringify(e.batch))}catch{t=new Blob([JSON.stringify(e.batch)],{type:`application/json`}),e.isCompressed=!1}else t=new Blob([JSON.stringify(e.batch)],{type:`application/json`});await this.send(t,e.isCompressed)?this.processPendingBatches():(this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries))}catch{this.debug&&console.warn(`[Replay] Retry ${e.retries} failed`),this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries)}}}async compress(e){if(!this.compressionSupported)throw Error(`Compression not supported`);let t=new TextEncoder().encode(e),n=new CompressionStream(`gzip`),r=n.writable.getWriter();r.write(t),r.close();let i=[],a=n.readable.getReader();for(;;){let{done:e,value:t}=await a.read();if(e)break;t&&i.push(t)}let o=i.reduce((e,t)=>e+t.length,0),s=new Uint8Array(o),c=0;for(let e of i)s.set(e,c),c+=e.length;if(this.debug){let e=(s.length/t.length*100).toFixed(1);console.log(`[Replay] Compressed: ${t.length} → ${s.length} bytes (${e}%)`)}return new Blob([s],{type:`application/octet-stream`})}async send(e,t){let n=t?`${this.endpoint}?encoding=gzip`:this.endpoint,r=(e.size/1024).toFixed(1);return window.__FA_sendData?.({url:n,data:e,contentType:t?`application/octet-stream`:`application/json`,headers:t?{"Content-Encoding":`gzip`}:void 0,debug:this.debug,debugPrefix:`[Replay] ${r}KB`})??Promise.resolve(!1)}getSessionId(){return window.__FA_getSessionId?.()}};typeof window<`u`&&(window.__FA_ReplayTracker=l);function u(e){return e?``:d()}function d(){if(typeof localStorage>`u`)return``;let e=localStorage.getItem(`faststats_anon_id`);if(e)return e;let t=crypto.randomUUID();return localStorage.setItem(`faststats_anon_id`,t),t}function f(e){return e||typeof localStorage>`u`?``:(localStorage.removeItem(`faststats_anon_id`),d())}function p(){if(typeof sessionStorage>`u`)return``;let e=sessionStorage.getItem(`session_id`),t=sessionStorage.getItem(`session_timestamp`);if(e&&t){if(Date.now()-Number.parseInt(t,10)<18e5)return sessionStorage.setItem(`session_timestamp`,Date.now().toString()),e;sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`)}let n=Date.now().toString(),r=crypto.randomUUID();return sessionStorage.setItem(`session_id`,r),sessionStorage.setItem(`session_timestamp`,n),sessionStorage.setItem(`session_start`,n),r}function m(){return typeof sessionStorage>`u`?``:(sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`),p())}function h(){typeof sessionStorage>`u`||sessionStorage.getItem(`session_id`)&&sessionStorage.setItem(`session_timestamp`,Date.now().toString())}function g(){if(typeof sessionStorage>`u`)return Date.now();let e=sessionStorage.getItem(`session_start`);if(e)return Number.parseInt(e,10);let t=sessionStorage.getItem(`session_timestamp`);return t?Number.parseInt(t,10):Date.now()}var _=class{endpoint;siteKey;debug;samplingPercentage;started=!1;sessionSamplingSeed;metricsMap=new Map;flushed=!1;constructor(e){this.siteKey=e.siteKey,this.endpoint=this.getVitalsEndpoint(e.endpoint??`https://metrics.faststats.dev/v1/web`),this.debug=e.debug??!1,this.samplingPercentage=c(e.samplingPercentage),this.sessionSamplingSeed=Math.random()*100}getVitalsEndpoint(e){let t=new URL(e),n=t.pathname.split(`/`);return n[n.length-1]=`vitals`,t.pathname=n.join(`/`),t.toString()}start(){this.started||typeof window>`u`||(this.started=!0,document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.finalizeAndFlush()}),window.addEventListener(`pagehide`,()=>{this.finalizeAndFlush()}),this.debug&&console.log(`[WebVitals] Tracking started`),n(e=>this.captureMetric(e)),i(e=>this.captureMetric(e)),a(e=>this.captureMetric(e)),r(e=>this.captureMetric(e)),o(e=>this.captureMetric(e)))}captureMetric(e){if(this.flushed||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;let t=e.name,n={id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...e.attribution??{}},r=t===`FCP`||t===`TTFB`;this.metricsMap.set(t,{value:e.value,attributes:n,final:r}),this.debug&&console.log(`[WebVitals] ${t} captured: ${e.value}`+(r?` (final)`:``))}finalizeAndFlush(){if(!(this.flushed||this.metricsMap.size===0)){for(let[e,t]of this.metricsMap.entries())t.final||(t.final=!0,this.debug&&console.log(`[WebVitals] ${e} finalized: ${t.value}`));this.flushWithBeacon()}}buildPayload(){if(this.metricsMap.size===0)return null;let e=Array.from(this.metricsMap.entries()).map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes}));return{body:JSON.stringify({sessionId:window.__FA_getSessionId?.(),vitals:e,metadata:{url:window.location.href}}),count:e.length}}flushWithBeacon(){let e=this.buildPayload();if(e){if(this.flushed=!0,this.debug){let t=Array.from(this.metricsMap.keys()).join(`, `);console.log(`[WebVitals] Sending final metrics (${e.count}): ${t}`)}fetch(this.endpoint,{method:`POST`,body:e.body,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.siteKey}`},keepalive:!0}).catch(()=>{this.debug&&console.warn(`[WebVitals] Failed to send metrics`)})}}};typeof window<`u`&&(window.__FA_WebVitalsTracker=_);function v(e,t){typeof window>`u`||y()||window.__FA_webAnalyticsInstance?.track(e,t??{})}function y(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function b(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`}=e,s=n instanceof Blob?n:new Blob([n],{type:r});if(navigator.sendBeacon?.(t,s))return a&&console.log(`${o} ✓ Sent via beacon`),!0;try{let e=await fetch(t,{method:`POST`,body:n,headers:{"Content-Type":r,...i},keepalive:!0}),s=e.ok;return a&&(s?console.log(`${o} ✓ Sent via fetch`):console.warn(`${o} ✗ Failed: ${e.status}`)),s}catch{return a&&console.warn(`${o} ✗ Failed to send`),!1}}function x(e){for(;e&&!(e.tagName===`A`&&e.href);)e=e.parentNode;return e}function S(){let e={};if(!location.search)return e;let t=new URLSearchParams(location.search);for(let n of[`utm_source`,`utm_medium`,`utm_campaign`,`utm_term`,`utm_content`]){let r=t.get(n);r&&(e[n]=r)}return e}var C=class{endpoint;debug;started=!1;pageKey=``;navTimer=null;heartbeatTimer=null;scrollDepth=0;pageEntryTime=0;pagePath=``;pageUrl=``;pageHash=``;hasLeftCurrentPage=!1;scrollHandler=null;consentMode;cookielessWhilePending;constructor(e){this.options=e,this.endpoint=e.endpoint??`https://metrics.faststats.dev/v1/web`,this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(y()){this.log(`disabled`);return}window.__FA_webAnalyticsInstance=this,`requestIdleCallback`in window?window.requestIdleCallback(()=>this.start()):setTimeout(()=>this.start(),1)}}start(){if(this.started||typeof window>`u`)return;if(window.__FA_webAnalyticsInstance&&window.__FA_webAnalyticsInstance!==this){this.log(`already started by another instance`);return}if(y()){this.log(`disabled`);return}this.started=!0,window.__FA_webAnalyticsInstance=this,window.__FA_getAnonymousId=()=>u(this.isCookielessMode()),window.__FA_getSessionId=p,window.__FA_sendData=b,window.__FA_identify=(e,t,n)=>this.identify(e,t,n),window.__FA_logout=e=>this.logout(e),window.__FA_setConsentMode=e=>this.setConsentMode(e),window.__FA_optIn=()=>this.optIn(),window.__FA_optOut=()=>this.optOut(),window.__FA_trackEvent=(e,t)=>this.track(e,t??{}),window.__FA_isTrackingDisabled=y;let e=this.options,t=e.webVitals?.sampling?.percentage!==void 0,n=e.replayOptions?.samplingPercentage!==void 0||e.sessionReplays?.sampling?.percentage!==void 0;if((e.errorTracking?.enabled??e.trackErrors)&&(new s({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,getCommonData:()=>({url:location.href,page:location.pathname,referrer:document.referrer||null})}).start(),this.log(`error loaded`)),(e.trackWebVitals??e.webVitals?.enabled??t)&&(new _({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,samplingPercentage:c(e.webVitals?.sampling?.percentage)}).start(),this.log(`web-vitals loaded`)),e.sessionReplays?.enabled??e.trackReplay??n){let t=e.replayOptions??{};new l({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,...t,samplingPercentage:c(t.samplingPercentage??e.sessionReplays?.sampling?.percentage)}).start(),this.log(`replay loaded`)}this.enterPage(),this.pageview({trigger:`load`}),this.links(),this.trackScroll(),this.startHeartbeat(),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`?this.leavePage():this.startHeartbeat()}),window.addEventListener(`pagehide`,()=>this.leavePage()),window.addEventListener(`popstate`,()=>this.navigate()),e.trackHash&&window.addEventListener(`hashchange`,()=>this.navigate()),this.patch()}pageview(e={}){let t=`${location.pathname}|${this.options.trackHash??!1?location.hash:``}`;t!==this.pageKey&&(this.pageKey=t,this.send(`pageview`,e))}track(e,t={}){this.send(e,t)}identify(e,t,n={}){if(y()||this.isCookielessMode())return;let r=e.trim(),i=t.trim();!r||!i||b({url:this.endpoint.replace(/\/v1\/web$/,`/v1/identify`),data:JSON.stringify({token:this.options.siteKey,identifier:u(!1),externalId:r,email:i,name:n.name?.trim()||void 0,phone:n.phone?.trim()||void 0,avatarUrl:n.avatarUrl?.trim()||void 0,traits:n.traits??{}}),contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] identify`})}logout(e=!0){y()||(e&&f(this.isCookielessMode()),m())}setConsentMode(e){this.consentMode=e}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,t={}){let n=u(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:p(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...S(),...t}});this.log(e),b({url:this.endpoint,data:r,contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] ${e}`})}enterPage(){this.pageEntryTime=Date.now(),this.pagePath=location.pathname,this.pageUrl=location.href,this.pageHash=location.hash,this.scrollDepth=0,this.hasLeftCurrentPage=!1}leavePage(){if(this.hasLeftCurrentPage)return;this.hasLeftCurrentPage=!0;let e=Date.now();this.send(`page_leave`,{page:this.pagePath,url:this.pageUrl,time_on_page:e-this.pageEntryTime,scroll_depth:this.scrollDepth,session_duration:e-g()})}trackScroll(){this.scrollHandler&&window.removeEventListener(`scroll`,this.scrollHandler);let e=()=>{let e=document.documentElement,t=document.body,n=window.innerHeight,r=Math.max(e.scrollHeight,t.scrollHeight);if(r<=n){this.scrollDepth=100;return}let i=Math.min(100,Math.round(((window.scrollY||e.scrollTop)+n)/r*100));i>this.scrollDepth&&(this.scrollDepth=i)};this.scrollHandler=e,e(),window.addEventListener(`scroll`,e,{passive:!0})}startHeartbeat(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval(()=>{if(document.visibilityState===`hidden`){clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;return}h()},300*1e3)}navigate(){this.navTimer&&clearTimeout(this.navTimer),this.navTimer=setTimeout(()=>{this.navTimer=null;let e=location.pathname!==this.pagePath,t=(this.options.trackHash??!1)&&location.hash!==this.pageHash;!e&&!t||(this.leavePage(),this.enterPage(),this.trackScroll(),this.pageview({trigger:`navigation`}))},300)}patch(){let e=()=>this.navigate();for(let t of[`pushState`,`replaceState`]){let n=history[t];history[t]=function(...t){let r=n.apply(this,t);return e(),r}}}links(){let e=e=>{let t=x(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};document.addEventListener(`click`,e),document.addEventListener(`auxclick`,e)}};export{C as WebAnalytics,v as trackEvent};
|
package/package.json
CHANGED
|
@@ -15,11 +15,10 @@
|
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
|
-
"version": "0.0.
|
|
18
|
+
"version": "0.0.4",
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "tsdown
|
|
21
|
-
"
|
|
22
|
-
"dev": "bun run build && bun run sync:example-assets",
|
|
20
|
+
"build": "tsdown",
|
|
21
|
+
"dev": "bun run build",
|
|
23
22
|
"deploy:worker": "wrangler deploy"
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
package/src/analytics.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import ErrorTracker from "./error";
|
|
1
2
|
import type { ReplayTrackerOptions } from "./replay";
|
|
3
|
+
import ReplayTracker from "./replay";
|
|
2
4
|
import {
|
|
3
5
|
getAnonymousId,
|
|
4
6
|
getOrCreateSessionId,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
normalizeSamplingPercentage,
|
|
12
14
|
type SendDataOptions,
|
|
13
15
|
} from "./utils/types";
|
|
16
|
+
import WebVitalsTracker from "./web-vitals";
|
|
14
17
|
|
|
15
18
|
export type { SendDataOptions };
|
|
16
19
|
|
|
@@ -139,7 +142,6 @@ export interface IdentifyOptions {
|
|
|
139
142
|
export interface WebAnalyticsOptions {
|
|
140
143
|
siteKey: string;
|
|
141
144
|
endpoint?: string;
|
|
142
|
-
baseUrl?: string;
|
|
143
145
|
debug?: boolean;
|
|
144
146
|
autoTrack?: boolean;
|
|
145
147
|
trackHash?: boolean;
|
|
@@ -170,7 +172,10 @@ declare global {
|
|
|
170
172
|
__FA_optIn?: () => void;
|
|
171
173
|
__FA_optOut?: () => void;
|
|
172
174
|
__FA_isTrackingDisabled?: () => boolean;
|
|
173
|
-
__FA_trackEvent?: (
|
|
175
|
+
__FA_trackEvent?: (
|
|
176
|
+
eventName: string,
|
|
177
|
+
properties?: Record<string, unknown>,
|
|
178
|
+
) => void;
|
|
174
179
|
__FA_webAnalyticsInstance?: WebAnalytics;
|
|
175
180
|
}
|
|
176
181
|
}
|
|
@@ -178,7 +183,6 @@ declare global {
|
|
|
178
183
|
export class WebAnalytics {
|
|
179
184
|
private readonly endpoint: string;
|
|
180
185
|
private readonly debug: boolean;
|
|
181
|
-
private readonly baseUrl: string;
|
|
182
186
|
private started = false;
|
|
183
187
|
private pageKey = "";
|
|
184
188
|
private navTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -199,16 +203,6 @@ export class WebAnalytics {
|
|
|
199
203
|
this.consentMode = options.consent?.mode ?? "granted";
|
|
200
204
|
this.cookielessWhilePending =
|
|
201
205
|
options.consent?.cookielessWhilePending ?? true;
|
|
202
|
-
const script =
|
|
203
|
-
typeof document !== "undefined"
|
|
204
|
-
? (document.currentScript as HTMLScriptElement | null)
|
|
205
|
-
: null;
|
|
206
|
-
const scriptBase = script?.src
|
|
207
|
-
? script.src.substring(0, script.src.lastIndexOf("/"))
|
|
208
|
-
: "";
|
|
209
|
-
this.baseUrl =
|
|
210
|
-
(options.baseUrl ?? scriptBase) ||
|
|
211
|
-
(typeof window !== "undefined" ? window.location.origin : "");
|
|
212
206
|
if (options.autoTrack ?? true) this.init();
|
|
213
207
|
}
|
|
214
208
|
|
|
@@ -222,6 +216,7 @@ export class WebAnalytics {
|
|
|
222
216
|
this.log("disabled");
|
|
223
217
|
return;
|
|
224
218
|
}
|
|
219
|
+
window.__FA_webAnalyticsInstance = this;
|
|
225
220
|
if ("requestIdleCallback" in window)
|
|
226
221
|
window.requestIdleCallback(() => this.start());
|
|
227
222
|
else setTimeout(() => this.start(), 1);
|
|
@@ -265,7 +260,7 @@ export class WebAnalytics {
|
|
|
265
260
|
opts.sessionReplays?.sampling?.percentage !== undefined;
|
|
266
261
|
|
|
267
262
|
if (opts.errorTracking?.enabled ?? opts.trackErrors) {
|
|
268
|
-
|
|
263
|
+
new ErrorTracker({
|
|
269
264
|
siteKey: opts.siteKey,
|
|
270
265
|
endpoint: this.endpoint,
|
|
271
266
|
debug: this.debug,
|
|
@@ -274,25 +269,27 @@ export class WebAnalytics {
|
|
|
274
269
|
page: location.pathname,
|
|
275
270
|
referrer: document.referrer || null,
|
|
276
271
|
}),
|
|
277
|
-
});
|
|
272
|
+
}).start();
|
|
273
|
+
this.log("error loaded");
|
|
278
274
|
}
|
|
279
275
|
if (
|
|
280
|
-
opts.webVitals?.enabled ??
|
|
281
276
|
opts.trackWebVitals ??
|
|
277
|
+
opts.webVitals?.enabled ??
|
|
282
278
|
hasWebVitalsSampling
|
|
283
279
|
) {
|
|
284
|
-
|
|
280
|
+
new WebVitalsTracker({
|
|
285
281
|
siteKey: opts.siteKey,
|
|
286
282
|
endpoint: this.endpoint,
|
|
287
283
|
debug: this.debug,
|
|
288
284
|
samplingPercentage: normalizeSamplingPercentage(
|
|
289
285
|
opts.webVitals?.sampling?.percentage,
|
|
290
286
|
),
|
|
291
|
-
});
|
|
287
|
+
}).start();
|
|
288
|
+
this.log("web-vitals loaded");
|
|
292
289
|
}
|
|
293
290
|
if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
|
|
294
291
|
const replayOpts = opts.replayOptions ?? {};
|
|
295
|
-
|
|
292
|
+
new ReplayTracker({
|
|
296
293
|
siteKey: opts.siteKey,
|
|
297
294
|
endpoint: this.endpoint,
|
|
298
295
|
debug: this.debug,
|
|
@@ -301,7 +298,8 @@ export class WebAnalytics {
|
|
|
301
298
|
replayOpts.samplingPercentage ??
|
|
302
299
|
opts.sessionReplays?.sampling?.percentage,
|
|
303
300
|
),
|
|
304
|
-
});
|
|
301
|
+
}).start();
|
|
302
|
+
this.log("replay loaded");
|
|
305
303
|
}
|
|
306
304
|
|
|
307
305
|
this.enterPage();
|
|
@@ -321,31 +319,6 @@ export class WebAnalytics {
|
|
|
321
319
|
this.patch();
|
|
322
320
|
}
|
|
323
321
|
|
|
324
|
-
private load(
|
|
325
|
-
script: string,
|
|
326
|
-
key: string,
|
|
327
|
-
trackerOpts: Record<string, unknown>,
|
|
328
|
-
): void {
|
|
329
|
-
const el = document.createElement("script");
|
|
330
|
-
el.src = `${this.baseUrl}/${script}.js`;
|
|
331
|
-
el.async = true;
|
|
332
|
-
el.onload = () => {
|
|
333
|
-
const Ctor = (window as unknown as Record<string, unknown>)[key] as
|
|
334
|
-
| (new (
|
|
335
|
-
o: Record<string, unknown>,
|
|
336
|
-
) => { start(): void })
|
|
337
|
-
| undefined;
|
|
338
|
-
if (Ctor) {
|
|
339
|
-
new Ctor(trackerOpts).start();
|
|
340
|
-
this.log(`${script} loaded`);
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
el.onerror = () => {
|
|
344
|
-
if (this.debug) console.warn(`[Analytics] ${script} failed`);
|
|
345
|
-
};
|
|
346
|
-
document.head.appendChild(el);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
322
|
pageview(extra: Dict = {}): void {
|
|
350
323
|
const key = `${location.pathname}|${(this.options.trackHash ?? false) ? location.hash : ""}`;
|
|
351
324
|
if (key === this.pageKey) return;
|
package/src/error.ts
CHANGED
|
@@ -57,12 +57,6 @@ class ErrorTracker {
|
|
|
57
57
|
|
|
58
58
|
start(): void {
|
|
59
59
|
if (this.started || typeof window === "undefined") return;
|
|
60
|
-
if (window.__FA_isTrackingDisabled?.()) {
|
|
61
|
-
if (this.debug) {
|
|
62
|
-
console.log("[ErrorTracker] Tracking disabled via localStorage");
|
|
63
|
-
}
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
60
|
this.started = true;
|
|
67
61
|
|
|
68
62
|
window.addEventListener("error", this.handleErrorEvent);
|