@faststats/web 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/error.iife.js +3 -0
- package/dist/error.js +3 -0
- package/dist/module.d.ts +502 -0
- package/dist/module.js +403 -0
- package/dist/replay.iife.js +29 -0
- package/dist/replay.js +29 -0
- package/dist/script.iife.js +1 -0
- package/dist/script.js +1 -0
- package/dist/web-vitals.iife.js +1 -0
- package/dist/web-vitals.js +1 -0
- package/package.json +38 -0
- package/src/analytics.ts +544 -0
- package/src/error.ts +324 -0
- package/src/index.ts +95 -0
- package/src/module.ts +6 -0
- package/src/replay.ts +488 -0
- package/src/utils/identifiers.ts +70 -0
- package/src/utils/types.ts +13 -0
- package/src/web-vitals.ts +207 -0
- package/tsconfig.json +30 -0
- package/tsdown.config.ts +51 -0
- package/worker/index.ts +22 -0
- package/wrangler.toml +8 -0
package/src/analytics.ts
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import type { ReplayTrackerOptions } from "./replay";
|
|
2
|
+
import {
|
|
3
|
+
getAnonymousId,
|
|
4
|
+
getOrCreateSessionId,
|
|
5
|
+
getSessionStart,
|
|
6
|
+
refreshSessionTimestamp,
|
|
7
|
+
resetAnonymousId,
|
|
8
|
+
resetSessionId,
|
|
9
|
+
} from "./utils/identifiers";
|
|
10
|
+
import {
|
|
11
|
+
normalizeSamplingPercentage,
|
|
12
|
+
type SendDataOptions,
|
|
13
|
+
} from "./utils/types";
|
|
14
|
+
|
|
15
|
+
export type { SendDataOptions };
|
|
16
|
+
|
|
17
|
+
export function isTrackingDisabled(): boolean {
|
|
18
|
+
if (typeof localStorage === "undefined") return false;
|
|
19
|
+
const value = localStorage.getItem("disable-faststats");
|
|
20
|
+
return value === "true" || value === "1";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function sendData(options: SendDataOptions): Promise<boolean> {
|
|
24
|
+
const {
|
|
25
|
+
url,
|
|
26
|
+
data,
|
|
27
|
+
contentType = "application/json",
|
|
28
|
+
headers = {},
|
|
29
|
+
debug = false,
|
|
30
|
+
debugPrefix = "[Analytics]",
|
|
31
|
+
} = options;
|
|
32
|
+
|
|
33
|
+
const blob =
|
|
34
|
+
data instanceof Blob ? data : new Blob([data], { type: contentType });
|
|
35
|
+
|
|
36
|
+
if (navigator.sendBeacon?.(url, blob)) {
|
|
37
|
+
if (debug) {
|
|
38
|
+
console.log(`${debugPrefix} ✓ Sent via beacon`);
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(url, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
body: data,
|
|
47
|
+
headers: {
|
|
48
|
+
"Content-Type": contentType,
|
|
49
|
+
...headers,
|
|
50
|
+
},
|
|
51
|
+
keepalive: true,
|
|
52
|
+
});
|
|
53
|
+
const success = response.ok;
|
|
54
|
+
if (debug) {
|
|
55
|
+
if (success) {
|
|
56
|
+
console.log(`${debugPrefix} ✓ Sent via fetch`);
|
|
57
|
+
} else {
|
|
58
|
+
console.warn(`${debugPrefix} ✗ Failed: ${response.status}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return success;
|
|
62
|
+
} catch {
|
|
63
|
+
if (debug) {
|
|
64
|
+
console.warn(`${debugPrefix} ✗ Failed to send`);
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getLinkEl(el: Node | null): HTMLAnchorElement | null {
|
|
71
|
+
while (
|
|
72
|
+
el &&
|
|
73
|
+
!((el as Element).tagName === "A" && (el as HTMLAnchorElement).href)
|
|
74
|
+
) {
|
|
75
|
+
el = el.parentNode;
|
|
76
|
+
}
|
|
77
|
+
return el as HTMLAnchorElement | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getUTM(): Record<string, string> {
|
|
81
|
+
const params: Record<string, string> = {};
|
|
82
|
+
if (!location.search) return params;
|
|
83
|
+
const sp = new URLSearchParams(location.search);
|
|
84
|
+
for (const k of [
|
|
85
|
+
"utm_source",
|
|
86
|
+
"utm_medium",
|
|
87
|
+
"utm_campaign",
|
|
88
|
+
"utm_term",
|
|
89
|
+
"utm_content",
|
|
90
|
+
]) {
|
|
91
|
+
const v = sp.get(k);
|
|
92
|
+
if (v) params[k] = v;
|
|
93
|
+
}
|
|
94
|
+
return params;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type Dict = Record<string, unknown>;
|
|
98
|
+
|
|
99
|
+
interface SamplingOptions {
|
|
100
|
+
percentage?: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface ErrorTrackingConfig {
|
|
104
|
+
enabled?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface WebVitalsConfig {
|
|
108
|
+
enabled?: boolean;
|
|
109
|
+
sampling?: SamplingOptions;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface SessionReplayConfig {
|
|
113
|
+
enabled?: boolean;
|
|
114
|
+
sampling?: SamplingOptions;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type ConsentMode = "pending" | "granted" | "denied";
|
|
118
|
+
|
|
119
|
+
interface ConsentConfig {
|
|
120
|
+
mode?: ConsentMode;
|
|
121
|
+
cookielessWhilePending?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface IdentifyOptions {
|
|
125
|
+
name?: string;
|
|
126
|
+
phone?: string;
|
|
127
|
+
avatarUrl?: string;
|
|
128
|
+
traits?: Record<string, unknown>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface WebAnalyticsOptions {
|
|
132
|
+
siteKey: string;
|
|
133
|
+
endpoint?: string;
|
|
134
|
+
baseUrl?: string;
|
|
135
|
+
debug?: boolean;
|
|
136
|
+
autoTrack?: boolean;
|
|
137
|
+
trackHash?: boolean;
|
|
138
|
+
trackErrors?: boolean;
|
|
139
|
+
trackWebVitals?: boolean;
|
|
140
|
+
trackReplay?: boolean;
|
|
141
|
+
cookieless?: boolean;
|
|
142
|
+
consent?: ConsentConfig;
|
|
143
|
+
errorTracking?: ErrorTrackingConfig;
|
|
144
|
+
webVitals?: WebVitalsConfig;
|
|
145
|
+
sessionReplays?: SessionReplayConfig;
|
|
146
|
+
replayOptions?: Partial<ReplayTrackerOptions>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
declare global {
|
|
150
|
+
interface Window {
|
|
151
|
+
WebAnalytics: typeof WebAnalytics;
|
|
152
|
+
__FA_getAnonymousId?: () => string;
|
|
153
|
+
__FA_getSessionId?: () => string;
|
|
154
|
+
__FA_sendData?: (options: SendDataOptions) => Promise<boolean>;
|
|
155
|
+
__FA_identify?: (
|
|
156
|
+
externalId: string,
|
|
157
|
+
email: string,
|
|
158
|
+
options?: IdentifyOptions,
|
|
159
|
+
) => void;
|
|
160
|
+
__FA_logout?: (resetAnonymousIdentity?: boolean) => void;
|
|
161
|
+
__FA_setConsentMode?: (mode: ConsentMode) => void;
|
|
162
|
+
__FA_optIn?: () => void;
|
|
163
|
+
__FA_optOut?: () => void;
|
|
164
|
+
__FA_isTrackingDisabled?: () => boolean;
|
|
165
|
+
__FA_webAnalyticsInstance?: WebAnalytics;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export class WebAnalytics {
|
|
170
|
+
private readonly endpoint: string;
|
|
171
|
+
private readonly debug: boolean;
|
|
172
|
+
private readonly baseUrl: string;
|
|
173
|
+
private started = false;
|
|
174
|
+
private pageKey = "";
|
|
175
|
+
private navTimer: ReturnType<typeof setTimeout> | null = null;
|
|
176
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
177
|
+
private scrollDepth = 0;
|
|
178
|
+
private pageEntryTime = 0;
|
|
179
|
+
private pagePath = "";
|
|
180
|
+
private pageUrl = "";
|
|
181
|
+
private pageHash = "";
|
|
182
|
+
private hasLeftCurrentPage = false;
|
|
183
|
+
private scrollHandler: (() => void) | null = null;
|
|
184
|
+
private consentMode: ConsentMode;
|
|
185
|
+
private readonly cookielessWhilePending: boolean;
|
|
186
|
+
|
|
187
|
+
constructor(private readonly options: WebAnalyticsOptions) {
|
|
188
|
+
this.endpoint = options.endpoint ?? "https://metrics.faststats.dev/v1/web";
|
|
189
|
+
this.debug = options.debug ?? false;
|
|
190
|
+
this.consentMode = options.consent?.mode ?? "granted";
|
|
191
|
+
this.cookielessWhilePending =
|
|
192
|
+
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
|
+
if (options.autoTrack ?? true) this.init();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private log(msg: string): void {
|
|
207
|
+
if (this.debug) console.log(`[Analytics] ${msg}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private init(): void {
|
|
211
|
+
if (typeof window === "undefined") return;
|
|
212
|
+
if (isTrackingDisabled()) {
|
|
213
|
+
this.log("disabled");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if ("requestIdleCallback" in window)
|
|
217
|
+
window.requestIdleCallback(() => this.start());
|
|
218
|
+
else setTimeout(() => this.start(), 1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
start(): void {
|
|
222
|
+
if (this.started || typeof window === "undefined") return;
|
|
223
|
+
if (
|
|
224
|
+
window.__FA_webAnalyticsInstance &&
|
|
225
|
+
window.__FA_webAnalyticsInstance !== this
|
|
226
|
+
) {
|
|
227
|
+
this.log("already started by another instance");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (isTrackingDisabled()) {
|
|
231
|
+
this.log("disabled");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.started = true;
|
|
235
|
+
window.__FA_webAnalyticsInstance = this;
|
|
236
|
+
|
|
237
|
+
window.__FA_getAnonymousId = () => getAnonymousId(this.isCookielessMode());
|
|
238
|
+
window.__FA_getSessionId = getOrCreateSessionId;
|
|
239
|
+
window.__FA_sendData = sendData;
|
|
240
|
+
window.__FA_identify = (externalId, email, options) =>
|
|
241
|
+
this.identify(externalId, email, options);
|
|
242
|
+
window.__FA_logout = (resetAnonymousIdentity) =>
|
|
243
|
+
this.logout(resetAnonymousIdentity);
|
|
244
|
+
window.__FA_setConsentMode = (mode) => this.setConsentMode(mode);
|
|
245
|
+
window.__FA_optIn = () => this.optIn();
|
|
246
|
+
window.__FA_optOut = () => this.optOut();
|
|
247
|
+
window.__FA_isTrackingDisabled = isTrackingDisabled;
|
|
248
|
+
|
|
249
|
+
const opts = this.options;
|
|
250
|
+
const hasWebVitalsSampling =
|
|
251
|
+
opts.webVitals?.sampling?.percentage !== undefined;
|
|
252
|
+
const hasReplaySampling =
|
|
253
|
+
opts.replayOptions?.samplingPercentage !== undefined ||
|
|
254
|
+
opts.sessionReplays?.sampling?.percentage !== undefined;
|
|
255
|
+
|
|
256
|
+
if (opts.errorTracking?.enabled ?? opts.trackErrors) {
|
|
257
|
+
this.load("error", "__FA_ErrorTracker", {
|
|
258
|
+
siteKey: opts.siteKey,
|
|
259
|
+
endpoint: this.endpoint,
|
|
260
|
+
debug: this.debug,
|
|
261
|
+
getCommonData: () => ({
|
|
262
|
+
url: location.href,
|
|
263
|
+
page: location.pathname,
|
|
264
|
+
referrer: document.referrer || null,
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (
|
|
269
|
+
opts.webVitals?.enabled ??
|
|
270
|
+
opts.trackWebVitals ??
|
|
271
|
+
hasWebVitalsSampling
|
|
272
|
+
) {
|
|
273
|
+
this.load("web-vitals", "__FA_WebVitalsTracker", {
|
|
274
|
+
siteKey: opts.siteKey,
|
|
275
|
+
endpoint: this.endpoint,
|
|
276
|
+
debug: this.debug,
|
|
277
|
+
samplingPercentage: normalizeSamplingPercentage(
|
|
278
|
+
opts.webVitals?.sampling?.percentage,
|
|
279
|
+
),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
|
|
283
|
+
const replayOpts = opts.replayOptions ?? {};
|
|
284
|
+
this.load("replay", "__FA_ReplayTracker", {
|
|
285
|
+
siteKey: opts.siteKey,
|
|
286
|
+
endpoint: this.endpoint,
|
|
287
|
+
debug: this.debug,
|
|
288
|
+
...replayOpts,
|
|
289
|
+
samplingPercentage: normalizeSamplingPercentage(
|
|
290
|
+
replayOpts.samplingPercentage ??
|
|
291
|
+
opts.sessionReplays?.sampling?.percentage,
|
|
292
|
+
),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.enterPage();
|
|
297
|
+
this.pageview({ trigger: "load" });
|
|
298
|
+
this.links();
|
|
299
|
+
this.trackScroll();
|
|
300
|
+
this.startHeartbeat();
|
|
301
|
+
|
|
302
|
+
document.addEventListener("visibilitychange", () => {
|
|
303
|
+
if (document.visibilityState === "hidden") this.leavePage();
|
|
304
|
+
else this.startHeartbeat();
|
|
305
|
+
});
|
|
306
|
+
window.addEventListener("pagehide", () => this.leavePage());
|
|
307
|
+
window.addEventListener("popstate", () => this.navigate());
|
|
308
|
+
if (opts.trackHash)
|
|
309
|
+
window.addEventListener("hashchange", () => this.navigate());
|
|
310
|
+
this.patch();
|
|
311
|
+
}
|
|
312
|
+
|
|
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
|
+
pageview(extra: Dict = {}): void {
|
|
339
|
+
const key = `${location.pathname}|${(this.options.trackHash ?? false) ? location.hash : ""}`;
|
|
340
|
+
if (key === this.pageKey) return;
|
|
341
|
+
this.pageKey = key;
|
|
342
|
+
this.send("pageview", extra);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
track(name: string, extra: Dict = {}): void {
|
|
346
|
+
this.send(name, extra);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
identify(
|
|
350
|
+
externalId: string,
|
|
351
|
+
email: string,
|
|
352
|
+
options: IdentifyOptions = {},
|
|
353
|
+
): void {
|
|
354
|
+
if (isTrackingDisabled()) return;
|
|
355
|
+
if (this.isCookielessMode()) return;
|
|
356
|
+
|
|
357
|
+
const trimmedExternalId = externalId.trim();
|
|
358
|
+
const trimmedEmail = email.trim();
|
|
359
|
+
if (!trimmedExternalId || !trimmedEmail) return;
|
|
360
|
+
|
|
361
|
+
const identifyEndpoint = this.endpoint.replace(
|
|
362
|
+
/\/v1\/web$/,
|
|
363
|
+
"/v1/identify",
|
|
364
|
+
);
|
|
365
|
+
const payload = JSON.stringify({
|
|
366
|
+
token: this.options.siteKey,
|
|
367
|
+
identifier: getAnonymousId(false),
|
|
368
|
+
externalId: trimmedExternalId,
|
|
369
|
+
email: trimmedEmail,
|
|
370
|
+
name: options.name?.trim() || undefined,
|
|
371
|
+
phone: options.phone?.trim() || undefined,
|
|
372
|
+
avatarUrl: options.avatarUrl?.trim() || undefined,
|
|
373
|
+
traits: options.traits ?? {},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
sendData({
|
|
377
|
+
url: identifyEndpoint,
|
|
378
|
+
data: payload,
|
|
379
|
+
contentType: "text/plain",
|
|
380
|
+
debug: this.debug,
|
|
381
|
+
debugPrefix: "[Analytics] identify",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
logout(resetAnonymousIdentity = true): void {
|
|
386
|
+
if (isTrackingDisabled()) return;
|
|
387
|
+
if (resetAnonymousIdentity) {
|
|
388
|
+
resetAnonymousId(this.isCookielessMode());
|
|
389
|
+
}
|
|
390
|
+
resetSessionId();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
setConsentMode(mode: ConsentMode): void {
|
|
394
|
+
this.consentMode = mode;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
optIn(): void {
|
|
398
|
+
this.setConsentMode("granted");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
optOut(): void {
|
|
402
|
+
this.setConsentMode("denied");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
getConsentMode(): ConsentMode {
|
|
406
|
+
return this.consentMode;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private isCookielessMode(): boolean {
|
|
410
|
+
if (this.options.cookieless) return true;
|
|
411
|
+
if (this.consentMode === "denied") return true;
|
|
412
|
+
if (this.consentMode === "pending") return this.cookielessWhilePending;
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private send(event: string, extra: Dict = {}): void {
|
|
417
|
+
const identifier = getAnonymousId(this.isCookielessMode());
|
|
418
|
+
const payload = JSON.stringify({
|
|
419
|
+
token: this.options.siteKey,
|
|
420
|
+
...(identifier ? { userId: identifier } : {}),
|
|
421
|
+
sessionId: getOrCreateSessionId(),
|
|
422
|
+
data: {
|
|
423
|
+
event,
|
|
424
|
+
page: location.pathname,
|
|
425
|
+
referrer: document.referrer || null,
|
|
426
|
+
title: document.title || "",
|
|
427
|
+
url: location.href,
|
|
428
|
+
...getUTM(),
|
|
429
|
+
...extra,
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
this.log(event);
|
|
433
|
+
sendData({
|
|
434
|
+
url: this.endpoint,
|
|
435
|
+
data: payload,
|
|
436
|
+
contentType: "text/plain",
|
|
437
|
+
debug: this.debug,
|
|
438
|
+
debugPrefix: `[Analytics] ${event}`,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private enterPage(): void {
|
|
443
|
+
this.pageEntryTime = Date.now();
|
|
444
|
+
this.pagePath = location.pathname;
|
|
445
|
+
this.pageUrl = location.href;
|
|
446
|
+
this.pageHash = location.hash;
|
|
447
|
+
this.scrollDepth = 0;
|
|
448
|
+
this.hasLeftCurrentPage = false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private leavePage(): void {
|
|
452
|
+
if (this.hasLeftCurrentPage) return;
|
|
453
|
+
this.hasLeftCurrentPage = true;
|
|
454
|
+
const now = Date.now();
|
|
455
|
+
this.send("page_leave", {
|
|
456
|
+
page: this.pagePath,
|
|
457
|
+
url: this.pageUrl,
|
|
458
|
+
time_on_page: now - this.pageEntryTime,
|
|
459
|
+
scroll_depth: this.scrollDepth,
|
|
460
|
+
session_duration: now - getSessionStart(),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private trackScroll(): void {
|
|
465
|
+
if (this.scrollHandler) {
|
|
466
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
|
467
|
+
}
|
|
468
|
+
const update = () => {
|
|
469
|
+
const doc = document.documentElement;
|
|
470
|
+
const body = document.body;
|
|
471
|
+
const viewportH = window.innerHeight;
|
|
472
|
+
const docH = Math.max(doc.scrollHeight, body.scrollHeight);
|
|
473
|
+
if (docH <= viewportH) {
|
|
474
|
+
this.scrollDepth = 100;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const depth = Math.min(
|
|
478
|
+
100,
|
|
479
|
+
Math.round(
|
|
480
|
+
(((window.scrollY || doc.scrollTop) + viewportH) / docH) * 100,
|
|
481
|
+
),
|
|
482
|
+
);
|
|
483
|
+
if (depth > this.scrollDepth) this.scrollDepth = depth;
|
|
484
|
+
};
|
|
485
|
+
this.scrollHandler = update;
|
|
486
|
+
update();
|
|
487
|
+
window.addEventListener("scroll", update, { passive: true });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private startHeartbeat(): void {
|
|
491
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
492
|
+
this.heartbeatTimer = setInterval(
|
|
493
|
+
() => {
|
|
494
|
+
if (document.visibilityState === "hidden") {
|
|
495
|
+
clearInterval(this.heartbeatTimer as ReturnType<typeof setInterval>);
|
|
496
|
+
this.heartbeatTimer = null;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
refreshSessionTimestamp();
|
|
500
|
+
},
|
|
501
|
+
5 * 60 * 1000,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private navigate(): void {
|
|
506
|
+
if (this.navTimer) clearTimeout(this.navTimer);
|
|
507
|
+
this.navTimer = setTimeout(() => {
|
|
508
|
+
this.navTimer = null;
|
|
509
|
+
const pathChanged = location.pathname !== this.pagePath;
|
|
510
|
+
const hashChanged =
|
|
511
|
+
(this.options.trackHash ?? false) && location.hash !== this.pageHash;
|
|
512
|
+
if (!pathChanged && !hashChanged) return;
|
|
513
|
+
this.leavePage();
|
|
514
|
+
this.enterPage();
|
|
515
|
+
this.trackScroll();
|
|
516
|
+
this.pageview({ trigger: "navigation" });
|
|
517
|
+
}, 300);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private patch(): void {
|
|
521
|
+
const fire = () => this.navigate();
|
|
522
|
+
for (const method of ["pushState", "replaceState"] as const) {
|
|
523
|
+
const orig = history[method];
|
|
524
|
+
history[method] = function (
|
|
525
|
+
this: History,
|
|
526
|
+
...args: Parameters<typeof orig>
|
|
527
|
+
) {
|
|
528
|
+
const result = orig.apply(this, args);
|
|
529
|
+
fire();
|
|
530
|
+
return result;
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private links(): void {
|
|
536
|
+
const handler = (event: MouseEvent) => {
|
|
537
|
+
const link = getLinkEl(event.target as Node);
|
|
538
|
+
if (link && link.host !== location.host)
|
|
539
|
+
this.track("outbound_link", { outbound_link: link.href });
|
|
540
|
+
};
|
|
541
|
+
document.addEventListener("click", handler);
|
|
542
|
+
document.addEventListener("auxclick", handler);
|
|
543
|
+
}
|
|
544
|
+
}
|