@statsig/web-analytics 3.20.2 → 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 +3 -3
- package/src/AutoCapture.d.ts +1 -0
- package/src/AutoCapture.js +3 -0
- package/src/AutoCaptureEvent.d.ts +1 -0
- package/src/AutoCaptureEvent.js +1 -0
- package/src/DeadClickManager.d.ts +34 -0
- package/src/DeadClickManager.js +162 -0
- package/src/utils/commonUtils.d.ts +2 -0
- package/src/utils/commonUtils.js +22 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statsig/web-analytics",
|
|
3
|
-
"version": "3.20.
|
|
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.
|
|
13
|
-
"@statsig/js-client": "3.20.
|
|
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",
|
package/src/AutoCapture.d.ts
CHANGED
|
@@ -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;
|
package/src/AutoCapture.js
CHANGED
|
@@ -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 & {
|
package/src/AutoCaptureEvent.js
CHANGED
|
@@ -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 {};
|
package/src/utils/commonUtils.js
CHANGED
|
@@ -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
|
}
|
|
@@ -133,3 +143,14 @@ function _getAnchorNodeInHierarchy(node) {
|
|
|
133
143
|
return null;
|
|
134
144
|
}
|
|
135
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;
|