@statsig/web-analytics 3.17.2 → 3.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statsig/web-analytics",
3
- "version": "3.17.2",
3
+ "version": "3.18.0",
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.17.2",
13
- "@statsig/js-client": "3.17.2"
12
+ "@statsig/client-core": "3.18.0",
13
+ "@statsig/js-client": "3.18.0"
14
14
  },
15
15
  "jsdelivr": "./build/statsig-web-analytics.min.js",
16
16
  "type": "commonjs",
@@ -13,26 +13,24 @@ export declare function runStatsigAutoCapture(client: PrecomputedEvaluationsInte
13
13
  export declare class AutoCapture {
14
14
  private _client;
15
15
  private _errorBoundary;
16
- private _startTime;
17
- private _deepestScroll;
18
16
  private _disabledEvents;
19
17
  private _previousLoggedPageViewUrl;
20
18
  private _eventFilterFunc?;
21
19
  private _hasLoggedPageViewEnd;
22
- private _inactiveTimer;
20
+ private _engagementManager;
21
+ private _rageClickManager;
23
22
  constructor(_client: PrecomputedEvaluationsInterface, options?: AutoCaptureOptions);
24
23
  private _addEventHandlers;
25
24
  private _addPageViewTracking;
26
25
  private _autoLogEvent;
27
- private _bumpInactiveTimer;
28
26
  private _initialize;
29
27
  private _logError;
30
28
  private _logSessionStart;
31
29
  private _tryLogPageView;
32
30
  private _tryLogPageViewEnd;
31
+ private _logRageClick;
33
32
  private _logPerformance;
34
33
  private _enqueueAutoCapture;
35
- private _scrollEventHandler;
36
34
  private _isNewSession;
37
35
  private _getSessionFromClient;
38
36
  }
@@ -3,9 +3,11 @@ 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 Utils_1 = require("./Utils");
7
- const payloadUtils_1 = require("./payloadUtils");
8
- const PAGE_INACTIVE_TIMEOUT = 600000;
6
+ const EngagementManager_1 = require("./EngagementManager");
7
+ const RageClickManager_1 = require("./RageClickManager");
8
+ const commonUtils_1 = require("./commonUtils");
9
+ const eventUtils_1 = require("./eventUtils");
10
+ const metadataUtils_1 = require("./metadataUtils");
9
11
  const AUTO_EVENT_MAPPING = {
10
12
  submit: AutoCaptureEvent_1.AutoCaptureEventName.FORM_SUBMIT,
11
13
  click: AutoCaptureEvent_1.AutoCaptureEventName.CLICK,
@@ -28,12 +30,9 @@ class AutoCapture {
28
30
  constructor(_client, options) {
29
31
  var _a, _b, _c;
30
32
  this._client = _client;
31
- this._startTime = Date.now();
32
- this._deepestScroll = 0;
33
33
  this._disabledEvents = {};
34
34
  this._previousLoggedPageViewUrl = null;
35
35
  this._hasLoggedPageViewEnd = false;
36
- this._inactiveTimer = null;
37
36
  const { sdkKey, errorBoundary, values } = _client.getContext();
38
37
  this._disabledEvents = (_b = (_a = values === null || values === void 0 ? void 0 : values.auto_capture_settings) === null || _a === void 0 ? void 0 : _a.disabled_events) !== null && _b !== void 0 ? _b : {};
39
38
  this._errorBoundary = errorBoundary;
@@ -44,6 +43,8 @@ class AutoCapture {
44
43
  this._disabledEvents =
45
44
  (_b = (_a = values === null || values === void 0 ? void 0 : values.auto_capture_settings) === null || _a === void 0 ? void 0 : _a.disabled_events) !== null && _b !== void 0 ? _b : this._disabledEvents;
46
45
  });
46
+ this._engagementManager = new EngagementManager_1.EngagementManager();
47
+ this._rageClickManager = new RageClickManager_1.default();
47
48
  this._eventFilterFunc = options === null || options === void 0 ? void 0 : options.eventFilterFunc;
48
49
  const doc = (0, client_core_1._getDocumentSafe)();
49
50
  if (!(0, client_core_1._isServerEnv)()) {
@@ -65,17 +66,24 @@ class AutoCapture {
65
66
  return;
66
67
  }
67
68
  const eventHandler = (event, userAction = true) => {
68
- this._autoLogEvent(event || win.event);
69
+ var _a;
70
+ const e = event || ((_a = (0, client_core_1._getWindowSafe)()) === null || _a === void 0 ? void 0 : _a.event);
71
+ this._autoLogEvent(e);
69
72
  if (userAction) {
70
- this._bumpInactiveTimer();
73
+ this._engagementManager.bumpInactiveTimer();
74
+ }
75
+ if (e.type === 'click' && e instanceof MouseEvent) {
76
+ const isRageClick = this._rageClickManager.isRageClick(e.clientX, e.clientY, Date.now());
77
+ if (isRageClick) {
78
+ this._logRageClick(e);
79
+ }
71
80
  }
72
81
  };
73
- (0, Utils_1._registerEventHandler)(doc, 'click', (e) => eventHandler(e));
74
- (0, Utils_1._registerEventHandler)(doc, 'submit', (e) => eventHandler(e));
75
- (0, Utils_1._registerEventHandler)(win, 'error', (e) => eventHandler(e, false));
76
- (0, Utils_1._registerEventHandler)(win, 'pagehide', () => this._tryLogPageViewEnd());
77
- (0, Utils_1._registerEventHandler)(win, 'beforeunload', () => this._tryLogPageViewEnd());
78
- (0, Utils_1._registerEventHandler)(win, 'scroll', () => this._scrollEventHandler());
82
+ (0, commonUtils_1._registerEventHandler)(doc, 'click', (e) => eventHandler(e));
83
+ (0, commonUtils_1._registerEventHandler)(doc, 'submit', (e) => eventHandler(e));
84
+ (0, commonUtils_1._registerEventHandler)(win, 'error', (e) => eventHandler(e, false));
85
+ (0, commonUtils_1._registerEventHandler)(win, 'pagehide', () => this._tryLogPageViewEnd());
86
+ (0, commonUtils_1._registerEventHandler)(win, 'beforeunload', () => this._tryLogPageViewEnd());
79
87
  }
80
88
  _addPageViewTracking() {
81
89
  const win = (0, client_core_1._getWindowSafe)();
@@ -83,7 +91,7 @@ class AutoCapture {
83
91
  if (!win || !doc) {
84
92
  return;
85
93
  }
86
- (0, Utils_1._registerEventHandler)(win, 'popstate', () => this._tryLogPageView());
94
+ (0, commonUtils_1._registerEventHandler)(win, 'popstate', () => this._tryLogPageView());
87
95
  window.history.pushState = new Proxy(window.history.pushState, {
88
96
  apply: (target, thisArg, [state, unused, url]) => {
89
97
  target.apply(thisArg, [state, unused, url]);
@@ -99,33 +107,23 @@ class AutoCapture {
99
107
  this._logError(event);
100
108
  return;
101
109
  }
102
- const target = (0, Utils_1._getTargetNode)(event);
110
+ const target = (0, commonUtils_1._getTargetNode)(event);
103
111
  if (!target) {
104
112
  return;
105
113
  }
106
- if (!(0, Utils_1._shouldLogEvent)(event, target)) {
114
+ if (!(0, commonUtils_1._shouldLogEvent)(event, target)) {
107
115
  return;
108
116
  }
109
117
  const eventName = AUTO_EVENT_MAPPING[eventType];
110
118
  if (!eventName) {
111
119
  return;
112
120
  }
113
- const { value, metadata } = (0, Utils_1._gatherEventData)(target);
114
- this._enqueueAutoCapture(eventName, value, metadata);
115
- }
116
- _bumpInactiveTimer() {
117
- const win = (0, client_core_1._getWindowSafe)();
118
- if (!win) {
119
- return;
120
- }
121
- if (this._inactiveTimer) {
122
- clearTimeout(this._inactiveTimer);
123
- }
124
- this._inactiveTimer = win.setTimeout(() => {
125
- this._tryLogPageViewEnd(true);
126
- }, PAGE_INACTIVE_TIMEOUT);
121
+ const { value, metadata } = (0, eventUtils_1._gatherEventData)(target);
122
+ const allMetadata = (0, metadataUtils_1._gatherAllMetadata)((0, commonUtils_1._getSafeUrl)());
123
+ this._enqueueAutoCapture(eventName, value, Object.assign(Object.assign({}, allMetadata), metadata));
127
124
  }
128
125
  _initialize() {
126
+ this._engagementManager.startInactivityTracking(() => this._tryLogPageViewEnd(true));
129
127
  this._addEventHandlers();
130
128
  this._addPageViewTracking();
131
129
  this._logSessionStart();
@@ -159,40 +157,49 @@ class AutoCapture {
159
157
  if (!this._isNewSession(session)) {
160
158
  return;
161
159
  }
162
- this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.SESSION_START, (0, Utils_1._getSanitizedPageUrl)(), { sessionID: session.data.sessionID }, { flushImmediately: true });
160
+ this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.SESSION_START, (0, commonUtils_1._getSanitizedPageUrl)(), { sessionID: session.data.sessionID }, { flushImmediately: true });
163
161
  }
164
162
  catch (err) {
165
163
  this._errorBoundary.logError('AC::logSession', err);
166
164
  }
167
165
  }
168
166
  _tryLogPageView() {
169
- const url = (0, Utils_1._getSafeUrl)();
167
+ const url = (0, commonUtils_1._getSafeUrl)();
170
168
  const last = this._previousLoggedPageViewUrl;
171
169
  if (last && url.href === last.href) {
172
170
  return;
173
171
  }
172
+ this._engagementManager.setLastPageViewTime(Date.now());
174
173
  this._previousLoggedPageViewUrl = url;
175
174
  this._hasLoggedPageViewEnd = false;
176
- const payload = (0, payloadUtils_1._gatherPageViewPayload)(url);
177
- this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PAGE_VIEW, (0, Utils_1._getSanitizedPageUrl)(), payload, {
175
+ const payload = (0, metadataUtils_1._gatherAllMetadata)(url);
176
+ this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PAGE_VIEW, (0, commonUtils_1._getSanitizedPageUrl)(), payload, {
178
177
  flushImmediately: true,
179
178
  addNewSessionMetadata: true,
180
179
  });
181
- this._bumpInactiveTimer();
180
+ this._engagementManager.bumpInactiveTimer();
182
181
  }
183
182
  _tryLogPageViewEnd(dueToInactivity = false) {
184
183
  if (this._hasLoggedPageViewEnd) {
185
184
  return;
186
185
  }
187
186
  this._hasLoggedPageViewEnd = true;
188
- this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PAGE_VIEW_END, (0, Utils_1._getSanitizedPageUrl)(), {
189
- scrollDepth: this._deepestScroll,
190
- pageViewLength: Date.now() - this._startTime,
191
- dueToInactivity,
192
- }, { flushImmediately: true });
187
+ const scrollMetrics = this._engagementManager.getScrollMetrics();
188
+ const pageViewLength = this._engagementManager.getPageViewLength();
189
+ this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PAGE_VIEW_END, (0, commonUtils_1._getSanitizedPageUrl)(), Object.assign(Object.assign({}, scrollMetrics), { pageViewLength,
190
+ dueToInactivity }), {
191
+ flushImmediately: true,
192
+ });
193
+ }
194
+ _logRageClick(e) {
195
+ const { value, metadata } = (0, eventUtils_1._gatherEventData)(e.target);
196
+ this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.RAGE_CLICK, value, Object.assign(Object.assign({ x: e.clientX, y: e.clientY, timestamp: Date.now() }, (0, metadataUtils_1._gatherAllMetadata)((0, commonUtils_1._getSafeUrl)())), metadata));
193
197
  }
194
198
  _logPerformance() {
195
199
  const win = (0, client_core_1._getWindowSafe)();
200
+ if (!win || !win.performance) {
201
+ return;
202
+ }
196
203
  if (typeof (win === null || win === void 0 ? void 0 : win.performance) === 'undefined' ||
197
204
  typeof win.performance.getEntriesByType !== 'function' ||
198
205
  typeof win.performance.getEntriesByName !== 'function') {
@@ -217,14 +224,7 @@ class AutoCapture {
217
224
  fpEntries[0] instanceof PerformancePaintTiming) {
218
225
  metadata['first_contentful_paint_time_ms'] = fpEntries[0].startTime;
219
226
  }
220
- const networkInfo = (0, Utils_1._getSafeNetworkInformation)();
221
- if (networkInfo) {
222
- metadata['effective_connection_type'] = networkInfo.effectiveType;
223
- metadata['rtt_ms'] = networkInfo.rtt;
224
- metadata['downlink_kbps'] = networkInfo.downlink;
225
- metadata['save_data'] = networkInfo.saveData;
226
- }
227
- this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PERFORMANCE, (0, Utils_1._getSanitizedPageUrl)(), metadata);
227
+ this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PERFORMANCE, (0, commonUtils_1._getSanitizedPageUrl)(), Object.assign(Object.assign({}, metadata), (0, metadataUtils_1._getNetworkInfo)()));
228
228
  }, 1);
229
229
  }
230
230
  _enqueueAutoCapture(eventName, value, metadata, options) {
@@ -258,15 +258,6 @@ class AutoCapture {
258
258
  this._errorBoundary.logError('AC::enqueue', err);
259
259
  }
260
260
  }
261
- _scrollEventHandler() {
262
- var _a, _b, _c, _d;
263
- const scrollHeight = (_b = (_a = (0, client_core_1._getDocumentSafe)()) === null || _a === void 0 ? void 0 : _a.body.scrollHeight) !== null && _b !== void 0 ? _b : 1;
264
- const win = (0, client_core_1._getWindowSafe)();
265
- const scrollY = (_c = win === null || win === void 0 ? void 0 : win.scrollY) !== null && _c !== void 0 ? _c : 1;
266
- const innerHeight = (_d = win === null || win === void 0 ? void 0 : win.innerHeight) !== null && _d !== void 0 ? _d : 1;
267
- this._deepestScroll = Math.max(this._deepestScroll, Math.min(100, Math.round(((scrollY + innerHeight) / scrollHeight) * 100)));
268
- this._bumpInactiveTimer();
269
- }
270
261
  _isNewSession(session) {
271
262
  // within the last second
272
263
  return Math.abs(session.data.startTime - Date.now()) < 1000;
@@ -7,6 +7,7 @@ export declare const AutoCaptureEventName: {
7
7
  readonly PERFORMANCE: "auto_capture::performance";
8
8
  readonly FORM_SUBMIT: "auto_capture::form_submit";
9
9
  readonly CLICK: "auto_capture::click";
10
+ readonly RAGE_CLICK: "auto_capture::rage_click";
10
11
  };
11
12
  export type AutoCaptureEventName = (typeof AutoCaptureEventName)[keyof typeof AutoCaptureEventName] & string;
12
13
  export type AutoCaptureEvent = StatsigEvent & {
@@ -9,4 +9,5 @@ exports.AutoCaptureEventName = {
9
9
  PERFORMANCE: 'auto_capture::performance',
10
10
  FORM_SUBMIT: 'auto_capture::form_submit',
11
11
  CLICK: 'auto_capture::click',
12
+ RAGE_CLICK: 'auto_capture::rage_click',
12
13
  };
@@ -0,0 +1,23 @@
1
+ export declare class EngagementManager {
2
+ private _lastScrollY;
3
+ private _maxScrollY;
4
+ private _lastScrollPercentage;
5
+ private _maxScrollPercentage;
6
+ private _lastPageViewTime;
7
+ private _inactiveTimer;
8
+ private _onInactivityCallback;
9
+ constructor();
10
+ private _initializeScrollTracking;
11
+ private _handleScroll;
12
+ getScrollMetrics(): {
13
+ lastScrollY: number;
14
+ maxScrollY: number;
15
+ lastScrollPercentage: number;
16
+ maxScrollPercentage: number;
17
+ scrollDepth: number;
18
+ };
19
+ getPageViewLength(): number;
20
+ setLastPageViewTime(time: number): void;
21
+ startInactivityTracking(callback: () => void): void;
22
+ bumpInactiveTimer(): void;
23
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EngagementManager = void 0;
4
+ const client_core_1 = require("@statsig/client-core");
5
+ const PAGE_INACTIVE_TIMEOUT = 600000; // 10 minutes
6
+ class EngagementManager {
7
+ constructor() {
8
+ this._lastScrollY = 0;
9
+ this._maxScrollY = 0;
10
+ this._lastScrollPercentage = 0;
11
+ this._maxScrollPercentage = 0;
12
+ this._lastPageViewTime = Date.now();
13
+ this._inactiveTimer = null;
14
+ this._onInactivityCallback = null;
15
+ this._initializeScrollTracking();
16
+ }
17
+ _initializeScrollTracking() {
18
+ const win = (0, client_core_1._getWindowSafe)();
19
+ if (!win)
20
+ return;
21
+ win.addEventListener('scroll', () => this._handleScroll());
22
+ win.addEventListener('scrollend', () => this._handleScroll());
23
+ win.addEventListener('resize', () => this._handleScroll());
24
+ }
25
+ _handleScroll() {
26
+ const win = (0, client_core_1._getWindowSafe)();
27
+ const doc = (0, client_core_1._getDocumentSafe)();
28
+ if (!win || !doc)
29
+ return;
30
+ const scrollHeight = doc.body.scrollHeight;
31
+ const scrollY = win.scrollY || doc.documentElement.scrollTop || 0;
32
+ const innerHeight = win.innerHeight;
33
+ this._lastScrollY = scrollY;
34
+ this._maxScrollY = Math.max(this._maxScrollY, scrollY);
35
+ const currentScrollPercentage = Math.min(100, Math.round(((scrollY + innerHeight) / scrollHeight) * 100));
36
+ this._lastScrollPercentage = currentScrollPercentage;
37
+ this._maxScrollPercentage = Math.max(this._maxScrollPercentage, currentScrollPercentage);
38
+ this.bumpInactiveTimer();
39
+ }
40
+ getScrollMetrics() {
41
+ return {
42
+ lastScrollY: this._lastScrollY,
43
+ maxScrollY: this._maxScrollY,
44
+ lastScrollPercentage: this._lastScrollPercentage,
45
+ maxScrollPercentage: this._maxScrollPercentage,
46
+ scrollDepth: this._maxScrollPercentage, // deprecated
47
+ };
48
+ }
49
+ getPageViewLength() {
50
+ return Date.now() - this._lastPageViewTime;
51
+ }
52
+ setLastPageViewTime(time) {
53
+ this._lastPageViewTime = time;
54
+ }
55
+ startInactivityTracking(callback) {
56
+ this._onInactivityCallback = callback;
57
+ }
58
+ bumpInactiveTimer() {
59
+ const win = (0, client_core_1._getWindowSafe)();
60
+ if (!win) {
61
+ return;
62
+ }
63
+ if (this._inactiveTimer) {
64
+ clearTimeout(this._inactiveTimer);
65
+ }
66
+ this._inactiveTimer = win.setTimeout(() => {
67
+ if (this._onInactivityCallback) {
68
+ this._onInactivityCallback();
69
+ }
70
+ }, PAGE_INACTIVE_TIMEOUT);
71
+ }
72
+ }
73
+ exports.EngagementManager = EngagementManager;
@@ -0,0 +1,4 @@
1
+ export default class RageClickManager {
2
+ private _clicks;
3
+ isRageClick(x: number, y: number, timestamp: number): boolean;
4
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const RAGE_CLICK_THRESHOLD_PX = 30;
4
+ const RAGE_CLICK_TIMEOUT_MS = 1000;
5
+ const RAGE_CLICK_CLICK_COUNT = 3;
6
+ class RageClickManager {
7
+ constructor() {
8
+ this._clicks = [];
9
+ }
10
+ isRageClick(x, y, timestamp) {
11
+ // Remove clicks outside the timeout window
12
+ this._clicks = this._clicks.filter((click) => timestamp - click.timestamp < RAGE_CLICK_TIMEOUT_MS);
13
+ const isCloseEnough = (click) => {
14
+ const dx = x - click.x;
15
+ const dy = y - click.y;
16
+ return Math.abs(dx) + Math.abs(dy) <= RAGE_CLICK_THRESHOLD_PX;
17
+ };
18
+ // If previous clicks exist, check spatial threshold
19
+ if (this._clicks.length > 0 &&
20
+ !isCloseEnough(this._clicks[this._clicks.length - 1])) {
21
+ this._clicks = [];
22
+ }
23
+ this._clicks.push({ x, y, timestamp });
24
+ return this._clicks.length >= RAGE_CLICK_CLICK_COUNT;
25
+ }
26
+ }
27
+ exports.default = RageClickManager;
@@ -4,15 +4,14 @@ interface NetworkInformation {
4
4
  rtt: number;
5
5
  saveData: boolean;
6
6
  }
7
- export declare function _gatherDatasetProperties(el: Element): Record<string, string>;
8
- export declare function _gatherEventData(target: Element): {
9
- value: string;
10
- metadata: Record<string, string | null>;
11
- };
7
+ export declare function _stripEmptyValues<T extends Record<string, string | number | null | undefined>>(obj: T): Partial<Record<keyof T, string | number>>;
12
8
  export declare function _getTargetNode(e: Event): Element | null;
13
9
  export declare function _shouldLogEvent(e: Event, el: Element): boolean;
14
10
  export declare function _getSafeUrl(): URL;
15
11
  export declare function _getSanitizedPageUrl(): string;
16
12
  export declare function _registerEventHandler(element: Document | Window, eventType: string, handler: (event: Event) => void): void;
17
13
  export declare function _getSafeNetworkInformation(): NetworkInformation | null;
14
+ export declare function _getSafeTimezone(): string | null;
15
+ export declare function _getSafeTimezoneOffset(): number | null;
16
+ export declare function _getAnchorNodeInHierarchy(node: Element | null): Element | null;
18
17
  export {};
@@ -1,51 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._getSafeNetworkInformation = exports._registerEventHandler = exports._getSanitizedPageUrl = exports._getSafeUrl = exports._shouldLogEvent = exports._getTargetNode = exports._gatherEventData = exports._gatherDatasetProperties = void 0;
3
+ exports._getAnchorNodeInHierarchy = exports._getSafeTimezoneOffset = exports._getSafeTimezone = exports._getSafeNetworkInformation = exports._registerEventHandler = exports._getSanitizedPageUrl = exports._getSafeUrl = exports._shouldLogEvent = exports._getTargetNode = exports._stripEmptyValues = void 0;
4
4
  const client_core_1 = require("@statsig/client-core");
5
- function _gatherDatasetProperties(el) {
6
- const dataset = {};
7
- if (!el) {
8
- return dataset;
9
- }
10
- const attr = el === null || el === void 0 ? void 0 : el.dataset;
11
- if (!attr) {
12
- return dataset;
13
- }
14
- for (const key in attr) {
15
- dataset[`data-${key}`] = attr[key] || '';
16
- }
17
- return dataset;
18
- }
19
- exports._gatherDatasetProperties = _gatherDatasetProperties;
20
- function _gatherEventData(target) {
21
- var _a;
22
- const tagName = target.tagName.toLowerCase();
23
- const metadata = {};
24
- const value = (0, client_core_1._getCurrentPageUrlSafe)() || '';
25
- metadata['tagName'] = tagName;
26
- if (tagName === 'form') {
27
- metadata['action'] = target.getAttribute('action');
28
- metadata['method'] = (_a = target.getAttribute('method')) !== null && _a !== void 0 ? _a : 'GET';
29
- metadata['formName'] = target.getAttribute('name');
30
- metadata['formId'] = target.getAttribute('id');
31
- }
32
- if (['input', 'select', 'textarea'].includes(tagName) &&
33
- target.getAttribute('type') !== 'password') {
34
- metadata['content'] = target.value;
35
- metadata['inputName'] = target.getAttribute('name');
36
- }
37
- const anchor = _getAnchorNodeInHierarchy(target);
38
- if (anchor) {
39
- metadata['href'] = anchor.getAttribute('href');
40
- }
41
- if (tagName === 'button' || anchor) {
42
- metadata['content'] = (target.textContent || '').trim();
43
- const dataset = _gatherDatasetProperties(anchor || target);
44
- Object.assign(metadata, dataset);
45
- }
46
- return { value, metadata };
5
+ function _stripEmptyValues(obj) {
6
+ return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value != null && value !== '' && value !== undefined));
47
7
  }
48
- exports._gatherEventData = _gatherEventData;
8
+ exports._stripEmptyValues = _stripEmptyValues;
49
9
  function _getTargetNode(e) {
50
10
  if (!e) {
51
11
  return null;
@@ -125,6 +85,24 @@ function _getSafeNetworkInformation() {
125
85
  return connection;
126
86
  }
127
87
  exports._getSafeNetworkInformation = _getSafeNetworkInformation;
88
+ function _getSafeTimezone() {
89
+ try {
90
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
91
+ }
92
+ catch (e) {
93
+ return null;
94
+ }
95
+ }
96
+ exports._getSafeTimezone = _getSafeTimezone;
97
+ function _getSafeTimezoneOffset() {
98
+ try {
99
+ return new Date().getTimezoneOffset();
100
+ }
101
+ catch (e) {
102
+ return null;
103
+ }
104
+ }
105
+ exports._getSafeTimezoneOffset = _getSafeTimezoneOffset;
128
106
  function _getAnchorNodeInHierarchy(node) {
129
107
  if (!node) {
130
108
  return null;
@@ -142,3 +120,4 @@ function _getAnchorNodeInHierarchy(node) {
142
120
  }
143
121
  return null;
144
122
  }
123
+ exports._getAnchorNodeInHierarchy = _getAnchorNodeInHierarchy;
@@ -0,0 +1,5 @@
1
+ export declare function _gatherEventData(target: Element): {
2
+ value: string;
3
+ metadata: Record<string, unknown>;
4
+ };
5
+ export declare function _getMetadataFromElement(target: Element): Record<string, unknown>;
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._getMetadataFromElement = exports._gatherEventData = void 0;
4
+ const client_core_1 = require("@statsig/client-core");
5
+ const commonUtils_1 = require("./commonUtils");
6
+ const MAX_ATTRIBUTE_LENGTH = 1000;
7
+ const MAX_CLASS_LIST_LENGTH = 100;
8
+ const MAX_SELECTOR_DEPTH = 50;
9
+ function _gatherEventData(target) {
10
+ const tagName = target.tagName.toLowerCase();
11
+ const metadata = {};
12
+ const value = (0, client_core_1._getCurrentPageUrlSafe)() || '';
13
+ metadata['tagName'] = tagName;
14
+ const elementMetadata = _getMetadataFromElement(target);
15
+ Object.assign(metadata, elementMetadata);
16
+ if (tagName === 'form') {
17
+ Object.assign(metadata, _getFormMetadata(target));
18
+ }
19
+ if (['input', 'select', 'textarea'].includes(tagName) &&
20
+ target.getAttribute('type') !== 'password') {
21
+ Object.assign(metadata, _getInputMetadata(target));
22
+ }
23
+ const anchor = (0, commonUtils_1._getAnchorNodeInHierarchy)(target);
24
+ if (anchor) {
25
+ Object.assign(metadata, _getAnchorMetadata(anchor));
26
+ }
27
+ if (tagName === 'button' || anchor) {
28
+ Object.assign(metadata, _getButtonMetadata(anchor || target));
29
+ }
30
+ return { value, metadata };
31
+ }
32
+ exports._gatherEventData = _gatherEventData;
33
+ function _getFormMetadata(target) {
34
+ var _a;
35
+ const metadata = {};
36
+ metadata['action'] = target.getAttribute('action');
37
+ metadata['method'] = (_a = target.getAttribute('method')) !== null && _a !== void 0 ? _a : 'GET';
38
+ metadata['formName'] = target.getAttribute('name');
39
+ metadata['formId'] = target.getAttribute('id');
40
+ return metadata;
41
+ }
42
+ function _getInputMetadata(target) {
43
+ const metadata = {};
44
+ metadata['content'] = target.value;
45
+ metadata['inputName'] = target.getAttribute('name');
46
+ return metadata;
47
+ }
48
+ function _getAnchorMetadata(anchor) {
49
+ const metadata = {};
50
+ metadata['href'] = anchor.getAttribute('href');
51
+ return metadata;
52
+ }
53
+ function _getButtonMetadata(target) {
54
+ const metadata = {};
55
+ metadata['content'] = (target.textContent || '').trim();
56
+ const dataset = _gatherDatasetProperties(target);
57
+ Object.assign(metadata, dataset);
58
+ return metadata;
59
+ }
60
+ function _gatherDatasetProperties(el) {
61
+ const dataset = {};
62
+ if (!el) {
63
+ return dataset;
64
+ }
65
+ const attr = el === null || el === void 0 ? void 0 : el.dataset;
66
+ if (!attr) {
67
+ return dataset;
68
+ }
69
+ for (const key in attr) {
70
+ dataset[`data-${key}`] = attr[key] || '';
71
+ }
72
+ return dataset;
73
+ }
74
+ function _truncateString(str, maxLength) {
75
+ if (!str)
76
+ return null;
77
+ return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
78
+ }
79
+ function _getMetadataFromElement(target) {
80
+ const metadata = {};
81
+ const classList = Array.from(target.classList);
82
+ metadata['classList'] =
83
+ classList.length > 0 ? classList.slice(0, MAX_CLASS_LIST_LENGTH) : null;
84
+ metadata['class'] = _normalizeClassAttribute(_truncateString(target.getAttribute('class'), MAX_ATTRIBUTE_LENGTH) || '');
85
+ metadata['id'] = _truncateString(target.getAttribute('id'), MAX_ATTRIBUTE_LENGTH);
86
+ metadata['ariaLabel'] = _truncateString(target.getAttribute('aria-label'), MAX_ATTRIBUTE_LENGTH);
87
+ metadata['selector'] = _generateCssSelector(target);
88
+ return metadata;
89
+ }
90
+ exports._getMetadataFromElement = _getMetadataFromElement;
91
+ function _normalizeClassAttribute(className) {
92
+ return className.replace(/\s+/g, ' ').trim();
93
+ }
94
+ function hasNextSiblingWithSameTag(element) {
95
+ let sibling = element.nextElementSibling;
96
+ while (sibling) {
97
+ if (sibling.tagName === element.tagName) {
98
+ return true;
99
+ }
100
+ sibling = sibling.nextElementSibling;
101
+ }
102
+ return false;
103
+ }
104
+ function getElementSelector(element) {
105
+ const tagName = element.tagName.toLowerCase();
106
+ // 1. Use ID if available
107
+ if (element.id) {
108
+ return `#${element.id}`;
109
+ }
110
+ // 2. Build class-based selector
111
+ let selector = tagName;
112
+ if (element.className && typeof element.className === 'string') {
113
+ const classes = element.className.trim().split(/\s+/);
114
+ if (classes.length > 0 && classes[0] !== '') {
115
+ selector += '.' + classes.join('.');
116
+ }
117
+ }
118
+ const parent = element.parentElement;
119
+ if (parent && parent.children.length > 1) {
120
+ let nthChild = 1;
121
+ let nthOfType = 1;
122
+ let sibling = element.previousElementSibling;
123
+ while (sibling) {
124
+ nthChild++;
125
+ if (sibling.tagName === element.tagName) {
126
+ nthOfType++;
127
+ }
128
+ sibling = sibling.previousElementSibling;
129
+ }
130
+ selector += `:nth-child(${nthChild})`;
131
+ // Only add nth-of-type if there are other elements with the same tag
132
+ if (nthOfType > 1 || hasNextSiblingWithSameTag(element)) {
133
+ selector += `:nth-of-type(${nthOfType})`;
134
+ }
135
+ }
136
+ return selector;
137
+ }
138
+ function _generateCssSelector(element) {
139
+ if (!element) {
140
+ return '';
141
+ }
142
+ // Handle case where element has no parent (e.g., detached element)
143
+ if (!element.parentNode) {
144
+ const tagName = element.tagName.toLowerCase();
145
+ if (element.id) {
146
+ return `#${element.id}`;
147
+ }
148
+ let selector = tagName;
149
+ if (element.className && typeof element.className === 'string') {
150
+ const classes = element.className.trim().split(/\s+/);
151
+ if (classes.length > 0 && classes[0] !== '') {
152
+ selector += '.' + classes.join('.');
153
+ }
154
+ }
155
+ return selector;
156
+ }
157
+ // Build the full selector path
158
+ const selectors = [];
159
+ let currentElement = element;
160
+ let depth = 0;
161
+ while (currentElement &&
162
+ currentElement.nodeType === Node.ELEMENT_NODE &&
163
+ depth < MAX_SELECTOR_DEPTH) {
164
+ const selector = getElementSelector(currentElement);
165
+ selectors.unshift(selector);
166
+ // Stop if we found an ID (since IDs should be unique)
167
+ if (currentElement.id) {
168
+ break;
169
+ }
170
+ currentElement = currentElement.parentElement;
171
+ // Stop at document body to avoid going too far up
172
+ if (currentElement && currentElement.tagName.toLowerCase() === 'body') {
173
+ break;
174
+ }
175
+ depth++;
176
+ }
177
+ return selectors.join(' > ');
178
+ }
@@ -0,0 +1,3 @@
1
+ export declare function _gatherCommonMetadata(url: URL): Record<string, string | number | null>;
2
+ export declare function _gatherAllMetadata(url: URL): Record<string, string | number>;
3
+ export declare function _getNetworkInfo(): Record<string, string | number | boolean>;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._getNetworkInfo = exports._gatherAllMetadata = exports._gatherCommonMetadata = void 0;
4
+ const client_core_1 = require("@statsig/client-core");
5
+ const commonUtils_1 = require("./commonUtils");
6
+ function _gatherCommonMetadata(url) {
7
+ var _a, _b, _c, _d, _e, _f, _g, _h;
8
+ const safeDoc = (0, client_core_1._getDocumentSafe)();
9
+ const safeWnd = (0, client_core_1._getWindowSafe)();
10
+ return (0, commonUtils_1._stripEmptyValues)(Object.assign({ title: safeDoc === null || safeDoc === void 0 ? void 0 : safeDoc.title, current_url: (_a = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.location) === null || _a === void 0 ? void 0 : _a.href, user_agent: ((_b = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.navigator) === null || _b === void 0 ? void 0 : _b.userAgent) &&
11
+ ((_d = (_c = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.navigator) === null || _c === void 0 ? void 0 : _c.userAgent) === null || _d === void 0 ? void 0 : _d.length) > 200
12
+ ? safeWnd.navigator.userAgent.substring(0, 200)
13
+ : (_e = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.navigator) === null || _e === void 0 ? void 0 : _e.userAgent, locale: (_f = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.navigator) === null || _f === void 0 ? void 0 : _f.language, hostname: url.hostname, pathname: url.pathname, screen_width: (_g = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.screen) === null || _g === void 0 ? void 0 : _g.width, screen_height: (_h = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.screen) === null || _h === void 0 ? void 0 : _h.height, viewport_width: safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.innerWidth, viewport_height: safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.innerHeight, timestamp: Date.now(), timezone: (0, commonUtils_1._getSafeTimezone)(), timezone_offset: (0, commonUtils_1._getSafeTimezoneOffset)() }, _getNetworkInfo()));
14
+ }
15
+ exports._gatherCommonMetadata = _gatherCommonMetadata;
16
+ function _gatherAllMetadata(url) {
17
+ const safeDoc = (0, client_core_1._getDocumentSafe)();
18
+ const safeWnd = (0, client_core_1._getWindowSafe)();
19
+ if (!safeDoc || !safeWnd) {
20
+ return {};
21
+ }
22
+ const referrerInfo = getReferrerInfo(safeDoc);
23
+ const commonInfo = _gatherCommonMetadata(url);
24
+ const campaignParams = getCampaignParams(url);
25
+ const queryParams = {};
26
+ url.searchParams.forEach((v, k) => {
27
+ queryParams[k] = v;
28
+ });
29
+ return Object.assign(Object.assign({}, commonInfo), (0, commonUtils_1._stripEmptyValues)(Object.assign(Object.assign(Object.assign({}, referrerInfo), campaignParams), queryParams)));
30
+ }
31
+ exports._gatherAllMetadata = _gatherAllMetadata;
32
+ function _getNetworkInfo() {
33
+ const networkInfo = (0, commonUtils_1._getSafeNetworkInformation)();
34
+ const result = {};
35
+ if ((networkInfo === null || networkInfo === void 0 ? void 0 : networkInfo.effectiveType) !== undefined) {
36
+ result['effective_connection_type'] = networkInfo.effectiveType;
37
+ }
38
+ if ((networkInfo === null || networkInfo === void 0 ? void 0 : networkInfo.rtt) !== undefined) {
39
+ result['rtt_ms'] = networkInfo.rtt;
40
+ }
41
+ if ((networkInfo === null || networkInfo === void 0 ? void 0 : networkInfo.downlink) !== undefined) {
42
+ result['downlink_mbps'] = networkInfo.downlink;
43
+ result['downlink_kbps'] = networkInfo.downlink * 1000; // deprecated
44
+ }
45
+ if ((networkInfo === null || networkInfo === void 0 ? void 0 : networkInfo.saveData) !== undefined) {
46
+ result['save_data'] = networkInfo.saveData;
47
+ }
48
+ return result;
49
+ }
50
+ exports._getNetworkInfo = _getNetworkInfo;
51
+ function getReferrerInfo(safeDoc) {
52
+ const referrer = (safeDoc === null || safeDoc === void 0 ? void 0 : safeDoc.referrer) || '';
53
+ if (!referrer) {
54
+ return {
55
+ referrer: null,
56
+ referrer_domain: null,
57
+ referrer_path: null,
58
+ searchEngine: '',
59
+ searchQuery: '',
60
+ };
61
+ }
62
+ try {
63
+ const url = new URL(referrer);
64
+ const host = url.hostname;
65
+ const searchEngine = ['google', 'bing', 'yahoo', 'duckduckgo', 'baidu'].find((e) => host.includes(e + '.')) || '';
66
+ const searchQuery = url.searchParams.get(searchEngine === 'yahoo' ? 'p' : 'q') || '';
67
+ return {
68
+ referrer,
69
+ referrer_domain: url.hostname,
70
+ referrer_path: url.pathname,
71
+ searchEngine,
72
+ searchQuery,
73
+ };
74
+ }
75
+ catch (e) {
76
+ return {
77
+ referrer: null,
78
+ referrer_domain: null,
79
+ referrer_path: null,
80
+ searchEngine: '',
81
+ searchQuery: '',
82
+ };
83
+ }
84
+ }
85
+ function getCampaignParams(url) {
86
+ const urlParams = url.searchParams;
87
+ const campaignParams = {};
88
+ const commonUtms = [
89
+ 'utm_source',
90
+ 'utm_medium',
91
+ 'utm_campaign',
92
+ 'utm_term',
93
+ 'utm_content',
94
+ 'msclkid', // Bing
95
+ 'dclid', // DoubleClick
96
+ 'fbclid', // Facebook
97
+ 'gad_source', // Google
98
+ 'gclid', // Google
99
+ 'gclsrc', // Google
100
+ 'wbraid', // Google
101
+ 'utm_id', // Hubspot
102
+ 'irclid', // Impact
103
+ 'igshid', // Instagram
104
+ '_kx', // Klaviyo
105
+ 'li_fat_id', // LinkedIn
106
+ 'mc_cid', // Mailchimp
107
+ 'mc_eid', // Mailchimp
108
+ 'epik', // Pinterest
109
+ 'qclid', // Quora
110
+ 'rdt_cid', // Reddit
111
+ 'sccid', // Snapchat
112
+ 'ttc', // TikTok
113
+ 'ttclid', // TikTok
114
+ 'ttc_id', // TikTok
115
+ 'twclid', // Twitter
116
+ ];
117
+ commonUtms.forEach((p) => {
118
+ const val = urlParams.get(p);
119
+ if (val) {
120
+ campaignParams[p] = val;
121
+ }
122
+ });
123
+ return campaignParams;
124
+ }
@@ -1 +0,0 @@
1
- export declare function _gatherPageViewPayload(url: URL): Record<string, string | number>;
@@ -1,73 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._gatherPageViewPayload = void 0;
4
- const client_core_1 = require("@statsig/client-core");
5
- function _gatherPageViewPayload(url) {
6
- var _a, _b;
7
- const safeDoc = (0, client_core_1._getDocumentSafe)();
8
- const safeWnd = (0, client_core_1._getWindowSafe)();
9
- if (!safeDoc || !safeWnd) {
10
- return {};
11
- }
12
- const navigator = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.navigator;
13
- const referrer = (safeDoc === null || safeDoc === void 0 ? void 0 : safeDoc.referrer) || '';
14
- let refUrl = new URL('empty:');
15
- if (referrer) {
16
- try {
17
- refUrl = new URL(referrer || 'empty:');
18
- }
19
- catch (e) {
20
- /* empty */
21
- }
22
- }
23
- const searchInfo = getSearchInfo(refUrl);
24
- const campaignParams = getCampaignParams(url);
25
- const queryParams = {};
26
- url.searchParams.forEach((v, k) => {
27
- queryParams[k] = v;
28
- });
29
- return Object.assign(Object.assign(Object.assign(Object.assign({}, searchInfo), campaignParams), queryParams), { title: (safeDoc === null || safeDoc === void 0 ? void 0 : safeDoc.title) || '', locale: (navigator === null || navigator === void 0 ? void 0 : navigator.language) || 'unknown', hostname: url.hostname || 'unknown', pathname: url.pathname || 'unknown', referrer, screen_width: ((_a = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.screen) === null || _a === void 0 ? void 0 : _a.width) || 'unknown', screen_height: ((_b = safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.screen) === null || _b === void 0 ? void 0 : _b.height) || 'unknown', viewport_width: (safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.innerWidth) || 'unknown', viewport_height: (safeWnd === null || safeWnd === void 0 ? void 0 : safeWnd.innerHeight) || 'unknown' });
30
- }
31
- exports._gatherPageViewPayload = _gatherPageViewPayload;
32
- function getCampaignParams(url) {
33
- const urlParams = url.searchParams;
34
- const campaignParams = {};
35
- const commonUtms = [
36
- 'utm_source',
37
- 'utm_medium',
38
- 'utm_campaign',
39
- 'utm_term',
40
- 'utm_content',
41
- 'gclid', // Google
42
- 'gclsrc', // Google
43
- 'dclid', // DoubleClick
44
- 'fbclid', // Facebook
45
- 'msclkid', // Bing
46
- 'mc_eid', // Mailchimp
47
- 'mc_cid', // Mailchimp
48
- 'twclid', // Twitter
49
- 'li_fat_id', // LinkedIn
50
- 'igshid', // Instagram
51
- 'utm_id', // Hubspot
52
- 'ttc', // TikTok
53
- 'ttclid', // TikTok
54
- 'ttc_id', // TikTok
55
- ];
56
- commonUtms.forEach((p) => {
57
- const val = urlParams.get(p);
58
- if (val) {
59
- campaignParams[p] = val;
60
- }
61
- });
62
- return campaignParams;
63
- }
64
- function getSearchEngine(refUrl) {
65
- const host = refUrl.hostname;
66
- const engine = ['google', 'bing', 'yahoo', 'duckduckgo', 'baidu'].find((e) => host.includes(e + '.'));
67
- return engine || '';
68
- }
69
- function getSearchInfo(refUrl) {
70
- const searchEngine = getSearchEngine(refUrl);
71
- const searchQuery = refUrl.searchParams.get(searchEngine === 'yahoo' ? 'p' : 'q') || '';
72
- return { searchEngine, searchQuery };
73
- }