@faststats/web 0.2.14 → 0.3.0
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/dist/chunks/api-urls-nKmJnyjD.js +1 -0
- package/dist/chunks/identifiers-wylBcW5K.js +1 -0
- package/dist/chunks/{replay-DvJYurEC.d.ts → replay-Dpl1P6cw.d.ts} +11 -14
- package/dist/chunks/send-data-D6WdryL7.js +1 -0
- package/dist/error.d.ts +4 -12
- package/dist/error.js +2 -1
- package/dist/feature-flags.d.ts +19 -2
- package/dist/feature-flags.js +1 -1
- package/dist/index.d.ts +14 -37
- package/dist/index.js +1 -1
- package/dist/replay.d.ts +2 -2
- package/dist/replay.js +1 -1
- package/dist/web-vitals.d.ts +4 -7
- package/dist/web-vitals.js +1 -1
- package/package.json +7 -6
- package/CHANGELOG.md +0 -184
- package/REPLAY_PAYLOAD.md +0 -64
- package/dist/chunks/api-urls-DaeYkG0_.js +0 -1
- package/dist/chunks/error-Cd9PTS5v.js +0 -2
- package/dist/chunks/feature-flags-BClx56v5.d.ts +0 -19
- package/dist/chunks/feature-flags-DSOCIZHK.js +0 -1
- package/dist/chunks/replay-rTqcjOo2.js +0 -1
- package/dist/chunks/rolldown-runtime-MP-BAFHD.js +0 -1
- package/dist/chunks/send-data-B2fYGj6v.js +0 -1
- package/dist/chunks/session-manager-Cy63ptPF.js +0 -1
- package/dist/chunks/types-CYzR5xtT.js +0 -1
- package/dist/chunks/web-vitals-Be-Cg4Po.js +0 -1
- package/scripts/check-bundle-size.mjs +0 -96
- package/src/analytics.ts +0 -787
- package/src/entries/error.ts +0 -6
- package/src/entries/feature-flags.ts +0 -5
- package/src/entries/main.ts +0 -21
- package/src/entries/replay.ts +0 -1
- package/src/entries/web-vitals.ts +0 -1
- package/src/env.d.ts +0 -2
- package/src/error.ts +0 -257
- package/src/feature-flags.ts +0 -84
- package/src/replay.ts +0 -596
- package/src/sdk.ts +0 -8
- package/src/utils/api-urls.ts +0 -24
- package/src/utils/identifiers.ts +0 -47
- package/src/utils/send-data.ts +0 -52
- package/src/utils/session-manager.ts +0 -416
- package/src/utils/types.ts +0 -15
- package/src/web-vitals.ts +0 -159
- package/tests/analytics.test.ts +0 -453
- package/tests/identifiers.test.ts +0 -78
- package/tests/replay.test.ts +0 -582
- package/tests/session-manager.test.ts +0 -161
- package/tests/web-vitals.test.ts +0 -208
- package/tsconfig.json +0 -30
- package/tsdown.config.ts +0 -27
- package/worker/index.ts +0 -22
package/src/analytics.ts
DELETED
|
@@ -1,787 +0,0 @@
|
|
|
1
|
-
import type ErrorTracker from "./error";
|
|
2
|
-
import type { FeatureFlagEvaluation } from "./feature-flags";
|
|
3
|
-
import type { ReplayTrackerOptions } from "./replay";
|
|
4
|
-
import {
|
|
5
|
-
normalizeAnalyticsBaseUrl,
|
|
6
|
-
normalizeFeatureFlagsBaseUrl,
|
|
7
|
-
URLS,
|
|
8
|
-
} from "./utils/api-urls";
|
|
9
|
-
import { getAnonymousId, resetAnonymousId } from "./utils/identifiers";
|
|
10
|
-
import { sendData } from "./utils/send-data";
|
|
11
|
-
import {
|
|
12
|
-
getOrCreateSessionId,
|
|
13
|
-
getSessionContext,
|
|
14
|
-
getSessionStart,
|
|
15
|
-
refreshSessionTimestamp,
|
|
16
|
-
resetSession,
|
|
17
|
-
setCookielessMode,
|
|
18
|
-
setDefaultSiteKey,
|
|
19
|
-
} from "./utils/session-manager";
|
|
20
|
-
import {
|
|
21
|
-
normalizeSamplingPercentage,
|
|
22
|
-
type SendDataOptions,
|
|
23
|
-
} from "./utils/types";
|
|
24
|
-
|
|
25
|
-
export type { SendDataOptions };
|
|
26
|
-
export { sendData };
|
|
27
|
-
|
|
28
|
-
type ChildTracker = {
|
|
29
|
-
start(): void;
|
|
30
|
-
stop?(): void;
|
|
31
|
-
trackPageChange?(url?: string): void;
|
|
32
|
-
};
|
|
33
|
-
type TrackerFactory<T extends ChildTracker> = () => Promise<T>;
|
|
34
|
-
|
|
35
|
-
type Dict = Record<string, unknown>;
|
|
36
|
-
|
|
37
|
-
interface SamplingOptions {
|
|
38
|
-
percentage?: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface WebVitalsConfig {
|
|
42
|
-
sampling?: SamplingOptions;
|
|
43
|
-
attribution?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface SessionReplayConfig {
|
|
47
|
-
enabled?: boolean;
|
|
48
|
-
sampling?: SamplingOptions;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export type ConsentMode = "pending" | "granted" | "denied";
|
|
52
|
-
|
|
53
|
-
interface ConsentConfig {
|
|
54
|
-
mode?: ConsentMode;
|
|
55
|
-
cookielessWhilePending?: boolean;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface IdentifyOptions {
|
|
59
|
-
name?: string;
|
|
60
|
-
phone?: string;
|
|
61
|
-
avatarUrl?: string;
|
|
62
|
-
traits?: Record<string, unknown>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface WebAnalyticsOptions {
|
|
66
|
-
siteKey: string;
|
|
67
|
-
baseUrl?: string;
|
|
68
|
-
featureFlagsBaseUrl?: string;
|
|
69
|
-
debug?: boolean;
|
|
70
|
-
autoTrack?: boolean;
|
|
71
|
-
trackHash?: boolean;
|
|
72
|
-
trackErrors?: boolean;
|
|
73
|
-
trackWebVitals?: boolean;
|
|
74
|
-
trackReplay?: boolean;
|
|
75
|
-
cookieless?: boolean;
|
|
76
|
-
consent?: ConsentConfig;
|
|
77
|
-
webVitals?: WebVitalsConfig;
|
|
78
|
-
sessionReplays?: SessionReplayConfig;
|
|
79
|
-
replayOptions?: Partial<ReplayTrackerOptions>;
|
|
80
|
-
/** @internal */
|
|
81
|
-
sdkName?: string;
|
|
82
|
-
/** @internal */
|
|
83
|
-
sdkVersion?: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function hasReplaySamplingConfig(options: WebAnalyticsOptions): boolean {
|
|
87
|
-
return (
|
|
88
|
-
options.replayOptions?.samplingPercentage !== undefined ||
|
|
89
|
-
options.sessionReplays?.sampling?.percentage !== undefined
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function resolveReplayTrackerOptions(
|
|
94
|
-
options: WebAnalyticsOptions,
|
|
95
|
-
baseUrl: string,
|
|
96
|
-
debug: boolean,
|
|
97
|
-
): ReplayTrackerOptions | null {
|
|
98
|
-
const replayEnabled =
|
|
99
|
-
options.sessionReplays?.enabled ??
|
|
100
|
-
options.trackReplay ??
|
|
101
|
-
hasReplaySamplingConfig(options);
|
|
102
|
-
|
|
103
|
-
if (!replayEnabled) return null;
|
|
104
|
-
|
|
105
|
-
const replayOptions = options.replayOptions ?? {};
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
siteKey: options.siteKey,
|
|
109
|
-
baseUrl,
|
|
110
|
-
debug,
|
|
111
|
-
...replayOptions,
|
|
112
|
-
samplingPercentage: normalizeSamplingPercentage(
|
|
113
|
-
replayOptions.samplingPercentage ??
|
|
114
|
-
options.sessionReplays?.sampling?.percentage,
|
|
115
|
-
),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const moduleState: {
|
|
120
|
-
instance: WebAnalytics | null;
|
|
121
|
-
pendingConsentMode: ConsentMode | undefined;
|
|
122
|
-
} = {
|
|
123
|
-
instance: null,
|
|
124
|
-
pendingConsentMode: undefined,
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
export function getInstance(): WebAnalytics | null {
|
|
128
|
-
return moduleState.instance;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function trackEvent(
|
|
132
|
-
eventName: string,
|
|
133
|
-
properties?: Record<string, unknown>,
|
|
134
|
-
): void {
|
|
135
|
-
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
136
|
-
moduleState.instance?.track(eventName, properties ?? {});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function identify(
|
|
140
|
-
externalId: string,
|
|
141
|
-
email: string,
|
|
142
|
-
options?: IdentifyOptions,
|
|
143
|
-
): Promise<boolean> {
|
|
144
|
-
if (typeof window === "undefined" || isTrackingDisabled()) {
|
|
145
|
-
return Promise.resolve(false);
|
|
146
|
-
}
|
|
147
|
-
return (
|
|
148
|
-
moduleState.instance?.identify(externalId, email, options ?? {}) ??
|
|
149
|
-
Promise.resolve(false)
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function logout(resetAnonymousIdentity = true): void {
|
|
154
|
-
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
155
|
-
moduleState.instance?.logout(resetAnonymousIdentity);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function setConsentMode(mode: ConsentMode): void {
|
|
159
|
-
if (moduleState.instance) {
|
|
160
|
-
moduleState.instance.setConsentMode(mode);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
moduleState.pendingConsentMode = mode;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function optIn(): void {
|
|
167
|
-
setConsentMode("granted");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function optOut(): void {
|
|
171
|
-
setConsentMode("denied");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function reportError(error: Error): void {
|
|
175
|
-
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
176
|
-
moduleState.instance?.reportError(error);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function isTrackingDisabled(): boolean {
|
|
180
|
-
if (typeof localStorage === "undefined") return false;
|
|
181
|
-
const value = localStorage.getItem("disable-faststats");
|
|
182
|
-
return value === "true" || value === "1";
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function getLinkEl(el: Node | null): HTMLAnchorElement | null {
|
|
186
|
-
while (el) {
|
|
187
|
-
if (el instanceof HTMLAnchorElement && el.href) return el;
|
|
188
|
-
el = el.parentNode;
|
|
189
|
-
}
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function getUTM(): Record<string, string> {
|
|
194
|
-
const params: Record<string, string> = {};
|
|
195
|
-
if (!location.search) return params;
|
|
196
|
-
const sp = new URLSearchParams(location.search);
|
|
197
|
-
for (const k of [
|
|
198
|
-
"utm_source",
|
|
199
|
-
"utm_medium",
|
|
200
|
-
"utm_campaign",
|
|
201
|
-
"utm_term",
|
|
202
|
-
"utm_content",
|
|
203
|
-
]) {
|
|
204
|
-
const v = sp.get(k);
|
|
205
|
-
if (v) params[k] = v;
|
|
206
|
-
}
|
|
207
|
-
return params;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export class WebAnalytics {
|
|
211
|
-
private readonly webEndpoint: string;
|
|
212
|
-
private readonly baseUrl: string;
|
|
213
|
-
private readonly featureFlagsBaseUrl: string;
|
|
214
|
-
private readonly debug: boolean;
|
|
215
|
-
private started = false;
|
|
216
|
-
private destroyed = false;
|
|
217
|
-
private pageKey = "";
|
|
218
|
-
private navTimer: ReturnType<typeof setTimeout> | null = null;
|
|
219
|
-
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
220
|
-
private scrollDepth = 0;
|
|
221
|
-
private pageEntryTime = 0;
|
|
222
|
-
private pagePath = "";
|
|
223
|
-
private pageUrl = "";
|
|
224
|
-
private pageHash = "";
|
|
225
|
-
private hasLeftCurrentPage = false;
|
|
226
|
-
private scrollHandler: (() => void) | null = null;
|
|
227
|
-
private consentMode: ConsentMode;
|
|
228
|
-
private readonly cookielessWhilePending: boolean;
|
|
229
|
-
private readonly cleanupCallbacks: Array<() => void> = [];
|
|
230
|
-
private readonly childTrackers: ChildTracker[] = [];
|
|
231
|
-
private readonly pendingReportedErrors: Error[] = [];
|
|
232
|
-
private errorTracker: ErrorTracker | null = null;
|
|
233
|
-
private readonly handleVisibilityChange = (): void => {
|
|
234
|
-
if (document.visibilityState === "hidden") {
|
|
235
|
-
refreshSessionTimestamp();
|
|
236
|
-
this.leavePage();
|
|
237
|
-
this.stopHeartbeat();
|
|
238
|
-
} else {
|
|
239
|
-
refreshSessionTimestamp();
|
|
240
|
-
this.enterPage();
|
|
241
|
-
this.startHeartbeat();
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
private readonly handlePageHide = (): void => {
|
|
245
|
-
this.leavePage();
|
|
246
|
-
};
|
|
247
|
-
private readonly handlePopState = (): void => {
|
|
248
|
-
this.navigate();
|
|
249
|
-
};
|
|
250
|
-
private readonly handleHashChange = (): void => {
|
|
251
|
-
this.navigate();
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
constructor(private readonly options: WebAnalyticsOptions) {
|
|
255
|
-
this.baseUrl = normalizeAnalyticsBaseUrl(options.baseUrl);
|
|
256
|
-
this.featureFlagsBaseUrl = normalizeFeatureFlagsBaseUrl(
|
|
257
|
-
options.featureFlagsBaseUrl,
|
|
258
|
-
);
|
|
259
|
-
this.webEndpoint = `${this.baseUrl}${URLS.events}`;
|
|
260
|
-
this.debug = options.debug ?? false;
|
|
261
|
-
this.consentMode = options.consent?.mode ?? "granted";
|
|
262
|
-
this.cookielessWhilePending =
|
|
263
|
-
options.consent?.cookielessWhilePending ?? true;
|
|
264
|
-
if (moduleState.pendingConsentMode !== undefined) {
|
|
265
|
-
this.consentMode = moduleState.pendingConsentMode;
|
|
266
|
-
moduleState.pendingConsentMode = undefined;
|
|
267
|
-
}
|
|
268
|
-
if (options.autoTrack ?? true) this.init();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
private log(msg: string): void {
|
|
272
|
-
if (this.debug) console.log(`[Analytics] ${msg}`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private init(): void {
|
|
276
|
-
if (typeof window === "undefined") return;
|
|
277
|
-
if (isTrackingDisabled()) {
|
|
278
|
-
this.log("disabled");
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
moduleState.instance = this;
|
|
282
|
-
setDefaultSiteKey(this.options.siteKey);
|
|
283
|
-
setCookielessMode(this.isCookielessMode());
|
|
284
|
-
setTimeout(() => void this.start(), 0);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private registerCleanup(cleanup: () => void): void {
|
|
288
|
-
this.cleanupCallbacks.push(cleanup);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private addWindowListener<K extends keyof WindowEventMap>(
|
|
292
|
-
type: K,
|
|
293
|
-
listener: (event: WindowEventMap[K]) => void,
|
|
294
|
-
): void {
|
|
295
|
-
window.addEventListener(type, listener);
|
|
296
|
-
this.registerCleanup(() => window.removeEventListener(type, listener));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private addDocumentListener<K extends keyof DocumentEventMap>(
|
|
300
|
-
type: K,
|
|
301
|
-
listener: (event: DocumentEventMap[K]) => void,
|
|
302
|
-
): void {
|
|
303
|
-
document.addEventListener(type, listener);
|
|
304
|
-
this.registerCleanup(() => document.removeEventListener(type, listener));
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private patchHistory(): void {
|
|
308
|
-
const originalPushState = history.pushState.bind(history);
|
|
309
|
-
const originalReplaceState = history.replaceState.bind(history);
|
|
310
|
-
|
|
311
|
-
history.pushState = (data, unused, url) => {
|
|
312
|
-
originalPushState(data, unused, url);
|
|
313
|
-
this.navigate();
|
|
314
|
-
};
|
|
315
|
-
history.replaceState = (data, unused, url) => {
|
|
316
|
-
originalReplaceState(data, unused, url);
|
|
317
|
-
this.navigate();
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
this.registerCleanup(() => {
|
|
321
|
-
history.pushState = originalPushState;
|
|
322
|
-
history.replaceState = originalReplaceState;
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private ensureStarted(): boolean {
|
|
327
|
-
if (
|
|
328
|
-
typeof window === "undefined" ||
|
|
329
|
-
this.destroyed ||
|
|
330
|
-
isTrackingDisabled()
|
|
331
|
-
) {
|
|
332
|
-
return false;
|
|
333
|
-
}
|
|
334
|
-
if (!this.started) {
|
|
335
|
-
void this.start();
|
|
336
|
-
}
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private stopHeartbeat(): void {
|
|
341
|
-
if (this.heartbeatTimer) {
|
|
342
|
-
clearInterval(this.heartbeatTimer);
|
|
343
|
-
this.heartbeatTimer = null;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
private stopNavigationTimer(): void {
|
|
348
|
-
if (this.navTimer) {
|
|
349
|
-
clearTimeout(this.navTimer);
|
|
350
|
-
this.navTimer = null;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private stopChildTrackers(): void {
|
|
355
|
-
for (const tracker of this.childTrackers.splice(0)) {
|
|
356
|
-
tracker.stop?.();
|
|
357
|
-
}
|
|
358
|
-
this.errorTracker = null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private registerChildTracker(tracker: ChildTracker): boolean {
|
|
362
|
-
if (!this.started || this.destroyed || moduleState.instance !== this) {
|
|
363
|
-
tracker.stop?.();
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
this.childTrackers.push(tracker);
|
|
368
|
-
return true;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
private async startChildTracker<T extends ChildTracker>(
|
|
372
|
-
name: string,
|
|
373
|
-
createTracker: TrackerFactory<T>,
|
|
374
|
-
onStarted?: (tracker: T) => void,
|
|
375
|
-
): Promise<T | null> {
|
|
376
|
-
try {
|
|
377
|
-
const tracker = await createTracker();
|
|
378
|
-
if (!this.started || this.destroyed || moduleState.instance !== this) {
|
|
379
|
-
tracker.stop?.();
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
tracker.start();
|
|
384
|
-
if (!this.registerChildTracker(tracker)) return null;
|
|
385
|
-
|
|
386
|
-
onStarted?.(tracker);
|
|
387
|
-
this.log(`${name} loaded`);
|
|
388
|
-
return tracker;
|
|
389
|
-
} catch (error) {
|
|
390
|
-
this.log(`failed to initialize ${name} tracker: ${String(error)}`);
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
private async startErrorTracker(): Promise<void> {
|
|
396
|
-
await this.startChildTracker(
|
|
397
|
-
"error",
|
|
398
|
-
async () => {
|
|
399
|
-
const { default: ErrorTrackerClass } = await import("./error");
|
|
400
|
-
return new ErrorTrackerClass({
|
|
401
|
-
siteKey: this.options.siteKey,
|
|
402
|
-
baseUrl: this.baseUrl,
|
|
403
|
-
debug: this.debug,
|
|
404
|
-
sdkName: this.options.sdkName,
|
|
405
|
-
sdkVersion: this.options.sdkVersion,
|
|
406
|
-
});
|
|
407
|
-
},
|
|
408
|
-
(errorTracker) => {
|
|
409
|
-
this.errorTracker = errorTracker;
|
|
410
|
-
|
|
411
|
-
while (this.pendingReportedErrors.length > 0) {
|
|
412
|
-
const pending = this.pendingReportedErrors.shift();
|
|
413
|
-
if (pending) errorTracker.captureError(pending);
|
|
414
|
-
}
|
|
415
|
-
},
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private async startWebVitalsTracker(): Promise<void> {
|
|
420
|
-
await this.startChildTracker("web-vitals", async () => {
|
|
421
|
-
const { default: WebVitalsTrackerClass } = await import("./web-vitals");
|
|
422
|
-
return new WebVitalsTrackerClass({
|
|
423
|
-
siteKey: this.options.siteKey,
|
|
424
|
-
baseUrl: this.baseUrl,
|
|
425
|
-
debug: this.debug,
|
|
426
|
-
samplingPercentage: normalizeSamplingPercentage(
|
|
427
|
-
this.options.webVitals?.sampling?.percentage,
|
|
428
|
-
),
|
|
429
|
-
attribution: this.options.webVitals?.attribution ?? false,
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
private async startReplayTracker(
|
|
435
|
-
replayOptions: ReplayTrackerOptions,
|
|
436
|
-
): Promise<void> {
|
|
437
|
-
await this.startChildTracker("replay", async () => {
|
|
438
|
-
const { default: ReplayTrackerClass } = await import("./replay");
|
|
439
|
-
return new ReplayTrackerClass(replayOptions);
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async start(): Promise<void> {
|
|
444
|
-
if (this.started || this.destroyed || typeof window === "undefined") return;
|
|
445
|
-
if (moduleState.instance && moduleState.instance !== this) {
|
|
446
|
-
this.log("already started by another instance");
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (isTrackingDisabled()) {
|
|
450
|
-
this.log("disabled");
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
this.started = true;
|
|
455
|
-
moduleState.instance = this;
|
|
456
|
-
setDefaultSiteKey(this.options.siteKey);
|
|
457
|
-
setCookielessMode(this.isCookielessMode());
|
|
458
|
-
getOrCreateSessionId();
|
|
459
|
-
|
|
460
|
-
const opts = this.options;
|
|
461
|
-
const replayOptions = resolveReplayTrackerOptions(
|
|
462
|
-
opts,
|
|
463
|
-
this.baseUrl,
|
|
464
|
-
this.debug,
|
|
465
|
-
);
|
|
466
|
-
if (replayOptions) {
|
|
467
|
-
void this.startReplayTracker({
|
|
468
|
-
...replayOptions,
|
|
469
|
-
cookieless: this.isCookielessMode(),
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
if (opts.trackErrors) {
|
|
473
|
-
void this.startErrorTracker();
|
|
474
|
-
}
|
|
475
|
-
if (opts.trackWebVitals) {
|
|
476
|
-
void this.startWebVitalsTracker();
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
this.enterPage();
|
|
480
|
-
this.pageview({ trigger: "load" });
|
|
481
|
-
this.links();
|
|
482
|
-
this.trackScroll();
|
|
483
|
-
this.startHeartbeat();
|
|
484
|
-
|
|
485
|
-
this.addDocumentListener("visibilitychange", this.handleVisibilityChange);
|
|
486
|
-
this.addWindowListener("pagehide", this.handlePageHide);
|
|
487
|
-
this.addWindowListener("popstate", this.handlePopState);
|
|
488
|
-
if (opts.trackHash) {
|
|
489
|
-
this.addWindowListener("hashchange", this.handleHashChange);
|
|
490
|
-
}
|
|
491
|
-
this.patchHistory();
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
destroy(): void {
|
|
495
|
-
if (this.destroyed) return;
|
|
496
|
-
if (this.started && typeof window !== "undefined") {
|
|
497
|
-
this.leavePage();
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
this.pendingReportedErrors.length = 0;
|
|
501
|
-
|
|
502
|
-
this.stopNavigationTimer();
|
|
503
|
-
this.stopHeartbeat();
|
|
504
|
-
|
|
505
|
-
if (this.scrollHandler && typeof window !== "undefined") {
|
|
506
|
-
window.removeEventListener("scroll", this.scrollHandler);
|
|
507
|
-
this.scrollHandler = null;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
while (this.cleanupCallbacks.length > 0) {
|
|
511
|
-
const cleanup = this.cleanupCallbacks.pop();
|
|
512
|
-
cleanup?.();
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
this.stopChildTrackers();
|
|
516
|
-
if (moduleState.instance === this) {
|
|
517
|
-
moduleState.instance = null;
|
|
518
|
-
}
|
|
519
|
-
this.started = false;
|
|
520
|
-
this.destroyed = true;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
pageview(extra: Dict = {}): void {
|
|
524
|
-
if (!this.ensureStarted()) return;
|
|
525
|
-
const key = `${location.pathname}|${(this.options.trackHash ?? false) ? location.hash : ""}`;
|
|
526
|
-
if (key === this.pageKey) return;
|
|
527
|
-
this.pageKey = key;
|
|
528
|
-
this.send("pageview", extra);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
track(name: string, extra: Dict = {}): void {
|
|
532
|
-
if (!this.ensureStarted()) return;
|
|
533
|
-
this.send(name, extra);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
identify(
|
|
537
|
-
externalId: string,
|
|
538
|
-
email: string,
|
|
539
|
-
options: IdentifyOptions = {},
|
|
540
|
-
): Promise<boolean> {
|
|
541
|
-
if (!this.ensureStarted()) return Promise.resolve(false);
|
|
542
|
-
if (this.isCookielessMode()) return Promise.resolve(false);
|
|
543
|
-
|
|
544
|
-
const trimmedExternalId = externalId.trim();
|
|
545
|
-
const trimmedEmail = email.trim();
|
|
546
|
-
if (!trimmedExternalId || !trimmedEmail) return Promise.resolve(false);
|
|
547
|
-
|
|
548
|
-
const identifyEndpoint = `${this.baseUrl}${URLS.identify}`;
|
|
549
|
-
const payload = JSON.stringify({
|
|
550
|
-
token: this.options.siteKey,
|
|
551
|
-
identifier: getAnonymousId(false),
|
|
552
|
-
externalId: trimmedExternalId,
|
|
553
|
-
email: trimmedEmail,
|
|
554
|
-
name: options.name?.trim() || undefined,
|
|
555
|
-
phone: options.phone?.trim() || undefined,
|
|
556
|
-
avatarUrl: options.avatarUrl?.trim() || undefined,
|
|
557
|
-
traits: options.traits ?? {},
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
return sendData({
|
|
561
|
-
url: identifyEndpoint,
|
|
562
|
-
data: payload,
|
|
563
|
-
contentType: "text/plain",
|
|
564
|
-
debug: this.debug,
|
|
565
|
-
debugPrefix: "[Analytics] identify",
|
|
566
|
-
useBeacon: false,
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
logout(resetAnonymousIdentity = true): void {
|
|
571
|
-
if (!this.ensureStarted()) return;
|
|
572
|
-
if (resetAnonymousIdentity) {
|
|
573
|
-
resetAnonymousId(this.isCookielessMode());
|
|
574
|
-
}
|
|
575
|
-
resetSession(this.options.siteKey);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
setConsentMode(mode: ConsentMode): void {
|
|
579
|
-
this.consentMode = mode;
|
|
580
|
-
setCookielessMode(this.isCookielessMode());
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
optIn(): void {
|
|
584
|
-
this.setConsentMode("granted");
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
optOut(): void {
|
|
588
|
-
this.setConsentMode("denied");
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
getConsentMode(): ConsentMode {
|
|
592
|
-
return this.consentMode;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
getAnonymousId(): string {
|
|
596
|
-
return getAnonymousId(this.isCookielessMode());
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
getSessionId(): string {
|
|
600
|
-
return getOrCreateSessionId();
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
getWindowId(): string {
|
|
604
|
-
return getSessionContext(this.options.siteKey, this.isCookielessMode())
|
|
605
|
-
.windowId;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async checkFeatureFlag(
|
|
609
|
-
key: string,
|
|
610
|
-
attributes?: Record<string, unknown>,
|
|
611
|
-
opts?: { externalId?: string; signal?: AbortSignal },
|
|
612
|
-
): Promise<FeatureFlagEvaluation> {
|
|
613
|
-
if (typeof window === "undefined" || isTrackingDisabled()) {
|
|
614
|
-
return { value: "false" };
|
|
615
|
-
}
|
|
616
|
-
const externalId = opts?.externalId?.trim();
|
|
617
|
-
const identifier = this.getAnonymousId();
|
|
618
|
-
if (!externalId && !identifier) {
|
|
619
|
-
return { value: "false" };
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const { fetchFeatureFlagEvaluation } = await import("./feature-flags");
|
|
623
|
-
return fetchFeatureFlagEvaluation(key, {
|
|
624
|
-
baseUrl: this.featureFlagsBaseUrl,
|
|
625
|
-
projectToken: this.options.siteKey,
|
|
626
|
-
...(identifier ? { identifier } : {}),
|
|
627
|
-
...(externalId ? { externalId } : {}),
|
|
628
|
-
sessionId: this.getSessionId(),
|
|
629
|
-
attributes,
|
|
630
|
-
signal: opts?.signal,
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
reportError(error: Error): void {
|
|
635
|
-
if (
|
|
636
|
-
this.destroyed ||
|
|
637
|
-
typeof window === "undefined" ||
|
|
638
|
-
isTrackingDisabled()
|
|
639
|
-
) {
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
if (!(this.options.trackErrors ?? false)) return;
|
|
643
|
-
if (!this.ensureStarted()) return;
|
|
644
|
-
const tracker = this.errorTracker;
|
|
645
|
-
if (tracker) {
|
|
646
|
-
tracker.captureError(error);
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
if (this.pendingReportedErrors.length >= 50) {
|
|
650
|
-
this.pendingReportedErrors.shift();
|
|
651
|
-
}
|
|
652
|
-
this.pendingReportedErrors.push(error);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
private isCookielessMode(): boolean {
|
|
656
|
-
if (this.options.cookieless) return true;
|
|
657
|
-
if (this.consentMode === "denied") return true;
|
|
658
|
-
if (this.consentMode === "pending") return this.cookielessWhilePending;
|
|
659
|
-
return false;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
private send(event: string, extra: Dict = {}): void {
|
|
663
|
-
if (
|
|
664
|
-
typeof window === "undefined" ||
|
|
665
|
-
this.destroyed ||
|
|
666
|
-
isTrackingDisabled()
|
|
667
|
-
) {
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const identifier = getAnonymousId(this.isCookielessMode());
|
|
672
|
-
const payload = JSON.stringify({
|
|
673
|
-
token: this.options.siteKey,
|
|
674
|
-
...(identifier ? { userId: identifier } : {}),
|
|
675
|
-
sessionId: getOrCreateSessionId(),
|
|
676
|
-
data: {
|
|
677
|
-
event,
|
|
678
|
-
page: location.pathname,
|
|
679
|
-
referrer: document.referrer || null,
|
|
680
|
-
title: document.title || "",
|
|
681
|
-
url: location.href,
|
|
682
|
-
...getUTM(),
|
|
683
|
-
...extra,
|
|
684
|
-
},
|
|
685
|
-
});
|
|
686
|
-
this.log(event);
|
|
687
|
-
void sendData({
|
|
688
|
-
url: this.webEndpoint,
|
|
689
|
-
data: payload,
|
|
690
|
-
contentType: "text/plain",
|
|
691
|
-
debug: this.debug,
|
|
692
|
-
debugPrefix: `[Analytics] ${event}`,
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
private enterPage(): void {
|
|
697
|
-
this.pageEntryTime = Date.now();
|
|
698
|
-
this.pagePath = location.pathname;
|
|
699
|
-
this.pageUrl = location.href;
|
|
700
|
-
this.pageHash = location.hash;
|
|
701
|
-
this.scrollDepth = 0;
|
|
702
|
-
this.hasLeftCurrentPage = false;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
private leavePage(): void {
|
|
706
|
-
if (this.destroyed || this.hasLeftCurrentPage) return;
|
|
707
|
-
this.hasLeftCurrentPage = true;
|
|
708
|
-
const now = Date.now();
|
|
709
|
-
this.send("page_leave", {
|
|
710
|
-
page: this.pagePath,
|
|
711
|
-
url: this.pageUrl,
|
|
712
|
-
time_on_page: now - this.pageEntryTime,
|
|
713
|
-
scroll_depth: this.scrollDepth,
|
|
714
|
-
session_duration: now - getSessionStart(),
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
private trackScroll(): void {
|
|
719
|
-
if (this.scrollHandler) {
|
|
720
|
-
window.removeEventListener("scroll", this.scrollHandler);
|
|
721
|
-
}
|
|
722
|
-
const update = () => {
|
|
723
|
-
const doc = document.documentElement;
|
|
724
|
-
const body = document.body;
|
|
725
|
-
const viewportH = window.innerHeight;
|
|
726
|
-
const docH = Math.max(doc.scrollHeight, body.scrollHeight);
|
|
727
|
-
if (docH <= viewportH) {
|
|
728
|
-
this.scrollDepth = 100;
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
const depth = Math.min(
|
|
732
|
-
100,
|
|
733
|
-
Math.round(
|
|
734
|
-
(((window.scrollY || doc.scrollTop) + viewportH) / docH) * 100,
|
|
735
|
-
),
|
|
736
|
-
);
|
|
737
|
-
if (depth > this.scrollDepth) this.scrollDepth = depth;
|
|
738
|
-
};
|
|
739
|
-
this.scrollHandler = update;
|
|
740
|
-
update();
|
|
741
|
-
window.addEventListener("scroll", update, { passive: true });
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
private startHeartbeat(): void {
|
|
745
|
-
this.stopHeartbeat();
|
|
746
|
-
this.heartbeatTimer = setInterval(
|
|
747
|
-
() => {
|
|
748
|
-
if (document.visibilityState === "hidden") {
|
|
749
|
-
this.stopHeartbeat();
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
refreshSessionTimestamp();
|
|
753
|
-
},
|
|
754
|
-
5 * 60 * 1000,
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
private navigate(): void {
|
|
759
|
-
if (!this.started || this.destroyed) return;
|
|
760
|
-
this.stopNavigationTimer();
|
|
761
|
-
this.navTimer = setTimeout(() => {
|
|
762
|
-
this.navTimer = null;
|
|
763
|
-
const pathChanged = location.pathname !== this.pagePath;
|
|
764
|
-
const hashChanged =
|
|
765
|
-
(this.options.trackHash ?? false) && location.hash !== this.pageHash;
|
|
766
|
-
if (!pathChanged && !hashChanged) return;
|
|
767
|
-
for (const tracker of this.childTrackers) {
|
|
768
|
-
tracker.trackPageChange?.(location.href);
|
|
769
|
-
}
|
|
770
|
-
this.leavePage();
|
|
771
|
-
this.enterPage();
|
|
772
|
-
this.trackScroll();
|
|
773
|
-
this.pageview({ trigger: "navigation" });
|
|
774
|
-
}, 300);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
private links(): void {
|
|
778
|
-
const handler = (event: MouseEvent) => {
|
|
779
|
-
const link = getLinkEl(event.target as Node);
|
|
780
|
-
if (link && link.host !== location.host) {
|
|
781
|
-
this.track("outbound_link", { outbound_link: link.href });
|
|
782
|
-
}
|
|
783
|
-
};
|
|
784
|
-
this.addDocumentListener("click", handler);
|
|
785
|
-
this.addDocumentListener("auxclick", handler);
|
|
786
|
-
}
|
|
787
|
-
}
|