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
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.72.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
@@ -22,7 +22,9 @@ if (typeof(window) === 'undefined') {
22
22
  screen: { width: 0, height: 0 },
23
23
  location: loc,
24
24
  addEventListener: function() {},
25
- removeEventListener: function() {}
25
+ removeEventListener: function() {},
26
+ dispatchEvent: function() {},
27
+ CustomEvent: function () {}
26
28
  };
27
29
  } else {
28
30
  win = window;
@@ -1502,8 +1504,17 @@ function _storageWrapper(storage, name, is_supported_fn) {
1502
1504
  };
1503
1505
  }
1504
1506
 
1505
- _.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
1506
- _.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);
1507
1518
 
1508
1519
  _.register_event = (function() {
1509
1520
  // written by Dean Edwards, 2005
@@ -2653,6 +2664,18 @@ function shouldTrackDomEvent(el, ev) {
2653
2664
  }
2654
2665
  }
2655
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
+
2656
2679
  /*
2657
2680
  * Check whether a DOM element should be "tracked" or if it may contain sensitive data
2658
2681
  * using a variety of heuristics.
@@ -2705,13 +2728,8 @@ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)
2705
2728
  }
2706
2729
  }
2707
2730
 
2708
- // filter out data from fields that look like sensitive fields
2709
- var name = el.name || el.id || '';
2710
- 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"]
2711
- var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
2712
- if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
2713
- return false;
2714
- }
2731
+ if (elementLooksSensitive(el)) {
2732
+ return false;
2715
2733
  }
2716
2734
 
2717
2735
  return true;
@@ -6835,12 +6853,13 @@ var mixpanel_master; // main mixpanel instance / object
6835
6853
  var INIT_MODULE = 0;
6836
6854
  var INIT_SNIPPET = 1;
6837
6855
 
6838
- var IDENTITY_FUNC = function(x) {return x;};
6839
-
6840
6856
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
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;
@@ -7002,6 +7022,17 @@ var create_mplib = function(token, config, name) {
7002
7022
  // global debug to be true
7003
7023
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
7004
7024
 
7025
+ var source = init_type === INIT_MODULE ? 'module' : 'snippet';
7026
+ win.dispatchEvent(new win.CustomEvent('$mp_sdk_to_extension_event', {
7027
+ 'detail': {
7028
+ 'instance': instance,
7029
+ 'source': source,
7030
+ 'token': token,
7031
+ 'name': name,
7032
+ 'info': _.info
7033
+ }
7034
+ }));
7035
+
7005
7036
  // if target is not defined, we called init after the lib already
7006
7037
  // loaded, so there won't be an array of things to execute
7007
7038
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -7072,6 +7103,8 @@ MixpanelLib.prototype._init = function(token, config, name) {
7072
7103
  }
7073
7104
  }
7074
7105
 
7106
+ this.hooks = {};
7107
+
7075
7108
  this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
7076
7109
  'name': name,
7077
7110
  'token': token,
@@ -7163,7 +7196,16 @@ MixpanelLib.prototype._init = function(token, config, name) {
7163
7196
  this.autocapture.init();
7164
7197
 
7165
7198
  this._init_tab_id();
7166
- 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
+ }
7167
7209
  };
7168
7210
 
7169
7211
  /**
@@ -7588,6 +7630,77 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
7588
7630
  return succeeded;
7589
7631
  };
7590
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
+
7591
7704
  /**
7592
7705
  * _execute_array() deals with processing any mixpanel function
7593
7706
  * calls that were called before the Mixpanel library were loaded
@@ -7672,7 +7785,12 @@ MixpanelLib.prototype.init_batchers = function() {
7672
7785
  );
7673
7786
  }, this),
7674
7787
  beforeSendHook: _.bind(function(item) {
7675
- return this._run_hook('before_send_' + attrs.type, item);
7788
+ var ret = this._run_hook('before_send_' + attrs.type, item);
7789
+ if (ret) {
7790
+ return ret[0];
7791
+ } else {
7792
+ return null;
7793
+ }
7676
7794
  }, this),
7677
7795
  stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
7678
7796
  usePersistence: true,
@@ -7765,6 +7883,9 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
7765
7883
  var send_request_immediately = _.bind(function() {
7766
7884
  if (!send_request_options.skip_hooks) {
7767
7885
  truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
7886
+ if (truncated_data) {
7887
+ truncated_data = truncated_data[0];
7888
+ }
7768
7889
  }
7769
7890
  if (truncated_data) {
7770
7891
  console.log('MIXPANEL REQUEST:');
@@ -7819,6 +7940,17 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
7819
7940
  * with the tracking payload sent to the API server is returned; otherwise false.
7820
7941
  */
7821
7942
  MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) {
7943
+ var ret;
7944
+ if (!(options && options.skip_hooks)) {
7945
+ ret = this._run_hook('before_track', event_name, properties);
7946
+ if (ret === null) {
7947
+ return;
7948
+ } else {
7949
+ event_name = ret[0];
7950
+ properties = ret[1];
7951
+ }
7952
+ }
7953
+
7822
7954
  if (!callback && typeof options === 'function') {
7823
7955
  callback = options;
7824
7956
  options = null;
@@ -7888,7 +8020,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
7888
8020
  'event': event_name,
7889
8021
  'properties': properties
7890
8022
  };
7891
- var ret = this._track_or_batch({
8023
+ ret = this._track_or_batch({
7892
8024
  type: 'events',
7893
8025
  data: data,
7894
8026
  endpoint: this.get_api_host('events') + '/' + this.get_config('api_routes')['track'],
@@ -8234,6 +8366,14 @@ var options_for_register = function(days_or_options) {
8234
8366
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
8235
8367
  */
8236
8368
  MixpanelLib.prototype.register = function(props, days_or_options) {
8369
+ var ret = this._run_hook('before_register', props, days_or_options);
8370
+ if (ret === null) {
8371
+ return;
8372
+ } else {
8373
+ props = ret[0];
8374
+ days_or_options = ret[1];
8375
+ }
8376
+
8237
8377
  var options = options_for_register(days_or_options);
8238
8378
  if (options['persistent']) {
8239
8379
  this['persistence'].register(props, options['days']);
@@ -8270,6 +8410,15 @@ MixpanelLib.prototype.register = function(props, days_or_options) {
8270
8410
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
8271
8411
  */
8272
8412
  MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) {
8413
+ var ret = this._run_hook('before_register_once', props, default_value, days_or_options);
8414
+ if (ret === null) {
8415
+ return;
8416
+ } else {
8417
+ props = ret[0];
8418
+ default_value = ret[1];
8419
+ days_or_options = ret[2];
8420
+ }
8421
+
8273
8422
  var options = options_for_register(days_or_options);
8274
8423
  if (options['persistent']) {
8275
8424
  this['persistence'].register_once(props, default_value, options['days']);
@@ -8293,6 +8442,14 @@ MixpanelLib.prototype.register_once = function(props, default_value, days_or_opt
8293
8442
  * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage)
8294
8443
  */
8295
8444
  MixpanelLib.prototype.unregister = function(property, options) {
8445
+ var ret = this._run_hook('before_unregister', property, options);
8446
+ if (ret === null) {
8447
+ return;
8448
+ } else {
8449
+ property = ret[0];
8450
+ options = ret[1];
8451
+ }
8452
+
8296
8453
  options = options_for_register(options);
8297
8454
  if (options['persistent']) {
8298
8455
  this['persistence'].unregister(property);
@@ -8341,6 +8498,13 @@ MixpanelLib.prototype.identify = function(
8341
8498
  // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed
8342
8499
  // _union_callback:function A callback to be run if and when the People union queue is flushed
8343
8500
  // _unset_callback:function A callback to be run if and when the People unset queue is flushed
8501
+ var ret = this._run_hook('before_identify', new_distinct_id);
8502
+
8503
+ if (ret === null) {
8504
+ return -1;
8505
+ } else {
8506
+ new_distinct_id = ret[0];
8507
+ }
8344
8508
 
8345
8509
  var previous_distinct_id = this.get_distinct_id();
8346
8510
  if (new_distinct_id && previous_distinct_id !== new_distinct_id) {
@@ -8665,6 +8829,25 @@ MixpanelLib.prototype.set_config = function(config) {
8665
8829
  if (('autocapture' in config || 'record_heatmap_data' in config) && this.autocapture) {
8666
8830
  this.autocapture.init();
8667
8831
  }
8832
+
8833
+ if (_.isObject(config['hooks'])) {
8834
+ this.hooks = {};
8835
+ _.each(config['hooks'], function(hook_value, hook_name) {
8836
+ if (_.isFunction(hook_value)) {
8837
+ this.hooks[hook_name] = [hook_value];
8838
+ } else if (_.isArray(hook_value)) {
8839
+ this.hooks[hook_name] = [];
8840
+ for (var i = 0; i < hook_value.length; i++) {
8841
+ if (!_.isFunction(hook_value[i])) {
8842
+ console.critical('Invalid hook added. Hook is not a function');
8843
+ }
8844
+ this.hooks[hook_name].push(hook_value[i]);
8845
+ }
8846
+ } else {
8847
+ console.critical('Invalid hooks added. Ensure that the hook values passed into config.hooks are functions or arrays of functions.');
8848
+ }
8849
+ }, this);
8850
+ }
8668
8851
  }
8669
8852
  };
8670
8853
 
@@ -8682,12 +8865,26 @@ MixpanelLib.prototype.get_config = function(prop_name) {
8682
8865
  * @returns {any|null} return value of user-provided hook, or null if nothing was returned
8683
8866
  */
8684
8867
  MixpanelLib.prototype._run_hook = function(hook_name) {
8685
- var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
8686
- if (typeof ret === 'undefined') {
8687
- this.report_error(hook_name + ' hook did not return a value');
8688
- ret = null;
8689
- }
8690
- return ret;
8868
+ var hook_data = slice.call(arguments, 1);
8869
+ _.each(this.hooks[hook_name], function(hook) {
8870
+ if (hook_data === null) {
8871
+ return null;
8872
+ }
8873
+
8874
+ var ret = hook.apply(this, hook_data);
8875
+
8876
+ if (typeof ret === 'undefined') {
8877
+ this.report_error(hook_name + ' hook did not return a valid value');
8878
+ hook_data = null;
8879
+ } else {
8880
+ if (!_.isArray(ret)) {
8881
+ ret = [ret];
8882
+ }
8883
+ hook_data.splice.apply(hook_data, [0, ret.length].concat(ret));
8884
+ }
8885
+ }, this);
8886
+
8887
+ return hook_data;
8691
8888
  };
8692
8889
 
8693
8890
  /**
@@ -8998,6 +9195,25 @@ MixpanelLib.prototype.report_error = function(msg, err) {
8998
9195
  }
8999
9196
  };
9000
9197
 
9198
+ MixpanelLib.prototype.add_hook = function(hook_name, hook_fn) {
9199
+ if (!this.hooks[hook_name]) {
9200
+ this.hooks[hook_name] = [];
9201
+ }
9202
+ this.hooks[hook_name].push(hook_fn);
9203
+ };
9204
+
9205
+ MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
9206
+ var fn_index;
9207
+ if (this.hooks[hook_name]) {
9208
+ fn_index = this.hooks[hook_name].indexOf(hook_fn);
9209
+ if (fn_index !== -1) {
9210
+ this.hooks[hook_name].splice(fn_index, 1);
9211
+ } else {
9212
+ console.log('remove_hook failed. Matching hook was not found');
9213
+ }
9214
+ }
9215
+ };
9216
+
9001
9217
  // EXPORTS (for closure compiler)
9002
9218
 
9003
9219
  // MixpanelLib Exports
@@ -9030,6 +9246,8 @@ MixpanelLib.prototype['get_group'] = MixpanelLib.protot
9030
9246
  MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group;
9031
9247
  MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group;
9032
9248
  MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group;
9249
+ MixpanelLib.prototype['add_hook'] = MixpanelLib.prototype.add_hook;
9250
+ MixpanelLib.prototype['remove_hook'] = MixpanelLib.prototype.remove_hook;
9033
9251
  MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups;
9034
9252
  MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders;
9035
9253
  MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;