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
@@ -3,7 +3,7 @@
3
3
 
4
4
  var Config = {
5
5
  DEBUG: false,
6
- LIB_VERSION: '2.73.0'
6
+ LIB_VERSION: '2.75.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
@@ -594,15 +594,8 @@
594
594
  return toString.call(obj) === '[object Array]';
595
595
  };
596
596
 
597
- // from a comment on http://dbj.org/dbj/?p=286
598
- // fails on only one very rare and deliberate custom object:
599
- // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
600
597
  _.isFunction = function(f) {
601
- try {
602
- return /^\s*\bfunction\b/.test(f);
603
- } catch (x) {
604
- return false;
605
- }
598
+ return typeof f === 'function';
606
599
  };
607
600
 
608
601
  _.isArguments = function(obj) {
@@ -1505,8 +1498,17 @@
1505
1498
  };
1506
1499
  }
1507
1500
 
1508
- _.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
1509
- _.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
1501
+ // Safari errors out accessing localStorage/sessionStorage when cookies are disabled,
1502
+ // so create dummy storage wrappers that silently fail as a fallback.
1503
+ var windowLocalStorage = null, windowSessionStorage = null;
1504
+ try {
1505
+ windowLocalStorage = win.localStorage;
1506
+ windowSessionStorage = win.sessionStorage;
1507
+ // eslint-disable-next-line no-empty
1508
+ } catch (_err) {}
1509
+
1510
+ _.localStorage = _storageWrapper(windowLocalStorage, 'localStorage', localStorageSupported);
1511
+ _.sessionStorage = _storageWrapper(windowSessionStorage, 'sessionStorage', sessionStorageSupported);
1510
1512
 
1511
1513
  _.register_event = (function() {
1512
1514
  // written by Dean Edwards, 2005
@@ -2151,6 +2153,16 @@
2151
2153
  return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
2152
2154
  };
2153
2155
 
2156
+ /**
2157
+ * Shared global window property names used across modules
2158
+ */
2159
+
2160
+ // Targeting library global (used by flags and targeting modules)
2161
+ var TARGETING_GLOBAL_NAME = '__mp_targeting';
2162
+
2163
+ // Recorder library global (used by recorder and mixpanel-core)
2164
+ var RECORDER_GLOBAL_NAME = '__mp_recorder';
2165
+
2154
2166
  // stateless utils
2155
2167
  // mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
2156
2168
 
@@ -2656,6 +2668,18 @@
2656
2668
  }
2657
2669
  }
2658
2670
 
2671
+ function elementLooksSensitive(el) {
2672
+ var name = (el.name || el.id || '').toString().toLowerCase();
2673
+ 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"]
2674
+ var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
2675
+ if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
2676
+ return true;
2677
+ }
2678
+ }
2679
+
2680
+ return false;
2681
+ }
2682
+
2659
2683
  /*
2660
2684
  * Check whether a DOM element should be "tracked" or if it may contain sensitive data
2661
2685
  * using a variety of heuristics.
@@ -2708,13 +2732,8 @@
2708
2732
  }
2709
2733
  }
2710
2734
 
2711
- // filter out data from fields that look like sensitive fields
2712
- var name = el.name || el.id || '';
2713
- 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"]
2714
- var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
2715
- if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
2716
- return false;
2717
- }
2735
+ if (elementLooksSensitive(el)) {
2736
+ return false;
2718
2737
  }
2719
2738
 
2720
2739
  return true;
@@ -3892,14 +3911,62 @@
3892
3911
  // TODO integrate error_reporter from mixpanel instance
3893
3912
  safewrapClass(Autocapture);
3894
3913
 
3895
- var logger$3 = console_with_prefix('flags');
3914
+ /**
3915
+ * Get the promise-based targeting loader
3916
+ * @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
3917
+ * @param {string} targetingSrc - URL to targeting bundle
3918
+ * @returns {Promise} Promise that resolves with targeting library
3919
+ */
3920
+ var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
3921
+ // Return existing promise if already initialized or loading
3922
+ if (win[TARGETING_GLOBAL_NAME] && typeof win[TARGETING_GLOBAL_NAME].then === 'function') {
3923
+ return win[TARGETING_GLOBAL_NAME];
3924
+ }
3925
+
3926
+ // Create loading promise and set it as the global immediately
3927
+ // This makes minified build behavior consistent with dev/CJS builds
3928
+ win[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
3929
+ loadExtraBundle(targetingSrc, resolve);
3930
+ }).then(function () {
3931
+ var p = win[TARGETING_GLOBAL_NAME];
3932
+ if (p && typeof p.then === 'function') {
3933
+ return p;
3934
+ }
3935
+ throw new Error('targeting failed to load');
3936
+ }).catch(function (err) {
3937
+ delete win[TARGETING_GLOBAL_NAME];
3938
+ throw err;
3939
+ });
3896
3940
 
3941
+ return win[TARGETING_GLOBAL_NAME];
3942
+ };
3943
+
3944
+ var logger$3 = console_with_prefix('flags');
3897
3945
  var FLAGS_CONFIG_KEY = 'flags';
3898
3946
 
3899
3947
  var CONFIG_CONTEXT = 'context';
3900
3948
  var CONFIG_DEFAULTS = {};
3901
3949
  CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
3902
3950
 
3951
+ /**
3952
+ * Generate a unique key for a pending first-time event
3953
+ * @param {string} flagKey - The flag key
3954
+ * @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
3955
+ * @returns {string} Composite key in format "flagKey:firstTimeEventHash"
3956
+ */
3957
+ var getPendingEventKey = function(flagKey, firstTimeEventHash) {
3958
+ return flagKey + ':' + firstTimeEventHash;
3959
+ };
3960
+
3961
+ /**
3962
+ * Extract the flag key from a pending event key
3963
+ * @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
3964
+ * @returns {string} The flag key portion
3965
+ */
3966
+ var getFlagKeyFromPendingEventKey = function(eventKey) {
3967
+ return eventKey.split(':')[0];
3968
+ };
3969
+
3903
3970
  /**
3904
3971
  * FeatureFlagManager: support for Mixpanel's feature flagging product
3905
3972
  * @constructor
@@ -3911,6 +3978,8 @@
3911
3978
  this.setMpConfig = initOptions.setConfigFunc;
3912
3979
  this.getMpProperty = initOptions.getPropertyFunc;
3913
3980
  this.track = initOptions.trackingFunc;
3981
+ this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
3982
+ this.targetingSrc = initOptions.targetingSrc || '';
3914
3983
  };
3915
3984
 
3916
3985
  FeatureFlagManager.prototype.init = function() {
@@ -3923,6 +3992,8 @@
3923
3992
  this.fetchFlags();
3924
3993
 
3925
3994
  this.trackedFeatures = new Set();
3995
+ this.pendingFirstTimeEvents = {};
3996
+ this.activatedFirstTimeEvents = {};
3926
3997
  };
3927
3998
 
3928
3999
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -4003,17 +4074,78 @@
4003
4074
  throw new Error('No flags in API response');
4004
4075
  }
4005
4076
  var flags = new Map();
4077
+ var pendingFirstTimeEvents = {};
4078
+
4079
+ // Process flags from response
4006
4080
  _.each(responseFlags, function(data, key) {
4007
- flags.set(key, {
4008
- 'key': data['variant_key'],
4009
- 'value': data['variant_value'],
4010
- 'experiment_id': data['experiment_id'],
4011
- 'is_experiment_active': data['is_experiment_active'],
4012
- 'is_qa_tester': data['is_qa_tester']
4081
+ // Check if this flag has any activated first-time events this session
4082
+ var hasActivatedEvent = false;
4083
+ var prefix = key + ':';
4084
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4085
+ if (eventKey.startsWith(prefix)) {
4086
+ hasActivatedEvent = true;
4087
+ }
4013
4088
  });
4014
- });
4089
+
4090
+ if (hasActivatedEvent) {
4091
+ // Preserve the activated variant, don't overwrite with server's current variant
4092
+ var currentFlag = this.flags && this.flags.get(key);
4093
+ if (currentFlag) {
4094
+ flags.set(key, currentFlag);
4095
+ }
4096
+ } else {
4097
+ // Use server's current variant
4098
+ flags.set(key, {
4099
+ 'key': data['variant_key'],
4100
+ 'value': data['variant_value'],
4101
+ 'experiment_id': data['experiment_id'],
4102
+ 'is_experiment_active': data['is_experiment_active'],
4103
+ 'is_qa_tester': data['is_qa_tester']
4104
+ });
4105
+ }
4106
+ }, this);
4107
+
4108
+ // Process top-level pending_first_time_events array
4109
+ var topLevelDefinitions = responseBody['pending_first_time_events'];
4110
+ if (topLevelDefinitions && topLevelDefinitions.length > 0) {
4111
+ _.each(topLevelDefinitions, function(def) {
4112
+ var flagKey = def['flag_key'];
4113
+ var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
4114
+
4115
+ // Skip if this specific event has already been activated this session
4116
+ if (this.activatedFirstTimeEvents[eventKey]) {
4117
+ return;
4118
+ }
4119
+
4120
+ // Store pending event definition using composite key
4121
+ pendingFirstTimeEvents[eventKey] = {
4122
+ 'flag_key': flagKey,
4123
+ 'flag_id': def['flag_id'],
4124
+ 'project_id': def['project_id'],
4125
+ 'first_time_event_hash': def['first_time_event_hash'],
4126
+ 'event_name': def['event_name'],
4127
+ 'property_filters': def['property_filters'],
4128
+ 'pending_variant': def['pending_variant']
4129
+ };
4130
+ }, this);
4131
+ }
4132
+
4133
+ // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
4134
+ if (this.activatedFirstTimeEvents) {
4135
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4136
+ var flagKey = getFlagKeyFromPendingEventKey(eventKey);
4137
+ if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
4138
+ // Keep the activated flag even though it's not in the new response
4139
+ flags.set(flagKey, this.flags.get(flagKey));
4140
+ }
4141
+ }, this);
4142
+ }
4143
+
4015
4144
  this.flags = flags;
4145
+ this.pendingFirstTimeEvents = pendingFirstTimeEvents;
4016
4146
  this._traceparent = traceparent;
4147
+
4148
+ this._loadTargetingIfNeeded();
4017
4149
  }.bind(this)).catch(function(error) {
4018
4150
  this.markFetchComplete();
4019
4151
  logger$3.error(error);
@@ -4037,6 +4169,177 @@
4037
4169
  this._fetchInProgressStartTime = null;
4038
4170
  };
4039
4171
 
4172
+ /**
4173
+ * Proactively load targeting bundle if any pending events have property filters
4174
+ */
4175
+ FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
4176
+ var hasPropertyFilters = false;
4177
+ _.each(this.pendingFirstTimeEvents, function(evt) {
4178
+ if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
4179
+ hasPropertyFilters = true;
4180
+ }
4181
+ });
4182
+
4183
+ if (hasPropertyFilters) {
4184
+ this.getTargeting().then(function() {
4185
+ logger$3.log('targeting loaded for property filter evaluation');
4186
+ });
4187
+ }
4188
+ };
4189
+
4190
+ /**
4191
+ * Get the targeting library (initializes if not already loaded)
4192
+ * This method is primarily for testing - production code should rely on automatic loading
4193
+ * @returns {Promise} Promise that resolves with targeting library
4194
+ */
4195
+ FeatureFlagManager.prototype.getTargeting = function() {
4196
+ return getTargetingPromise(
4197
+ this.loadExtraBundle.bind(this),
4198
+ this.targetingSrc
4199
+ ).catch(function(error) {
4200
+ logger$3.error('Failed to load targeting: ' + error);
4201
+ }.bind(this));
4202
+ };
4203
+
4204
+ /**
4205
+ * Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
4206
+ * @param {string} eventName - The name of the event being tracked
4207
+ * @param {Object} properties - Event properties to evaluate against property filters
4208
+ *
4209
+ * When a match is found (event name matches and property filters pass), this method:
4210
+ * - Switches the flag to the pending variant
4211
+ * - Marks the event as activated for this session
4212
+ * - Records the activation via the API (fire-and-forget)
4213
+ */
4214
+ FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
4215
+ if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
4216
+ return;
4217
+ }
4218
+
4219
+ // Check if targeting promise exists (either bundled or async loaded)
4220
+ if (win[TARGETING_GLOBAL_NAME] && _.isFunction(win[TARGETING_GLOBAL_NAME].then)) {
4221
+ win[TARGETING_GLOBAL_NAME].then(function(library) {
4222
+ this._processFirstTimeEventCheck(eventName, properties, library);
4223
+ }.bind(this)).catch(function() {
4224
+ // If targeting failed to load, process with null
4225
+ // Events without property filters will still match
4226
+ this._processFirstTimeEventCheck(eventName, properties, null);
4227
+ }.bind(this));
4228
+ } else {
4229
+ // No targeting available, process with null
4230
+ // Events without property filters will still match
4231
+ this._processFirstTimeEventCheck(eventName, properties, null);
4232
+ }
4233
+ };
4234
+
4235
+ /**
4236
+ * Internal method to process first-time event checks with loaded targeting library
4237
+ * @param {string} eventName - The name of the event being tracked
4238
+ * @param {Object} properties - Event properties to evaluate against property filters
4239
+ * @param {Object} targeting - The loaded targeting library
4240
+ */
4241
+ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
4242
+ _.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
4243
+ if (this.activatedFirstTimeEvents[eventKey]) {
4244
+ return;
4245
+ }
4246
+
4247
+ var flagKey = pendingEvent['flag_key'];
4248
+
4249
+ // Use targeting module to check if event matches
4250
+ var matchResult;
4251
+
4252
+ // If no targeting library and event has property filters, skip it
4253
+ if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
4254
+ logger$3.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
4255
+ return;
4256
+ }
4257
+
4258
+ // For simple events (no property filters), just check event name
4259
+ if (!targeting) {
4260
+ matchResult = {
4261
+ matches: eventName === pendingEvent['event_name'],
4262
+ error: null
4263
+ };
4264
+ } else {
4265
+ var criteria = {
4266
+ 'event_name': pendingEvent['event_name'],
4267
+ 'property_filters': pendingEvent['property_filters']
4268
+ };
4269
+ matchResult = targeting['eventMatchesCriteria'](
4270
+ eventName,
4271
+ properties,
4272
+ criteria
4273
+ );
4274
+ }
4275
+
4276
+ if (matchResult.error) {
4277
+ logger$3.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
4278
+ return;
4279
+ }
4280
+
4281
+ if (!matchResult.matches) {
4282
+ return;
4283
+ }
4284
+
4285
+ logger$3.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
4286
+
4287
+ var newVariant = {
4288
+ 'key': pendingEvent['pending_variant']['variant_key'],
4289
+ 'value': pendingEvent['pending_variant']['variant_value'],
4290
+ 'experiment_id': pendingEvent['pending_variant']['experiment_id'],
4291
+ 'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
4292
+ };
4293
+
4294
+ this.flags.set(flagKey, newVariant);
4295
+ this.activatedFirstTimeEvents[eventKey] = true;
4296
+
4297
+ this.recordFirstTimeEvent(
4298
+ pendingEvent['flag_id'],
4299
+ pendingEvent['project_id'],
4300
+ pendingEvent['first_time_event_hash']
4301
+ );
4302
+ }, this);
4303
+ };
4304
+
4305
+ FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
4306
+ // Construct URL: {api_host}/flags/{flagId}/first-time-events
4307
+ return this.getFullApiRoute() + '/' + flagId + '/first-time-events';
4308
+ };
4309
+
4310
+ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
4311
+ var distinctId = this.getMpProperty('distinct_id');
4312
+ var traceparent = generateTraceparent();
4313
+
4314
+ // Build URL with query string parameters
4315
+ var searchParams = new URLSearchParams();
4316
+ searchParams.set('mp_lib', 'web');
4317
+ searchParams.set('$lib_version', Config.LIB_VERSION);
4318
+ var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
4319
+
4320
+ var payload = {
4321
+ 'distinct_id': distinctId,
4322
+ 'project_id': projectId,
4323
+ 'first_time_event_hash': firstTimeEventHash
4324
+ };
4325
+
4326
+ logger$3.log('Recording first-time event for flag: ' + flagId);
4327
+
4328
+ // Fire-and-forget POST request
4329
+ this.fetch.call(win, url, {
4330
+ 'method': 'POST',
4331
+ 'headers': {
4332
+ 'Content-Type': 'application/json',
4333
+ 'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
4334
+ 'traceparent': traceparent
4335
+ },
4336
+ 'body': JSON.stringify(payload)
4337
+ }).catch(function(error) {
4338
+ // Silent failure - cohort sync will catch up
4339
+ logger$3.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
4340
+ });
4341
+ };
4342
+
4040
4343
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
4041
4344
  if (!this.fetchPromise) {
4042
4345
  return new Promise(function(resolve) {
@@ -4155,6 +4458,9 @@
4155
4458
  // Deprecated method
4156
4459
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
4157
4460
 
4461
+ // Exports intended only for testing
4462
+ FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
4463
+
4158
4464
  /* eslint camelcase: "off" */
4159
4465
 
4160
4466
 
@@ -6842,6 +7148,9 @@
6842
7148
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
6843
7149
  /** @const */ var PAYLOAD_TYPE_JSON = 'json';
6844
7150
  /** @const */ var DEVICE_ID_PREFIX = '$device:';
7151
+ /** @const */ var SETTING_STRICT = 'strict';
7152
+ /** @const */ var SETTING_FALLBACK = 'fallback';
7153
+ /** @const */ var SETTING_DISABLED = 'disabled';
6845
7154
 
6846
7155
 
6847
7156
  /*
@@ -6870,7 +7179,8 @@
6870
7179
  'engage': 'engage/',
6871
7180
  'groups': 'groups/',
6872
7181
  'record': 'record/',
6873
- 'flags': 'flags/'
7182
+ 'flags': 'flags/',
7183
+ 'settings': 'settings/'
6874
7184
  };
6875
7185
 
6876
7186
  /*
@@ -6934,12 +7244,13 @@
6934
7244
  'record_console': true,
6935
7245
  'record_heatmap_data': false,
6936
7246
  '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': '*',
7247
+ 'record_mask_inputs': true,
6939
7248
  'record_max_ms': MAX_RECORDING_MS,
6940
7249
  'record_min_ms': 0,
6941
7250
  'record_sessions_percent': 0,
6942
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
7251
+ 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
7252
+ 'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
7253
+ 'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
6943
7254
  };
6944
7255
 
6945
7256
  var DOM_LOADED = false;
@@ -7168,7 +7479,9 @@
7168
7479
  getConfigFunc: _.bind(this.get_config, this),
7169
7480
  setConfigFunc: _.bind(this.set_config, this),
7170
7481
  getPropertyFunc: _.bind(this.get_property, this),
7171
- trackingFunc: _.bind(this.track, this)
7482
+ trackingFunc: _.bind(this.track, this),
7483
+ loadExtraBundle: load_extra_bundle,
7484
+ targetingSrc: this.get_config('targeting_src')
7172
7485
  });
7173
7486
  this.flags.init();
7174
7487
  this['flags'] = this.flags;
@@ -7177,7 +7490,16 @@
7177
7490
  this.autocapture.init();
7178
7491
 
7179
7492
  this._init_tab_id();
7180
- this._check_and_start_session_recording();
7493
+
7494
+ // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
7495
+ var mode = this.get_config('remote_settings_mode');
7496
+ if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
7497
+ this._fetch_remote_settings(mode).then(_.bind(function() {
7498
+ this._check_and_start_session_recording();
7499
+ }, this));
7500
+ } else {
7501
+ this._check_and_start_session_recording();
7502
+ }
7181
7503
  };
7182
7504
 
7183
7505
  /**
@@ -7255,11 +7577,11 @@
7255
7577
 
7256
7578
  var loadRecorder = _.bind(function(startNewIfInactive) {
7257
7579
  var handleLoadedRecorder = _.bind(function() {
7258
- this._recorder = this._recorder || new win['__mp_recorder'](this);
7580
+ this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this);
7259
7581
  this._recorder['resumeRecording'](startNewIfInactive);
7260
7582
  }, this);
7261
7583
 
7262
- if (_.isUndefined(win['__mp_recorder'])) {
7584
+ if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
7263
7585
  load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
7264
7586
  } else {
7265
7587
  handleLoadedRecorder();
@@ -7602,6 +7924,77 @@
7602
7924
  return succeeded;
7603
7925
  };
7604
7926
 
7927
+ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
7928
+ var disableRecordingIfStrict = function() {
7929
+ if (mode === 'strict') {
7930
+ self.set_config({'record_sessions_percent': 0});
7931
+ }
7932
+ };
7933
+
7934
+ if (!win['AbortController']) {
7935
+ console.critical('Remote settings unavailable: missing minimum required APIs');
7936
+ disableRecordingIfStrict();
7937
+ return Promise.resolve();
7938
+ }
7939
+
7940
+ var settings_endpoint = this.get_api_host('settings') + '/' + this.get_config('api_routes')['settings'];
7941
+ var request_params = {
7942
+ '$lib_version': Config.LIB_VERSION,
7943
+ 'mp_lib': 'web',
7944
+ 'sdk_config': '1',
7945
+ };
7946
+ var query_string = _.HTTPBuildQuery(request_params);
7947
+ var full_url = settings_endpoint + '?' + query_string;
7948
+ var self = this;
7949
+
7950
+ var abortController = new AbortController();
7951
+ var timeout_id = setTimeout(function() {
7952
+ abortController.abort();
7953
+ }, 500);
7954
+ var fetchOptions = {
7955
+ 'method': 'GET',
7956
+ 'headers': {
7957
+ 'Authorization': 'Basic ' + btoa(self.get_config('token') + ':'),
7958
+ },
7959
+ 'signal': abortController.signal
7960
+ };
7961
+
7962
+ return win['fetch'](full_url, fetchOptions).then(function(response) {
7963
+ clearTimeout(timeout_id);
7964
+ if (!response['ok']) {
7965
+ console.critical('Network response was not ok');
7966
+ disableRecordingIfStrict();
7967
+ return;
7968
+ }
7969
+ return response.json();
7970
+ }).then(function(result) {
7971
+ if (result && result['sdk_config'] && result['sdk_config']['config']) {
7972
+ var remote_config = result['sdk_config']['config'];
7973
+
7974
+ // Verify that remote config contains only valid keys from DEFAULT_CONFIG
7975
+ var valid_config = {};
7976
+ _.each(remote_config, function(value, key) {
7977
+ if (DEFAULT_CONFIG.hasOwnProperty(key)) {
7978
+ valid_config[key] = value;
7979
+ }
7980
+ });
7981
+
7982
+ if (_.isEmptyObject(valid_config)) {
7983
+ console.critical('No valid config keys found in remote settings.');
7984
+ disableRecordingIfStrict();
7985
+ } else {
7986
+ self.set_config(valid_config);
7987
+ }
7988
+ } else {
7989
+ disableRecordingIfStrict();
7990
+ }
7991
+ }).catch(function(err) {
7992
+ clearTimeout(timeout_id);
7993
+ console.critical('Failed to fetch remote settings', err);
7994
+ disableRecordingIfStrict();
7995
+ });
7996
+ };
7997
+
7605
7998
  /**
7606
7999
  * _execute_array() deals with processing any mixpanel function
7607
8000
  * calls that were called before the Mixpanel library were loaded
@@ -7930,6 +8323,11 @@
7930
8323
  send_request_options: options
7931
8324
  }, callback);
7932
8325
 
8326
+ // Check for first-time event matches
8327
+ if (this.flags && this.flags.checkFirstTimeEvents) {
8328
+ this.flags.checkFirstTimeEvents(event_name, properties);
8329
+ }
8330
+
7933
8331
  return ret;
7934
8332
  });
7935
8333