mixpanel-browser 2.72.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 (44) hide show
  1. package/.claude/settings.local.json +5 -2
  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 +12 -0
  6. package/README.md +1 -1
  7. package/build.sh +1 -5
  8. package/dist/mixpanel-core.cjs.d.ts +49 -4
  9. package/dist/mixpanel-core.cjs.js +244 -26
  10. package/dist/mixpanel-recorder.js +5258 -688
  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 +49 -4
  14. package/dist/mixpanel-with-async-recorder.cjs.js +244 -26
  15. package/dist/mixpanel-with-recorder.d.ts +49 -4
  16. package/dist/mixpanel-with-recorder.js +6858 -2099
  17. package/dist/mixpanel-with-recorder.min.d.ts +49 -4
  18. package/dist/mixpanel-with-recorder.min.js +1 -1
  19. package/dist/mixpanel.amd.d.ts +49 -4
  20. package/dist/mixpanel.amd.js +6858 -2099
  21. package/dist/mixpanel.cjs.d.ts +49 -4
  22. package/dist/mixpanel.cjs.js +6858 -2099
  23. package/dist/mixpanel.globals.js +244 -26
  24. package/dist/mixpanel.min.js +175 -171
  25. package/dist/mixpanel.module.d.ts +49 -4
  26. package/dist/mixpanel.module.js +6858 -2099
  27. package/dist/mixpanel.umd.d.ts +49 -4
  28. package/dist/mixpanel.umd.js +6858 -2099
  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 +49 -4
  36. package/src/mixpanel-core.js +215 -15
  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/src/window.js +3 -1
  43. package/testServer.js +51 -7
  44. package/.github/workflows/tests.yml +0 -25
@@ -3,7 +3,7 @@
3
3
 
4
4
  var Config = {
5
5
  DEBUG: false,
6
- LIB_VERSION: '2.72.0'
6
+ LIB_VERSION: '2.74.0'
7
7
  };
8
8
 
9
9
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -23,7 +23,9 @@
23
23
  screen: { width: 0, height: 0 },
24
24
  location: loc,
25
25
  addEventListener: function() {},
26
- removeEventListener: function() {}
26
+ removeEventListener: function() {},
27
+ dispatchEvent: function() {},
28
+ CustomEvent: function () {}
27
29
  };
28
30
  } else {
29
31
  win = window;
@@ -1503,8 +1505,17 @@
1503
1505
  };
1504
1506
  }
1505
1507
 
1506
- _.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
1507
- _.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
1508
+ // Safari errors out accessing localStorage/sessionStorage when cookies are disabled,
1509
+ // so create dummy storage wrappers that silently fail as a fallback.
1510
+ var windowLocalStorage = null, windowSessionStorage = null;
1511
+ try {
1512
+ windowLocalStorage = win.localStorage;
1513
+ windowSessionStorage = win.sessionStorage;
1514
+ // eslint-disable-next-line no-empty
1515
+ } catch (_err) {}
1516
+
1517
+ _.localStorage = _storageWrapper(windowLocalStorage, 'localStorage', localStorageSupported);
1518
+ _.sessionStorage = _storageWrapper(windowSessionStorage, 'sessionStorage', sessionStorageSupported);
1508
1519
 
1509
1520
  _.register_event = (function() {
1510
1521
  // written by Dean Edwards, 2005
@@ -2654,6 +2665,18 @@
2654
2665
  }
2655
2666
  }
2656
2667
 
2668
+ function elementLooksSensitive(el) {
2669
+ var name = (el.name || el.id || '').toString().toLowerCase();
2670
+ 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"]
2671
+ var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
2672
+ if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
2673
+ return true;
2674
+ }
2675
+ }
2676
+
2677
+ return false;
2678
+ }
2679
+
2657
2680
  /*
2658
2681
  * Check whether a DOM element should be "tracked" or if it may contain sensitive data
2659
2682
  * using a variety of heuristics.
@@ -2706,13 +2729,8 @@
2706
2729
  }
2707
2730
  }
2708
2731
 
2709
- // filter out data from fields that look like sensitive fields
2710
- var name = el.name || el.id || '';
2711
- 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"]
2712
- var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
2713
- if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
2714
- return false;
2715
- }
2732
+ if (elementLooksSensitive(el)) {
2733
+ return false;
2716
2734
  }
2717
2735
 
2718
2736
  return true;
@@ -6836,12 +6854,13 @@
6836
6854
  var INIT_MODULE = 0;
6837
6855
  var INIT_SNIPPET = 1;
6838
6856
 
6839
- var IDENTITY_FUNC = function(x) {return x;};
6840
-
6841
6857
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
6842
6858
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
6843
6859
  /** @const */ var PAYLOAD_TYPE_JSON = 'json';
6844
6860
  /** @const */ var DEVICE_ID_PREFIX = '$device:';
6861
+ /** @const */ var SETTING_STRICT = 'strict';
6862
+ /** @const */ var SETTING_FALLBACK = 'fallback';
6863
+ /** @const */ var SETTING_DISABLED = 'disabled';
6845
6864
 
6846
6865
 
6847
6866
  /*
@@ -6870,7 +6889,8 @@
6870
6889
  'engage': 'engage/',
6871
6890
  'groups': 'groups/',
6872
6891
  'record': 'record/',
6873
- 'flags': 'flags/'
6892
+ 'flags': 'flags/',
6893
+ 'settings': 'settings/'
6874
6894
  };
6875
6895
 
6876
6896
  /*
@@ -6934,12 +6954,12 @@
6934
6954
  'record_console': true,
6935
6955
  'record_heatmap_data': false,
6936
6956
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
6937
- 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
6938
- 'record_mask_text_selector': '*',
6957
+ 'record_mask_inputs': true,
6939
6958
  'record_max_ms': MAX_RECORDING_MS,
6940
6959
  'record_min_ms': 0,
6941
6960
  'record_sessions_percent': 0,
6942
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
6961
+ 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
6962
+ 'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
6943
6963
  };
6944
6964
 
6945
6965
  var DOM_LOADED = false;
@@ -7003,6 +7023,17 @@
7003
7023
  // global debug to be true
7004
7024
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
7005
7025
 
7026
+ var source = init_type === INIT_MODULE ? 'module' : 'snippet';
7027
+ win.dispatchEvent(new win.CustomEvent('$mp_sdk_to_extension_event', {
7028
+ 'detail': {
7029
+ 'instance': instance,
7030
+ 'source': source,
7031
+ 'token': token,
7032
+ 'name': name,
7033
+ 'info': _.info
7034
+ }
7035
+ }));
7036
+
7006
7037
  // if target is not defined, we called init after the lib already
7007
7038
  // loaded, so there won't be an array of things to execute
7008
7039
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -7073,6 +7104,8 @@
7073
7104
  }
7074
7105
  }
7075
7106
 
7107
+ this.hooks = {};
7108
+
7076
7109
  this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
7077
7110
  'name': name,
7078
7111
  'token': token,
@@ -7164,7 +7197,16 @@
7164
7197
  this.autocapture.init();
7165
7198
 
7166
7199
  this._init_tab_id();
7167
- this._check_and_start_session_recording();
7200
+
7201
+ // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
7202
+ var mode = this.get_config('remote_settings_mode');
7203
+ if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
7204
+ this._fetch_remote_settings(mode).then(_.bind(function() {
7205
+ this._check_and_start_session_recording();
7206
+ }, this));
7207
+ } else {
7208
+ this._check_and_start_session_recording();
7209
+ }
7168
7210
  };
7169
7211
 
7170
7212
  /**
@@ -7589,6 +7631,77 @@
7589
7631
  return succeeded;
7590
7632
  };
7591
7633
 
7634
+ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
7635
+ var disableRecordingIfStrict = function() {
7636
+ if (mode === 'strict') {
7637
+ self.set_config({'record_sessions_percent': 0});
7638
+ }
7639
+ };
7640
+
7641
+ if (!win['AbortController']) {
7642
+ console.critical('Remote settings unavailable: missing minimum required APIs');
7643
+ disableRecordingIfStrict();
7644
+ return Promise.resolve();
7645
+ }
7646
+
7647
+ var settings_endpoint = this.get_api_host('settings') + '/' + this.get_config('api_routes')['settings'];
7648
+ var request_params = {
7649
+ '$lib_version': Config.LIB_VERSION,
7650
+ 'mp_lib': 'web',
7651
+ 'sdk_config': '1',
7652
+ };
7653
+ var query_string = _.HTTPBuildQuery(request_params);
7654
+ var full_url = settings_endpoint + '?' + query_string;
7655
+ var self = this;
7656
+
7657
+ var abortController = new AbortController();
7658
+ var timeout_id = setTimeout(function() {
7659
+ abortController.abort();
7660
+ }, 500);
7661
+ var fetchOptions = {
7662
+ 'method': 'GET',
7663
+ 'headers': {
7664
+ 'Authorization': 'Basic ' + btoa(self.get_config('token') + ':'),
7665
+ },
7666
+ 'signal': abortController.signal
7667
+ };
7668
+
7669
+ return win['fetch'](full_url, fetchOptions).then(function(response) {
7670
+ clearTimeout(timeout_id);
7671
+ if (!response['ok']) {
7672
+ console.critical('Network response was not ok');
7673
+ disableRecordingIfStrict();
7674
+ return;
7675
+ }
7676
+ return response.json();
7677
+ }).then(function(result) {
7678
+ if (result && result['sdk_config'] && result['sdk_config']['config']) {
7679
+ var remote_config = result['sdk_config']['config'];
7680
+
7681
+ // Verify that remote config contains only valid keys from DEFAULT_CONFIG
7682
+ var valid_config = {};
7683
+ _.each(remote_config, function(value, key) {
7684
+ if (DEFAULT_CONFIG.hasOwnProperty(key)) {
7685
+ valid_config[key] = value;
7686
+ }
7687
+ });
7688
+
7689
+ if (_.isEmptyObject(valid_config)) {
7690
+ console.critical('No valid config keys found in remote settings.');
7691
+ disableRecordingIfStrict();
7692
+ } else {
7693
+ self.set_config(valid_config);
7694
+ }
7695
+ } else {
7696
+ disableRecordingIfStrict();
7697
+ }
7698
+ }).catch(function(err) {
7699
+ clearTimeout(timeout_id);
7700
+ console.critical('Failed to fetch remote settings', err);
7701
+ disableRecordingIfStrict();
7702
+ });
7703
+ };
7704
+
7592
7705
  /**
7593
7706
  * _execute_array() deals with processing any mixpanel function
7594
7707
  * calls that were called before the Mixpanel library were loaded
@@ -7673,7 +7786,12 @@
7673
7786
  );
7674
7787
  }, this),
7675
7788
  beforeSendHook: _.bind(function(item) {
7676
- return this._run_hook('before_send_' + attrs.type, item);
7789
+ var ret = this._run_hook('before_send_' + attrs.type, item);
7790
+ if (ret) {
7791
+ return ret[0];
7792
+ } else {
7793
+ return null;
7794
+ }
7677
7795
  }, this),
7678
7796
  stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
7679
7797
  usePersistence: true,
@@ -7766,6 +7884,9 @@
7766
7884
  var send_request_immediately = _.bind(function() {
7767
7885
  if (!send_request_options.skip_hooks) {
7768
7886
  truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
7887
+ if (truncated_data) {
7888
+ truncated_data = truncated_data[0];
7889
+ }
7769
7890
  }
7770
7891
  if (truncated_data) {
7771
7892
  console.log('MIXPANEL REQUEST:');
@@ -7820,6 +7941,17 @@
7820
7941
  * with the tracking payload sent to the API server is returned; otherwise false.
7821
7942
  */
7822
7943
  MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) {
7944
+ var ret;
7945
+ if (!(options && options.skip_hooks)) {
7946
+ ret = this._run_hook('before_track', event_name, properties);
7947
+ if (ret === null) {
7948
+ return;
7949
+ } else {
7950
+ event_name = ret[0];
7951
+ properties = ret[1];
7952
+ }
7953
+ }
7954
+
7823
7955
  if (!callback && typeof options === 'function') {
7824
7956
  callback = options;
7825
7957
  options = null;
@@ -7889,7 +8021,7 @@
7889
8021
  'event': event_name,
7890
8022
  'properties': properties
7891
8023
  };
7892
- var ret = this._track_or_batch({
8024
+ ret = this._track_or_batch({
7893
8025
  type: 'events',
7894
8026
  data: data,
7895
8027
  endpoint: this.get_api_host('events') + '/' + this.get_config('api_routes')['track'],
@@ -8235,6 +8367,14 @@
8235
8367
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
8236
8368
  */
8237
8369
  MixpanelLib.prototype.register = function(props, days_or_options) {
8370
+ var ret = this._run_hook('before_register', props, days_or_options);
8371
+ if (ret === null) {
8372
+ return;
8373
+ } else {
8374
+ props = ret[0];
8375
+ days_or_options = ret[1];
8376
+ }
8377
+
8238
8378
  var options = options_for_register(days_or_options);
8239
8379
  if (options['persistent']) {
8240
8380
  this['persistence'].register(props, options['days']);
@@ -8271,6 +8411,15 @@
8271
8411
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
8272
8412
  */
8273
8413
  MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) {
8414
+ var ret = this._run_hook('before_register_once', props, default_value, days_or_options);
8415
+ if (ret === null) {
8416
+ return;
8417
+ } else {
8418
+ props = ret[0];
8419
+ default_value = ret[1];
8420
+ days_or_options = ret[2];
8421
+ }
8422
+
8274
8423
  var options = options_for_register(days_or_options);
8275
8424
  if (options['persistent']) {
8276
8425
  this['persistence'].register_once(props, default_value, options['days']);
@@ -8294,6 +8443,14 @@
8294
8443
  * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage)
8295
8444
  */
8296
8445
  MixpanelLib.prototype.unregister = function(property, options) {
8446
+ var ret = this._run_hook('before_unregister', property, options);
8447
+ if (ret === null) {
8448
+ return;
8449
+ } else {
8450
+ property = ret[0];
8451
+ options = ret[1];
8452
+ }
8453
+
8297
8454
  options = options_for_register(options);
8298
8455
  if (options['persistent']) {
8299
8456
  this['persistence'].unregister(property);
@@ -8342,6 +8499,13 @@
8342
8499
  // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed
8343
8500
  // _union_callback:function A callback to be run if and when the People union queue is flushed
8344
8501
  // _unset_callback:function A callback to be run if and when the People unset queue is flushed
8502
+ var ret = this._run_hook('before_identify', new_distinct_id);
8503
+
8504
+ if (ret === null) {
8505
+ return -1;
8506
+ } else {
8507
+ new_distinct_id = ret[0];
8508
+ }
8345
8509
 
8346
8510
  var previous_distinct_id = this.get_distinct_id();
8347
8511
  if (new_distinct_id && previous_distinct_id !== new_distinct_id) {
@@ -8666,6 +8830,25 @@
8666
8830
  if (('autocapture' in config || 'record_heatmap_data' in config) && this.autocapture) {
8667
8831
  this.autocapture.init();
8668
8832
  }
8833
+
8834
+ if (_.isObject(config['hooks'])) {
8835
+ this.hooks = {};
8836
+ _.each(config['hooks'], function(hook_value, hook_name) {
8837
+ if (_.isFunction(hook_value)) {
8838
+ this.hooks[hook_name] = [hook_value];
8839
+ } else if (_.isArray(hook_value)) {
8840
+ this.hooks[hook_name] = [];
8841
+ for (var i = 0; i < hook_value.length; i++) {
8842
+ if (!_.isFunction(hook_value[i])) {
8843
+ console.critical('Invalid hook added. Hook is not a function');
8844
+ }
8845
+ this.hooks[hook_name].push(hook_value[i]);
8846
+ }
8847
+ } else {
8848
+ console.critical('Invalid hooks added. Ensure that the hook values passed into config.hooks are functions or arrays of functions.');
8849
+ }
8850
+ }, this);
8851
+ }
8669
8852
  }
8670
8853
  };
8671
8854
 
@@ -8683,12 +8866,26 @@
8683
8866
  * @returns {any|null} return value of user-provided hook, or null if nothing was returned
8684
8867
  */
8685
8868
  MixpanelLib.prototype._run_hook = function(hook_name) {
8686
- var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
8687
- if (typeof ret === 'undefined') {
8688
- this.report_error(hook_name + ' hook did not return a value');
8689
- ret = null;
8690
- }
8691
- return ret;
8869
+ var hook_data = slice.call(arguments, 1);
8870
+ _.each(this.hooks[hook_name], function(hook) {
8871
+ if (hook_data === null) {
8872
+ return null;
8873
+ }
8874
+
8875
+ var ret = hook.apply(this, hook_data);
8876
+
8877
+ if (typeof ret === 'undefined') {
8878
+ this.report_error(hook_name + ' hook did not return a valid value');
8879
+ hook_data = null;
8880
+ } else {
8881
+ if (!_.isArray(ret)) {
8882
+ ret = [ret];
8883
+ }
8884
+ hook_data.splice.apply(hook_data, [0, ret.length].concat(ret));
8885
+ }
8886
+ }, this);
8887
+
8888
+ return hook_data;
8692
8889
  };
8693
8890
 
8694
8891
  /**
@@ -8999,6 +9196,25 @@
8999
9196
  }
9000
9197
  };
9001
9198
 
9199
+ MixpanelLib.prototype.add_hook = function(hook_name, hook_fn) {
9200
+ if (!this.hooks[hook_name]) {
9201
+ this.hooks[hook_name] = [];
9202
+ }
9203
+ this.hooks[hook_name].push(hook_fn);
9204
+ };
9205
+
9206
+ MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
9207
+ var fn_index;
9208
+ if (this.hooks[hook_name]) {
9209
+ fn_index = this.hooks[hook_name].indexOf(hook_fn);
9210
+ if (fn_index !== -1) {
9211
+ this.hooks[hook_name].splice(fn_index, 1);
9212
+ } else {
9213
+ console.log('remove_hook failed. Matching hook was not found');
9214
+ }
9215
+ }
9216
+ };
9217
+
9002
9218
  // EXPORTS (for closure compiler)
9003
9219
 
9004
9220
  // MixpanelLib Exports
@@ -9031,6 +9247,8 @@
9031
9247
  MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group;
9032
9248
  MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group;
9033
9249
  MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group;
9250
+ MixpanelLib.prototype['add_hook'] = MixpanelLib.prototype.add_hook;
9251
+ MixpanelLib.prototype['remove_hook'] = MixpanelLib.prototype.remove_hook;
9034
9252
  MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups;
9035
9253
  MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders;
9036
9254
  MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;