@statsig/web-analytics 3.20.1 → 3.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statsig/web-analytics",
3
- "version": "3.20.1",
3
+ "version": "3.20.3",
4
4
  "license": "ISC",
5
5
  "homepage": "https://github.com/statsig-io/js-client-monorepo",
6
6
  "repository": {
@@ -9,8 +9,8 @@
9
9
  "directory": "packages/web-analytics"
10
10
  },
11
11
  "dependencies": {
12
- "@statsig/client-core": "3.20.1",
13
- "@statsig/js-client": "3.20.1",
12
+ "@statsig/client-core": "3.20.3",
13
+ "@statsig/js-client": "3.20.3",
14
14
  "web-vitals": "5.0.3"
15
15
  },
16
16
  "jsdelivr": "./build/statsig-web-analytics.min.js",
@@ -21,6 +21,7 @@ export declare class AutoCapture {
21
21
  private _rageClickManager;
22
22
  private _pageViewLogged;
23
23
  private _webVitalsManager;
24
+ private _deadClickManager;
24
25
  constructor(_client: PrecomputedEvaluationsInterface, options?: AutoCaptureOptions);
25
26
  private _addEventHandlers;
26
27
  private _addPageViewTracking;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AutoCapture = exports.runStatsigAutoCapture = exports.StatsigAutoCapturePlugin = void 0;
4
4
  const client_core_1 = require("@statsig/client-core");
5
5
  const AutoCaptureEvent_1 = require("./AutoCaptureEvent");
6
+ const DeadClickManager_1 = require("./DeadClickManager");
6
7
  const EngagementManager_1 = require("./EngagementManager");
7
8
  const RageClickManager_1 = require("./RageClickManager");
8
9
  const WebVitalsManager_1 = require("./WebVitalsManager");
@@ -57,6 +58,7 @@ class AutoCapture {
57
58
  this._engagementManager = new EngagementManager_1.EngagementManager();
58
59
  this._rageClickManager = new RageClickManager_1.default();
59
60
  this._webVitalsManager = new WebVitalsManager_1.WebVitalsManager(this._enqueueAutoCapture.bind(this));
61
+ this._deadClickManager = new DeadClickManager_1.default(this._enqueueAutoCapture.bind(this));
60
62
  this._eventFilterFunc = options === null || options === void 0 ? void 0 : options.eventFilterFunc;
61
63
  const doc = (0, client_core_1._getDocumentSafe)();
62
64
  if (!(0, client_core_1._isServerEnv)()) {
@@ -136,6 +138,7 @@ class AutoCapture {
136
138
  }
137
139
  _initialize() {
138
140
  this._webVitalsManager.startTracking();
141
+ this._deadClickManager.startTracking();
139
142
  this._engagementManager.startInactivityTracking(() => this._tryLogPageViewEnd(true));
140
143
  this._addEventHandlers();
141
144
  this._addPageViewTracking();
@@ -9,6 +9,7 @@ export declare const AutoCaptureEventName: {
9
9
  readonly CLICK: "auto_capture::click";
10
10
  readonly RAGE_CLICK: "auto_capture::rage_click";
11
11
  readonly WEB_VITALS: "auto_capture::web_vitals";
12
+ readonly DEAD_CLICK: "auto_capture::dead_click";
12
13
  };
13
14
  export type AutoCaptureEventName = (typeof AutoCaptureEventName)[keyof typeof AutoCaptureEventName] & string;
14
15
  export type AutoCaptureEvent = StatsigEvent & {
@@ -11,4 +11,5 @@ exports.AutoCaptureEventName = {
11
11
  CLICK: 'auto_capture::click',
12
12
  RAGE_CLICK: 'auto_capture::rage_click',
13
13
  WEB_VITALS: 'auto_capture::web_vitals',
14
+ DEAD_CLICK: 'auto_capture::dead_click',
14
15
  };
@@ -0,0 +1,34 @@
1
+ import { AutoCaptureEventName } from './AutoCaptureEvent';
2
+ export declare const DeadClickConfig: {
3
+ CLICK_CHECK_TIMEOUT: number;
4
+ SCROLL_DELAY_MS: number;
5
+ SELECTION_CHANGE_DELAY_MS: number;
6
+ MUTATION_DELAY_MS: number;
7
+ ABSOLUTE_DEAD_CLICK_TIMEOUT: number;
8
+ };
9
+ export interface PossibleDeadClick {
10
+ timestamp: number;
11
+ eventTarget: HTMLElement;
12
+ scrollDelayMs?: number;
13
+ selectionChangeDelayMs?: number;
14
+ mutationDelayMs?: number;
15
+ absoluteDelayMs?: number;
16
+ }
17
+ export default class DeadClickManager {
18
+ private _enqueueFn;
19
+ private _lastMutationTime;
20
+ private _lastSelectionChangeTime;
21
+ private _clickCheckTimer;
22
+ private _observer;
23
+ private _clicks;
24
+ private _deadClickConfig;
25
+ constructor(_enqueueFn: (eventName: AutoCaptureEventName, value: string, metadata: Record<string, unknown>) => void);
26
+ startTracking(): void;
27
+ private _handleClick;
28
+ private _handleScroll;
29
+ private _handleSelectionChange;
30
+ private _setupMutationObserver;
31
+ private _checkForDeadClick;
32
+ private _logDeadClick;
33
+ private _updateClickDelayMs;
34
+ }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeadClickConfig = void 0;
4
+ const client_core_1 = require("@statsig/client-core");
5
+ const AutoCaptureEvent_1 = require("./AutoCaptureEvent");
6
+ const commonUtils_1 = require("./utils/commonUtils");
7
+ const eventUtils_1 = require("./utils/eventUtils");
8
+ exports.DeadClickConfig = {
9
+ CLICK_CHECK_TIMEOUT: 1000,
10
+ SCROLL_DELAY_MS: 100,
11
+ SELECTION_CHANGE_DELAY_MS: 100,
12
+ MUTATION_DELAY_MS: 2500,
13
+ ABSOLUTE_DEAD_CLICK_TIMEOUT: 2750,
14
+ };
15
+ // A dead click is a click that fires an event but produces no meaningful change within a set timeframe.
16
+ class DeadClickManager {
17
+ constructor(_enqueueFn) {
18
+ this._enqueueFn = _enqueueFn;
19
+ this._lastMutationTime = 0;
20
+ this._lastSelectionChangeTime = 0;
21
+ this._clicks = [];
22
+ this._deadClickConfig = exports.DeadClickConfig;
23
+ this._handleScroll = (0, commonUtils_1.throttle)(() => {
24
+ const scrollTime = Date.now();
25
+ this._clicks.forEach((click) => {
26
+ if (!click.scrollDelayMs) {
27
+ click.scrollDelayMs = scrollTime - click.timestamp;
28
+ }
29
+ });
30
+ }, 50);
31
+ }
32
+ startTracking() {
33
+ const win = (0, client_core_1._getWindowSafe)();
34
+ if (!win) {
35
+ return;
36
+ }
37
+ // `capture: true` - Needed to listen to scroll events on all scrollable elements, not just the window.
38
+ // docs: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture
39
+ //
40
+ // `passive: true` - Indicates the scroll handler won’t call preventDefault(),
41
+ // allowing the browser to optimize scrolling performance by not blocking it.
42
+ // docs: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#passive
43
+ win.addEventListener('click', (event) => this._handleClick(event), {
44
+ capture: true,
45
+ });
46
+ win.addEventListener('scroll', () => this._handleScroll(), {
47
+ capture: true,
48
+ passive: true,
49
+ });
50
+ win.addEventListener('selectionchange', () => this._handleSelectionChange());
51
+ this._setupMutationObserver();
52
+ }
53
+ _handleClick(event) {
54
+ var _a, _b;
55
+ const eventTarget = event.target;
56
+ if (!eventTarget) {
57
+ return;
58
+ }
59
+ const click = {
60
+ timestamp: Date.now(),
61
+ eventTarget,
62
+ };
63
+ if (!commonUtils_1.interactiveElements.includes((_a = eventTarget === null || eventTarget === void 0 ? void 0 : eventTarget.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase())) {
64
+ this._clicks.push(click);
65
+ }
66
+ if (this._clicks.length && !this._clickCheckTimer) {
67
+ this._clickCheckTimer = (_b = (0, client_core_1._getWindowSafe)()) === null || _b === void 0 ? void 0 : _b.setTimeout(() => {
68
+ this._checkForDeadClick();
69
+ }, this._deadClickConfig.CLICK_CHECK_TIMEOUT);
70
+ }
71
+ }
72
+ _handleSelectionChange() {
73
+ this._lastSelectionChangeTime = Date.now();
74
+ }
75
+ _setupMutationObserver() {
76
+ const doc = (0, client_core_1._getDocumentSafe)();
77
+ if (!doc) {
78
+ return;
79
+ }
80
+ this._observer = new MutationObserver(() => {
81
+ this._lastMutationTime = Date.now();
82
+ });
83
+ this._observer.observe(doc.body, {
84
+ childList: true,
85
+ subtree: true,
86
+ attributes: true,
87
+ characterData: true,
88
+ });
89
+ }
90
+ _checkForDeadClick() {
91
+ var _a;
92
+ if (!this._clicks.length) {
93
+ return;
94
+ }
95
+ clearTimeout(this._clickCheckTimer);
96
+ this._clickCheckTimer = undefined;
97
+ const clicksToCheck = this._clicks;
98
+ this._clicks = [];
99
+ for (const click of clicksToCheck) {
100
+ this._updateClickDelayMs(click);
101
+ const hadScroll = click.scrollDelayMs != null &&
102
+ click.scrollDelayMs < this._deadClickConfig.SCROLL_DELAY_MS;
103
+ const hadSelectionChange = click.selectionChangeDelayMs != null &&
104
+ click.selectionChangeDelayMs <
105
+ this._deadClickConfig.SELECTION_CHANGE_DELAY_MS;
106
+ const hadMutation = click.mutationDelayMs != null &&
107
+ click.mutationDelayMs < this._deadClickConfig.MUTATION_DELAY_MS;
108
+ if (hadScroll || hadSelectionChange || hadMutation) {
109
+ continue;
110
+ }
111
+ const scrollTimeout = click.scrollDelayMs != null &&
112
+ click.scrollDelayMs > this._deadClickConfig.SCROLL_DELAY_MS;
113
+ const selectionChangeTimeout = click.selectionChangeDelayMs != null &&
114
+ click.selectionChangeDelayMs >
115
+ this._deadClickConfig.SELECTION_CHANGE_DELAY_MS;
116
+ const mutationTimeout = click.mutationDelayMs != null &&
117
+ click.mutationDelayMs > this._deadClickConfig.MUTATION_DELAY_MS;
118
+ const absoluteTimeout = click.absoluteDelayMs != null &&
119
+ click.absoluteDelayMs >
120
+ this._deadClickConfig.ABSOLUTE_DEAD_CLICK_TIMEOUT;
121
+ if (scrollTimeout ||
122
+ selectionChangeTimeout ||
123
+ mutationTimeout ||
124
+ absoluteTimeout) {
125
+ this._logDeadClick(click, {
126
+ scrollTimeout,
127
+ selectionChangeTimeout,
128
+ mutationTimeout,
129
+ absoluteTimeout,
130
+ });
131
+ }
132
+ else if (click.absoluteDelayMs != null &&
133
+ click.absoluteDelayMs <
134
+ this._deadClickConfig.ABSOLUTE_DEAD_CLICK_TIMEOUT) {
135
+ this._clicks.push(click);
136
+ }
137
+ }
138
+ if (this._clicks.length && !this._clickCheckTimer) {
139
+ this._clickCheckTimer = (_a = (0, client_core_1._getWindowSafe)()) === null || _a === void 0 ? void 0 : _a.setTimeout(() => {
140
+ this._checkForDeadClick();
141
+ }, this._deadClickConfig.CLICK_CHECK_TIMEOUT);
142
+ }
143
+ }
144
+ _logDeadClick(click, extraMetadata) {
145
+ const { value, metadata } = (0, eventUtils_1._gatherEventData)(click.eventTarget);
146
+ this._enqueueFn(AutoCaptureEvent_1.AutoCaptureEventName.DEAD_CLICK, value, Object.assign(Object.assign(Object.assign({}, metadata), extraMetadata), { scrollDelayMs: click.scrollDelayMs, selectionChangeDelayMs: click.selectionChangeDelayMs, mutationDelayMs: click.mutationDelayMs, absoluteDelayMs: click.absoluteDelayMs }));
147
+ }
148
+ _updateClickDelayMs(click) {
149
+ if (!click.mutationDelayMs &&
150
+ this._lastMutationTime > 0 &&
151
+ click.timestamp <= this._lastMutationTime) {
152
+ click.mutationDelayMs = Date.now() - this._lastMutationTime;
153
+ }
154
+ if (!click.selectionChangeDelayMs &&
155
+ this._lastSelectionChangeTime > 0 &&
156
+ click.timestamp <= this._lastSelectionChangeTime) {
157
+ click.selectionChangeDelayMs = Date.now() - this._lastSelectionChangeTime;
158
+ }
159
+ click.absoluteDelayMs = Date.now() - click.timestamp;
160
+ }
161
+ }
162
+ exports.default = DeadClickManager;
@@ -4,6 +4,7 @@ interface NetworkInformation {
4
4
  rtt: number;
5
5
  saveData: boolean;
6
6
  }
7
+ export declare const interactiveElements: string[];
7
8
  export declare function _stripEmptyValues<T extends Record<string, string | number | null | undefined>>(obj: T): Partial<Record<keyof T, string | number>>;
8
9
  export declare function _getTargetNode(e: Event): Element | null;
9
10
  export declare function _shouldLogEvent(e: Event, el: Element): boolean;
@@ -15,4 +16,5 @@ export declare function _getSafeNetworkInformation(): NetworkInformation | null;
15
16
  export declare function _getSafeTimezone(): string | null;
16
17
  export declare function _getSafeTimezoneOffset(): number | null;
17
18
  export declare function _getAnchorNodeInHierarchy(node: Element | null): Element | null;
19
+ export declare function throttle<T extends (...args: unknown[]) => void>(fn: T, limit: number): T;
18
20
  export {};
@@ -1,7 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._getAnchorNodeInHierarchy = exports._getSafeTimezoneOffset = exports._getSafeTimezone = exports._getSafeNetworkInformation = exports._registerEventHandler = exports._getSanitizedPageUrl = exports._getSafeUrlString = exports._getSafeUrl = exports._shouldLogEvent = exports._getTargetNode = exports._stripEmptyValues = void 0;
3
+ exports.throttle = exports._getAnchorNodeInHierarchy = exports._getSafeTimezoneOffset = exports._getSafeTimezone = exports._getSafeNetworkInformation = exports._registerEventHandler = exports._getSanitizedPageUrl = exports._getSafeUrlString = exports._getSafeUrl = exports._shouldLogEvent = exports._getTargetNode = exports._stripEmptyValues = exports.interactiveElements = void 0;
4
4
  const client_core_1 = require("@statsig/client-core");
5
+ exports.interactiveElements = [
6
+ 'button',
7
+ 'a',
8
+ 'input',
9
+ 'select',
10
+ 'textarea',
11
+ 'form',
12
+ 'select',
13
+ 'label',
14
+ ];
5
15
  function _stripEmptyValues(obj) {
6
16
  return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value != null && value !== '' && value !== undefined));
7
17
  }
@@ -26,6 +36,10 @@ function _shouldLogEvent(e, el) {
26
36
  }
27
37
  const tagName = el.tagName.toLowerCase();
28
38
  const eventType = e.type.toLowerCase();
39
+ const classList = el.classList;
40
+ if (classList.contains('statsig-no-capture')) {
41
+ return false;
42
+ }
29
43
  switch (tagName) {
30
44
  case 'html':
31
45
  return false;
@@ -129,3 +143,14 @@ function _getAnchorNodeInHierarchy(node) {
129
143
  return null;
130
144
  }
131
145
  exports._getAnchorNodeInHierarchy = _getAnchorNodeInHierarchy;
146
+ function throttle(fn, limit) {
147
+ let lastCall = 0;
148
+ return function (...args) {
149
+ const now = Date.now();
150
+ if (now - lastCall >= limit) {
151
+ lastCall = now;
152
+ fn(...args);
153
+ }
154
+ };
155
+ }
156
+ exports.throttle = throttle;