@faststats/web 0.1.2 → 0.1.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 +8 -0
- package/dist/analytics-D0RCcMlv.js +1 -0
- package/dist/error-BNd3BvFI.js +2 -0
- package/dist/module.d.ts +34 -18
- package/dist/module.js +1 -1
- package/dist/replay-D9AlW2Jv.js +1 -0
- package/dist/types-8FXsUqbi.js +1 -0
- package/dist/web-vitals-Bj8FoiTR.js +1 -0
- package/package.json +5 -2
- package/src/analytics.ts +303 -129
- package/src/error.ts +120 -116
- package/src/module.ts +1 -0
- package/src/replay.ts +12 -7
- package/src/utils/api-urls.ts +26 -0
- package/src/web-vitals.ts +25 -20
- package/tests/analytics.test.ts +311 -0
- package/tsdown.config.ts +1 -1
- package/dist/analytics-p8pLmlA-.js +0 -1
- package/dist/error-CMM8PYcW.js +0 -3
- package/dist/replay-DeRZXYG3.js +0 -1
- package/dist/types-B5zbhOWK.js +0 -1
- package/dist/web-vitals-DU_i1Ejx.js +0 -1
package/src/analytics.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import type ErrorTracker from "./error";
|
|
2
|
+
import type ReplayTracker from "./replay";
|
|
1
3
|
import type { ReplayTrackerOptions } from "./replay";
|
|
4
|
+
import {
|
|
5
|
+
identifyEventsUrl,
|
|
6
|
+
normalizeAnalyticsBaseUrl,
|
|
7
|
+
webEventsUrl,
|
|
8
|
+
} from "./utils/api-urls";
|
|
2
9
|
import {
|
|
3
10
|
getAnonymousId,
|
|
4
11
|
getOrCreateSessionId,
|
|
@@ -12,9 +19,59 @@ import {
|
|
|
12
19
|
normalizeSamplingPercentage,
|
|
13
20
|
type SendDataOptions,
|
|
14
21
|
} from "./utils/types";
|
|
22
|
+
import type WebVitalsTracker from "./web-vitals";
|
|
15
23
|
|
|
16
24
|
export type { SendDataOptions };
|
|
17
25
|
|
|
26
|
+
type ChildTracker = {
|
|
27
|
+
start(): void;
|
|
28
|
+
stop?(): void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type Dict = Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
interface SamplingOptions {
|
|
34
|
+
percentage?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface WebVitalsConfig {
|
|
38
|
+
sampling?: SamplingOptions;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SessionReplayConfig {
|
|
42
|
+
sampling?: SamplingOptions;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ConsentMode = "pending" | "granted" | "denied";
|
|
46
|
+
|
|
47
|
+
interface ConsentConfig {
|
|
48
|
+
mode?: ConsentMode;
|
|
49
|
+
cookielessWhilePending?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface IdentifyOptions {
|
|
53
|
+
name?: string;
|
|
54
|
+
phone?: string;
|
|
55
|
+
avatarUrl?: string;
|
|
56
|
+
traits?: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WebAnalyticsOptions {
|
|
60
|
+
siteKey: string;
|
|
61
|
+
baseUrl?: string;
|
|
62
|
+
debug?: boolean;
|
|
63
|
+
autoTrack?: boolean;
|
|
64
|
+
trackHash?: boolean;
|
|
65
|
+
trackErrors?: boolean;
|
|
66
|
+
trackWebVitals?: boolean;
|
|
67
|
+
trackReplay?: boolean;
|
|
68
|
+
cookieless?: boolean;
|
|
69
|
+
consent?: ConsentConfig;
|
|
70
|
+
webVitals?: WebVitalsConfig;
|
|
71
|
+
sessionReplays?: SessionReplayConfig;
|
|
72
|
+
replayOptions?: Partial<ReplayTrackerOptions>;
|
|
73
|
+
}
|
|
74
|
+
|
|
18
75
|
let _instance: WebAnalytics | null = null;
|
|
19
76
|
|
|
20
77
|
export function getInstance(): WebAnalytics | null {
|
|
@@ -55,6 +112,11 @@ export function optOut(): void {
|
|
|
55
112
|
setConsentMode("denied");
|
|
56
113
|
}
|
|
57
114
|
|
|
115
|
+
export function reportError(error: Error): void {
|
|
116
|
+
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
117
|
+
_instance?.reportError(error);
|
|
118
|
+
}
|
|
119
|
+
|
|
58
120
|
export function isTrackingDisabled(): boolean {
|
|
59
121
|
if (typeof localStorage === "undefined") return false;
|
|
60
122
|
const value = localStorage.getItem("disable-faststats");
|
|
@@ -76,7 +138,7 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
|
|
|
76
138
|
|
|
77
139
|
if (navigator.sendBeacon?.(url, blob)) {
|
|
78
140
|
if (debug) {
|
|
79
|
-
console.log(`${debugPrefix}
|
|
141
|
+
console.log(`${debugPrefix} Sent via beacon`);
|
|
80
142
|
}
|
|
81
143
|
return true;
|
|
82
144
|
}
|
|
@@ -94,15 +156,15 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
|
|
|
94
156
|
const success = response.ok;
|
|
95
157
|
if (debug) {
|
|
96
158
|
if (success) {
|
|
97
|
-
console.log(`${debugPrefix}
|
|
159
|
+
console.log(`${debugPrefix} Sent via fetch`);
|
|
98
160
|
} else {
|
|
99
|
-
console.warn(`${debugPrefix}
|
|
161
|
+
console.warn(`${debugPrefix} Failed: ${response.status}`);
|
|
100
162
|
}
|
|
101
163
|
}
|
|
102
164
|
return success;
|
|
103
165
|
} catch {
|
|
104
166
|
if (debug) {
|
|
105
|
-
console.warn(`${debugPrefix}
|
|
167
|
+
console.warn(`${debugPrefix} Failed to send`);
|
|
106
168
|
}
|
|
107
169
|
return false;
|
|
108
170
|
}
|
|
@@ -133,59 +195,12 @@ function getUTM(): Record<string, string> {
|
|
|
133
195
|
return params;
|
|
134
196
|
}
|
|
135
197
|
|
|
136
|
-
type Dict = Record<string, unknown>;
|
|
137
|
-
|
|
138
|
-
interface SamplingOptions {
|
|
139
|
-
percentage?: number;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
interface ErrorTrackingConfig {
|
|
143
|
-
enabled?: boolean;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
interface WebVitalsConfig {
|
|
147
|
-
sampling?: SamplingOptions;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
interface SessionReplayConfig {
|
|
151
|
-
sampling?: SamplingOptions;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export type ConsentMode = "pending" | "granted" | "denied";
|
|
155
|
-
|
|
156
|
-
interface ConsentConfig {
|
|
157
|
-
mode?: ConsentMode;
|
|
158
|
-
cookielessWhilePending?: boolean;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface IdentifyOptions {
|
|
162
|
-
name?: string;
|
|
163
|
-
phone?: string;
|
|
164
|
-
avatarUrl?: string;
|
|
165
|
-
traits?: Record<string, unknown>;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export interface WebAnalyticsOptions {
|
|
169
|
-
siteKey: string;
|
|
170
|
-
endpoint?: string;
|
|
171
|
-
debug?: boolean;
|
|
172
|
-
autoTrack?: boolean;
|
|
173
|
-
trackHash?: boolean;
|
|
174
|
-
trackErrors?: boolean;
|
|
175
|
-
trackWebVitals?: boolean;
|
|
176
|
-
trackReplay?: boolean;
|
|
177
|
-
cookieless?: boolean;
|
|
178
|
-
consent?: ConsentConfig;
|
|
179
|
-
errorTracking?: ErrorTrackingConfig;
|
|
180
|
-
webVitals?: WebVitalsConfig;
|
|
181
|
-
sessionReplays?: SessionReplayConfig;
|
|
182
|
-
replayOptions?: Partial<ReplayTrackerOptions>;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
198
|
export class WebAnalytics {
|
|
186
|
-
private readonly
|
|
199
|
+
private readonly webEndpoint: string;
|
|
200
|
+
private readonly baseUrl: string;
|
|
187
201
|
private readonly debug: boolean;
|
|
188
202
|
private started = false;
|
|
203
|
+
private destroyed = false;
|
|
189
204
|
private pageKey = "";
|
|
190
205
|
private navTimer: ReturnType<typeof setTimeout> | null = null;
|
|
191
206
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -198,9 +213,30 @@ export class WebAnalytics {
|
|
|
198
213
|
private scrollHandler: (() => void) | null = null;
|
|
199
214
|
private consentMode: ConsentMode;
|
|
200
215
|
private readonly cookielessWhilePending: boolean;
|
|
216
|
+
private readonly cleanupCallbacks: Array<() => void> = [];
|
|
217
|
+
private readonly childTrackers: ChildTracker[] = [];
|
|
218
|
+
private readonly pendingReportedErrors: Error[] = [];
|
|
219
|
+
private readonly handleVisibilityChange = (): void => {
|
|
220
|
+
if (document.visibilityState === "hidden") {
|
|
221
|
+
this.leavePage();
|
|
222
|
+
this.stopHeartbeat();
|
|
223
|
+
} else {
|
|
224
|
+
this.startHeartbeat();
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
private readonly handlePageHide = (): void => {
|
|
228
|
+
this.leavePage();
|
|
229
|
+
};
|
|
230
|
+
private readonly handlePopState = (): void => {
|
|
231
|
+
this.navigate();
|
|
232
|
+
};
|
|
233
|
+
private readonly handleHashChange = (): void => {
|
|
234
|
+
this.navigate();
|
|
235
|
+
};
|
|
201
236
|
|
|
202
237
|
constructor(private readonly options: WebAnalyticsOptions) {
|
|
203
|
-
this.
|
|
238
|
+
this.baseUrl = normalizeAnalyticsBaseUrl(options.baseUrl);
|
|
239
|
+
this.webEndpoint = webEventsUrl(this.baseUrl);
|
|
204
240
|
this.debug = options.debug ?? false;
|
|
205
241
|
this.consentMode = options.consent?.mode ?? "granted";
|
|
206
242
|
this.cookielessWhilePending =
|
|
@@ -219,11 +255,92 @@ export class WebAnalytics {
|
|
|
219
255
|
return;
|
|
220
256
|
}
|
|
221
257
|
_instance = this;
|
|
258
|
+
setCookielessMode(this.isCookielessMode());
|
|
222
259
|
setTimeout(() => void this.start(), 0);
|
|
223
260
|
}
|
|
224
261
|
|
|
262
|
+
private registerCleanup(cleanup: () => void): void {
|
|
263
|
+
this.cleanupCallbacks.push(cleanup);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private addWindowListener<K extends keyof WindowEventMap>(
|
|
267
|
+
type: K,
|
|
268
|
+
listener: (event: WindowEventMap[K]) => void,
|
|
269
|
+
): void {
|
|
270
|
+
window.addEventListener(type, listener);
|
|
271
|
+
this.registerCleanup(() => window.removeEventListener(type, listener));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private addDocumentListener<K extends keyof DocumentEventMap>(
|
|
275
|
+
type: K,
|
|
276
|
+
listener: (event: DocumentEventMap[K]) => void,
|
|
277
|
+
): void {
|
|
278
|
+
document.addEventListener(type, listener);
|
|
279
|
+
this.registerCleanup(() => document.removeEventListener(type, listener));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private patchHistory(): void {
|
|
283
|
+
const originalPushState = history.pushState.bind(history);
|
|
284
|
+
const originalReplaceState = history.replaceState.bind(history);
|
|
285
|
+
|
|
286
|
+
history.pushState = (data, unused, url) => {
|
|
287
|
+
originalPushState(data, unused, url);
|
|
288
|
+
this.navigate();
|
|
289
|
+
};
|
|
290
|
+
history.replaceState = (data, unused, url) => {
|
|
291
|
+
originalReplaceState(data, unused, url);
|
|
292
|
+
this.navigate();
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
this.registerCleanup(() => {
|
|
296
|
+
history.pushState = originalPushState;
|
|
297
|
+
history.replaceState = originalReplaceState;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private ensureStarted(): boolean {
|
|
302
|
+
if (
|
|
303
|
+
typeof window === "undefined" ||
|
|
304
|
+
this.destroyed ||
|
|
305
|
+
isTrackingDisabled()
|
|
306
|
+
) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
if (!this.started) {
|
|
310
|
+
void this.start();
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private stopHeartbeat(): void {
|
|
316
|
+
if (this.heartbeatTimer) {
|
|
317
|
+
clearInterval(this.heartbeatTimer);
|
|
318
|
+
this.heartbeatTimer = null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private stopNavigationTimer(): void {
|
|
323
|
+
if (this.navTimer) {
|
|
324
|
+
clearTimeout(this.navTimer);
|
|
325
|
+
this.navTimer = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private stopChildTrackers(): void {
|
|
330
|
+
for (const tracker of this.childTrackers.splice(0)) {
|
|
331
|
+
tracker.stop?.();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private getErrorTracker(): ErrorTracker | null {
|
|
336
|
+
for (const t of this.childTrackers) {
|
|
337
|
+
if ("captureError" in t) return t as ErrorTracker;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
225
342
|
async start(): Promise<void> {
|
|
226
|
-
if (this.started || typeof window === "undefined") return;
|
|
343
|
+
if (this.started || this.destroyed || typeof window === "undefined") return;
|
|
227
344
|
if (_instance && _instance !== this) {
|
|
228
345
|
this.log("already started by another instance");
|
|
229
346
|
return;
|
|
@@ -232,42 +349,58 @@ export class WebAnalytics {
|
|
|
232
349
|
this.log("disabled");
|
|
233
350
|
return;
|
|
234
351
|
}
|
|
352
|
+
|
|
235
353
|
this.started = true;
|
|
236
354
|
_instance = this;
|
|
237
|
-
|
|
238
355
|
setCookielessMode(this.isCookielessMode());
|
|
239
|
-
const opts = this.options;
|
|
240
356
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
357
|
+
const opts = this.options;
|
|
358
|
+
try {
|
|
359
|
+
if (opts.trackErrors) {
|
|
360
|
+
const { default: ErrorTrackerClass } = await import("./error");
|
|
361
|
+
const errorTracker: ErrorTracker = new ErrorTrackerClass({
|
|
362
|
+
siteKey: opts.siteKey,
|
|
363
|
+
baseUrl: this.baseUrl,
|
|
364
|
+
debug: this.debug,
|
|
365
|
+
});
|
|
366
|
+
errorTracker.start();
|
|
367
|
+
this.childTrackers.push(errorTracker);
|
|
368
|
+
while (this.pendingReportedErrors.length > 0) {
|
|
369
|
+
const pending = this.pendingReportedErrors.shift();
|
|
370
|
+
if (pending) errorTracker.captureError(pending);
|
|
371
|
+
}
|
|
372
|
+
this.log("error loaded");
|
|
373
|
+
}
|
|
374
|
+
if (opts.trackWebVitals) {
|
|
375
|
+
const { default: WebVitalsTrackerClass } = await import("./web-vitals");
|
|
376
|
+
const webVitalsTracker: WebVitalsTracker = new WebVitalsTrackerClass({
|
|
377
|
+
siteKey: opts.siteKey,
|
|
378
|
+
baseUrl: this.baseUrl,
|
|
379
|
+
debug: this.debug,
|
|
380
|
+
samplingPercentage: normalizeSamplingPercentage(
|
|
381
|
+
opts.webVitals?.sampling?.percentage,
|
|
382
|
+
),
|
|
383
|
+
});
|
|
384
|
+
webVitalsTracker.start();
|
|
385
|
+
this.childTrackers.push(webVitalsTracker);
|
|
386
|
+
this.log("web-vitals loaded");
|
|
387
|
+
}
|
|
388
|
+
if (opts.trackReplay) {
|
|
389
|
+
const { default: ReplayTrackerClass } = await import("./replay");
|
|
390
|
+
const replayTracker: ReplayTracker = new ReplayTrackerClass({
|
|
391
|
+
siteKey: opts.siteKey,
|
|
392
|
+
baseUrl: this.baseUrl,
|
|
393
|
+
debug: this.debug,
|
|
394
|
+
...opts.replayOptions,
|
|
395
|
+
});
|
|
396
|
+
replayTracker.start();
|
|
397
|
+
this.childTrackers.push(replayTracker);
|
|
398
|
+
this.log("replay loaded");
|
|
399
|
+
}
|
|
400
|
+
} catch (error) {
|
|
401
|
+
this.log(`failed to initialize trackers: ${String(error)}`);
|
|
402
|
+
this.destroy();
|
|
403
|
+
return;
|
|
271
404
|
}
|
|
272
405
|
|
|
273
406
|
this.enterPage();
|
|
@@ -276,18 +409,46 @@ export class WebAnalytics {
|
|
|
276
409
|
this.trackScroll();
|
|
277
410
|
this.startHeartbeat();
|
|
278
411
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
412
|
+
this.addDocumentListener("visibilitychange", this.handleVisibilityChange);
|
|
413
|
+
this.addWindowListener("pagehide", this.handlePageHide);
|
|
414
|
+
this.addWindowListener("popstate", this.handlePopState);
|
|
415
|
+
if (opts.trackHash) {
|
|
416
|
+
this.addWindowListener("hashchange", this.handleHashChange);
|
|
417
|
+
}
|
|
418
|
+
this.patchHistory();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
destroy(): void {
|
|
422
|
+
if (this.destroyed) return;
|
|
423
|
+
if (this.started && typeof window !== "undefined") {
|
|
424
|
+
this.leavePage();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
this.pendingReportedErrors.length = 0;
|
|
428
|
+
|
|
429
|
+
this.stopNavigationTimer();
|
|
430
|
+
this.stopHeartbeat();
|
|
431
|
+
|
|
432
|
+
if (this.scrollHandler && typeof window !== "undefined") {
|
|
433
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
|
434
|
+
this.scrollHandler = null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
while (this.cleanupCallbacks.length > 0) {
|
|
438
|
+
const cleanup = this.cleanupCallbacks.pop();
|
|
439
|
+
cleanup?.();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.stopChildTrackers();
|
|
443
|
+
if (_instance === this) {
|
|
444
|
+
_instance = null;
|
|
445
|
+
}
|
|
446
|
+
this.started = false;
|
|
447
|
+
this.destroyed = true;
|
|
288
448
|
}
|
|
289
449
|
|
|
290
450
|
pageview(extra: Dict = {}): void {
|
|
451
|
+
if (!this.ensureStarted()) return;
|
|
291
452
|
const key = `${location.pathname}|${(this.options.trackHash ?? false) ? location.hash : ""}`;
|
|
292
453
|
if (key === this.pageKey) return;
|
|
293
454
|
this.pageKey = key;
|
|
@@ -295,6 +456,7 @@ export class WebAnalytics {
|
|
|
295
456
|
}
|
|
296
457
|
|
|
297
458
|
track(name: string, extra: Dict = {}): void {
|
|
459
|
+
if (!this.ensureStarted()) return;
|
|
298
460
|
this.send(name, extra);
|
|
299
461
|
}
|
|
300
462
|
|
|
@@ -303,17 +465,14 @@ export class WebAnalytics {
|
|
|
303
465
|
email: string,
|
|
304
466
|
options: IdentifyOptions = {},
|
|
305
467
|
): void {
|
|
306
|
-
if (
|
|
468
|
+
if (!this.ensureStarted()) return;
|
|
307
469
|
if (this.isCookielessMode()) return;
|
|
308
470
|
|
|
309
471
|
const trimmedExternalId = externalId.trim();
|
|
310
472
|
const trimmedEmail = email.trim();
|
|
311
473
|
if (!trimmedExternalId || !trimmedEmail) return;
|
|
312
474
|
|
|
313
|
-
const identifyEndpoint = this.
|
|
314
|
-
/\/v1\/web$/,
|
|
315
|
-
"/v1/identify",
|
|
316
|
-
);
|
|
475
|
+
const identifyEndpoint = identifyEventsUrl(this.baseUrl);
|
|
317
476
|
const payload = JSON.stringify({
|
|
318
477
|
token: this.options.siteKey,
|
|
319
478
|
identifier: getAnonymousId(false),
|
|
@@ -325,7 +484,7 @@ export class WebAnalytics {
|
|
|
325
484
|
traits: options.traits ?? {},
|
|
326
485
|
});
|
|
327
486
|
|
|
328
|
-
sendData({
|
|
487
|
+
void sendData({
|
|
329
488
|
url: identifyEndpoint,
|
|
330
489
|
data: payload,
|
|
331
490
|
contentType: "text/plain",
|
|
@@ -335,7 +494,7 @@ export class WebAnalytics {
|
|
|
335
494
|
}
|
|
336
495
|
|
|
337
496
|
logout(resetAnonymousIdentity = true): void {
|
|
338
|
-
if (
|
|
497
|
+
if (!this.ensureStarted()) return;
|
|
339
498
|
if (resetAnonymousIdentity) {
|
|
340
499
|
resetAnonymousId(this.isCookielessMode());
|
|
341
500
|
}
|
|
@@ -367,6 +526,27 @@ export class WebAnalytics {
|
|
|
367
526
|
return getOrCreateSessionId();
|
|
368
527
|
}
|
|
369
528
|
|
|
529
|
+
reportError(error: Error): void {
|
|
530
|
+
if (
|
|
531
|
+
this.destroyed ||
|
|
532
|
+
typeof window === "undefined" ||
|
|
533
|
+
isTrackingDisabled()
|
|
534
|
+
) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (!(this.options.trackErrors ?? false)) return;
|
|
538
|
+
if (!this.ensureStarted()) return;
|
|
539
|
+
const tracker = this.getErrorTracker();
|
|
540
|
+
if (tracker) {
|
|
541
|
+
tracker.captureError(error);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (this.pendingReportedErrors.length >= 50) {
|
|
545
|
+
this.pendingReportedErrors.shift();
|
|
546
|
+
}
|
|
547
|
+
this.pendingReportedErrors.push(error);
|
|
548
|
+
}
|
|
549
|
+
|
|
370
550
|
private isCookielessMode(): boolean {
|
|
371
551
|
if (this.options.cookieless) return true;
|
|
372
552
|
if (this.consentMode === "denied") return true;
|
|
@@ -375,6 +555,14 @@ export class WebAnalytics {
|
|
|
375
555
|
}
|
|
376
556
|
|
|
377
557
|
private send(event: string, extra: Dict = {}): void {
|
|
558
|
+
if (
|
|
559
|
+
typeof window === "undefined" ||
|
|
560
|
+
this.destroyed ||
|
|
561
|
+
isTrackingDisabled()
|
|
562
|
+
) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
378
566
|
const identifier = getAnonymousId(this.isCookielessMode());
|
|
379
567
|
const payload = JSON.stringify({
|
|
380
568
|
token: this.options.siteKey,
|
|
@@ -391,8 +579,8 @@ export class WebAnalytics {
|
|
|
391
579
|
},
|
|
392
580
|
});
|
|
393
581
|
this.log(event);
|
|
394
|
-
sendData({
|
|
395
|
-
url: this.
|
|
582
|
+
void sendData({
|
|
583
|
+
url: this.webEndpoint,
|
|
396
584
|
data: payload,
|
|
397
585
|
contentType: "text/plain",
|
|
398
586
|
debug: this.debug,
|
|
@@ -410,7 +598,7 @@ export class WebAnalytics {
|
|
|
410
598
|
}
|
|
411
599
|
|
|
412
600
|
private leavePage(): void {
|
|
413
|
-
if (this.hasLeftCurrentPage) return;
|
|
601
|
+
if (this.destroyed || this.hasLeftCurrentPage) return;
|
|
414
602
|
this.hasLeftCurrentPage = true;
|
|
415
603
|
const now = Date.now();
|
|
416
604
|
this.send("page_leave", {
|
|
@@ -449,12 +637,11 @@ export class WebAnalytics {
|
|
|
449
637
|
}
|
|
450
638
|
|
|
451
639
|
private startHeartbeat(): void {
|
|
452
|
-
|
|
640
|
+
this.stopHeartbeat();
|
|
453
641
|
this.heartbeatTimer = setInterval(
|
|
454
642
|
() => {
|
|
455
643
|
if (document.visibilityState === "hidden") {
|
|
456
|
-
|
|
457
|
-
this.heartbeatTimer = null;
|
|
644
|
+
this.stopHeartbeat();
|
|
458
645
|
return;
|
|
459
646
|
}
|
|
460
647
|
refreshSessionTimestamp();
|
|
@@ -464,7 +651,8 @@ export class WebAnalytics {
|
|
|
464
651
|
}
|
|
465
652
|
|
|
466
653
|
private navigate(): void {
|
|
467
|
-
if (this.
|
|
654
|
+
if (!this.started || this.destroyed) return;
|
|
655
|
+
this.stopNavigationTimer();
|
|
468
656
|
this.navTimer = setTimeout(() => {
|
|
469
657
|
this.navTimer = null;
|
|
470
658
|
const pathChanged = location.pathname !== this.pagePath;
|
|
@@ -478,28 +666,14 @@ export class WebAnalytics {
|
|
|
478
666
|
}, 300);
|
|
479
667
|
}
|
|
480
668
|
|
|
481
|
-
private patch(): void {
|
|
482
|
-
const fire = () => this.navigate();
|
|
483
|
-
for (const method of ["pushState", "replaceState"] as const) {
|
|
484
|
-
const orig = history[method];
|
|
485
|
-
history[method] = function (
|
|
486
|
-
this: History,
|
|
487
|
-
...args: Parameters<typeof orig>
|
|
488
|
-
) {
|
|
489
|
-
const result = orig.apply(this, args);
|
|
490
|
-
fire();
|
|
491
|
-
return result;
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
669
|
private links(): void {
|
|
497
670
|
const handler = (event: MouseEvent) => {
|
|
498
671
|
const link = getLinkEl(event.target as Node);
|
|
499
|
-
if (link && link.host !== location.host)
|
|
672
|
+
if (link && link.host !== location.host) {
|
|
500
673
|
this.track("outbound_link", { outbound_link: link.href });
|
|
674
|
+
}
|
|
501
675
|
};
|
|
502
|
-
|
|
503
|
-
|
|
676
|
+
this.addDocumentListener("click", handler);
|
|
677
|
+
this.addDocumentListener("auxclick", handler);
|
|
504
678
|
}
|
|
505
679
|
}
|