mixpanel-browser 2.73.0 → 2.75.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 (55) hide show
  1. package/.eslintrc.json +7 -4
  2. package/.github/workflows/integration-tests.yml +52 -0
  3. package/.github/workflows/unit-tests.yml +40 -0
  4. package/CHANGELOG.md +12 -0
  5. package/README.md +3 -3
  6. package/build.sh +1 -5
  7. package/dist/mixpanel-core.cjs.d.ts +12 -1
  8. package/dist/mixpanel-core.cjs.js +432 -34
  9. package/dist/mixpanel-recorder.js +5364 -684
  10. package/dist/mixpanel-recorder.min.js +1 -1
  11. package/dist/mixpanel-recorder.min.js.map +1 -1
  12. package/dist/mixpanel-targeting.js +2576 -0
  13. package/dist/mixpanel-targeting.min.js +2 -0
  14. package/dist/mixpanel-targeting.min.js.map +1 -0
  15. package/dist/mixpanel-with-async-modules.cjs.d.ts +522 -0
  16. package/dist/mixpanel-with-async-modules.cjs.js +9700 -0
  17. package/dist/mixpanel-with-async-recorder.cjs.d.ts +12 -1
  18. package/dist/mixpanel-with-async-recorder.cjs.js +432 -34
  19. package/dist/mixpanel-with-recorder.d.ts +12 -1
  20. package/dist/mixpanel-with-recorder.js +7889 -2839
  21. package/dist/mixpanel-with-recorder.min.d.ts +12 -1
  22. package/dist/mixpanel-with-recorder.min.js +1 -1
  23. package/dist/mixpanel.amd.d.ts +12 -1
  24. package/dist/mixpanel.amd.js +8446 -2813
  25. package/dist/mixpanel.cjs.d.ts +12 -1
  26. package/dist/mixpanel.cjs.js +8446 -2813
  27. package/dist/mixpanel.globals.js +432 -34
  28. package/dist/mixpanel.min.js +182 -173
  29. package/dist/mixpanel.module.d.ts +12 -1
  30. package/dist/mixpanel.module.js +8446 -2813
  31. package/dist/mixpanel.umd.d.ts +12 -1
  32. package/dist/mixpanel.umd.js +8446 -2813
  33. package/dist/rrweb-bundled.js +4434 -596
  34. package/dist/rrweb-compiled.js +5078 -646
  35. package/package.json +33 -7
  36. package/rollup.config.mjs +286 -224
  37. package/src/autocapture/utils.js +15 -7
  38. package/src/config.js +1 -1
  39. package/src/flags/index.js +269 -8
  40. package/src/globals.js +14 -0
  41. package/src/index.d.ts +12 -1
  42. package/src/loaders/loader-module.js +1 -0
  43. package/src/mixpanel-core.js +101 -8
  44. package/src/recorder/index.js +2 -1
  45. package/src/recorder/masking.js +197 -0
  46. package/src/recorder/rrweb-entrypoint.js +2 -1
  47. package/src/recorder/session-recording.js +43 -4
  48. package/src/recorder/utils.js +5 -1
  49. package/src/targeting/event-matcher.js +97 -0
  50. package/src/targeting/index.js +11 -0
  51. package/src/targeting/loader.js +36 -0
  52. package/src/utils.js +12 -10
  53. package/testServer.js +51 -7
  54. package/.github/workflows/tests.yml +0 -25
  55. /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +0 -0
@@ -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
+ };
@@ -0,0 +1,97 @@
1
+ import { _ } from '../utils';
2
+ import jsonLogic from 'json-logic-js';
3
+
4
+ /**
5
+ * Shared helper to recursively lowercase strings in nested structures
6
+ * @param {*} obj - Value to process
7
+ * @param {boolean} lowercaseKeys - Whether to lowercase object keys
8
+ * @returns {*} Processed value with lowercased strings
9
+ */
10
+ var lowercaseJson = function(obj, lowercaseKeys) {
11
+ if (obj === null || obj === undefined) {
12
+ return obj;
13
+ } else if (typeof obj === 'string') {
14
+ return obj.toLowerCase();
15
+ } else if (Array.isArray(obj)) {
16
+ return obj.map(function(item) {
17
+ return lowercaseJson(item, lowercaseKeys);
18
+ });
19
+ } else if (obj === Object(obj)) {
20
+ var result = {};
21
+ for (var key in obj) {
22
+ if (obj.hasOwnProperty(key)) {
23
+ var newKey = lowercaseKeys && typeof key === 'string' ? key.toLowerCase() : key;
24
+ result[newKey] = lowercaseJson(obj[key], lowercaseKeys);
25
+ }
26
+ }
27
+ return result;
28
+ } else {
29
+ return obj;
30
+ }
31
+ };
32
+
33
+ /**
34
+ * Lowercase all string keys and values in a nested structure
35
+ * @param {*} val - Value to process
36
+ * @returns {*} Processed value with lowercased strings
37
+ */
38
+ var lowercaseKeysAndValues = function(val) {
39
+ return lowercaseJson(val, true);
40
+ };
41
+
42
+ /**
43
+ * Lowercase only leaf node string values in a nested structure (keys unchanged)
44
+ * @param {*} val - Value to process
45
+ * @returns {*} Processed value with lowercased leaf strings
46
+ */
47
+ var lowercaseOnlyLeafNodes = function(val) {
48
+ return lowercaseJson(val, false);
49
+ };
50
+
51
+ /**
52
+ * Check if an event matches the given criteria
53
+ * @param {string} eventName - The name of the event being checked
54
+ * @param {Object} properties - Event properties to evaluate against property filters
55
+ * @param {Object} criteria - Criteria to match against, with:
56
+ * - event_name: string - Required event name (case-sensitive match)
57
+ * - property_filters: Object - Optional JsonLogic filters for properties
58
+ * @returns {Object} Result object with:
59
+ * - matches: boolean - Whether the event matches the criteria
60
+ * - error: string|undefined - Error message if evaluation failed
61
+ */
62
+ var eventMatchesCriteria = function(eventName, properties, criteria) {
63
+ // Check exact event name match (case-sensitive)
64
+ if (eventName !== criteria.event_name) {
65
+ return { matches: false };
66
+ }
67
+
68
+ // Evaluate property filters using JsonLogic
69
+ var propertyFilters = criteria.property_filters;
70
+ var filtersMatch = true; // default to true if no filters
71
+
72
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
73
+ try {
74
+ // Lowercase all keys and values in event properties for case-insensitive matching
75
+ var lowercasedProperties = lowercaseKeysAndValues(properties || {});
76
+
77
+ // Lowercase only leaf nodes in JsonLogic filters (keep operators intact)
78
+ var lowercasedFilters = lowercaseOnlyLeafNodes(propertyFilters);
79
+
80
+ filtersMatch = jsonLogic.apply(lowercasedFilters, lowercasedProperties);
81
+ } catch (error) {
82
+ return {
83
+ matches: false,
84
+ error: error.toString()
85
+ };
86
+ }
87
+ }
88
+
89
+ return { matches: filtersMatch };
90
+ };
91
+
92
+ export {
93
+ lowercaseJson,
94
+ lowercaseKeysAndValues,
95
+ lowercaseOnlyLeafNodes,
96
+ eventMatchesCriteria
97
+ };
@@ -0,0 +1,11 @@
1
+ import { window } from '../window';
2
+ import { TARGETING_GLOBAL_NAME } from '../globals';
3
+ import { eventMatchesCriteria } from './event-matcher';
4
+
5
+ // Create targeting library object
6
+ var targetingLibrary = {};
7
+ targetingLibrary['eventMatchesCriteria'] = eventMatchesCriteria;
8
+
9
+ // Set global Promise (use bracket notation to prevent minification)
10
+ // This is the ONE AND ONLY global - matches recorder pattern
11
+ window[TARGETING_GLOBAL_NAME] = Promise.resolve(targetingLibrary);
@@ -0,0 +1,36 @@
1
+ import { window } from '../window';
2
+ import { TARGETING_GLOBAL_NAME } from '../globals';
3
+
4
+ /**
5
+ * Get the promise-based targeting loader
6
+ * @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
7
+ * @param {string} targetingSrc - URL to targeting bundle
8
+ * @returns {Promise} Promise that resolves with targeting library
9
+ */
10
+ var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
11
+ // Return existing promise if already initialized or loading
12
+ if (window[TARGETING_GLOBAL_NAME] && typeof window[TARGETING_GLOBAL_NAME].then === 'function') {
13
+ return window[TARGETING_GLOBAL_NAME];
14
+ }
15
+
16
+ // Create loading promise and set it as the global immediately
17
+ // This makes minified build behavior consistent with dev/CJS builds
18
+ window[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
19
+ loadExtraBundle(targetingSrc, resolve);
20
+ }).then(function () {
21
+ var p = window[TARGETING_GLOBAL_NAME];
22
+ if (p && typeof p.then === 'function') {
23
+ return p;
24
+ }
25
+ throw new Error('targeting failed to load');
26
+ }).catch(function (err) {
27
+ delete window[TARGETING_GLOBAL_NAME];
28
+ throw err;
29
+ });
30
+
31
+ return window[TARGETING_GLOBAL_NAME];
32
+ };
33
+
34
+ export {
35
+ getTargetingPromise
36
+ };
package/src/utils.js CHANGED
@@ -204,15 +204,8 @@ _.isArray = nativeIsArray || function(obj) {
204
204
  return toString.call(obj) === '[object Array]';
205
205
  };
206
206
 
207
- // from a comment on http://dbj.org/dbj/?p=286
208
- // fails on only one very rare and deliberate custom object:
209
- // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
210
207
  _.isFunction = function(f) {
211
- try {
212
- return /^\s*\bfunction\b/.test(f);
213
- } catch (x) {
214
- return false;
215
- }
208
+ return typeof f === 'function';
216
209
  };
217
210
 
218
211
  _.isArguments = function(obj) {
@@ -1115,8 +1108,17 @@ function _storageWrapper(storage, name, is_supported_fn) {
1115
1108
  };
1116
1109
  }
1117
1110
 
1118
- _.localStorage = _storageWrapper(window.localStorage, 'localStorage', localStorageSupported);
1119
- _.sessionStorage = _storageWrapper(window.sessionStorage, 'sessionStorage', sessionStorageSupported);
1111
+ // Safari errors out accessing localStorage/sessionStorage when cookies are disabled,
1112
+ // so create dummy storage wrappers that silently fail as a fallback.
1113
+ var windowLocalStorage = null, windowSessionStorage = null;
1114
+ try {
1115
+ windowLocalStorage = window.localStorage;
1116
+ windowSessionStorage = window.sessionStorage;
1117
+ // eslint-disable-next-line no-empty
1118
+ } catch (_err) {}
1119
+
1120
+ _.localStorage = _storageWrapper(windowLocalStorage, 'localStorage', localStorageSupported);
1121
+ _.sessionStorage = _storageWrapper(windowSessionStorage, 'sessionStorage', sessionStorageSupported);
1120
1122
 
1121
1123
  _.register_event = (function() {
1122
1124
  // 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