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