@statsig/web-analytics 3.20.3 → 3.21.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.20.3",
3
+ "version": "3.21.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.20.3",
13
- "@statsig/js-client": "3.20.3",
12
+ "@statsig/client-core": "3.21.0",
13
+ "@statsig/js-client": "3.21.0",
14
14
  "web-vitals": "5.0.3"
15
15
  },
16
16
  "jsdelivr": "./build/statsig-web-analytics.min.js",
@@ -1,8 +1,5 @@
1
1
  import { PrecomputedEvaluationsInterface, StatsigPlugin } from '@statsig/client-core';
2
- import { AutoCaptureEvent } from './AutoCaptureEvent';
3
- export type AutoCaptureOptions = {
4
- eventFilterFunc?: (event: AutoCaptureEvent) => boolean;
5
- };
2
+ import { AutoCaptureOptions } from './AutoCaptureOptions';
6
3
  export declare class StatsigAutoCapturePlugin implements StatsigPlugin<PrecomputedEvaluationsInterface> {
7
4
  private _options?;
8
5
  readonly __plugin = "auto-capture";
@@ -22,6 +19,7 @@ export declare class AutoCapture {
22
19
  private _pageViewLogged;
23
20
  private _webVitalsManager;
24
21
  private _deadClickManager;
22
+ private _consoleLogManager;
25
23
  constructor(_client: PrecomputedEvaluationsInterface, options?: AutoCaptureOptions);
26
24
  private _addEventHandlers;
27
25
  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 ConsoleLogManager_1 = require("./ConsoleLogManager");
6
7
  const DeadClickManager_1 = require("./DeadClickManager");
7
8
  const EngagementManager_1 = require("./EngagementManager");
8
9
  const RageClickManager_1 = require("./RageClickManager");
@@ -13,6 +14,8 @@ const metadataUtils_1 = require("./utils/metadataUtils");
13
14
  const AUTO_EVENT_MAPPING = {
14
15
  submit: AutoCaptureEvent_1.AutoCaptureEventName.FORM_SUBMIT,
15
16
  click: AutoCaptureEvent_1.AutoCaptureEventName.CLICK,
17
+ copy: AutoCaptureEvent_1.AutoCaptureEventName.COPY,
18
+ cut: AutoCaptureEvent_1.AutoCaptureEventName.COPY,
16
19
  };
17
20
  class StatsigAutoCapturePlugin {
18
21
  constructor(_options) {
@@ -39,7 +42,7 @@ function runStatsigAutoCapture(client, options) {
39
42
  exports.runStatsigAutoCapture = runStatsigAutoCapture;
40
43
  class AutoCapture {
41
44
  constructor(_client, options) {
42
- var _a, _b, _c;
45
+ var _a, _b, _c, _d;
43
46
  this._client = _client;
44
47
  this._disabledEvents = {};
45
48
  this._previousLoggedPageViewUrl = null;
@@ -59,11 +62,12 @@ class AutoCapture {
59
62
  this._rageClickManager = new RageClickManager_1.default();
60
63
  this._webVitalsManager = new WebVitalsManager_1.WebVitalsManager(this._enqueueAutoCapture.bind(this));
61
64
  this._deadClickManager = new DeadClickManager_1.default(this._enqueueAutoCapture.bind(this));
65
+ this._consoleLogManager = new ConsoleLogManager_1.ConsoleLogManager(this._enqueueAutoCapture.bind(this), (_c = options === null || options === void 0 ? void 0 : options.consoleLogAutoCaptureSettings) !== null && _c !== void 0 ? _c : { enabled: false });
62
66
  this._eventFilterFunc = options === null || options === void 0 ? void 0 : options.eventFilterFunc;
63
67
  const doc = (0, client_core_1._getDocumentSafe)();
64
68
  if (!(0, client_core_1._isServerEnv)()) {
65
69
  __STATSIG__ = (0, client_core_1._getStatsigGlobal)();
66
- const instances = (_c = __STATSIG__.acInstances) !== null && _c !== void 0 ? _c : {};
70
+ const instances = (_d = __STATSIG__.acInstances) !== null && _d !== void 0 ? _d : {};
67
71
  instances[sdkKey] = this;
68
72
  __STATSIG__.acInstances = instances;
69
73
  }
@@ -95,6 +99,8 @@ class AutoCapture {
95
99
  };
96
100
  (0, commonUtils_1._registerEventHandler)(doc, 'click', (e) => eventHandler(e));
97
101
  (0, commonUtils_1._registerEventHandler)(doc, 'submit', (e) => eventHandler(e));
102
+ (0, commonUtils_1._registerEventHandler)(doc, 'copy', (e) => eventHandler(e));
103
+ (0, commonUtils_1._registerEventHandler)(doc, 'cut', (e) => eventHandler(e));
98
104
  (0, commonUtils_1._registerEventHandler)(win, 'error', (e) => eventHandler(e, false));
99
105
  (0, commonUtils_1._registerEventHandler)(win, 'pagehide', () => this._tryLogPageViewEnd());
100
106
  (0, commonUtils_1._registerEventHandler)(win, 'beforeunload', () => this._tryLogPageViewEnd());
@@ -115,7 +121,7 @@ class AutoCapture {
115
121
  this._tryLogPageView();
116
122
  }
117
123
  _autoLogEvent(event) {
118
- var _a;
124
+ var _a, _b, _c;
119
125
  const eventType = (_a = event.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();
120
126
  if (eventType === 'error' && event instanceof ErrorEvent) {
121
127
  this._logError(event);
@@ -125,20 +131,33 @@ class AutoCapture {
125
131
  if (!target) {
126
132
  return;
127
133
  }
128
- if (!(0, commonUtils_1._shouldLogEvent)(event, target)) {
129
- return;
130
- }
131
134
  const eventName = AUTO_EVENT_MAPPING[eventType];
132
135
  if (!eventName) {
133
136
  return;
134
137
  }
135
- const { value, metadata } = (0, eventUtils_1._gatherEventData)(target);
138
+ const isCopyEvent = eventName === AutoCaptureEvent_1.AutoCaptureEventName.COPY;
139
+ if (!(0, commonUtils_1._shouldLogEvent)(event, target, isCopyEvent)) {
140
+ return;
141
+ }
142
+ const metadata = {};
143
+ if (isCopyEvent) {
144
+ const selectedText = (_c = (_b = (0, client_core_1._getWindowSafe)()) === null || _b === void 0 ? void 0 : _b.getSelection()) === null || _c === void 0 ? void 0 : _c.toString();
145
+ if (!selectedText) {
146
+ return;
147
+ }
148
+ metadata['selectedText'] = (0, commonUtils_1._sanitizeString)(selectedText);
149
+ const clipType = event.type || 'clipboard';
150
+ metadata['clipType'] = clipType;
151
+ }
152
+ const { value, metadata: eventMetadata } = (0, eventUtils_1._gatherEventData)(target);
153
+ Object.assign(metadata, eventMetadata);
136
154
  const allMetadata = (0, metadataUtils_1._gatherAllMetadata)((0, commonUtils_1._getSafeUrl)());
137
155
  this._enqueueAutoCapture(eventName, value, Object.assign(Object.assign({}, allMetadata), metadata));
138
156
  }
139
157
  _initialize() {
140
158
  this._webVitalsManager.startTracking();
141
159
  this._deadClickManager.startTracking();
160
+ this._consoleLogManager.startTracking();
142
161
  this._engagementManager.startInactivityTracking(() => this._tryLogPageViewEnd(true));
143
162
  this._addEventHandlers();
144
163
  this._addPageViewTracking();
@@ -10,6 +10,8 @@ export declare const AutoCaptureEventName: {
10
10
  readonly RAGE_CLICK: "auto_capture::rage_click";
11
11
  readonly WEB_VITALS: "auto_capture::web_vitals";
12
12
  readonly DEAD_CLICK: "auto_capture::dead_click";
13
+ readonly COPY: "auto_capture::copy";
14
+ readonly CONSOLE_LOG: "statsig::log_line";
13
15
  };
14
16
  export type AutoCaptureEventName = (typeof AutoCaptureEventName)[keyof typeof AutoCaptureEventName] & string;
15
17
  export type AutoCaptureEvent = StatsigEvent & {
@@ -12,4 +12,7 @@ exports.AutoCaptureEventName = {
12
12
  RAGE_CLICK: 'auto_capture::rage_click',
13
13
  WEB_VITALS: 'auto_capture::web_vitals',
14
14
  DEAD_CLICK: 'auto_capture::dead_click',
15
+ COPY: 'auto_capture::copy',
16
+ // log line is a special event name used to populate logs metrics explorer
17
+ CONSOLE_LOG: 'statsig::log_line',
15
18
  };
@@ -0,0 +1,14 @@
1
+ import { LogLevel } from '@statsig/client-core';
2
+ import { AutoCaptureEvent } from './AutoCaptureEvent';
3
+ export type AutoCaptureOptions = {
4
+ eventFilterFunc?: (event: AutoCaptureEvent) => boolean;
5
+ consoleLogAutoCaptureSettings?: ConsoleLogAutoCaptureSettings;
6
+ };
7
+ export type ConsoleLogAutoCaptureSettings = {
8
+ enabled: boolean;
9
+ logLevel?: LogLevel;
10
+ serviceName?: string;
11
+ serviceVersion?: string;
12
+ sampleRate?: number;
13
+ resourceMetadata?: Record<string, string | number | boolean>;
14
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,27 @@
1
+ import { AutoCaptureEventName } from './AutoCaptureEvent';
2
+ import { ConsoleLogAutoCaptureSettings } from './AutoCaptureOptions';
3
+ export type LogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';
4
+ export type ConsoleLogEvent = {
5
+ eventName: 'statsig::console_log';
6
+ value: string;
7
+ metadata: {
8
+ timestamp: string;
9
+ log_level: LogLevel;
10
+ payload: string[];
11
+ trace: string[];
12
+ };
13
+ };
14
+ export declare class ConsoleLogManager {
15
+ private _enqueueFn;
16
+ private _options;
17
+ private _restoreFns;
18
+ private _isTracking;
19
+ constructor(_enqueueFn: (eventName: AutoCaptureEventName, value: string, metadata: Record<string, unknown>) => void, _options: ConsoleLogAutoCaptureSettings);
20
+ startTracking(): void;
21
+ stopTracking(): void;
22
+ private _patchConsole;
23
+ private _enqueueConsoleLog;
24
+ private _shouldLog;
25
+ private _safeStringify;
26
+ private _getStackTrace;
27
+ }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConsoleLogManager = void 0;
4
+ /* eslint-disable no-console */
5
+ const client_core_1 = require("@statsig/client-core");
6
+ const AutoCaptureEvent_1 = require("./AutoCaptureEvent");
7
+ const commonUtils_1 = require("./utils/commonUtils");
8
+ const metadataUtils_1 = require("./utils/metadataUtils");
9
+ class ConsoleLogManager {
10
+ constructor(_enqueueFn, _options) {
11
+ this._enqueueFn = _enqueueFn;
12
+ this._options = _options;
13
+ this._restoreFns = [];
14
+ this._isTracking = false;
15
+ }
16
+ startTracking() {
17
+ if (this._isTracking || !this._options.enabled)
18
+ return;
19
+ const win = (0, client_core_1._getWindowSafe)();
20
+ if (!win)
21
+ return;
22
+ this._isTracking = true;
23
+ this._patchConsole();
24
+ }
25
+ stopTracking() {
26
+ if (!this._isTracking)
27
+ return;
28
+ this._restoreFns.forEach((restore) => restore());
29
+ this._restoreFns = [];
30
+ this._isTracking = false;
31
+ }
32
+ _patchConsole() {
33
+ ['log', 'info', 'warn', 'error', 'debug'].forEach((level) => {
34
+ if (!console[level])
35
+ return;
36
+ const original = console[level].bind(console);
37
+ let inStack = false;
38
+ const restore = (0, commonUtils_1.patch)(console, level, (originalFn) => {
39
+ return (...args) => {
40
+ originalFn(...args);
41
+ if (inStack)
42
+ return;
43
+ inStack = true;
44
+ try {
45
+ const payload = args.map((a) => this._safeStringify(a));
46
+ const trace = this._getStackTrace();
47
+ this._enqueueConsoleLog(level, payload, trace);
48
+ }
49
+ catch (err) {
50
+ original('console observer error:', err, ...args);
51
+ }
52
+ finally {
53
+ inStack = false;
54
+ }
55
+ };
56
+ });
57
+ this._restoreFns.push(restore);
58
+ });
59
+ }
60
+ _enqueueConsoleLog(level, payload, trace) {
61
+ var _a, _b, _c;
62
+ if (!this._shouldLog())
63
+ return;
64
+ const metadata = Object.assign(Object.assign({ log_level: level, payload,
65
+ trace, timestamp: Date.now(), serviceName: (_a = this._options.serviceName) !== null && _a !== void 0 ? _a : '', serviceVersion: (_b = this._options.serviceVersion) !== null && _b !== void 0 ? _b : '' }, ((_c = this._options.resourceMetadata) !== null && _c !== void 0 ? _c : {})), (0, metadataUtils_1._gatherAllMetadata)((0, commonUtils_1._getSafeUrl)()));
66
+ this._enqueueFn(AutoCaptureEvent_1.AutoCaptureEventName.CONSOLE_LOG, (0, commonUtils_1._getSafeUrlString)(), metadata);
67
+ }
68
+ _shouldLog() {
69
+ if (!this._options.sampleRate ||
70
+ typeof this._options.sampleRate !== 'number' ||
71
+ this._options.sampleRate <= 0 ||
72
+ this._options.sampleRate >= 1) {
73
+ return true;
74
+ }
75
+ return Math.random() < this._options.sampleRate;
76
+ }
77
+ _safeStringify(val) {
78
+ try {
79
+ if (typeof val === 'string')
80
+ return val;
81
+ if (typeof val === 'object' && val !== null)
82
+ return JSON.stringify(val);
83
+ return String(val);
84
+ }
85
+ catch (_a) {
86
+ return '[Unserializable]';
87
+ }
88
+ }
89
+ _getStackTrace() {
90
+ var _a, _b;
91
+ try {
92
+ return (_b = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split('\n').slice(2)) !== null && _b !== void 0 ? _b : [];
93
+ }
94
+ catch (_c) {
95
+ return [];
96
+ }
97
+ }
98
+ }
99
+ exports.ConsoleLogManager = ConsoleLogManager;
@@ -7,7 +7,7 @@ interface NetworkInformation {
7
7
  export declare const interactiveElements: string[];
8
8
  export declare function _stripEmptyValues<T extends Record<string, string | number | null | undefined>>(obj: T): Partial<Record<keyof T, string | number>>;
9
9
  export declare function _getTargetNode(e: Event): Element | null;
10
- export declare function _shouldLogEvent(e: Event, el: Element): boolean;
10
+ export declare function _shouldLogEvent(e: Event, el: Element, isCopyEvent?: boolean): boolean;
11
11
  export declare function _getSafeUrl(): URL;
12
12
  export declare function _getSafeUrlString(): string;
13
13
  export declare function _getSanitizedPageUrl(): string;
@@ -16,5 +16,7 @@ export declare function _getSafeNetworkInformation(): NetworkInformation | null;
16
16
  export declare function _getSafeTimezone(): string | null;
17
17
  export declare function _getSafeTimezoneOffset(): number | null;
18
18
  export declare function _getAnchorNodeInHierarchy(node: Element | null): Element | null;
19
+ export declare function _sanitizeString(maybeString: string | null | undefined): string | null;
19
20
  export declare function throttle<T extends (...args: unknown[]) => void>(fn: T, limit: number): T;
21
+ export declare function patch(source: Record<string, unknown>, name: string, replacement: (original: (...args: unknown[]) => void) => (...args: unknown[]) => void): () => void;
20
22
  export {};
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
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;
3
+ exports.patch = exports.throttle = exports._sanitizeString = 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
+ const coreCCPattern = `(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11})`;
6
+ const CC_REGEX = new RegExp(`^(?:${coreCCPattern})$`);
7
+ const coreSSNPattern = `\\d{3}-?\\d{2}-?\\d{4}`;
8
+ const SSN_REGEX = new RegExp(`^(${coreSSNPattern})$`);
5
9
  exports.interactiveElements = [
6
10
  'button',
7
11
  'a',
@@ -30,7 +34,7 @@ function _getTargetNode(e) {
30
34
  return target;
31
35
  }
32
36
  exports._getTargetNode = _getTargetNode;
33
- function _shouldLogEvent(e, el) {
37
+ function _shouldLogEvent(e, el, isCopyEvent = false) {
34
38
  if (!e || !el || el.nodeType !== 1) {
35
39
  return false;
36
40
  }
@@ -40,15 +44,19 @@ function _shouldLogEvent(e, el) {
40
44
  if (classList.contains('statsig-no-capture')) {
41
45
  return false;
42
46
  }
47
+ if (isCopyEvent) {
48
+ // We don't want to force strict event filtering for copy events
49
+ return true;
50
+ }
43
51
  switch (tagName) {
44
52
  case 'html':
45
53
  return false;
46
54
  case 'form':
47
- return eventType === 'submit';
55
+ return ['submit'].indexOf(eventType) >= 0;
48
56
  case 'input':
49
57
  case 'select':
50
58
  case 'textarea':
51
- return ['change'].includes(eventType);
59
+ return ['change', 'click'].indexOf(eventType) >= 0;
52
60
  default:
53
61
  if (eventType === 'click') {
54
62
  if (tagName === 'button') {
@@ -143,6 +151,30 @@ function _getAnchorNodeInHierarchy(node) {
143
151
  return null;
144
152
  }
145
153
  exports._getAnchorNodeInHierarchy = _getAnchorNodeInHierarchy;
154
+ function _sanitizeString(maybeString) {
155
+ if (!maybeString) {
156
+ return null;
157
+ }
158
+ return maybeString
159
+ .replace(/<[^>]*>/g, '')
160
+ .trim()
161
+ .split(/(\s+)/)
162
+ .filter((s) => _shouldCaptureTextValue(s))
163
+ .join('')
164
+ .replace(/[\r\n]/g, ' ')
165
+ .replace(/[ ]+/g, ' ')
166
+ .substring(0, 255);
167
+ }
168
+ exports._sanitizeString = _sanitizeString;
169
+ function _shouldCaptureTextValue(text) {
170
+ if (CC_REGEX.test((text || '').replace(/[- ]/g, ''))) {
171
+ return false;
172
+ }
173
+ if (SSN_REGEX.test((text || '').replace(/[- ]/g, ''))) {
174
+ return false;
175
+ }
176
+ return true;
177
+ }
146
178
  function throttle(fn, limit) {
147
179
  let lastCall = 0;
148
180
  return function (...args) {
@@ -154,3 +186,38 @@ function throttle(fn, limit) {
154
186
  };
155
187
  }
156
188
  exports.throttle = throttle;
189
+ // copy from https://github.com/getsentry/sentry-javascript/blob/b2109071975af8bf0316d3b5b38f519bdaf5dc15/packages/utils/src/object.ts
190
+ function patch(source, name, replacement) {
191
+ try {
192
+ if (!source[name])
193
+ return () => {
194
+ // noop
195
+ };
196
+ const original = source[name];
197
+ const wrapped = replacement(original);
198
+ // Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work
199
+ // otherwise it'll throw "TypeError: Object.defineProperties called on non-object"
200
+ if (typeof wrapped === 'function') {
201
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
202
+ wrapped.prototype = wrapped.prototype || {};
203
+ Object.defineProperties(wrapped, {
204
+ __statsig_original__: {
205
+ enumerable: false,
206
+ value: original,
207
+ },
208
+ });
209
+ }
210
+ source[name] = wrapped;
211
+ return () => {
212
+ source[name] = original;
213
+ };
214
+ }
215
+ catch (err) {
216
+ return () => {
217
+ // noop
218
+ };
219
+ // This can throw if multiple fill happens on a global object like XMLHttpRequest
220
+ // Fixes https://github.com/getsentry/sentry-javascript/issues/2043
221
+ }
222
+ }
223
+ exports.patch = patch;
@@ -2,4 +2,5 @@ export declare function _gatherEventData(target: Element): {
2
2
  value: string;
3
3
  metadata: Record<string, unknown>;
4
4
  };
5
+ export declare function _gatherCopyEventData(e: Event): Record<string, unknown>;
5
6
  export declare function _getMetadataFromElement(target: Element): Record<string, unknown>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._getMetadataFromElement = exports._gatherEventData = void 0;
3
+ exports._getMetadataFromElement = exports._gatherCopyEventData = exports._gatherEventData = void 0;
4
4
  const client_core_1 = require("@statsig/client-core");
5
5
  const commonUtils_1 = require("./commonUtils");
6
6
  const MAX_ATTRIBUTE_LENGTH = 1000;
@@ -30,6 +30,16 @@ function _gatherEventData(target) {
30
30
  return { value, metadata };
31
31
  }
32
32
  exports._gatherEventData = _gatherEventData;
33
+ function _gatherCopyEventData(e) {
34
+ var _a, _b;
35
+ const selectedText = (_b = (_a = (0, client_core_1._getWindowSafe)()) === null || _a === void 0 ? void 0 : _a.getSelection()) === null || _b === void 0 ? void 0 : _b.toString();
36
+ const metadata = {};
37
+ metadata['selectedText'] = (0, commonUtils_1._sanitizeString)(selectedText);
38
+ const clipType = e.type || 'clipboard';
39
+ metadata['clipType'] = clipType;
40
+ return metadata;
41
+ }
42
+ exports._gatherCopyEventData = _gatherCopyEventData;
33
43
  function _getFormMetadata(target) {
34
44
  var _a;
35
45
  const metadata = {};