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
@@ -0,0 +1,12 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(mkdir:*)",
5
+ "Bash(cat:*)",
6
+ "Bash(node --input-type=module -e \"import { expect } from 'chai'; console.log\\('works'\\);\":*)",
7
+ "Bash(BABEL_ENV=test node:*)"
8
+ ],
9
+ "deny": [],
10
+ "ask": []
11
+ }
12
+ }
package/.eslintrc.json CHANGED
@@ -8,7 +8,9 @@
8
8
  "sourceType": "module"
9
9
  },
10
10
  "rules": {
11
- "camelcase": "error",
11
+ "camelcase": ["error", {
12
+ "properties": "never"
13
+ }],
12
14
  "eol-last": "error",
13
15
  "eqeqeq": "error",
14
16
  "indent":
@@ -29,13 +31,14 @@
29
31
  ]
30
32
  },
31
33
  "overrides": [{
32
- "files": ["tests/unit/**/*.js"],
34
+ "files": ["tests/unit/**/*.js", "tests/browser/**/*.js", "*.mjs"],
33
35
  "parserOptions": {
34
- "ecmaVersion": 8,
36
+ "ecmaVersion": 9,
35
37
  "sourceType": "module"
36
38
  },
37
39
  "env": {
38
- "mocha": true
40
+ "mocha": true,
41
+ "node": true
39
42
  },
40
43
  "rules": {
41
44
  "camelcase": "error",
@@ -0,0 +1,52 @@
1
+ name: Integration 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
+ permissions:
14
+ contents: read
15
+ checks: write # for dorny/test-reporter to post results
16
+
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ browser: [chrome-latest, edge-latest, safari-latest, firefox-latest, ios-safari-sim, android-chrome-sim]
21
+
22
+ env:
23
+ SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
24
+ SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
25
+ SAUCE_TUNNEL_NAME: ci-js-sdk-test-${{ matrix.browser }}-${{ github.run_id }}
26
+
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - name: Use Node.js 22.x
30
+ uses: actions/setup-node@v4
31
+ with:
32
+ node-version: 22.x
33
+
34
+ - name: Install Dependencies
35
+ run: npm ci
36
+
37
+ - name: Build
38
+ run: npm run build-full
39
+
40
+ - name: Sauce test
41
+ # for some reason, android emulator tests don't work with localhost so we need an IP to give to the runner...
42
+ run: BROWSER=${{ matrix.browser }} ROOT_DIR=$(pwd) SAUCE_HOST=$(hostname -I | awk '{print $1; exit}') npm run integration-test:sauce
43
+
44
+ - name: Test Report
45
+ uses: dorny/test-reporter@7b7927aa7da8b82e81e755810cb51f39941a2cc7 # v2
46
+ if: success() || failure() # run this step even if previous step failed
47
+ with:
48
+ name: Browser Tests # Name of the check run which will be created
49
+ reporter: mocha-json # Format of test results
50
+ path: 'tests/browser/results/*.json' # Path to test results
51
+ list-tests: 'failed'
52
+ fail-on-error: 'false'
@@ -0,0 +1,40 @@
1
+ name: Unit 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
+ permissions:
14
+ contents: read
15
+ checks: write # for dorny/test-reporter to post results
16
+
17
+ strategy:
18
+ matrix:
19
+ node-version: [20.x, 22.x]
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - name: Use Node.js ${{ matrix.node-version }}
24
+ uses: actions/setup-node@v4
25
+ with:
26
+ node-version: ${{ matrix.node-version }}
27
+ - run: npm ci
28
+ - run: npm run test:ci
29
+ - run: npm run build-dist
30
+
31
+ - name: Test Report
32
+ uses: dorny/test-reporter@7b7927aa7da8b82e81e755810cb51f39941a2cc7 # v2
33
+ if: success() || failure()
34
+ with:
35
+ name: Unit Tests
36
+ reporter: mocha-json
37
+ path: 'tests/unit/results/*.json'
38
+ list-tests: 'failed'
39
+ fail-on-error: 'false'
40
+
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ **2.74.0** (27 Jan 2026)
2
+ - New session recording masking configuration options, including the ability to unmask inputs and allowlist-based selector masking.
3
+ - Adds initial support for the remote settings API, allowing remote configuration of SDK config options.
4
+ - Adds new automated browser test suite that runs in CI and locally in chrome headless. See tests/browser/README.html for more information.
5
+ - Fixes type definitions for api_routes
6
+ - Removes outdated examples in the examples/ dir
7
+
1
8
  **2.73.0** (23 Dec 2025)
2
9
  - Adds several new hooks: `before_identify`, `before_register`, `before_register_once`, `before_track`, `before_unregister`
3
10
  - Adds instance-initialization notification to allow Data Inspector browser extension to hook into SDK actions
package/README.md CHANGED
@@ -67,7 +67,7 @@ mixpanel.init('YOUR_TOKEN', {autocapture: true, debug: true, persistence: 'local
67
67
  - Install development dependencies: `npm install`
68
68
  - Run unit tests: `npm test`
69
69
  - Start test server for browser tests: `npm run integration_test`
70
- - Browse to [http://localhost:3000/tests/](http://localhost:3000/tests/) and choose a scenario to run
70
+ - Browse to [http://localhost:3001/tests/](http://localhost:3001/tests/) and choose a scenario to run
71
71
 
72
72
  In the future we plan to automate the last step with a headless browser to streamline development (although
73
73
  Mixpanel production releases are tested against a large matrix of browsers and operating systems).
package/build.sh CHANGED
@@ -19,15 +19,11 @@ if [ ! -z "$FULL" ]; then
19
19
  npx webpack tests/module-cjs.js tests/module-cjs.bundle.js
20
20
  npx browserify tests/module-es2015.js -t [ babelify --compact false ] --outfile tests/module-es2015.bundle.js
21
21
 
22
- echo 'Bundling module-loader examples'
23
- pushd examples/commonjs-browserify; npm install && npm run build; popd
24
- pushd examples/es2015-babelify; npm install && npm run build; popd
25
- pushd examples/umd-webpack; npm install && npm run build; popd
26
22
  pushd examples/typescript; npm install && npm run build; popd
27
23
  fi
28
24
 
29
25
  if [ ! -z "$DIST" ]; then
30
26
  echo 'Copying to dist/'
31
27
  rm -r dist
32
- cp -r build dist
28
+ rsync -av --exclude='test' build/ dist/
33
29
  fi
@@ -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;
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.73.0'
5
+ LIB_VERSION: '2.74.0'
6
6
  };
7
7
 
8
8
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -1504,8 +1504,17 @@ function _storageWrapper(storage, name, is_supported_fn) {
1504
1504
  };
1505
1505
  }
1506
1506
 
1507
- _.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
1508
- _.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
1507
+ // Safari errors out accessing localStorage/sessionStorage when cookies are disabled,
1508
+ // so create dummy storage wrappers that silently fail as a fallback.
1509
+ var windowLocalStorage = null, windowSessionStorage = null;
1510
+ try {
1511
+ windowLocalStorage = win.localStorage;
1512
+ windowSessionStorage = win.sessionStorage;
1513
+ // eslint-disable-next-line no-empty
1514
+ } catch (_err) {}
1515
+
1516
+ _.localStorage = _storageWrapper(windowLocalStorage, 'localStorage', localStorageSupported);
1517
+ _.sessionStorage = _storageWrapper(windowSessionStorage, 'sessionStorage', sessionStorageSupported);
1509
1518
 
1510
1519
  _.register_event = (function() {
1511
1520
  // written by Dean Edwards, 2005
@@ -2655,6 +2664,18 @@ function shouldTrackDomEvent(el, ev) {
2655
2664
  }
2656
2665
  }
2657
2666
 
2667
+ function elementLooksSensitive(el) {
2668
+ var name = (el.name || el.id || '').toString().toLowerCase();
2669
+ if (typeof name === 'string') { // it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"]
2670
+ var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
2671
+ if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
2672
+ return true;
2673
+ }
2674
+ }
2675
+
2676
+ return false;
2677
+ }
2678
+
2658
2679
  /*
2659
2680
  * Check whether a DOM element should be "tracked" or if it may contain sensitive data
2660
2681
  * using a variety of heuristics.
@@ -2707,13 +2728,8 @@ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)
2707
2728
  }
2708
2729
  }
2709
2730
 
2710
- // filter out data from fields that look like sensitive fields
2711
- var name = el.name || el.id || '';
2712
- if (typeof name === 'string') { // it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"]
2713
- var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
2714
- if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
2715
- return false;
2716
- }
2731
+ if (elementLooksSensitive(el)) {
2732
+ return false;
2717
2733
  }
2718
2734
 
2719
2735
  return true;
@@ -6841,6 +6857,9 @@ var INIT_SNIPPET = 1;
6841
6857
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
6842
6858
  /** @const */ var PAYLOAD_TYPE_JSON = 'json';
6843
6859
  /** @const */ var DEVICE_ID_PREFIX = '$device:';
6860
+ /** @const */ var SETTING_STRICT = 'strict';
6861
+ /** @const */ var SETTING_FALLBACK = 'fallback';
6862
+ /** @const */ var SETTING_DISABLED = 'disabled';
6844
6863
 
6845
6864
 
6846
6865
  /*
@@ -6869,7 +6888,8 @@ var DEFAULT_API_ROUTES = {
6869
6888
  'engage': 'engage/',
6870
6889
  'groups': 'groups/',
6871
6890
  'record': 'record/',
6872
- 'flags': 'flags/'
6891
+ 'flags': 'flags/',
6892
+ 'settings': 'settings/'
6873
6893
  };
6874
6894
 
6875
6895
  /*
@@ -6933,12 +6953,12 @@ var DEFAULT_CONFIG = {
6933
6953
  'record_console': true,
6934
6954
  'record_heatmap_data': false,
6935
6955
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
6936
- 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
6937
- 'record_mask_text_selector': '*',
6956
+ 'record_mask_inputs': true,
6938
6957
  'record_max_ms': MAX_RECORDING_MS,
6939
6958
  'record_min_ms': 0,
6940
6959
  'record_sessions_percent': 0,
6941
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
6960
+ 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
6961
+ 'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
6942
6962
  };
6943
6963
 
6944
6964
  var DOM_LOADED = false;
@@ -7176,7 +7196,16 @@ MixpanelLib.prototype._init = function(token, config, name) {
7176
7196
  this.autocapture.init();
7177
7197
 
7178
7198
  this._init_tab_id();
7179
- this._check_and_start_session_recording();
7199
+
7200
+ // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
7201
+ var mode = this.get_config('remote_settings_mode');
7202
+ if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
7203
+ this._fetch_remote_settings(mode).then(_.bind(function() {
7204
+ this._check_and_start_session_recording();
7205
+ }, this));
7206
+ } else {
7207
+ this._check_and_start_session_recording();
7208
+ }
7180
7209
  };
7181
7210
 
7182
7211
  /**
@@ -7601,6 +7630,77 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
7601
7630
  return succeeded;
7602
7631
  };
7603
7632
 
7633
+ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
7634
+ var disableRecordingIfStrict = function() {
7635
+ if (mode === 'strict') {
7636
+ self.set_config({'record_sessions_percent': 0});
7637
+ }
7638
+ };
7639
+
7640
+ if (!win['AbortController']) {
7641
+ console.critical('Remote settings unavailable: missing minimum required APIs');
7642
+ disableRecordingIfStrict();
7643
+ return Promise.resolve();
7644
+ }
7645
+
7646
+ var settings_endpoint = this.get_api_host('settings') + '/' + this.get_config('api_routes')['settings'];
7647
+ var request_params = {
7648
+ '$lib_version': Config.LIB_VERSION,
7649
+ 'mp_lib': 'web',
7650
+ 'sdk_config': '1',
7651
+ };
7652
+ var query_string = _.HTTPBuildQuery(request_params);
7653
+ var full_url = settings_endpoint + '?' + query_string;
7654
+ var self = this;
7655
+
7656
+ var abortController = new AbortController();
7657
+ var timeout_id = setTimeout(function() {
7658
+ abortController.abort();
7659
+ }, 500);
7660
+ var fetchOptions = {
7661
+ 'method': 'GET',
7662
+ 'headers': {
7663
+ 'Authorization': 'Basic ' + btoa(self.get_config('token') + ':'),
7664
+ },
7665
+ 'signal': abortController.signal
7666
+ };
7667
+
7668
+ return win['fetch'](full_url, fetchOptions).then(function(response) {
7669
+ clearTimeout(timeout_id);
7670
+ if (!response['ok']) {
7671
+ console.critical('Network response was not ok');
7672
+ disableRecordingIfStrict();
7673
+ return;
7674
+ }
7675
+ return response.json();
7676
+ }).then(function(result) {
7677
+ if (result && result['sdk_config'] && result['sdk_config']['config']) {
7678
+ var remote_config = result['sdk_config']['config'];
7679
+
7680
+ // Verify that remote config contains only valid keys from DEFAULT_CONFIG
7681
+ var valid_config = {};
7682
+ _.each(remote_config, function(value, key) {
7683
+ if (DEFAULT_CONFIG.hasOwnProperty(key)) {
7684
+ valid_config[key] = value;
7685
+ }
7686
+ });
7687
+
7688
+ if (_.isEmptyObject(valid_config)) {
7689
+ console.critical('No valid config keys found in remote settings.');
7690
+ disableRecordingIfStrict();
7691
+ } else {
7692
+ self.set_config(valid_config);
7693
+ }
7694
+ } else {
7695
+ disableRecordingIfStrict();
7696
+ }
7697
+ }).catch(function(err) {
7698
+ clearTimeout(timeout_id);
7699
+ console.critical('Failed to fetch remote settings', err);
7700
+ disableRecordingIfStrict();
7701
+ });
7702
+ };
7703
+
7604
7704
  /**
7605
7705
  * _execute_array() deals with processing any mixpanel function
7606
7706
  * calls that were called before the Mixpanel library were loaded