@statsig/web-analytics 3.25.0-beta.1 → 3.25.1

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.25.0-beta.1",
3
+ "version": "3.25.1",
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.25.0-beta.1",
13
- "@statsig/js-client": "3.25.0-beta.1",
12
+ "@statsig/client-core": "3.25.1",
13
+ "@statsig/js-client": "3.25.1",
14
14
  "web-vitals": "5.0.3"
15
15
  },
16
16
  "jsdelivr": "./build/statsig-web-analytics.min.js",
@@ -20,6 +20,7 @@ export declare class AutoCapture {
20
20
  private _webVitalsManager;
21
21
  private _deadClickManager;
22
22
  private _consoleLogManager;
23
+ private _options?;
23
24
  constructor(_client: PrecomputedEvaluationsInterface, options?: AutoCaptureOptions);
24
25
  private _addEventHandlers;
25
26
  private _addPageViewTracking;
@@ -49,6 +49,7 @@ class AutoCapture {
49
49
  this._hasLoggedPageViewEnd = false;
50
50
  this._pageViewLogged = false;
51
51
  const { sdkKey, errorBoundary, values } = _client.getContext();
52
+ this._options = options;
52
53
  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 : {};
53
54
  this._errorBoundary = errorBoundary;
54
55
  this._errorBoundary.wrap(this, 'autoCapture:');
@@ -111,23 +112,23 @@ class AutoCapture {
111
112
  if (!win || !doc) {
112
113
  return;
113
114
  }
114
- (0, commonUtils_1._registerEventHandler)(win, 'popstate', () => this._tryLogPageView());
115
+ (0, commonUtils_1._registerEventHandler)(win, 'popstate', () => this._tryLogPageView('popstate'));
115
116
  window.history.pushState = new Proxy(window.history.pushState, {
116
117
  apply: (target, thisArg, [state, unused, url]) => {
117
118
  target.apply(thisArg, [state, unused, url]);
118
- this._tryLogPageView();
119
+ this._tryLogPageView('pushstate');
119
120
  },
120
121
  });
121
122
  window.history.replaceState = new Proxy(window.history.replaceState, {
122
123
  apply: (target, thisArg, [state, unused, url]) => {
123
124
  target.apply(thisArg, [state, unused, url]);
124
- this._tryLogPageView();
125
+ this._tryLogPageView('replacestate');
125
126
  },
126
127
  });
127
128
  this._tryLogPageView();
128
129
  }
129
130
  _autoLogEvent(event) {
130
- var _a, _b, _c;
131
+ var _a, _b, _c, _d;
131
132
  const eventType = (_a = event.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();
132
133
  if (eventType === 'error' && event instanceof ErrorEvent) {
133
134
  this._logError(event);
@@ -151,11 +152,16 @@ class AutoCapture {
151
152
  if (!selectedText) {
152
153
  return;
153
154
  }
154
- metadata['selectedText'] = (0, commonUtils_1._sanitizeString)(selectedText);
155
+ if ((_d = this._options) === null || _d === void 0 ? void 0 : _d.captureCopyText) {
156
+ metadata['selectedText'] = (0, commonUtils_1._sanitizeString)(selectedText);
157
+ }
155
158
  const clipType = event.type || 'clipboard';
156
159
  metadata['clipType'] = clipType;
157
160
  }
158
161
  const { value, metadata: eventMetadata } = (0, eventUtils_1._gatherEventData)(target);
162
+ if (eventMetadata['isOutbound']) {
163
+ this._engagementManager.setMeaningfulEngagementOccurred(true);
164
+ }
159
165
  Object.assign(metadata, eventMetadata);
160
166
  const allMetadata = (0, metadataUtils_1._gatherAllMetadata)((0, commonUtils_1._getSafeUrl)());
161
167
  this._enqueueAutoCapture(eventName, value, Object.assign(Object.assign({}, allMetadata), metadata));
@@ -204,15 +210,18 @@ class AutoCapture {
204
210
  this._errorBoundary.logError('AC::logSession', err);
205
211
  }
206
212
  }
207
- _tryLogPageView() {
213
+ _tryLogPageView(navigationType) {
208
214
  const url = (0, commonUtils_1._getSafeUrl)();
209
215
  const last = this._previousLoggedPageViewUrl;
210
- if (last && url.href === last.href) {
216
+ if (last && url.pathname === last.pathname) {
211
217
  return;
212
218
  }
213
219
  this._engagementManager.setLastPageViewTime(Date.now());
214
220
  this._hasLoggedPageViewEnd = false;
215
221
  const payload = (0, metadataUtils_1._gatherAllMetadata)(url);
222
+ if (navigationType) {
223
+ payload['navigation_type'] = navigationType;
224
+ }
216
225
  if (this._previousLoggedPageViewUrl) {
217
226
  payload['last_page_view_url'] = this._previousLoggedPageViewUrl.href;
218
227
  }
@@ -232,14 +241,14 @@ class AutoCapture {
232
241
  return;
233
242
  }
234
243
  this._hasLoggedPageViewEnd = true;
235
- const scrollMetrics = this._engagementManager.getScrollMetrics();
236
- const pageViewLength = this._engagementManager.getPageViewLength();
237
- this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PAGE_VIEW_END, (0, commonUtils_1._getSanitizedPageUrl)(), Object.assign(Object.assign({}, scrollMetrics), { pageViewLength,
238
- dueToInactivity }), {
244
+ this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PAGE_VIEW_END, (0, commonUtils_1._getSanitizedPageUrl)(), Object.assign(Object.assign({}, this._engagementManager.getPageViewEndMetadata()), { dueToInactivity }), {
239
245
  flushImmediately: true,
240
246
  });
241
247
  }
242
248
  _logRageClick(e) {
249
+ if (!(0, commonUtils_1._shouldLogEvent)(e, e.target)) {
250
+ return;
251
+ }
243
252
  const { value, metadata } = (0, eventUtils_1._gatherEventData)(e.target);
244
253
  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));
245
254
  }
@@ -311,7 +320,10 @@ class AutoCapture {
311
320
  const typedClient = this._client;
312
321
  typedClient._possibleFirstTouchMetadata =
313
322
  (0, metadataUtils_1._getPossibleFirstTouchMetadata)((0, commonUtils_1._getSafeUrl)());
314
- typedClient._user = Object.assign(Object.assign({}, typedClient._user), { analyticsOnlyMetadata: Object.assign(Object.assign({}, typedClient._possibleFirstTouchMetadata), typedClient._user.analyticsOnlyMetadata) });
323
+ // Only update user if first touch metadata is not empty
324
+ if (Object.keys(typedClient._possibleFirstTouchMetadata).length > 0) {
325
+ typedClient._user = Object.assign(Object.assign({}, typedClient._user), { analyticsOnlyMetadata: Object.assign(Object.assign({}, typedClient._possibleFirstTouchMetadata), typedClient._user.analyticsOnlyMetadata) });
326
+ }
315
327
  }
316
328
  _flushImmediately() {
317
329
  this._client.flush().catch((e) => {
@@ -1,11 +1,54 @@
1
1
  import { AutoCaptureEvent } from './AutoCaptureEvent';
2
2
  import { ConsoleLogLevel } from './ConsoleLogManager';
3
3
  export type AutoCaptureOptions = {
4
+ /**
5
+ * Optional function to filter which auto-capture events should be recorded.
6
+ * If provided, this function will be called for each auto-capture event.
7
+ * Return true to capture the event, false to ignore it.
8
+ *
9
+ * @param event - The auto-capture event to evaluate
10
+ * @returns true if the event should be captured, false otherwise
11
+ */
4
12
  eventFilterFunc?: (event: AutoCaptureEvent) => boolean;
13
+ /**
14
+ * Settings for automatically capturing console log events.
15
+ * When enabled, console.log, console.warn, console.error, etc. calls
16
+ * will be automatically tracked as statsig::log_line events.
17
+ */
5
18
  consoleLogAutoCaptureSettings?: ConsoleLogAutoCaptureSettings;
19
+ /**
20
+ * Whether to capture text content when copy events occur.
21
+ * When true, the actual text being copied will be sanitized and included in copy event data.
22
+ * When false or undefined, copy events will be tracked without the text content.
23
+ *
24
+ * @default false
25
+ */
26
+ captureCopyText?: boolean;
6
27
  };
7
28
  export type ConsoleLogAutoCaptureSettings = {
29
+ /**
30
+ * Whether console log auto-capture is enabled.
31
+ * When true, console method calls will be automatically tracked.
32
+ * When false, console events will not be captured.
33
+ */
8
34
  enabled: boolean;
35
+ /**
36
+ * The minimum log level to capture.
37
+ * Only console methods at or above this level will be tracked.
38
+ * For example, if set to 'warn', only console.warn and console.error will be captured.
39
+ *
40
+ * @default 'info' - captures info, log, warn, error levels
41
+ */
9
42
  logLevel?: ConsoleLogLevel;
43
+ /**
44
+ * Sampling rate for console log events (0.0 to 1.0).
45
+ * Determines what percentage of console events should be captured.
46
+ * 1.0 means capture all events, 0.5 means capture 50% of events, etc.
47
+ *
48
+ * @default 1.0 - capture all events
49
+ */
10
50
  sampleRate?: number;
51
+ maxKeys?: number;
52
+ maxDepth?: number;
53
+ maxStringLength?: number;
11
54
  };
@@ -10,12 +10,13 @@ export declare class ConsoleLogManager {
10
10
  private _isTracking;
11
11
  private _logLevel;
12
12
  private readonly __source;
13
+ private readonly _maxKeys;
14
+ private readonly _maxDepth;
15
+ private readonly _maxStringLength;
13
16
  constructor(_enqueueFn: (eventName: AutoCaptureEventName, value: string, metadata: Record<string, unknown>) => void, _errorBoundary: ErrorBoundary, _options: ConsoleLogAutoCaptureSettings);
14
17
  startTracking(): void;
15
18
  stopTracking(): void;
16
19
  private _patchConsole;
17
20
  private _enqueueConsoleLog;
18
21
  private _shouldLog;
19
- private _safeStringify;
20
- private _getStackTrace;
21
22
  }
@@ -5,6 +5,7 @@ exports.ConsoleLogManager = void 0;
5
5
  const client_core_1 = require("@statsig/client-core");
6
6
  const AutoCaptureEvent_1 = require("./AutoCaptureEvent");
7
7
  const commonUtils_1 = require("./utils/commonUtils");
8
+ const consoleLogsUtils_1 = require("./utils/consoleLogsUtils");
8
9
  const metadataUtils_1 = require("./utils/metadataUtils");
9
10
  const ConsoleLogPriority = {
10
11
  trace: 10,
@@ -14,9 +15,12 @@ const ConsoleLogPriority = {
14
15
  warn: 40,
15
16
  error: 50,
16
17
  };
18
+ const DEFAULT_MAX_KEYS = 10;
19
+ const DEFAULT_MAX_DEPTH = 10;
20
+ const DEFAULT_MAX_STRING_LENGTH = 500;
17
21
  class ConsoleLogManager {
18
22
  constructor(_enqueueFn, _errorBoundary, _options) {
19
- var _a;
23
+ var _a, _b, _c, _d;
20
24
  this._enqueueFn = _enqueueFn;
21
25
  this._errorBoundary = _errorBoundary;
22
26
  this._options = _options;
@@ -25,6 +29,10 @@ class ConsoleLogManager {
25
29
  this._logLevel = 'info';
26
30
  this.__source = 'js-auto-capture';
27
31
  this._logLevel = (_a = this._options.logLevel) !== null && _a !== void 0 ? _a : 'info';
32
+ this._maxKeys = (_b = this._options.maxKeys) !== null && _b !== void 0 ? _b : DEFAULT_MAX_KEYS;
33
+ this._maxDepth = (_c = this._options.maxDepth) !== null && _c !== void 0 ? _c : DEFAULT_MAX_DEPTH;
34
+ this._maxStringLength =
35
+ (_d = this._options.maxStringLength) !== null && _d !== void 0 ? _d : DEFAULT_MAX_STRING_LENGTH;
28
36
  }
29
37
  startTracking() {
30
38
  try {
@@ -63,8 +71,8 @@ class ConsoleLogManager {
63
71
  return;
64
72
  inStack = true;
65
73
  try {
66
- const payload = args.map((a) => this._safeStringify(a));
67
- const trace = this._getStackTrace();
74
+ const payload = args.map((a) => (0, consoleLogsUtils_1._safeStringify)(a, this._maxKeys, this._maxDepth, this._maxStringLength));
75
+ const trace = (0, consoleLogsUtils_1._getStackTrace)();
68
76
  this._enqueueConsoleLog(level, payload, trace);
69
77
  }
70
78
  catch (err) {
@@ -95,26 +103,5 @@ class ConsoleLogManager {
95
103
  }
96
104
  return Math.random() < this._options.sampleRate;
97
105
  }
98
- _safeStringify(val) {
99
- try {
100
- if (typeof val === 'string')
101
- return val;
102
- if (typeof val === 'object' && val !== null)
103
- return JSON.stringify(val);
104
- return String(val);
105
- }
106
- catch (_a) {
107
- return '[Unserializable]';
108
- }
109
- }
110
- _getStackTrace() {
111
- var _a, _b;
112
- try {
113
- return (_b = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split('\n').slice(2)) !== null && _b !== void 0 ? _b : [];
114
- }
115
- catch (_c) {
116
- return [];
117
- }
118
- }
119
106
  }
120
107
  exports.ConsoleLogManager = ConsoleLogManager;
@@ -6,6 +6,7 @@ export declare class EngagementManager {
6
6
  private _lastPageViewTime;
7
7
  private _inactiveTimer;
8
8
  private _onInactivityCallback;
9
+ private _meaningfulEngagementOccurred;
9
10
  constructor();
10
11
  private _initializeScrollTracking;
11
12
  private _handleScroll;
@@ -20,4 +21,6 @@ export declare class EngagementManager {
20
21
  setLastPageViewTime(time: number): void;
21
22
  startInactivityTracking(callback: () => void): void;
22
23
  bumpInactiveTimer(): void;
24
+ setMeaningfulEngagementOccurred(occurred: boolean): void;
25
+ getPageViewEndMetadata(): Record<string, unknown>;
23
26
  }
@@ -12,6 +12,7 @@ class EngagementManager {
12
12
  this._lastPageViewTime = Date.now();
13
13
  this._inactiveTimer = null;
14
14
  this._onInactivityCallback = null;
15
+ this._meaningfulEngagementOccurred = false;
15
16
  this._initializeScrollTracking();
16
17
  }
17
18
  _initializeScrollTracking() {
@@ -69,5 +70,13 @@ class EngagementManager {
69
70
  }
70
71
  }, PAGE_INACTIVE_TIMEOUT);
71
72
  }
73
+ setMeaningfulEngagementOccurred(occurred) {
74
+ this._meaningfulEngagementOccurred = occurred;
75
+ }
76
+ getPageViewEndMetadata() {
77
+ const pageviewEndMetadata = Object.assign(Object.assign({}, this.getScrollMetrics()), { pageViewLength: this.getPageViewLength(), meaningfulEngagementOccurred: this._meaningfulEngagementOccurred });
78
+ this.setMeaningfulEngagementOccurred(false);
79
+ return pageviewEndMetadata;
80
+ }
72
81
  }
73
82
  exports.EngagementManager = EngagementManager;
@@ -59,7 +59,14 @@ function _shouldLogEvent(e, el, isCopyEvent = false) {
59
59
  return ['change', 'click'].indexOf(eventType) >= 0;
60
60
  default:
61
61
  if (eventType === 'click') {
62
- if (tagName === 'button') {
62
+ const compStyles = window.getComputedStyle(el);
63
+ if (compStyles && compStyles.getPropertyValue('cursor') === 'pointer') {
64
+ return true;
65
+ }
66
+ if (exports.interactiveElements.includes(tagName)) {
67
+ return true;
68
+ }
69
+ if (el.getAttribute('contenteditable') === 'true') {
63
70
  return true;
64
71
  }
65
72
  const anchor = _getAnchorNodeInHierarchy(el);
@@ -0,0 +1,2 @@
1
+ export declare function _safeStringify(val: unknown, maxKeysCount: number, maxDepth: number, maxLength: number): string;
2
+ export declare function _getStackTrace(): string[];
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._getStackTrace = exports._safeStringify = void 0;
4
+ function _truncateString(str, maxLength) {
5
+ if (str.length <= maxLength) {
6
+ return str;
7
+ }
8
+ return str.slice(0, maxLength) + '...';
9
+ }
10
+ function _safeStringify(val, maxKeysCount, maxDepth, maxLength) {
11
+ try {
12
+ if (_shouldNotStringify(val, maxKeysCount, maxDepth)) {
13
+ return _simpleStringify(val, maxLength);
14
+ }
15
+ if (typeof val === 'string') {
16
+ return _truncateString(val, maxLength);
17
+ }
18
+ if (typeof val === 'object' && val !== null) {
19
+ return _truncateString(JSON.stringify(val), maxLength);
20
+ }
21
+ return _truncateString(String(val), maxLength);
22
+ }
23
+ catch (_a) {
24
+ return _truncateString('[Unserializable]', maxLength);
25
+ }
26
+ }
27
+ exports._safeStringify = _safeStringify;
28
+ function _getStackTrace() {
29
+ var _a, _b;
30
+ try {
31
+ return (_b = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split('\n').slice(2)) !== null && _b !== void 0 ? _b : [];
32
+ }
33
+ catch (_c) {
34
+ return [];
35
+ }
36
+ }
37
+ exports._getStackTrace = _getStackTrace;
38
+ function _shouldNotStringify(val, maxKeysCount, maxDepth) {
39
+ if (_isPlainObject(val)) {
40
+ if (Object.keys(val).length > maxKeysCount) {
41
+ return true;
42
+ }
43
+ if (_isObjectTooDeep(val, maxDepth)) {
44
+ return true;
45
+ }
46
+ }
47
+ if (typeof val === 'function') {
48
+ return true;
49
+ }
50
+ return false;
51
+ }
52
+ function _isPlainObject(obj) {
53
+ return Object.prototype.toString.call(obj) === '[object Object]';
54
+ }
55
+ function _isObjectTooDeep(obj, maxDepth) {
56
+ if (maxDepth <= 0) {
57
+ return true;
58
+ }
59
+ if (typeof obj !== 'object' || obj === null) {
60
+ return false;
61
+ }
62
+ return Object.keys(obj).some((key) => _isObjectTooDeep(obj[key], maxDepth - 1));
63
+ }
64
+ function _simpleStringify(val, maxLength) {
65
+ return _truncateString(val.toString(), maxLength);
66
+ }
@@ -1,5 +1,5 @@
1
1
  export declare function _gatherEventData(target: Element): {
2
- value: string | null;
2
+ value: string;
3
3
  metadata: Record<string, unknown>;
4
4
  };
5
5
  export declare function _gatherCopyEventData(e: Event): Record<string, unknown>;
@@ -7,12 +7,13 @@ const MAX_ATTRIBUTE_LENGTH = 1000;
7
7
  const MAX_CLASS_LIST_LENGTH = 100;
8
8
  const MAX_SELECTOR_DEPTH = 50;
9
9
  function _gatherEventData(target) {
10
+ const value = (0, commonUtils_1._getSanitizedPageUrl)() || '';
11
+ const tagName = target.tagName.toLowerCase();
12
+ // If the element is sensitive, we only gather safe attribute fields
10
13
  if (_isSensitiveElement(target)) {
11
- return { value: null, metadata: {} };
14
+ return { value, metadata: _getSafeMetadataAttributes(target) };
12
15
  }
13
- const tagName = target.tagName.toLowerCase();
14
16
  const metadata = {};
15
- const value = (0, commonUtils_1._getSanitizedPageUrl)() || '';
16
17
  metadata['tagName'] = tagName;
17
18
  const elementMetadata = _getMetadataFromElement(target);
18
19
  Object.assign(metadata, elementMetadata);
@@ -29,6 +30,9 @@ function _gatherEventData(target) {
29
30
  if (tagName === 'button' || anchor) {
30
31
  Object.assign(metadata, _getButtonMetadata(anchor || target));
31
32
  }
33
+ if (_isOutboundLink(metadata)) {
34
+ metadata['isOutbound'] = 'true';
35
+ }
32
36
  return { value, metadata };
33
37
  }
34
38
  exports._gatherEventData = _gatherEventData;
@@ -95,14 +99,35 @@ function _truncateString(str, maxLength) {
95
99
  return null;
96
100
  return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
97
101
  }
102
+ function _isOutboundLink(metadata) {
103
+ var _a;
104
+ if (Array.isArray(metadata['classList']) &&
105
+ ((_a = metadata['classList']) === null || _a === void 0 ? void 0 : _a.includes('statsig-ctr-capture'))) {
106
+ return true;
107
+ }
108
+ const href = metadata['href'];
109
+ if (href) {
110
+ const currentUrl = (0, commonUtils_1._getSafeUrl)();
111
+ const linkUrl = new URL(href);
112
+ return currentUrl.host !== linkUrl.host;
113
+ }
114
+ return false;
115
+ }
116
+ function _getSafeMetadataAttributes(elem) {
117
+ const metadata = {};
118
+ metadata['class'] = _normalizeClassAttribute(_truncateString(elem.getAttribute('class'), MAX_ATTRIBUTE_LENGTH) || '');
119
+ metadata['id'] = _truncateString(elem.getAttribute('id'), MAX_ATTRIBUTE_LENGTH);
120
+ metadata['ariaLabel'] = _truncateString(elem.getAttribute('aria-label'), MAX_ATTRIBUTE_LENGTH);
121
+ metadata['name'] = _truncateString(elem.getAttribute('name'), MAX_ATTRIBUTE_LENGTH);
122
+ return metadata;
123
+ }
98
124
  function _getMetadataFromElement(target) {
99
125
  const metadata = {};
126
+ const safeAttributes = _getSafeMetadataAttributes(target);
127
+ Object.assign(metadata, safeAttributes);
100
128
  const classList = Array.from(target.classList);
101
129
  metadata['classList'] =
102
130
  classList.length > 0 ? classList.slice(0, MAX_CLASS_LIST_LENGTH) : null;
103
- metadata['class'] = _normalizeClassAttribute(_truncateString(target.getAttribute('class'), MAX_ATTRIBUTE_LENGTH) || '');
104
- metadata['id'] = _truncateString(target.getAttribute('id'), MAX_ATTRIBUTE_LENGTH);
105
- metadata['ariaLabel'] = _truncateString(target.getAttribute('aria-label'), MAX_ATTRIBUTE_LENGTH);
106
131
  metadata['selector'] = _generateCssSelector(target);
107
132
  return metadata;
108
133
  }