@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.
Files changed (53) hide show
  1. package/dist/chunks/api-urls-nKmJnyjD.js +1 -0
  2. package/dist/chunks/identifiers-wylBcW5K.js +1 -0
  3. package/dist/chunks/{replay-DvJYurEC.d.ts → replay-Dpl1P6cw.d.ts} +11 -14
  4. package/dist/chunks/send-data-D6WdryL7.js +1 -0
  5. package/dist/error.d.ts +4 -12
  6. package/dist/error.js +2 -1
  7. package/dist/feature-flags.d.ts +19 -2
  8. package/dist/feature-flags.js +1 -1
  9. package/dist/index.d.ts +14 -37
  10. package/dist/index.js +1 -1
  11. package/dist/replay.d.ts +2 -2
  12. package/dist/replay.js +1 -1
  13. package/dist/web-vitals.d.ts +4 -7
  14. package/dist/web-vitals.js +1 -1
  15. package/package.json +7 -6
  16. package/CHANGELOG.md +0 -184
  17. package/REPLAY_PAYLOAD.md +0 -64
  18. package/dist/chunks/api-urls-DaeYkG0_.js +0 -1
  19. package/dist/chunks/error-Cd9PTS5v.js +0 -2
  20. package/dist/chunks/feature-flags-BClx56v5.d.ts +0 -19
  21. package/dist/chunks/feature-flags-DSOCIZHK.js +0 -1
  22. package/dist/chunks/replay-rTqcjOo2.js +0 -1
  23. package/dist/chunks/rolldown-runtime-MP-BAFHD.js +0 -1
  24. package/dist/chunks/send-data-B2fYGj6v.js +0 -1
  25. package/dist/chunks/session-manager-Cy63ptPF.js +0 -1
  26. package/dist/chunks/types-CYzR5xtT.js +0 -1
  27. package/dist/chunks/web-vitals-Be-Cg4Po.js +0 -1
  28. package/scripts/check-bundle-size.mjs +0 -96
  29. package/src/analytics.ts +0 -787
  30. package/src/entries/error.ts +0 -6
  31. package/src/entries/feature-flags.ts +0 -5
  32. package/src/entries/main.ts +0 -21
  33. package/src/entries/replay.ts +0 -1
  34. package/src/entries/web-vitals.ts +0 -1
  35. package/src/env.d.ts +0 -2
  36. package/src/error.ts +0 -257
  37. package/src/feature-flags.ts +0 -84
  38. package/src/replay.ts +0 -596
  39. package/src/sdk.ts +0 -8
  40. package/src/utils/api-urls.ts +0 -24
  41. package/src/utils/identifiers.ts +0 -47
  42. package/src/utils/send-data.ts +0 -52
  43. package/src/utils/session-manager.ts +0 -416
  44. package/src/utils/types.ts +0 -15
  45. package/src/web-vitals.ts +0 -159
  46. package/tests/analytics.test.ts +0 -453
  47. package/tests/identifiers.test.ts +0 -78
  48. package/tests/replay.test.ts +0 -582
  49. package/tests/session-manager.test.ts +0 -161
  50. package/tests/web-vitals.test.ts +0 -208
  51. package/tsconfig.json +0 -30
  52. package/tsdown.config.ts +0 -27
  53. 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
- }