@faststats/web 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/module.d.ts +3 -4
- package/dist/module.js +3 -403
- package/package.json +4 -4
- package/src/analytics.ts +26 -43
- package/src/module.ts +1 -0
- 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
ADDED
package/dist/module.d.ts
CHANGED
|
@@ -399,6 +399,7 @@ declare global {
|
|
|
399
399
|
}
|
|
400
400
|
//#endregion
|
|
401
401
|
//#region src/analytics.d.ts
|
|
402
|
+
declare function trackEvent(eventName: string, properties?: Record<string, unknown>): void;
|
|
402
403
|
type Dict = Record<string, unknown>;
|
|
403
404
|
interface SamplingOptions {
|
|
404
405
|
percentage?: number;
|
|
@@ -428,7 +429,6 @@ interface IdentifyOptions {
|
|
|
428
429
|
interface WebAnalyticsOptions {
|
|
429
430
|
siteKey: string;
|
|
430
431
|
endpoint?: string;
|
|
431
|
-
baseUrl?: string;
|
|
432
432
|
debug?: boolean;
|
|
433
433
|
autoTrack?: boolean;
|
|
434
434
|
trackHash?: boolean;
|
|
@@ -454,6 +454,7 @@ declare global {
|
|
|
454
454
|
__FA_optIn?: () => void;
|
|
455
455
|
__FA_optOut?: () => void;
|
|
456
456
|
__FA_isTrackingDisabled?: () => boolean;
|
|
457
|
+
__FA_trackEvent?: (eventName: string, properties?: Record<string, unknown>) => void;
|
|
457
458
|
__FA_webAnalyticsInstance?: WebAnalytics;
|
|
458
459
|
}
|
|
459
460
|
}
|
|
@@ -461,7 +462,6 @@ declare class WebAnalytics {
|
|
|
461
462
|
private readonly options;
|
|
462
463
|
private readonly endpoint;
|
|
463
464
|
private readonly debug;
|
|
464
|
-
private readonly baseUrl;
|
|
465
465
|
private started;
|
|
466
466
|
private pageKey;
|
|
467
467
|
private navTimer;
|
|
@@ -479,7 +479,6 @@ declare class WebAnalytics {
|
|
|
479
479
|
private log;
|
|
480
480
|
private init;
|
|
481
481
|
start(): void;
|
|
482
|
-
private load;
|
|
483
482
|
pageview(extra?: Dict): void;
|
|
484
483
|
track(name: string, extra?: Dict): void;
|
|
485
484
|
identify(externalId: string, email: string, options?: IdentifyOptions): void;
|
|
@@ -499,4 +498,4 @@ declare class WebAnalytics {
|
|
|
499
498
|
private links;
|
|
500
499
|
}
|
|
501
500
|
//#endregion
|
|
502
|
-
export { type ConsentMode, type IdentifyOptions, WebAnalytics, type WebAnalyticsOptions };
|
|
501
|
+
export { type ConsentMode, type IdentifyOptions, WebAnalytics, type WebAnalyticsOptions, trackEvent };
|
package/dist/module.js
CHANGED
|
@@ -1,403 +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 isTrackingDisabled() {
|
|
69
|
-
if (typeof localStorage === "undefined") return false;
|
|
70
|
-
const value = localStorage.getItem("disable-faststats");
|
|
71
|
-
return value === "true" || value === "1";
|
|
72
|
-
}
|
|
73
|
-
async function sendData(options) {
|
|
74
|
-
const { url, data, contentType = "application/json", headers = {}, debug = false, debugPrefix = "[Analytics]" } = options;
|
|
75
|
-
const blob = data instanceof Blob ? data : new Blob([data], { type: contentType });
|
|
76
|
-
if (navigator.sendBeacon?.(url, blob)) {
|
|
77
|
-
if (debug) console.log(`${debugPrefix} ✓ Sent via beacon`);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
const response = await fetch(url, {
|
|
82
|
-
method: "POST",
|
|
83
|
-
body: data,
|
|
84
|
-
headers: {
|
|
85
|
-
"Content-Type": contentType,
|
|
86
|
-
...headers
|
|
87
|
-
},
|
|
88
|
-
keepalive: true
|
|
89
|
-
});
|
|
90
|
-
const success = response.ok;
|
|
91
|
-
if (debug) if (success) console.log(`${debugPrefix} ✓ Sent via fetch`);
|
|
92
|
-
else console.warn(`${debugPrefix} ✗ Failed: ${response.status}`);
|
|
93
|
-
return success;
|
|
94
|
-
} catch {
|
|
95
|
-
if (debug) console.warn(`${debugPrefix} ✗ Failed to send`);
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function getLinkEl(el) {
|
|
100
|
-
while (el && !(el.tagName === "A" && el.href)) el = el.parentNode;
|
|
101
|
-
return el;
|
|
102
|
-
}
|
|
103
|
-
function getUTM() {
|
|
104
|
-
const params = {};
|
|
105
|
-
if (!location.search) return params;
|
|
106
|
-
const sp = new URLSearchParams(location.search);
|
|
107
|
-
for (const k of [
|
|
108
|
-
"utm_source",
|
|
109
|
-
"utm_medium",
|
|
110
|
-
"utm_campaign",
|
|
111
|
-
"utm_term",
|
|
112
|
-
"utm_content"
|
|
113
|
-
]) {
|
|
114
|
-
const v = sp.get(k);
|
|
115
|
-
if (v) params[k] = v;
|
|
116
|
-
}
|
|
117
|
-
return params;
|
|
118
|
-
}
|
|
119
|
-
var WebAnalytics = class {
|
|
120
|
-
endpoint;
|
|
121
|
-
debug;
|
|
122
|
-
baseUrl;
|
|
123
|
-
started = false;
|
|
124
|
-
pageKey = "";
|
|
125
|
-
navTimer = null;
|
|
126
|
-
heartbeatTimer = null;
|
|
127
|
-
scrollDepth = 0;
|
|
128
|
-
pageEntryTime = 0;
|
|
129
|
-
pagePath = "";
|
|
130
|
-
pageUrl = "";
|
|
131
|
-
pageHash = "";
|
|
132
|
-
hasLeftCurrentPage = false;
|
|
133
|
-
scrollHandler = null;
|
|
134
|
-
consentMode;
|
|
135
|
-
cookielessWhilePending;
|
|
136
|
-
constructor(options) {
|
|
137
|
-
this.options = options;
|
|
138
|
-
this.endpoint = options.endpoint ?? "https://metrics.faststats.dev/v1/web";
|
|
139
|
-
this.debug = options.debug ?? false;
|
|
140
|
-
this.consentMode = options.consent?.mode ?? "granted";
|
|
141
|
-
this.cookielessWhilePending = options.consent?.cookielessWhilePending ?? true;
|
|
142
|
-
const script = typeof document !== "undefined" ? document.currentScript : null;
|
|
143
|
-
const scriptBase = script?.src ? script.src.substring(0, script.src.lastIndexOf("/")) : "";
|
|
144
|
-
this.baseUrl = (options.baseUrl ?? scriptBase) || (typeof window !== "undefined" ? window.location.origin : "");
|
|
145
|
-
if (options.autoTrack ?? true) this.init();
|
|
146
|
-
}
|
|
147
|
-
log(msg) {
|
|
148
|
-
if (this.debug) console.log(`[Analytics] ${msg}`);
|
|
149
|
-
}
|
|
150
|
-
init() {
|
|
151
|
-
if (typeof window === "undefined") return;
|
|
152
|
-
if (isTrackingDisabled()) {
|
|
153
|
-
this.log("disabled");
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if ("requestIdleCallback" in window) window.requestIdleCallback(() => this.start());
|
|
157
|
-
else setTimeout(() => this.start(), 1);
|
|
158
|
-
}
|
|
159
|
-
start() {
|
|
160
|
-
if (this.started || typeof window === "undefined") return;
|
|
161
|
-
if (window.__FA_webAnalyticsInstance && window.__FA_webAnalyticsInstance !== this) {
|
|
162
|
-
this.log("already started by another instance");
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (isTrackingDisabled()) {
|
|
166
|
-
this.log("disabled");
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
this.started = true;
|
|
170
|
-
window.__FA_webAnalyticsInstance = this;
|
|
171
|
-
window.__FA_getAnonymousId = () => getAnonymousId(this.isCookielessMode());
|
|
172
|
-
window.__FA_getSessionId = getOrCreateSessionId;
|
|
173
|
-
window.__FA_sendData = sendData;
|
|
174
|
-
window.__FA_identify = (externalId, email, options) => this.identify(externalId, email, options);
|
|
175
|
-
window.__FA_logout = (resetAnonymousIdentity) => this.logout(resetAnonymousIdentity);
|
|
176
|
-
window.__FA_setConsentMode = (mode) => this.setConsentMode(mode);
|
|
177
|
-
window.__FA_optIn = () => this.optIn();
|
|
178
|
-
window.__FA_optOut = () => this.optOut();
|
|
179
|
-
window.__FA_isTrackingDisabled = isTrackingDisabled;
|
|
180
|
-
const opts = this.options;
|
|
181
|
-
const hasWebVitalsSampling = opts.webVitals?.sampling?.percentage !== void 0;
|
|
182
|
-
const hasReplaySampling = opts.replayOptions?.samplingPercentage !== void 0 || opts.sessionReplays?.sampling?.percentage !== void 0;
|
|
183
|
-
if (opts.errorTracking?.enabled ?? opts.trackErrors) this.load("error", "__FA_ErrorTracker", {
|
|
184
|
-
siteKey: opts.siteKey,
|
|
185
|
-
endpoint: this.endpoint,
|
|
186
|
-
debug: this.debug,
|
|
187
|
-
getCommonData: () => ({
|
|
188
|
-
url: location.href,
|
|
189
|
-
page: location.pathname,
|
|
190
|
-
referrer: document.referrer || null
|
|
191
|
-
})
|
|
192
|
-
});
|
|
193
|
-
if (opts.webVitals?.enabled ?? opts.trackWebVitals ?? hasWebVitalsSampling) this.load("web-vitals", "__FA_WebVitalsTracker", {
|
|
194
|
-
siteKey: opts.siteKey,
|
|
195
|
-
endpoint: this.endpoint,
|
|
196
|
-
debug: this.debug,
|
|
197
|
-
samplingPercentage: normalizeSamplingPercentage(opts.webVitals?.sampling?.percentage)
|
|
198
|
-
});
|
|
199
|
-
if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
|
|
200
|
-
const replayOpts = opts.replayOptions ?? {};
|
|
201
|
-
this.load("replay", "__FA_ReplayTracker", {
|
|
202
|
-
siteKey: opts.siteKey,
|
|
203
|
-
endpoint: this.endpoint,
|
|
204
|
-
debug: this.debug,
|
|
205
|
-
...replayOpts,
|
|
206
|
-
samplingPercentage: normalizeSamplingPercentage(replayOpts.samplingPercentage ?? opts.sessionReplays?.sampling?.percentage)
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
this.enterPage();
|
|
210
|
-
this.pageview({ trigger: "load" });
|
|
211
|
-
this.links();
|
|
212
|
-
this.trackScroll();
|
|
213
|
-
this.startHeartbeat();
|
|
214
|
-
document.addEventListener("visibilitychange", () => {
|
|
215
|
-
if (document.visibilityState === "hidden") this.leavePage();
|
|
216
|
-
else this.startHeartbeat();
|
|
217
|
-
});
|
|
218
|
-
window.addEventListener("pagehide", () => this.leavePage());
|
|
219
|
-
window.addEventListener("popstate", () => this.navigate());
|
|
220
|
-
if (opts.trackHash) window.addEventListener("hashchange", () => this.navigate());
|
|
221
|
-
this.patch();
|
|
222
|
-
}
|
|
223
|
-
load(script, key, trackerOpts) {
|
|
224
|
-
const el = document.createElement("script");
|
|
225
|
-
el.src = `${this.baseUrl}/${script}.js`;
|
|
226
|
-
el.async = true;
|
|
227
|
-
el.onload = () => {
|
|
228
|
-
const Ctor = window[key];
|
|
229
|
-
if (Ctor) {
|
|
230
|
-
new Ctor(trackerOpts).start();
|
|
231
|
-
this.log(`${script} loaded`);
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
el.onerror = () => {
|
|
235
|
-
if (this.debug) console.warn(`[Analytics] ${script} failed`);
|
|
236
|
-
};
|
|
237
|
-
document.head.appendChild(el);
|
|
238
|
-
}
|
|
239
|
-
pageview(extra = {}) {
|
|
240
|
-
const key = `${location.pathname}|${this.options.trackHash ?? false ? location.hash : ""}`;
|
|
241
|
-
if (key === this.pageKey) return;
|
|
242
|
-
this.pageKey = key;
|
|
243
|
-
this.send("pageview", extra);
|
|
244
|
-
}
|
|
245
|
-
track(name, extra = {}) {
|
|
246
|
-
this.send(name, extra);
|
|
247
|
-
}
|
|
248
|
-
identify(externalId, email, options = {}) {
|
|
249
|
-
if (isTrackingDisabled()) return;
|
|
250
|
-
if (this.isCookielessMode()) return;
|
|
251
|
-
const trimmedExternalId = externalId.trim();
|
|
252
|
-
const trimmedEmail = email.trim();
|
|
253
|
-
if (!trimmedExternalId || !trimmedEmail) return;
|
|
254
|
-
sendData({
|
|
255
|
-
url: this.endpoint.replace(/\/v1\/web$/, "/v1/identify"),
|
|
256
|
-
data: JSON.stringify({
|
|
257
|
-
token: this.options.siteKey,
|
|
258
|
-
identifier: getAnonymousId(false),
|
|
259
|
-
externalId: trimmedExternalId,
|
|
260
|
-
email: trimmedEmail,
|
|
261
|
-
name: options.name?.trim() || void 0,
|
|
262
|
-
phone: options.phone?.trim() || void 0,
|
|
263
|
-
avatarUrl: options.avatarUrl?.trim() || void 0,
|
|
264
|
-
traits: options.traits ?? {}
|
|
265
|
-
}),
|
|
266
|
-
contentType: "text/plain",
|
|
267
|
-
debug: this.debug,
|
|
268
|
-
debugPrefix: "[Analytics] identify"
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
logout(resetAnonymousIdentity = true) {
|
|
272
|
-
if (isTrackingDisabled()) return;
|
|
273
|
-
if (resetAnonymousIdentity) resetAnonymousId(this.isCookielessMode());
|
|
274
|
-
resetSessionId();
|
|
275
|
-
}
|
|
276
|
-
setConsentMode(mode) {
|
|
277
|
-
this.consentMode = mode;
|
|
278
|
-
}
|
|
279
|
-
optIn() {
|
|
280
|
-
this.setConsentMode("granted");
|
|
281
|
-
}
|
|
282
|
-
optOut() {
|
|
283
|
-
this.setConsentMode("denied");
|
|
284
|
-
}
|
|
285
|
-
getConsentMode() {
|
|
286
|
-
return this.consentMode;
|
|
287
|
-
}
|
|
288
|
-
isCookielessMode() {
|
|
289
|
-
if (this.options.cookieless) return true;
|
|
290
|
-
if (this.consentMode === "denied") return true;
|
|
291
|
-
if (this.consentMode === "pending") return this.cookielessWhilePending;
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
send(event, extra = {}) {
|
|
295
|
-
const identifier = getAnonymousId(this.isCookielessMode());
|
|
296
|
-
const payload = JSON.stringify({
|
|
297
|
-
token: this.options.siteKey,
|
|
298
|
-
...identifier ? { userId: identifier } : {},
|
|
299
|
-
sessionId: getOrCreateSessionId(),
|
|
300
|
-
data: {
|
|
301
|
-
event,
|
|
302
|
-
page: location.pathname,
|
|
303
|
-
referrer: document.referrer || null,
|
|
304
|
-
title: document.title || "",
|
|
305
|
-
url: location.href,
|
|
306
|
-
...getUTM(),
|
|
307
|
-
...extra
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
this.log(event);
|
|
311
|
-
sendData({
|
|
312
|
-
url: this.endpoint,
|
|
313
|
-
data: payload,
|
|
314
|
-
contentType: "text/plain",
|
|
315
|
-
debug: this.debug,
|
|
316
|
-
debugPrefix: `[Analytics] ${event}`
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
enterPage() {
|
|
320
|
-
this.pageEntryTime = Date.now();
|
|
321
|
-
this.pagePath = location.pathname;
|
|
322
|
-
this.pageUrl = location.href;
|
|
323
|
-
this.pageHash = location.hash;
|
|
324
|
-
this.scrollDepth = 0;
|
|
325
|
-
this.hasLeftCurrentPage = false;
|
|
326
|
-
}
|
|
327
|
-
leavePage() {
|
|
328
|
-
if (this.hasLeftCurrentPage) return;
|
|
329
|
-
this.hasLeftCurrentPage = true;
|
|
330
|
-
const now = Date.now();
|
|
331
|
-
this.send("page_leave", {
|
|
332
|
-
page: this.pagePath,
|
|
333
|
-
url: this.pageUrl,
|
|
334
|
-
time_on_page: now - this.pageEntryTime,
|
|
335
|
-
scroll_depth: this.scrollDepth,
|
|
336
|
-
session_duration: now - getSessionStart()
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
trackScroll() {
|
|
340
|
-
if (this.scrollHandler) window.removeEventListener("scroll", this.scrollHandler);
|
|
341
|
-
const update = () => {
|
|
342
|
-
const doc = document.documentElement;
|
|
343
|
-
const body = document.body;
|
|
344
|
-
const viewportH = window.innerHeight;
|
|
345
|
-
const docH = Math.max(doc.scrollHeight, body.scrollHeight);
|
|
346
|
-
if (docH <= viewportH) {
|
|
347
|
-
this.scrollDepth = 100;
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const depth = Math.min(100, Math.round(((window.scrollY || doc.scrollTop) + viewportH) / docH * 100));
|
|
351
|
-
if (depth > this.scrollDepth) this.scrollDepth = depth;
|
|
352
|
-
};
|
|
353
|
-
this.scrollHandler = update;
|
|
354
|
-
update();
|
|
355
|
-
window.addEventListener("scroll", update, { passive: true });
|
|
356
|
-
}
|
|
357
|
-
startHeartbeat() {
|
|
358
|
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
359
|
-
this.heartbeatTimer = setInterval(() => {
|
|
360
|
-
if (document.visibilityState === "hidden") {
|
|
361
|
-
clearInterval(this.heartbeatTimer);
|
|
362
|
-
this.heartbeatTimer = null;
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
refreshSessionTimestamp();
|
|
366
|
-
}, 300 * 1e3);
|
|
367
|
-
}
|
|
368
|
-
navigate() {
|
|
369
|
-
if (this.navTimer) clearTimeout(this.navTimer);
|
|
370
|
-
this.navTimer = setTimeout(() => {
|
|
371
|
-
this.navTimer = null;
|
|
372
|
-
const pathChanged = location.pathname !== this.pagePath;
|
|
373
|
-
const hashChanged = (this.options.trackHash ?? false) && location.hash !== this.pageHash;
|
|
374
|
-
if (!pathChanged && !hashChanged) return;
|
|
375
|
-
this.leavePage();
|
|
376
|
-
this.enterPage();
|
|
377
|
-
this.trackScroll();
|
|
378
|
-
this.pageview({ trigger: "navigation" });
|
|
379
|
-
}, 300);
|
|
380
|
-
}
|
|
381
|
-
patch() {
|
|
382
|
-
const fire = () => this.navigate();
|
|
383
|
-
for (const method of ["pushState", "replaceState"]) {
|
|
384
|
-
const orig = history[method];
|
|
385
|
-
history[method] = function(...args) {
|
|
386
|
-
const result = orig.apply(this, args);
|
|
387
|
-
fire();
|
|
388
|
-
return result;
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
links() {
|
|
393
|
-
const handler = (event) => {
|
|
394
|
-
const link = getLinkEl(event.target);
|
|
395
|
-
if (link && link.host !== location.host) this.track("outbound_link", { outbound_link: link.href });
|
|
396
|
-
};
|
|
397
|
-
document.addEventListener("click", handler);
|
|
398
|
-
document.addEventListener("auxclick", handler);
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
//#endregion
|
|
403
|
-
export { WebAnalytics };
|
|
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(){if(!(this.started||typeof window>`u`)){if(window.__FA_isTrackingDisabled?.()){this.debug&&console.log(`[ErrorTracker] Tracking disabled via localStorage`);return}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`)return;if(window.__FA_isTrackingDisabled?.()){this.debug&&console.log(`[Replay] Tracking disabled via localStorage`);return}if(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(){if(!(this.started||typeof window>`u`)){if(window.__FA_isTrackingDisabled?.()){this.debug&&console.log(`[WebVitals] Tracking disabled`);return}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}`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.webVitals?.enabled??e.trackWebVitals??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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststats/web",
|
|
3
|
+
"repository": "https://github.com/faststats-dev/web-analytics",
|
|
3
4
|
"main": "dist/module.js",
|
|
4
5
|
"module": "dist/module.js",
|
|
5
6
|
"types": "dist/module.d.ts",
|
|
@@ -14,11 +15,10 @@
|
|
|
14
15
|
"publishConfig": {
|
|
15
16
|
"access": "public"
|
|
16
17
|
},
|
|
17
|
-
"version": "0.0.
|
|
18
|
+
"version": "0.0.3",
|
|
18
19
|
"scripts": {
|
|
19
|
-
"build": "tsdown
|
|
20
|
-
"
|
|
21
|
-
"dev": "bun run build && bun run sync:example-assets",
|
|
20
|
+
"build": "tsdown",
|
|
21
|
+
"dev": "bun run build",
|
|
22
22
|
"deploy:worker": "wrangler deploy"
|
|
23
23
|
},
|
|
24
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,9 +13,18 @@ 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
|
|
|
20
|
+
export function trackEvent(
|
|
21
|
+
eventName: string,
|
|
22
|
+
properties?: Record<string, unknown>,
|
|
23
|
+
): void {
|
|
24
|
+
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
25
|
+
window.__FA_webAnalyticsInstance?.track(eventName, properties ?? {});
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
export function isTrackingDisabled(): boolean {
|
|
18
29
|
if (typeof localStorage === "undefined") return false;
|
|
19
30
|
const value = localStorage.getItem("disable-faststats");
|
|
@@ -131,7 +142,6 @@ export interface IdentifyOptions {
|
|
|
131
142
|
export interface WebAnalyticsOptions {
|
|
132
143
|
siteKey: string;
|
|
133
144
|
endpoint?: string;
|
|
134
|
-
baseUrl?: string;
|
|
135
145
|
debug?: boolean;
|
|
136
146
|
autoTrack?: boolean;
|
|
137
147
|
trackHash?: boolean;
|
|
@@ -162,6 +172,10 @@ declare global {
|
|
|
162
172
|
__FA_optIn?: () => void;
|
|
163
173
|
__FA_optOut?: () => void;
|
|
164
174
|
__FA_isTrackingDisabled?: () => boolean;
|
|
175
|
+
__FA_trackEvent?: (
|
|
176
|
+
eventName: string,
|
|
177
|
+
properties?: Record<string, unknown>,
|
|
178
|
+
) => void;
|
|
165
179
|
__FA_webAnalyticsInstance?: WebAnalytics;
|
|
166
180
|
}
|
|
167
181
|
}
|
|
@@ -169,7 +183,6 @@ declare global {
|
|
|
169
183
|
export class WebAnalytics {
|
|
170
184
|
private readonly endpoint: string;
|
|
171
185
|
private readonly debug: boolean;
|
|
172
|
-
private readonly baseUrl: string;
|
|
173
186
|
private started = false;
|
|
174
187
|
private pageKey = "";
|
|
175
188
|
private navTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -190,16 +203,6 @@ export class WebAnalytics {
|
|
|
190
203
|
this.consentMode = options.consent?.mode ?? "granted";
|
|
191
204
|
this.cookielessWhilePending =
|
|
192
205
|
options.consent?.cookielessWhilePending ?? true;
|
|
193
|
-
const script =
|
|
194
|
-
typeof document !== "undefined"
|
|
195
|
-
? (document.currentScript as HTMLScriptElement | null)
|
|
196
|
-
: null;
|
|
197
|
-
const scriptBase = script?.src
|
|
198
|
-
? script.src.substring(0, script.src.lastIndexOf("/"))
|
|
199
|
-
: "";
|
|
200
|
-
this.baseUrl =
|
|
201
|
-
(options.baseUrl ?? scriptBase) ||
|
|
202
|
-
(typeof window !== "undefined" ? window.location.origin : "");
|
|
203
206
|
if (options.autoTrack ?? true) this.init();
|
|
204
207
|
}
|
|
205
208
|
|
|
@@ -244,6 +247,8 @@ export class WebAnalytics {
|
|
|
244
247
|
window.__FA_setConsentMode = (mode) => this.setConsentMode(mode);
|
|
245
248
|
window.__FA_optIn = () => this.optIn();
|
|
246
249
|
window.__FA_optOut = () => this.optOut();
|
|
250
|
+
window.__FA_trackEvent = (eventName, props) =>
|
|
251
|
+
this.track(eventName, props ?? {});
|
|
247
252
|
window.__FA_isTrackingDisabled = isTrackingDisabled;
|
|
248
253
|
|
|
249
254
|
const opts = this.options;
|
|
@@ -254,7 +259,7 @@ export class WebAnalytics {
|
|
|
254
259
|
opts.sessionReplays?.sampling?.percentage !== undefined;
|
|
255
260
|
|
|
256
261
|
if (opts.errorTracking?.enabled ?? opts.trackErrors) {
|
|
257
|
-
|
|
262
|
+
new ErrorTracker({
|
|
258
263
|
siteKey: opts.siteKey,
|
|
259
264
|
endpoint: this.endpoint,
|
|
260
265
|
debug: this.debug,
|
|
@@ -263,25 +268,27 @@ export class WebAnalytics {
|
|
|
263
268
|
page: location.pathname,
|
|
264
269
|
referrer: document.referrer || null,
|
|
265
270
|
}),
|
|
266
|
-
});
|
|
271
|
+
}).start();
|
|
272
|
+
this.log("error loaded");
|
|
267
273
|
}
|
|
268
274
|
if (
|
|
269
275
|
opts.webVitals?.enabled ??
|
|
270
276
|
opts.trackWebVitals ??
|
|
271
277
|
hasWebVitalsSampling
|
|
272
278
|
) {
|
|
273
|
-
|
|
279
|
+
new WebVitalsTracker({
|
|
274
280
|
siteKey: opts.siteKey,
|
|
275
281
|
endpoint: this.endpoint,
|
|
276
282
|
debug: this.debug,
|
|
277
283
|
samplingPercentage: normalizeSamplingPercentage(
|
|
278
284
|
opts.webVitals?.sampling?.percentage,
|
|
279
285
|
),
|
|
280
|
-
});
|
|
286
|
+
}).start();
|
|
287
|
+
this.log("web-vitals loaded");
|
|
281
288
|
}
|
|
282
289
|
if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
|
|
283
290
|
const replayOpts = opts.replayOptions ?? {};
|
|
284
|
-
|
|
291
|
+
new ReplayTracker({
|
|
285
292
|
siteKey: opts.siteKey,
|
|
286
293
|
endpoint: this.endpoint,
|
|
287
294
|
debug: this.debug,
|
|
@@ -290,7 +297,8 @@ export class WebAnalytics {
|
|
|
290
297
|
replayOpts.samplingPercentage ??
|
|
291
298
|
opts.sessionReplays?.sampling?.percentage,
|
|
292
299
|
),
|
|
293
|
-
});
|
|
300
|
+
}).start();
|
|
301
|
+
this.log("replay loaded");
|
|
294
302
|
}
|
|
295
303
|
|
|
296
304
|
this.enterPage();
|
|
@@ -310,31 +318,6 @@ export class WebAnalytics {
|
|
|
310
318
|
this.patch();
|
|
311
319
|
}
|
|
312
320
|
|
|
313
|
-
private load(
|
|
314
|
-
script: string,
|
|
315
|
-
key: string,
|
|
316
|
-
trackerOpts: Record<string, unknown>,
|
|
317
|
-
): void {
|
|
318
|
-
const el = document.createElement("script");
|
|
319
|
-
el.src = `${this.baseUrl}/${script}.js`;
|
|
320
|
-
el.async = true;
|
|
321
|
-
el.onload = () => {
|
|
322
|
-
const Ctor = (window as unknown as Record<string, unknown>)[key] as
|
|
323
|
-
| (new (
|
|
324
|
-
o: Record<string, unknown>,
|
|
325
|
-
) => { start(): void })
|
|
326
|
-
| undefined;
|
|
327
|
-
if (Ctor) {
|
|
328
|
-
new Ctor(trackerOpts).start();
|
|
329
|
-
this.log(`${script} loaded`);
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
el.onerror = () => {
|
|
333
|
-
if (this.debug) console.warn(`[Analytics] ${script} failed`);
|
|
334
|
-
};
|
|
335
|
-
document.head.appendChild(el);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
321
|
pageview(extra: Dict = {}): void {
|
|
339
322
|
const key = `${location.pathname}|${(this.options.trackHash ?? false) ? location.hash : ""}`;
|
|
340
323
|
if (key === this.pageKey) return;
|