mixpanel-browser 2.73.0 → 2.74.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.
Files changed (43) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/.eslintrc.json +7 -4
  3. package/.github/workflows/integration-tests.yml +52 -0
  4. package/.github/workflows/unit-tests.yml +40 -0
  5. package/CHANGELOG.md +7 -0
  6. package/README.md +1 -1
  7. package/build.sh +1 -5
  8. package/dist/mixpanel-core.cjs.d.ts +12 -1
  9. package/dist/mixpanel-core.cjs.js +115 -15
  10. package/dist/mixpanel-recorder.js +5255 -687
  11. package/dist/mixpanel-recorder.min.js +1 -1
  12. package/dist/mixpanel-recorder.min.js.map +1 -1
  13. package/dist/mixpanel-with-async-recorder.cjs.d.ts +12 -1
  14. package/dist/mixpanel-with-async-recorder.cjs.js +115 -15
  15. package/dist/mixpanel-with-recorder.d.ts +12 -1
  16. package/dist/mixpanel-with-recorder.js +6720 -2079
  17. package/dist/mixpanel-with-recorder.min.d.ts +12 -1
  18. package/dist/mixpanel-with-recorder.min.js +1 -1
  19. package/dist/mixpanel.amd.d.ts +12 -1
  20. package/dist/mixpanel.amd.js +6720 -2079
  21. package/dist/mixpanel.cjs.d.ts +12 -1
  22. package/dist/mixpanel.cjs.js +6720 -2079
  23. package/dist/mixpanel.globals.js +115 -15
  24. package/dist/mixpanel.min.js +174 -172
  25. package/dist/mixpanel.module.d.ts +12 -1
  26. package/dist/mixpanel.module.js +6720 -2079
  27. package/dist/mixpanel.umd.d.ts +12 -1
  28. package/dist/mixpanel.umd.js +6720 -2079
  29. package/dist/rrweb-bundled.js +4315 -591
  30. package/dist/rrweb-compiled.js +4962 -641
  31. package/package.json +30 -5
  32. package/rollup.config.mjs +254 -224
  33. package/src/autocapture/utils.js +15 -7
  34. package/src/config.js +1 -1
  35. package/src/index.d.ts +12 -1
  36. package/src/mixpanel-core.js +89 -5
  37. package/src/recorder/masking.js +197 -0
  38. package/src/recorder/rrweb-entrypoint.js +2 -1
  39. package/src/recorder/session-recording.js +43 -4
  40. package/src/recorder/utils.js +5 -1
  41. package/src/utils.js +11 -2
  42. package/testServer.js +51 -7
  43. package/.github/workflows/tests.yml +0 -25
package/src/index.d.ts CHANGED
@@ -6,6 +6,8 @@ export type PushItem = Array<string | Dict | ((this: Mixpanel) => void)>;
6
6
 
7
7
  export type Query = string | Element | Element[];
8
8
 
9
+ export type RemoteSettingType = "disabled" | "fallback" | "strict";
10
+
9
11
  export interface Dict {
10
12
  [key: string]: any;
11
13
  }
@@ -166,6 +168,8 @@ export interface Config {
166
168
  track?: string;
167
169
  engage?: string;
168
170
  groups?: string;
171
+ record?: string;
172
+ flags?: string;
169
173
  };
170
174
  api_method: string;
171
175
  api_transport: string;
@@ -225,12 +229,18 @@ export interface Config {
225
229
  record_idle_timeout_ms: number;
226
230
  record_inline_images: boolean;
227
231
  record_mask_text_class: string | RegExp;
228
- record_mask_text_selector: string;
232
+ record_mask_text_selector: string | string[];
233
+ record_unmask_text_selector: string | string[];
234
+ record_mask_all_text: boolean;
235
+ record_mask_input_selector: string | string[];
236
+ record_unmask_input_selector: string | string[];
237
+ record_mask_all_inputs: boolean;
229
238
  record_min_ms: number;
230
239
  record_max_ms: number;
231
240
  record_sessions_percent: number;
232
241
  record_canvas: boolean;
233
242
  record_heatmap_data: boolean;
243
+ remote_settings_mode: RemoteSettingType;
234
244
  hooks: {
235
245
  before_identify?: (new_distinct_id: string) => string | null;
236
246
  before_register?: (
@@ -256,6 +266,7 @@ export interface Config {
256
266
  };
257
267
  }
258
268
 
269
+
259
270
  export type VerboseResponse =
260
271
  | {
261
272
  status: 1;
@@ -61,6 +61,9 @@ var INIT_SNIPPET = 1;
61
61
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
62
62
  /** @const */ var PAYLOAD_TYPE_JSON = 'json';
63
63
  /** @const */ var DEVICE_ID_PREFIX = '$device:';
64
+ /** @const */ var SETTING_STRICT = 'strict';
65
+ /** @const */ var SETTING_FALLBACK = 'fallback';
66
+ /** @const */ var SETTING_DISABLED = 'disabled';
64
67
 
65
68
 
66
69
  /*
@@ -89,7 +92,8 @@ var DEFAULT_API_ROUTES = {
89
92
  'engage': 'engage/',
90
93
  'groups': 'groups/',
91
94
  'record': 'record/',
92
- 'flags': 'flags/'
95
+ 'flags': 'flags/',
96
+ 'settings': 'settings/'
93
97
  };
94
98
 
95
99
  /*
@@ -153,12 +157,12 @@ var DEFAULT_CONFIG = {
153
157
  'record_console': true,
154
158
  'record_heatmap_data': false,
155
159
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
156
- 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
157
- 'record_mask_text_selector': '*',
160
+ 'record_mask_inputs': true,
158
161
  'record_max_ms': MAX_RECORDING_MS,
159
162
  'record_min_ms': 0,
160
163
  'record_sessions_percent': 0,
161
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
164
+ 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
165
+ 'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
162
166
  };
163
167
 
164
168
  var DOM_LOADED = false;
@@ -396,7 +400,16 @@ MixpanelLib.prototype._init = function(token, config, name) {
396
400
  this.autocapture.init();
397
401
 
398
402
  this._init_tab_id();
399
- this._check_and_start_session_recording();
403
+
404
+ // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
405
+ var mode = this.get_config('remote_settings_mode');
406
+ if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
407
+ this._fetch_remote_settings(mode).then(_.bind(function() {
408
+ this._check_and_start_session_recording();
409
+ }, this));
410
+ } else {
411
+ this._check_and_start_session_recording();
412
+ }
400
413
  };
401
414
 
402
415
  /**
@@ -821,6 +834,77 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
821
834
  return succeeded;
822
835
  };
823
836
 
837
+ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
838
+ var disableRecordingIfStrict = function() {
839
+ if (mode === 'strict') {
840
+ self.set_config({'record_sessions_percent': 0});
841
+ }
842
+ };
843
+
844
+ if (!window['AbortController']) {
845
+ console.critical('Remote settings unavailable: missing minimum required APIs');
846
+ disableRecordingIfStrict();
847
+ return Promise.resolve();
848
+ }
849
+
850
+ var settings_endpoint = this.get_api_host('settings') + '/' + this.get_config('api_routes')['settings'];
851
+ var request_params = {
852
+ '$lib_version': Config.LIB_VERSION,
853
+ 'mp_lib': 'web',
854
+ 'sdk_config': '1',
855
+ };
856
+ var query_string = _.HTTPBuildQuery(request_params);
857
+ var full_url = settings_endpoint + '?' + query_string;
858
+ var self = this;
859
+
860
+ var abortController = new AbortController();
861
+ var timeout_id = setTimeout(function() {
862
+ abortController.abort();
863
+ }, 500);
864
+ var fetchOptions = {
865
+ 'method': 'GET',
866
+ 'headers': {
867
+ 'Authorization': 'Basic ' + btoa(self.get_config('token') + ':'),
868
+ },
869
+ 'signal': abortController.signal
870
+ };
871
+
872
+ return window['fetch'](full_url, fetchOptions).then(function(response) {
873
+ clearTimeout(timeout_id);
874
+ if (!response['ok']) {
875
+ console.critical('Network response was not ok');
876
+ disableRecordingIfStrict();
877
+ return;
878
+ }
879
+ return response.json();
880
+ }).then(function(result) {
881
+ if (result && result['sdk_config'] && result['sdk_config']['config']) {
882
+ var remote_config = result['sdk_config']['config'];
883
+
884
+ // Verify that remote config contains only valid keys from DEFAULT_CONFIG
885
+ var valid_config = {};
886
+ _.each(remote_config, function(value, key) {
887
+ if (DEFAULT_CONFIG.hasOwnProperty(key)) {
888
+ valid_config[key] = value;
889
+ }
890
+ });
891
+
892
+ if (_.isEmptyObject(valid_config)) {
893
+ console.critical('No valid config keys found in remote settings.');
894
+ disableRecordingIfStrict();
895
+ } else {
896
+ self.set_config(valid_config);
897
+ }
898
+ } else {
899
+ disableRecordingIfStrict();
900
+ }
901
+ }).catch(function(err) {
902
+ clearTimeout(timeout_id);
903
+ console.critical('Failed to fetch remote settings', err);
904
+ disableRecordingIfStrict();
905
+ });
906
+ };
907
+
824
908
  /**
825
909
  * _execute_array() deals with processing any mixpanel function
826
910
  * calls that were called before the Mixpanel library were loaded
@@ -0,0 +1,197 @@
1
+ /**
2
+ * @typedef {Object} MaskingConfig
3
+ * @property {string} maskingSelector - Combined CSS selector string to mask
4
+ * @property {string} unmaskingSelector - Combined CSS selector string to unmask
5
+ * @property {boolean} maskAll - Whether to mask all by default
6
+ * @property {RegExp} [_legacyClassRegex] - Legacy class regex for backwards compatibility
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} PrivacyConfig
11
+ * @property {MaskingConfig} input - Input masking configuration
12
+ * @property {MaskingConfig} text - Text masking configuration
13
+ */
14
+
15
+ import { classMatchesRegex } from './rrweb-entrypoint';
16
+ import { elementLooksSensitive } from '../autocapture/utils';
17
+
18
+ var COMMON_MASK_CLASSES_SELECTOR = '.mp-mask, .fs-mask, .amp-mask, .rr-mask, .ph-mask';
19
+ var RRWEB_PASSWORD_ATTRIBUTE = 'data-rr-is-password';
20
+ var ALWAYS_MASKED_INPUT_TYPES = ['password', 'email', 'tel', 'hidden'];
21
+
22
+ /**
23
+ * Normalizes a selector value to an array of selectors
24
+ * @param {string|string[]|undefined} selector
25
+ * @returns {string[]}
26
+ */
27
+ function normalizeSelectors(selector) {
28
+ if (!selector) {
29
+ return [];
30
+ }
31
+ if (Array.isArray(selector)) {
32
+ return selector;
33
+ }
34
+ return [selector];
35
+ }
36
+
37
+ /**
38
+ * Reads flat config options and normalizes them into internal privacy config structure
39
+ * @param {Object} mixpanelInstance
40
+ * @returns {PrivacyConfig} privacyConfig
41
+ */
42
+ function getPrivacyConfig(mixpanelInstance) {
43
+ var privacyConfig = {
44
+ input: {
45
+ maskingSelector: '',
46
+ unmaskingSelector: '',
47
+ maskAll: true
48
+ },
49
+ text: {
50
+ maskingSelector: '',
51
+ unmaskingSelector: '',
52
+ maskAll: true
53
+ }
54
+ };
55
+
56
+ // Input related config
57
+ var maskInputSelector = mixpanelInstance.get_config('record_mask_input_selector');
58
+ var unmaskInputSelector = mixpanelInstance.get_config('record_unmask_input_selector');
59
+ var maskAllInputs = mixpanelInstance.get_config('record_mask_all_inputs');
60
+
61
+ privacyConfig.input.maskingSelector = normalizeSelectors(maskInputSelector).join(',');
62
+ privacyConfig.input.unmaskingSelector = normalizeSelectors(unmaskInputSelector).join(',');
63
+ if (maskAllInputs !== undefined) {
64
+ privacyConfig.input.maskAll = maskAllInputs;
65
+ }
66
+
67
+ // Text related config
68
+ var maskTextSelector = mixpanelInstance.get_config('record_mask_text_selector');
69
+ var unmaskTextSelector = mixpanelInstance.get_config('record_unmask_text_selector');
70
+ var maskAllText = mixpanelInstance.get_config('record_mask_all_text');
71
+ var legacyMaskTextClass = mixpanelInstance.get_config('record_mask_text_class');
72
+
73
+ var textMaskingSelectors = normalizeSelectors(maskTextSelector);
74
+
75
+ // Handle legacy record_mask_text_class
76
+ if (legacyMaskTextClass) {
77
+ if (legacyMaskTextClass instanceof RegExp) {
78
+ // For RegExp classes, we'll need to handle this differently in the shouldMaskText function
79
+ privacyConfig.text._legacyClassRegex = legacyMaskTextClass;
80
+ } else {
81
+ // String class name - convert to selector
82
+ var classSelector = '.' + legacyMaskTextClass;
83
+ if (textMaskingSelectors.indexOf(classSelector) === -1) {
84
+ textMaskingSelectors.push(classSelector);
85
+ }
86
+ }
87
+ }
88
+
89
+ privacyConfig.text.maskingSelector = textMaskingSelectors.join(',');
90
+ privacyConfig.text.unmaskingSelector = normalizeSelectors(unmaskTextSelector).join(',');
91
+
92
+ // Migrate old config: if only record_mask_text_selector is specified, set maskAll to false
93
+ // preserves behavior where the masking selector defaulted to "*"
94
+ if (maskAllText === undefined && maskTextSelector !== undefined) {
95
+ privacyConfig.text.maskAll = false;
96
+ } else if (maskAllText !== undefined) {
97
+ privacyConfig.text.maskAll = maskAllText;
98
+ }
99
+
100
+ return privacyConfig;
101
+ }
102
+
103
+ /**
104
+ * Checks if element matches a combined CSS selector
105
+ * @param {HTMLElement} element
106
+ * @param {string} selector - Combined CSS selector (comma-separated)
107
+ * @returns {boolean}
108
+ */
109
+ function elementMatchesSelector(element, selector) {
110
+ if (!selector) {
111
+ return false;
112
+ }
113
+ return !!element.closest(selector);
114
+ }
115
+
116
+ /**
117
+ * Determines if an input should be masked based on privacy config
118
+ * @param {HTMLElement} element
119
+ * @param {PrivacyConfig} privacyConfig
120
+ * @returns {boolean}
121
+ */
122
+ function shouldMaskInput(element, privacyConfig) {
123
+ var inputType = (element.getAttribute('type') || '').toLowerCase();
124
+ if (ALWAYS_MASKED_INPUT_TYPES.indexOf(inputType) !== -1) {
125
+ return true;
126
+ }
127
+
128
+ var autocomplete = (element.getAttribute('autocomplete') || '').toLowerCase();
129
+ if (autocomplete && autocomplete !== '' && autocomplete !== 'off') {
130
+ return true;
131
+ }
132
+
133
+ if (element.hasAttribute(RRWEB_PASSWORD_ATTRIBUTE)) {
134
+ return true;
135
+ }
136
+
137
+ if (elementLooksSensitive(element)) {
138
+ return true;
139
+ }
140
+
141
+ if (privacyConfig.input.maskAll) {
142
+ if (elementMatchesSelector(element, privacyConfig.input.unmaskingSelector)) {
143
+ return false;
144
+ }
145
+
146
+ return true;
147
+ } else {
148
+ if (elementMatchesSelector(element, privacyConfig.input.maskingSelector)) {
149
+ return true;
150
+ }
151
+ if (elementMatchesSelector(element, COMMON_MASK_CLASSES_SELECTOR)) {
152
+ return true;
153
+ }
154
+ return false;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Determines if text should be masked based on privacy config
160
+ * @param {HTMLElement|null} element
161
+ * @param {PrivacyConfig} privacyConfig
162
+ * @returns {boolean}
163
+ */
164
+ function shouldMaskText(element, privacyConfig) {
165
+ if (!element) {
166
+ return false;
167
+ }
168
+
169
+ // Check legacy class regex if present (for backwards compatibility)
170
+ if (privacyConfig.text._legacyClassRegex) {
171
+ if (classMatchesRegex(element, privacyConfig.text._legacyClassRegex, true)) {
172
+ return true;
173
+ }
174
+ }
175
+
176
+ if (privacyConfig.text.maskAll) {
177
+ if (elementMatchesSelector(element, privacyConfig.text.unmaskingSelector)) {
178
+ return false;
179
+ }
180
+ return true;
181
+ } else {
182
+ if (elementMatchesSelector(element, privacyConfig.text.maskingSelector)) {
183
+ return true;
184
+ }
185
+ if (elementMatchesSelector(element, COMMON_MASK_CLASSES_SELECTOR)) {
186
+ return true;
187
+ }
188
+ return false;
189
+ }
190
+ }
191
+
192
+ export {
193
+ COMMON_MASK_CLASSES_SELECTOR,
194
+ getPrivacyConfig,
195
+ shouldMaskInput,
196
+ shouldMaskText
197
+ };
@@ -1,6 +1,7 @@
1
1
  // this file exists as an entry point to be able to transpile rrweb packages to es5
2
2
  // compatible code without needing to transpile the entire mixpanel-js codebase
3
3
  import {record, EventType, IncrementalSource} from '@mixpanel/rrweb';
4
+ import {classMatchesRegex} from '@mixpanel/rrweb-snapshot';
4
5
  import {getRecordConsolePlugin} from '@mixpanel/rrweb-plugin-console-record';
5
6
 
6
- export { record, EventType, IncrementalSource, getRecordConsolePlugin };
7
+ export { record, EventType, IncrementalSource, getRecordConsolePlugin, classMatchesRegex };
@@ -1,11 +1,17 @@
1
+ /**
2
+ * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
3
+ */
4
+
1
5
  import { window } from '../window';
2
- import { IncrementalSource, EventType, getRecordConsolePlugin } from './rrweb-entrypoint';
6
+ import { EventType, getRecordConsolePlugin, IncrementalSource } from './rrweb-entrypoint';
3
7
  import { MAX_RECORDING_MS, MAX_VALUE_FOR_MIN_RECORDING_MS, console_with_prefix, NOOP_FUNC, _, localStorageSupported, canUseCompressionStream, navigator, userAgent, windowOpera} from '../utils'; // eslint-disable-line camelcase
4
8
  import { IDBStorageWrapper, RECORDING_EVENTS_STORE_NAME } from '../storage/indexed-db';
5
9
  import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
6
10
  import { RequestBatcher } from '../request-batcher';
11
+
7
12
  import Config from '../config';
8
13
  import { RECORD_ENQUEUE_THROTTLE_MS } from './utils';
14
+ import { shouldMaskInput, shouldMaskText, getPrivacyConfig } from './masking';
9
15
 
10
16
  var logger = console_with_prefix('recorder');
11
17
  var CompressionStream = window['CompressionStream'];
@@ -52,7 +58,7 @@ function isUserEvent(ev) {
52
58
  * @property {String} [options.replayId] - unique uuid for a single replay
53
59
  * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
54
60
  * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
55
- * @property {Function} [options.rrwebRecord] - rrweb's `record` function
61
+ * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
56
62
  * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
57
63
  * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
58
64
  * optional properties for deserialization:
@@ -233,6 +239,8 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
233
239
  blockSelector = undefined;
234
240
  }
235
241
 
242
+ var privacyConfig = getPrivacyConfig(this._mixpanel);
243
+
236
244
  try {
237
245
  this._stopRecording = this._rrwebRecord({
238
246
  'emit': function (ev) {
@@ -262,9 +270,11 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
262
270
  'type': 'image/webp',
263
271
  'quality': 0.6
264
272
  },
273
+ // mask all inputs and text by default, unmasking is applied via callbacks maskInputFn and maskTextFn
265
274
  'maskAllInputs': true,
266
- 'maskTextClass': this.getConfig('record_mask_text_class'),
267
- 'maskTextSelector': this.getConfig('record_mask_text_selector'),
275
+ 'maskTextSelector': '*',
276
+ 'maskInputFn': this._getMaskFn(shouldMaskInput, privacyConfig),
277
+ 'maskTextFn': this._getMaskFn(shouldMaskText, privacyConfig),
268
278
  'recordCanvas': this.getConfig('record_canvas'),
269
279
  'sampling': {
270
280
  'canvas': 15
@@ -536,4 +546,33 @@ SessionRecording.prototype._getRecordMinMs = function() {
536
546
  return configValue;
537
547
  };
538
548
 
549
+ /**
550
+ * Creates a masking function for rrweb's maskInputFn or maskTextFn
551
+ * @param {(element: HTMLElement, privacyConfig: RecordPrivacyConfig) => boolean} shouldMaskFn - Function that determines if element should be masked
552
+ * @param {RecordPrivacyConfig} privacyConfig - Privacy configuration
553
+ * @returns {(text: string, element: HTMLElement) => string} Function that masks text based on privacy config
554
+ */
555
+ SessionRecording.prototype._getMaskFn = function(shouldMaskFn, privacyConfig) {
556
+ return function(text, element) {
557
+ // prevent visual artifacts from random whitespace
558
+ if (!text.trim().length) {
559
+ return '';
560
+ }
561
+
562
+ var shouldMask = true;
563
+ try {
564
+ shouldMask = shouldMaskFn(element, privacyConfig);
565
+ } catch (err) {
566
+ this.reportError('Error checking if text should be masked, defaulting to masked', err);
567
+ }
568
+
569
+ if (shouldMask) {
570
+ var textLength = Math.min(text.length, 10000); // limit to 10000 chars to optimize performance
571
+ return '*'.repeat(textLength);
572
+ } else {
573
+ return text;
574
+ }
575
+ }.bind(this);
576
+ };
577
+
539
578
  export { SessionRecording };
@@ -7,6 +7,10 @@ var isRecordingExpired = function(serializedRecording) {
7
7
  return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
8
8
  };
9
9
 
10
+
10
11
  var RECORD_ENQUEUE_THROTTLE_MS = 250;
11
12
 
12
- export { isRecordingExpired, RECORD_ENQUEUE_THROTTLE_MS};
13
+ export {
14
+ isRecordingExpired,
15
+ RECORD_ENQUEUE_THROTTLE_MS
16
+ };
package/src/utils.js CHANGED
@@ -1115,8 +1115,17 @@ function _storageWrapper(storage, name, is_supported_fn) {
1115
1115
  };
1116
1116
  }
1117
1117
 
1118
- _.localStorage = _storageWrapper(window.localStorage, 'localStorage', localStorageSupported);
1119
- _.sessionStorage = _storageWrapper(window.sessionStorage, 'sessionStorage', sessionStorageSupported);
1118
+ // Safari errors out accessing localStorage/sessionStorage when cookies are disabled,
1119
+ // so create dummy storage wrappers that silently fail as a fallback.
1120
+ var windowLocalStorage = null, windowSessionStorage = null;
1121
+ try {
1122
+ windowLocalStorage = window.localStorage;
1123
+ windowSessionStorage = window.sessionStorage;
1124
+ // eslint-disable-next-line no-empty
1125
+ } catch (_err) {}
1126
+
1127
+ _.localStorage = _storageWrapper(windowLocalStorage, 'localStorage', localStorageSupported);
1128
+ _.sessionStorage = _storageWrapper(windowSessionStorage, 'sessionStorage', sessionStorageSupported);
1120
1129
 
1121
1130
  _.register_event = (function() {
1122
1131
  // written by Dean Edwards, 2005
package/testServer.js CHANGED
@@ -1,14 +1,40 @@
1
1
  'use strict';
2
2
 
3
- var express = require('express');
4
- var cookieParser = require('cookie-parser');
5
- var logger = require('morgan');
3
+ const express = require('express');
4
+ const cookieParser = require('cookie-parser');
5
+ const logger = require('morgan');
6
6
 
7
- var app = express();
7
+ const app = express();
8
+
9
+ // Test suite definitions
10
+ const TEST_SUITES = {
11
+ 'dev': {
12
+ name: 'Development Build',
13
+ description: 'Unminified library for easier debugging',
14
+ customLibUrl: './static/build/mixpanel.js',
15
+ snippetUrl: './static/src/loaders/mixpanel-jslib-snippet.js',
16
+ testUrl: './static/build/test/browser/snippet-test.js'
17
+ },
18
+ 'minified': {
19
+ name: 'Minified Build',
20
+ description: 'Production build (closure compiled, served via Mixpanel CDN)',
21
+ customLibUrl: './static/build/mixpanel.min.js',
22
+ snippetUrl: './static/build/mixpanel-jslib-snippet.min.test.js',
23
+ testUrl: './static/build/test/browser/snippet-test.js'
24
+ },
25
+ 'module-cjs': {
26
+ name: 'CommonJS Module',
27
+ description: 'Node.js compatible module (served via npm install)',
28
+ testUrl: './static/build/test/browser/module-cjs-test.js'
29
+ }
30
+ };
8
31
 
9
32
  app.use(cookieParser());
10
33
  app.use(logger('dev'));
11
34
 
35
+ app.set('views', __dirname + '/tests');
36
+ app.set('view engine', 'pug');
37
+
12
38
  app.use('/tests', express.static(__dirname + "/tests"));
13
39
  app.get('/tests/cookie_included/:cookieName', function(req, res) {
14
40
  if (req.cookies && req.cookies[req.params.cookieName]) {
@@ -19,9 +45,27 @@ app.get('/tests/cookie_included/:cookieName', function(req, res) {
19
45
  });
20
46
  app.use(express.static(__dirname));
21
47
  app.get('/', function(req, res) {
22
- res.redirect(301, '/tests/');
48
+ res.redirect(301, '/tests/new');
23
49
  });
24
50
 
25
- var server = app.listen(3000, function () {
26
- console.log('Mixpanel test app listening on port %s', server.address().port);
51
+ app.get('/tests/new', function(req, res) {
52
+ res.render('directory.pug', { testSuites: TEST_SUITES });
53
+ });
54
+
55
+ app.use('/tests/new/static', express.static(__dirname));
56
+
57
+ // register routes for each test suite
58
+ for (const [suiteId, suite] of Object.entries(TEST_SUITES)) {
59
+ app.get('/tests/new/' + suiteId, function(req, res) {
60
+ res.render('integration.pug', {
61
+ suiteName: suite.name,
62
+ customLibUrl: suite.customLibUrl,
63
+ snippetUrl: suite.snippetUrl,
64
+ testUrl: suite.testUrl
65
+ });
66
+ });
67
+ }
68
+
69
+ const server = app.listen(3001, function () {
70
+ console.log(`Mixpanel test app listening on port ${server.address().port}`);
27
71
  });
@@ -1,25 +0,0 @@
1
- name: Tests
2
-
3
- on:
4
- push:
5
- branches: [master]
6
- pull_request:
7
- branches: [master]
8
-
9
- jobs:
10
- build:
11
- runs-on: ubuntu-latest
12
-
13
- strategy:
14
- matrix:
15
- node-version: [20.x, 22.x]
16
-
17
- steps:
18
- - uses: actions/checkout@v4
19
- - name: Use Node.js ${{ matrix.node-version }}
20
- uses: actions/setup-node@v4
21
- with:
22
- node-version: ${{ matrix.node-version }}
23
- - run: npm ci
24
- - run: npm test
25
- - run: npm run build-dist