mixpanel-browser 2.74.0 → 2.76.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 (61) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/.github/workflows/integration-tests.yml +2 -2
  3. package/.github/workflows/unit-tests.yml +3 -3
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +2 -2
  6. package/build.sh +10 -8
  7. package/dist/async-modules/mixpanel-recorder-bIS4LMGd.js +23595 -0
  8. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js +2 -0
  9. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js.map +1 -0
  10. package/dist/async-modules/mixpanel-targeting-BcAPS-Mz.js +2520 -0
  11. package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js +2 -0
  12. package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js.map +1 -0
  13. package/dist/mixpanel-core.cjs.d.ts +68 -0
  14. package/dist/mixpanel-core.cjs.js +802 -337
  15. package/dist/mixpanel-recorder.js +828 -40
  16. package/dist/mixpanel-recorder.min.js +1 -1
  17. package/dist/mixpanel-recorder.min.js.map +1 -1
  18. package/dist/mixpanel-targeting.js +2520 -0
  19. package/dist/mixpanel-targeting.min.js +2 -0
  20. package/dist/mixpanel-targeting.min.js.map +1 -0
  21. package/dist/mixpanel-with-async-modules.cjs.d.ts +590 -0
  22. package/dist/mixpanel-with-async-modules.cjs.js +9867 -0
  23. package/dist/mixpanel-with-async-recorder.cjs.d.ts +68 -0
  24. package/dist/mixpanel-with-async-recorder.cjs.js +802 -337
  25. package/dist/mixpanel-with-recorder.d.ts +68 -0
  26. package/dist/mixpanel-with-recorder.js +1591 -343
  27. package/dist/mixpanel-with-recorder.min.d.ts +68 -0
  28. package/dist/mixpanel-with-recorder.min.js +1 -1
  29. package/dist/mixpanel.amd.d.ts +68 -0
  30. package/dist/mixpanel.amd.js +2124 -345
  31. package/dist/mixpanel.cjs.d.ts +68 -0
  32. package/dist/mixpanel.cjs.js +2124 -345
  33. package/dist/mixpanel.globals.js +802 -337
  34. package/dist/mixpanel.min.js +185 -175
  35. package/dist/mixpanel.module.d.ts +68 -0
  36. package/dist/mixpanel.module.js +2124 -345
  37. package/dist/mixpanel.umd.d.ts +68 -0
  38. package/dist/mixpanel.umd.js +2124 -345
  39. package/dist/rrweb-bundled.js +119 -5
  40. package/dist/rrweb-compiled.js +116 -5
  41. package/logo.svg +5 -0
  42. package/package.json +5 -3
  43. package/rollup.config.mjs +189 -40
  44. package/src/autocapture/index.js +10 -27
  45. package/src/config.js +9 -3
  46. package/src/flags/index.js +269 -9
  47. package/src/index.d.ts +68 -0
  48. package/src/loaders/loader-module.js +1 -0
  49. package/src/mixpanel-core.js +83 -109
  50. package/src/recorder/index.js +2 -1
  51. package/src/recorder/recorder.js +5 -1
  52. package/src/recorder/rrweb-network-plugin.js +649 -0
  53. package/src/recorder/session-recording.js +31 -11
  54. package/src/recorder-manager.js +216 -0
  55. package/src/request-batcher.js +1 -1
  56. package/src/targeting/event-matcher.js +42 -0
  57. package/src/targeting/index.js +11 -0
  58. package/src/targeting/loader.js +36 -0
  59. package/src/utils.js +14 -9
  60. package/testServer.js +55 -0
  61. /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +0 -0
@@ -2,9 +2,17 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.74.0'
5
+ LIB_VERSION: '2.76.0'
6
6
  };
7
7
 
8
+ // Window global names for async modules
9
+ var TARGETING_GLOBAL_NAME = '__mp_targeting';
10
+ var RECORDER_GLOBAL_NAME = '__mp_recorder';
11
+
12
+ // Constants that are injected at build-time for the names of async modules.
13
+ var RECORDER_FILENAME = 'mixpanel-recorder-bIS4LMGd.js';
14
+ var TARGETING_FILENAME = 'mixpanel-targeting-BcAPS-Mz.js';
15
+
8
16
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
9
17
  var win;
10
18
  if (typeof(window) === 'undefined') {
@@ -593,15 +601,8 @@ _.isArray = nativeIsArray || function(obj) {
593
601
  return toString.call(obj) === '[object Array]';
594
602
  };
595
603
 
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
604
  _.isFunction = function(f) {
600
- try {
601
- return /^\s*\bfunction\b/.test(f);
602
- } catch (x) {
603
- return false;
604
- }
605
+ return typeof f === 'function';
605
606
  };
606
607
 
607
608
  _.isArguments = function(obj) {
@@ -2128,6 +2129,17 @@ var isOnline = function() {
2128
2129
 
2129
2130
  var NOOP_FUNC = function () {};
2130
2131
 
2132
+ var urlMatchesRegexList = function (url, regexList) {
2133
+ var matches = false;
2134
+ for (var i = 0; i < regexList.length; i++) {
2135
+ if (url.match(regexList[i])) {
2136
+ matches = true;
2137
+ break;
2138
+ }
2139
+ }
2140
+ return matches;
2141
+ };
2142
+
2131
2143
  var JSONStringify = null, JSONParse = null;
2132
2144
  if (typeof JSON !== 'undefined') {
2133
2145
  JSONStringify = JSON.stringify;
@@ -2150,15 +2162,6 @@ _['JSONEncode'] = _.JSONEncode;
2150
2162
  _['toArray'] = _.toArray;
2151
2163
  _['NPO'] = NpoPromise;
2152
2164
 
2153
- /**
2154
- * @param {import('./session-recording').SerializedRecording} serializedRecording
2155
- * @returns {boolean}
2156
- */
2157
- var isRecordingExpired = function(serializedRecording) {
2158
- var now = Date.now();
2159
- return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
2160
- };
2161
-
2162
2165
  // stateless utils
2163
2166
  // mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
2164
2167
 
@@ -3423,27 +3426,15 @@ Autocapture.prototype.getConfig = function(key) {
3423
3426
  };
3424
3427
 
3425
3428
  Autocapture.prototype.currentUrlBlocked = function() {
3426
- var i;
3427
3429
  var currentUrl = _.info.currentUrl();
3428
3430
 
3429
3431
  var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
3430
3432
  if (allowUrlRegexes.length) {
3431
3433
  // we're using an allowlist, only track if current URL matches
3432
- var allowed = false;
3433
- for (i = 0; i < allowUrlRegexes.length; i++) {
3434
- var allowRegex = allowUrlRegexes[i];
3435
- try {
3436
- if (currentUrl.match(allowRegex)) {
3437
- allowed = true;
3438
- break;
3439
- }
3440
- } catch (err) {
3441
- logger$4.critical('Error while checking block URL regex: ' + allowRegex, err);
3442
- return true;
3443
- }
3444
- }
3445
- if (!allowed) {
3446
- // wasn't allowed by any regex
3434
+ try {
3435
+ return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
3436
+ } catch (err) {
3437
+ logger$4.critical('Error while checking block URL regexes: ', err);
3447
3438
  return true;
3448
3439
  }
3449
3440
  }
@@ -3453,17 +3444,12 @@ Autocapture.prototype.currentUrlBlocked = function() {
3453
3444
  return false;
3454
3445
  }
3455
3446
 
3456
- for (i = 0; i < blockUrlRegexes.length; i++) {
3457
- try {
3458
- if (currentUrl.match(blockUrlRegexes[i])) {
3459
- return true;
3460
- }
3461
- } catch (err) {
3462
- logger$4.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
3463
- return true;
3464
- }
3447
+ try {
3448
+ return urlMatchesRegexList(currentUrl, blockUrlRegexes);
3449
+ } catch (err) {
3450
+ logger$4.critical('Error while checking block URL regexes: ', err);
3451
+ return true;
3465
3452
  }
3466
- return false;
3467
3453
  };
3468
3454
 
3469
3455
  Autocapture.prototype.pageviewTrackingConfig = function() {
@@ -3907,14 +3893,62 @@ Autocapture.prototype.stopDeadClickTracking = function() {
3907
3893
  // TODO integrate error_reporter from mixpanel instance
3908
3894
  safewrapClass(Autocapture);
3909
3895
 
3910
- var logger$3 = console_with_prefix('flags');
3896
+ /**
3897
+ * Get the promise-based targeting loader
3898
+ * @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
3899
+ * @param {string} targetingSrc - URL to targeting bundle
3900
+ * @returns {Promise} Promise that resolves with targeting library
3901
+ */
3902
+ var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
3903
+ // Return existing promise if already initialized or loading
3904
+ if (win[TARGETING_GLOBAL_NAME] && typeof win[TARGETING_GLOBAL_NAME].then === 'function') {
3905
+ return win[TARGETING_GLOBAL_NAME];
3906
+ }
3907
+
3908
+ // Create loading promise and set it as the global immediately
3909
+ // This makes minified build behavior consistent with dev/CJS builds
3910
+ win[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
3911
+ loadExtraBundle(targetingSrc, resolve);
3912
+ }).then(function () {
3913
+ var p = win[TARGETING_GLOBAL_NAME];
3914
+ if (p && typeof p.then === 'function') {
3915
+ return p;
3916
+ }
3917
+ throw new Error('targeting failed to load');
3918
+ }).catch(function (err) {
3919
+ delete win[TARGETING_GLOBAL_NAME];
3920
+ throw err;
3921
+ });
3922
+
3923
+ return win[TARGETING_GLOBAL_NAME];
3924
+ };
3911
3925
 
3926
+ var logger$3 = console_with_prefix('flags');
3912
3927
  var FLAGS_CONFIG_KEY = 'flags';
3913
3928
 
3914
3929
  var CONFIG_CONTEXT = 'context';
3915
3930
  var CONFIG_DEFAULTS = {};
3916
3931
  CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
3917
3932
 
3933
+ /**
3934
+ * Generate a unique key for a pending first-time event
3935
+ * @param {string} flagKey - The flag key
3936
+ * @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
3937
+ * @returns {string} Composite key in format "flagKey:firstTimeEventHash"
3938
+ */
3939
+ var getPendingEventKey = function(flagKey, firstTimeEventHash) {
3940
+ return flagKey + ':' + firstTimeEventHash;
3941
+ };
3942
+
3943
+ /**
3944
+ * Extract the flag key from a pending event key
3945
+ * @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
3946
+ * @returns {string} The flag key portion
3947
+ */
3948
+ var getFlagKeyFromPendingEventKey = function(eventKey) {
3949
+ return eventKey.split(':')[0];
3950
+ };
3951
+
3918
3952
  /**
3919
3953
  * FeatureFlagManager: support for Mixpanel's feature flagging product
3920
3954
  * @constructor
@@ -3926,6 +3960,8 @@ var FeatureFlagManager = function(initOptions) {
3926
3960
  this.setMpConfig = initOptions.setConfigFunc;
3927
3961
  this.getMpProperty = initOptions.getPropertyFunc;
3928
3962
  this.track = initOptions.trackingFunc;
3963
+ this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
3964
+ this.targetingSrc = initOptions.targetingSrc || '';
3929
3965
  };
3930
3966
 
3931
3967
  FeatureFlagManager.prototype.init = function() {
@@ -3938,6 +3974,8 @@ FeatureFlagManager.prototype.init = function() {
3938
3974
  this.fetchFlags();
3939
3975
 
3940
3976
  this.trackedFeatures = new Set();
3977
+ this.pendingFirstTimeEvents = {};
3978
+ this.activatedFirstTimeEvents = {};
3941
3979
  };
3942
3980
 
3943
3981
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -4018,17 +4056,78 @@ FeatureFlagManager.prototype.fetchFlags = function() {
4018
4056
  throw new Error('No flags in API response');
4019
4057
  }
4020
4058
  var flags = new Map();
4059
+ var pendingFirstTimeEvents = {};
4060
+
4061
+ // Process flags from response
4021
4062
  _.each(responseFlags, function(data, key) {
4022
- flags.set(key, {
4023
- 'key': data['variant_key'],
4024
- 'value': data['variant_value'],
4025
- 'experiment_id': data['experiment_id'],
4026
- 'is_experiment_active': data['is_experiment_active'],
4027
- 'is_qa_tester': data['is_qa_tester']
4063
+ // Check if this flag has any activated first-time events this session
4064
+ var hasActivatedEvent = false;
4065
+ var prefix = key + ':';
4066
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4067
+ if (eventKey.startsWith(prefix)) {
4068
+ hasActivatedEvent = true;
4069
+ }
4028
4070
  });
4029
- });
4071
+
4072
+ if (hasActivatedEvent) {
4073
+ // Preserve the activated variant, don't overwrite with server's current variant
4074
+ var currentFlag = this.flags && this.flags.get(key);
4075
+ if (currentFlag) {
4076
+ flags.set(key, currentFlag);
4077
+ }
4078
+ } else {
4079
+ // Use server's current variant
4080
+ flags.set(key, {
4081
+ 'key': data['variant_key'],
4082
+ 'value': data['variant_value'],
4083
+ 'experiment_id': data['experiment_id'],
4084
+ 'is_experiment_active': data['is_experiment_active'],
4085
+ 'is_qa_tester': data['is_qa_tester']
4086
+ });
4087
+ }
4088
+ }, this);
4089
+
4090
+ // Process top-level pending_first_time_events array
4091
+ var topLevelDefinitions = responseBody['pending_first_time_events'];
4092
+ if (topLevelDefinitions && topLevelDefinitions.length > 0) {
4093
+ _.each(topLevelDefinitions, function(def) {
4094
+ var flagKey = def['flag_key'];
4095
+ var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
4096
+
4097
+ // Skip if this specific event has already been activated this session
4098
+ if (this.activatedFirstTimeEvents[eventKey]) {
4099
+ return;
4100
+ }
4101
+
4102
+ // Store pending event definition using composite key
4103
+ pendingFirstTimeEvents[eventKey] = {
4104
+ 'flag_key': flagKey,
4105
+ 'flag_id': def['flag_id'],
4106
+ 'project_id': def['project_id'],
4107
+ 'first_time_event_hash': def['first_time_event_hash'],
4108
+ 'event_name': def['event_name'],
4109
+ 'property_filters': def['property_filters'],
4110
+ 'pending_variant': def['pending_variant']
4111
+ };
4112
+ }, this);
4113
+ }
4114
+
4115
+ // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
4116
+ if (this.activatedFirstTimeEvents) {
4117
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4118
+ var flagKey = getFlagKeyFromPendingEventKey(eventKey);
4119
+ if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
4120
+ // Keep the activated flag even though it's not in the new response
4121
+ flags.set(flagKey, this.flags.get(flagKey));
4122
+ }
4123
+ }, this);
4124
+ }
4125
+
4030
4126
  this.flags = flags;
4127
+ this.pendingFirstTimeEvents = pendingFirstTimeEvents;
4031
4128
  this._traceparent = traceparent;
4129
+
4130
+ this._loadTargetingIfNeeded();
4032
4131
  }.bind(this)).catch(function(error) {
4033
4132
  this.markFetchComplete();
4034
4133
  logger$3.error(error);
@@ -4052,6 +4151,177 @@ FeatureFlagManager.prototype.markFetchComplete = function() {
4052
4151
  this._fetchInProgressStartTime = null;
4053
4152
  };
4054
4153
 
4154
+ /**
4155
+ * Proactively load targeting bundle if any pending events have property filters
4156
+ */
4157
+ FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
4158
+ var hasPropertyFilters = false;
4159
+ _.each(this.pendingFirstTimeEvents, function(evt) {
4160
+ if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
4161
+ hasPropertyFilters = true;
4162
+ }
4163
+ });
4164
+
4165
+ if (hasPropertyFilters) {
4166
+ this.getTargeting().then(function() {
4167
+ logger$3.log('targeting loaded for property filter evaluation');
4168
+ });
4169
+ }
4170
+ };
4171
+
4172
+ /**
4173
+ * Get the targeting library (initializes if not already loaded)
4174
+ * This method is primarily for testing - production code should rely on automatic loading
4175
+ * @returns {Promise} Promise that resolves with targeting library
4176
+ */
4177
+ FeatureFlagManager.prototype.getTargeting = function() {
4178
+ return getTargetingPromise(
4179
+ this.loadExtraBundle.bind(this),
4180
+ this.targetingSrc
4181
+ ).catch(function(error) {
4182
+ logger$3.error('Failed to load targeting: ' + error);
4183
+ }.bind(this));
4184
+ };
4185
+
4186
+ /**
4187
+ * Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
4188
+ * @param {string} eventName - The name of the event being tracked
4189
+ * @param {Object} properties - Event properties to evaluate against property filters
4190
+ *
4191
+ * When a match is found (event name matches and property filters pass), this method:
4192
+ * - Switches the flag to the pending variant
4193
+ * - Marks the event as activated for this session
4194
+ * - Records the activation via the API (fire-and-forget)
4195
+ */
4196
+ FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
4197
+ if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
4198
+ return;
4199
+ }
4200
+
4201
+ // Check if targeting promise exists (either bundled or async loaded)
4202
+ if (win[TARGETING_GLOBAL_NAME] && _.isFunction(win[TARGETING_GLOBAL_NAME].then)) {
4203
+ win[TARGETING_GLOBAL_NAME].then(function(library) {
4204
+ this._processFirstTimeEventCheck(eventName, properties, library);
4205
+ }.bind(this)).catch(function() {
4206
+ // If targeting failed to load, process with null
4207
+ // Events without property filters will still match
4208
+ this._processFirstTimeEventCheck(eventName, properties, null);
4209
+ }.bind(this));
4210
+ } else {
4211
+ // No targeting available, process with null
4212
+ // Events without property filters will still match
4213
+ this._processFirstTimeEventCheck(eventName, properties, null);
4214
+ }
4215
+ };
4216
+
4217
+ /**
4218
+ * Internal method to process first-time event checks with loaded targeting library
4219
+ * @param {string} eventName - The name of the event being tracked
4220
+ * @param {Object} properties - Event properties to evaluate against property filters
4221
+ * @param {Object} targeting - The loaded targeting library
4222
+ */
4223
+ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
4224
+ _.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
4225
+ if (this.activatedFirstTimeEvents[eventKey]) {
4226
+ return;
4227
+ }
4228
+
4229
+ var flagKey = pendingEvent['flag_key'];
4230
+
4231
+ // Use targeting module to check if event matches
4232
+ var matchResult;
4233
+
4234
+ // If no targeting library and event has property filters, skip it
4235
+ if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
4236
+ logger$3.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
4237
+ return;
4238
+ }
4239
+
4240
+ // For simple events (no property filters), just check event name
4241
+ if (!targeting) {
4242
+ matchResult = {
4243
+ matches: eventName === pendingEvent['event_name'],
4244
+ error: null
4245
+ };
4246
+ } else {
4247
+ var criteria = {
4248
+ 'event_name': pendingEvent['event_name'],
4249
+ 'property_filters': pendingEvent['property_filters']
4250
+ };
4251
+ matchResult = targeting['eventMatchesCriteria'](
4252
+ eventName,
4253
+ properties,
4254
+ criteria
4255
+ );
4256
+ }
4257
+
4258
+ if (matchResult.error) {
4259
+ logger$3.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
4260
+ return;
4261
+ }
4262
+
4263
+ if (!matchResult.matches) {
4264
+ return;
4265
+ }
4266
+
4267
+ logger$3.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
4268
+
4269
+ var newVariant = {
4270
+ 'key': pendingEvent['pending_variant']['variant_key'],
4271
+ 'value': pendingEvent['pending_variant']['variant_value'],
4272
+ 'experiment_id': pendingEvent['pending_variant']['experiment_id'],
4273
+ 'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
4274
+ };
4275
+
4276
+ this.flags.set(flagKey, newVariant);
4277
+ this.activatedFirstTimeEvents[eventKey] = true;
4278
+
4279
+ this.recordFirstTimeEvent(
4280
+ pendingEvent['flag_id'],
4281
+ pendingEvent['project_id'],
4282
+ pendingEvent['first_time_event_hash']
4283
+ );
4284
+ }, this);
4285
+ };
4286
+
4287
+ FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
4288
+ // Construct URL: {api_host}/flags/{flagId}/first-time-events
4289
+ return this.getFullApiRoute() + '/' + flagId + '/first-time-events';
4290
+ };
4291
+
4292
+ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
4293
+ var distinctId = this.getMpProperty('distinct_id');
4294
+ var traceparent = generateTraceparent();
4295
+
4296
+ // Build URL with query string parameters
4297
+ var searchParams = new URLSearchParams();
4298
+ searchParams.set('mp_lib', 'web');
4299
+ searchParams.set('$lib_version', Config.LIB_VERSION);
4300
+ var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
4301
+
4302
+ var payload = {
4303
+ 'distinct_id': distinctId,
4304
+ 'project_id': projectId,
4305
+ 'first_time_event_hash': firstTimeEventHash
4306
+ };
4307
+
4308
+ logger$3.log('Recording first-time event for flag: ' + flagId);
4309
+
4310
+ // Fire-and-forget POST request
4311
+ this.fetch.call(win, url, {
4312
+ 'method': 'POST',
4313
+ 'headers': {
4314
+ 'Content-Type': 'application/json',
4315
+ 'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
4316
+ 'traceparent': traceparent
4317
+ },
4318
+ 'body': JSON.stringify(payload)
4319
+ }).catch(function(error) {
4320
+ // Silent failure - cohort sync will catch up
4321
+ logger$3.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
4322
+ });
4323
+ };
4324
+
4055
4325
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
4056
4326
  if (!this.fetchPromise) {
4057
4327
  return new Promise(function(resolve) {
@@ -4170,76 +4440,423 @@ FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.up
4170
4440
  // Deprecated method
4171
4441
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
4172
4442
 
4173
- /* eslint camelcase: "off" */
4443
+ // Exports intended only for testing
4444
+ FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
4445
+
4446
+ var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
4174
4447
 
4448
+ var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
4449
+ var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
4450
+
4451
+ // note: increment the version number when adding new object stores
4452
+ var DB_VERSION = 1;
4453
+ var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
4175
4454
 
4176
4455
  /**
4177
- * DomTracker Object
4178
- * @constructor
4456
+ * @type {import('./wrapper').StorageWrapper}
4179
4457
  */
4180
- var DomTracker = function() {};
4458
+ var IDBStorageWrapper = function (storeName) {
4459
+ /**
4460
+ * @type {Promise<IDBDatabase>|null}
4461
+ */
4462
+ this.dbPromise = null;
4463
+ this.storeName = storeName;
4464
+ };
4181
4465
 
4466
+ IDBStorageWrapper.prototype._openDb = function () {
4467
+ return new PromisePolyfill(function (resolve, reject) {
4468
+ var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
4469
+ openRequest['onerror'] = function () {
4470
+ reject(openRequest.error);
4471
+ };
4182
4472
 
4183
- // interface
4184
- DomTracker.prototype.create_properties = function() {};
4185
- DomTracker.prototype.event_handler = function() {};
4186
- DomTracker.prototype.after_track_handler = function() {};
4473
+ openRequest['onsuccess'] = function () {
4474
+ resolve(openRequest.result);
4475
+ };
4187
4476
 
4188
- DomTracker.prototype.init = function(mixpanel_instance) {
4189
- this.mp = mixpanel_instance;
4190
- return this;
4191
- };
4477
+ openRequest['onupgradeneeded'] = function (ev) {
4478
+ var db = ev.target.result;
4192
4479
 
4193
- /**
4194
- * @param {Object|string} query
4195
- * @param {string} event_name
4196
- * @param {Object=} properties
4197
- * @param {function=} user_callback
4198
- */
4199
- DomTracker.prototype.track = function(query, event_name, properties, user_callback) {
4200
- var that = this;
4201
- var elements = _.dom_query(query);
4480
+ OBJECT_STORES.forEach(function (storeName) {
4481
+ db.createObjectStore(storeName);
4482
+ });
4483
+ };
4484
+ });
4485
+ };
4202
4486
 
4203
- if (elements.length === 0) {
4204
- console.error('The DOM query (' + query + ') returned 0 elements');
4205
- return;
4487
+ IDBStorageWrapper.prototype.init = function () {
4488
+ if (!win.indexedDB) {
4489
+ return PromisePolyfill.reject('indexedDB is not supported in this browser');
4206
4490
  }
4207
4491
 
4208
- _.each(elements, function(element) {
4209
- _.register_event(element, this.override_event, function(e) {
4210
- var options = {};
4211
- var props = that.create_properties(properties, this);
4212
- var timeout = that.mp.get_config('track_links_timeout');
4213
-
4214
- that.event_handler(e, this, options);
4215
-
4216
- // in case the mixpanel servers don't get back to us in time
4217
- window.setTimeout(that.track_callback(user_callback, props, options, true), timeout);
4492
+ if (!this.dbPromise) {
4493
+ this.dbPromise = this._openDb();
4494
+ }
4218
4495
 
4219
- // fire the tracking event
4220
- that.mp.track(event_name, props, that.track_callback(user_callback, props, options));
4496
+ return this.dbPromise
4497
+ .then(function (dbOrError) {
4498
+ if (dbOrError instanceof win['IDBDatabase']) {
4499
+ return PromisePolyfill.resolve();
4500
+ } else {
4501
+ return PromisePolyfill.reject(dbOrError);
4502
+ }
4221
4503
  });
4222
- }, this);
4504
+ };
4223
4505
 
4224
- return true;
4506
+ IDBStorageWrapper.prototype.isInitialized = function () {
4507
+ return !!this.dbPromise;
4225
4508
  };
4226
4509
 
4227
4510
  /**
4228
- * @param {function} user_callback
4229
- * @param {Object} props
4230
- * @param {boolean=} timeout_occured
4511
+ * @param {IDBTransactionMode} mode
4512
+ * @param {function(IDBObjectStore): void} storeCb
4231
4513
  */
4232
- DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) {
4233
- timeout_occured = timeout_occured || false;
4234
- var that = this;
4235
-
4236
- return function() {
4237
- // options is referenced from both callbacks, so we can have
4238
- // a 'lock' of sorts to ensure only one fires
4239
- if (options.callback_fired) { return; }
4240
- options.callback_fired = true;
4241
-
4242
- if (user_callback && user_callback(timeout_occured, props) === false) {
4514
+ IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
4515
+ var storeName = this.storeName;
4516
+ var doTransaction = function (db) {
4517
+ return new PromisePolyfill(function (resolve, reject) {
4518
+ var transaction = db.transaction(storeName, mode);
4519
+ transaction.oncomplete = function () {
4520
+ resolve(transaction);
4521
+ };
4522
+ transaction.onabort = transaction.onerror = function () {
4523
+ reject(transaction.error);
4524
+ };
4525
+
4526
+ storeCb(transaction.objectStore(storeName));
4527
+ });
4528
+ };
4529
+
4530
+ return this.dbPromise
4531
+ .then(doTransaction)
4532
+ .catch(function (err) {
4533
+ if (err && err['name'] === 'InvalidStateError') {
4534
+ // try reopening the DB if the connection is closed
4535
+ this.dbPromise = this._openDb();
4536
+ return this.dbPromise.then(doTransaction);
4537
+ } else {
4538
+ return PromisePolyfill.reject(err);
4539
+ }
4540
+ }.bind(this));
4541
+ };
4542
+
4543
+ IDBStorageWrapper.prototype.setItem = function (key, value) {
4544
+ return this.makeTransaction('readwrite', function (objectStore) {
4545
+ objectStore.put(value, key);
4546
+ });
4547
+ };
4548
+
4549
+ IDBStorageWrapper.prototype.getItem = function (key) {
4550
+ var req;
4551
+ return this.makeTransaction('readonly', function (objectStore) {
4552
+ req = objectStore.get(key);
4553
+ }).then(function () {
4554
+ return req.result;
4555
+ });
4556
+ };
4557
+
4558
+ IDBStorageWrapper.prototype.removeItem = function (key) {
4559
+ return this.makeTransaction('readwrite', function (objectStore) {
4560
+ objectStore.delete(key);
4561
+ });
4562
+ };
4563
+
4564
+ IDBStorageWrapper.prototype.getAll = function () {
4565
+ var req;
4566
+ return this.makeTransaction('readonly', function (objectStore) {
4567
+ req = objectStore.getAll();
4568
+ }).then(function () {
4569
+ return req.result;
4570
+ });
4571
+ };
4572
+
4573
+ /**
4574
+ * @param {import('./session-recording').SerializedRecording} serializedRecording
4575
+ * @returns {boolean}
4576
+ */
4577
+ var isRecordingExpired = function(serializedRecording) {
4578
+ var now = Date.now();
4579
+ return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
4580
+ };
4581
+
4582
+ /* eslint camelcase: "off" */
4583
+
4584
+
4585
+ /**
4586
+ * RecorderManager: manages session recording initialization, lifecycle and state
4587
+ * @constructor
4588
+ */
4589
+ var RecorderManager = function(initOptions) {
4590
+ // TODO - Passing in mixpanel instance as it is still needed for recorder creation
4591
+ // but ideally we should be able to remove this dependency.
4592
+ this.mixpanelInstance = initOptions.mixpanelInstance;
4593
+
4594
+ this.getMpConfig = initOptions.getConfigFunc;
4595
+ this.getTabId = initOptions.getTabIdFunc;
4596
+ this.reportError = initOptions.reportErrorFunc;
4597
+ this.getDistinctId = initOptions.getDistinctIdFunc;
4598
+ this.loadExtraBundle = initOptions.loadExtraBundle;
4599
+ this.recorderSrc = initOptions.recorderSrc;
4600
+ this.targetingSrc = initOptions.targetingSrc;
4601
+ this.libBasePath = initOptions.libBasePath;
4602
+
4603
+ this._recorder = null;
4604
+ };
4605
+
4606
+ RecorderManager.prototype.shouldLoadRecorder = function() {
4607
+ if (this.getMpConfig('disable_persistence')) {
4608
+ console.log('Load recorder check skipped due to disable_persistence config');
4609
+ return PromisePolyfill.resolve(false);
4610
+ }
4611
+
4612
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
4613
+ var tab_id = this.getTabId();
4614
+ return recording_registry_idb.init()
4615
+ .then(function () {
4616
+ return recording_registry_idb.getAll();
4617
+ })
4618
+ .then(function (recordings) {
4619
+ for (var i = 0; i < recordings.length; i++) {
4620
+ // if there are expired recordings in the registry, we should load the recorder to flush them
4621
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
4622
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
4623
+ return true;
4624
+ }
4625
+ }
4626
+ return false;
4627
+ })
4628
+ .catch(_.bind(function (err) {
4629
+ this.reportError('Error checking recording registry', err);
4630
+ return false;
4631
+ }, this));
4632
+ };
4633
+
4634
+ RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
4635
+ if (!win['MutationObserver']) {
4636
+ console.critical('Browser does not support MutationObserver; skipping session recording');
4637
+ return PromisePolyfill.resolve();
4638
+ }
4639
+
4640
+ var loadRecorder = _.bind(function(startNewIfInactive) {
4641
+ return new PromisePolyfill(_.bind(function(resolve) {
4642
+ var handleLoadedRecorder = safewrap(_.bind(function() {
4643
+ this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
4644
+ this._recorder['resumeRecording'](startNewIfInactive);
4645
+ resolve();
4646
+ }, this));
4647
+
4648
+ if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
4649
+ var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
4650
+ this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
4651
+ } else {
4652
+ handleLoadedRecorder();
4653
+ }
4654
+ }, this));
4655
+ }, this);
4656
+
4657
+ /**
4658
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
4659
+ * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
4660
+ */
4661
+ var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
4662
+ var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
4663
+ if (force_start || is_sampled) {
4664
+ return loadRecorder(true);
4665
+ } else {
4666
+ return this.shouldLoadRecorder()
4667
+ .then(_.bind(function (shouldLoad) {
4668
+ if (shouldLoad) {
4669
+ return loadRecorder(false);
4670
+ }
4671
+ return PromisePolyfill.resolve();
4672
+ }, this));
4673
+ }
4674
+ };
4675
+
4676
+ RecorderManager.prototype.isRecording = function() {
4677
+ // Safety check: ensure isRecording method exists (older CDN builds may not have it)
4678
+ if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
4679
+ return false;
4680
+ }
4681
+ try {
4682
+ return this._recorder['isRecording']();
4683
+ } catch (e) {
4684
+ this.reportError('Error checking if recording is active', e);
4685
+ return false;
4686
+ }
4687
+ };
4688
+
4689
+ RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
4690
+ var isRecording = this.isRecording();
4691
+ var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
4692
+
4693
+ if (!isRecording && recordingTriggerEvents) {
4694
+ var trigger = recordingTriggerEvents[event_name];
4695
+ if (trigger && typeof trigger['percentage'] === 'number') {
4696
+ var newRate = trigger['percentage'];
4697
+ var propertyFilters = trigger['property_filters'];
4698
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
4699
+ var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
4700
+ getTargetingPromise(this.loadExtraBundle, targetingSrc)
4701
+ .then(function(targeting) {
4702
+ try {
4703
+ var result = targeting['eventMatchesCriteria'](
4704
+ event_name,
4705
+ properties,
4706
+ {
4707
+ 'event_name': event_name,
4708
+ 'property_filters': propertyFilters
4709
+ }
4710
+ );
4711
+ if (result['matches']) {
4712
+ this.checkAndStartSessionRecording(false, newRate);
4713
+ }
4714
+ } catch (err) {
4715
+ console.critical('Could not parse recording event trigger properties logic:', err);
4716
+ }
4717
+ }.bind(this)).catch(function(err) {
4718
+ console.critical('Failed to load targeting library:', err);
4719
+ });
4720
+ } else {
4721
+ this.checkAndStartSessionRecording(false, newRate);
4722
+ }
4723
+ }
4724
+ }
4725
+ };
4726
+
4727
+ RecorderManager.prototype.stopSessionRecording = function() {
4728
+ if (this._recorder) {
4729
+ return this._recorder['stopRecording']();
4730
+ }
4731
+ return PromisePolyfill.resolve();
4732
+ };
4733
+
4734
+ RecorderManager.prototype.pauseSessionRecording = function() {
4735
+ if (this._recorder) {
4736
+ return this._recorder['pauseRecording']();
4737
+ }
4738
+ return PromisePolyfill.resolve();
4739
+ };
4740
+
4741
+ RecorderManager.prototype.resumeSessionRecording = function() {
4742
+ if (this._recorder) {
4743
+ return this._recorder['resumeRecording']();
4744
+ }
4745
+ return PromisePolyfill.resolve();
4746
+ };
4747
+
4748
+ RecorderManager.prototype.isRecordingHeatmapData = function() {
4749
+ return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
4750
+ };
4751
+
4752
+ RecorderManager.prototype.getSessionRecordingProperties = function() {
4753
+ var props = {};
4754
+ var replay_id = this.getSessionReplayId();
4755
+ if (replay_id) {
4756
+ props['$mp_replay_id'] = replay_id;
4757
+ }
4758
+ return props;
4759
+ };
4760
+
4761
+ RecorderManager.prototype.getSessionReplayUrl = function() {
4762
+ var replay_url = null;
4763
+ var replay_id = this.getSessionReplayId();
4764
+ if (replay_id) {
4765
+ var query_params = _.HTTPBuildQuery({
4766
+ 'replay_id': replay_id,
4767
+ 'distinct_id': this.getDistinctId(),
4768
+ 'token': this.getMpConfig('token')
4769
+ });
4770
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
4771
+ }
4772
+ return replay_url;
4773
+ };
4774
+
4775
+ RecorderManager.prototype.getSessionReplayId = function() {
4776
+ var replay_id = null;
4777
+ if (this._recorder) {
4778
+ replay_id = this._recorder['replayId'];
4779
+ }
4780
+ return replay_id || null;
4781
+ };
4782
+
4783
+ // "private" public method to reach into the recorder in test cases
4784
+ RecorderManager.prototype.getRecorder = function() {
4785
+ return this._recorder;
4786
+ };
4787
+
4788
+ safewrapClass(RecorderManager);
4789
+
4790
+ /* eslint camelcase: "off" */
4791
+
4792
+
4793
+ /**
4794
+ * DomTracker Object
4795
+ * @constructor
4796
+ */
4797
+ var DomTracker = function() {};
4798
+
4799
+
4800
+ // interface
4801
+ DomTracker.prototype.create_properties = function() {};
4802
+ DomTracker.prototype.event_handler = function() {};
4803
+ DomTracker.prototype.after_track_handler = function() {};
4804
+
4805
+ DomTracker.prototype.init = function(mixpanel_instance) {
4806
+ this.mp = mixpanel_instance;
4807
+ return this;
4808
+ };
4809
+
4810
+ /**
4811
+ * @param {Object|string} query
4812
+ * @param {string} event_name
4813
+ * @param {Object=} properties
4814
+ * @param {function=} user_callback
4815
+ */
4816
+ DomTracker.prototype.track = function(query, event_name, properties, user_callback) {
4817
+ var that = this;
4818
+ var elements = _.dom_query(query);
4819
+
4820
+ if (elements.length === 0) {
4821
+ console.error('The DOM query (' + query + ') returned 0 elements');
4822
+ return;
4823
+ }
4824
+
4825
+ _.each(elements, function(element) {
4826
+ _.register_event(element, this.override_event, function(e) {
4827
+ var options = {};
4828
+ var props = that.create_properties(properties, this);
4829
+ var timeout = that.mp.get_config('track_links_timeout');
4830
+
4831
+ that.event_handler(e, this, options);
4832
+
4833
+ // in case the mixpanel servers don't get back to us in time
4834
+ window.setTimeout(that.track_callback(user_callback, props, options, true), timeout);
4835
+
4836
+ // fire the tracking event
4837
+ that.mp.track(event_name, props, that.track_callback(user_callback, props, options));
4838
+ });
4839
+ }, this);
4840
+
4841
+ return true;
4842
+ };
4843
+
4844
+ /**
4845
+ * @param {function} user_callback
4846
+ * @param {Object} props
4847
+ * @param {boolean=} timeout_occured
4848
+ */
4849
+ DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) {
4850
+ timeout_occured = timeout_occured || false;
4851
+ var that = this;
4852
+
4853
+ return function() {
4854
+ // options is referenced from both callbacks, so we can have
4855
+ // a 'lock' of sorts to ensure only one fires
4856
+ if (options.callback_fired) { return; }
4857
+ options.callback_fired = true;
4858
+
4859
+ if (user_callback && user_callback(timeout_occured, props) === false) {
4243
4860
  // user can prevent the default functionality by
4244
4861
  // returning false from their callback
4245
4862
  return;
@@ -6691,133 +7308,6 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
6691
7308
  return timestamp;
6692
7309
  };
6693
7310
 
6694
- var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
6695
-
6696
- var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
6697
- var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
6698
-
6699
- // note: increment the version number when adding new object stores
6700
- var DB_VERSION = 1;
6701
- var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
6702
-
6703
- /**
6704
- * @type {import('./wrapper').StorageWrapper}
6705
- */
6706
- var IDBStorageWrapper = function (storeName) {
6707
- /**
6708
- * @type {Promise<IDBDatabase>|null}
6709
- */
6710
- this.dbPromise = null;
6711
- this.storeName = storeName;
6712
- };
6713
-
6714
- IDBStorageWrapper.prototype._openDb = function () {
6715
- return new PromisePolyfill(function (resolve, reject) {
6716
- var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
6717
- openRequest['onerror'] = function () {
6718
- reject(openRequest.error);
6719
- };
6720
-
6721
- openRequest['onsuccess'] = function () {
6722
- resolve(openRequest.result);
6723
- };
6724
-
6725
- openRequest['onupgradeneeded'] = function (ev) {
6726
- var db = ev.target.result;
6727
-
6728
- OBJECT_STORES.forEach(function (storeName) {
6729
- db.createObjectStore(storeName);
6730
- });
6731
- };
6732
- });
6733
- };
6734
-
6735
- IDBStorageWrapper.prototype.init = function () {
6736
- if (!win.indexedDB) {
6737
- return PromisePolyfill.reject('indexedDB is not supported in this browser');
6738
- }
6739
-
6740
- if (!this.dbPromise) {
6741
- this.dbPromise = this._openDb();
6742
- }
6743
-
6744
- return this.dbPromise
6745
- .then(function (dbOrError) {
6746
- if (dbOrError instanceof win['IDBDatabase']) {
6747
- return PromisePolyfill.resolve();
6748
- } else {
6749
- return PromisePolyfill.reject(dbOrError);
6750
- }
6751
- });
6752
- };
6753
-
6754
- IDBStorageWrapper.prototype.isInitialized = function () {
6755
- return !!this.dbPromise;
6756
- };
6757
-
6758
- /**
6759
- * @param {IDBTransactionMode} mode
6760
- * @param {function(IDBObjectStore): void} storeCb
6761
- */
6762
- IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
6763
- var storeName = this.storeName;
6764
- var doTransaction = function (db) {
6765
- return new PromisePolyfill(function (resolve, reject) {
6766
- var transaction = db.transaction(storeName, mode);
6767
- transaction.oncomplete = function () {
6768
- resolve(transaction);
6769
- };
6770
- transaction.onabort = transaction.onerror = function () {
6771
- reject(transaction.error);
6772
- };
6773
-
6774
- storeCb(transaction.objectStore(storeName));
6775
- });
6776
- };
6777
-
6778
- return this.dbPromise
6779
- .then(doTransaction)
6780
- .catch(function (err) {
6781
- if (err && err['name'] === 'InvalidStateError') {
6782
- // try reopening the DB if the connection is closed
6783
- this.dbPromise = this._openDb();
6784
- return this.dbPromise.then(doTransaction);
6785
- } else {
6786
- return PromisePolyfill.reject(err);
6787
- }
6788
- }.bind(this));
6789
- };
6790
-
6791
- IDBStorageWrapper.prototype.setItem = function (key, value) {
6792
- return this.makeTransaction('readwrite', function (objectStore) {
6793
- objectStore.put(value, key);
6794
- });
6795
- };
6796
-
6797
- IDBStorageWrapper.prototype.getItem = function (key) {
6798
- var req;
6799
- return this.makeTransaction('readonly', function (objectStore) {
6800
- req = objectStore.get(key);
6801
- }).then(function () {
6802
- return req.result;
6803
- });
6804
- };
6805
-
6806
- IDBStorageWrapper.prototype.removeItem = function (key) {
6807
- return this.makeTransaction('readwrite', function (objectStore) {
6808
- objectStore.delete(key);
6809
- });
6810
- };
6811
-
6812
- IDBStorageWrapper.prototype.getAll = function () {
6813
- var req;
6814
- return this.makeTransaction('readonly', function (objectStore) {
6815
- req = objectStore.getAll();
6816
- }).then(function () {
6817
- return req.result;
6818
- });
6819
- };
6820
-
6821
7311
  /* eslint camelcase: "off" */
6822
7312
 
6823
7313
  /*
@@ -6952,12 +7442,17 @@ var DEFAULT_CONFIG = {
6952
7442
  'record_collect_fonts': false,
6953
7443
  'record_console': true,
6954
7444
  'record_heatmap_data': false,
7445
+ 'recording_event_triggers': {},
6955
7446
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
6956
7447
  'record_mask_inputs': true,
6957
7448
  'record_max_ms': MAX_RECORDING_MS,
6958
7449
  'record_min_ms': 0,
7450
+ 'record_network': false,
7451
+ 'record_network_options': {},
6959
7452
  'record_sessions_percent': 0,
6960
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
7453
+ 'recorder_src': null,
7454
+ 'targeting_src': null,
7455
+ 'lib_base_path': 'https://cdn.mxpnl.com/libs/',
6961
7456
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
6962
7457
  };
6963
7458
 
@@ -7111,6 +7606,19 @@ MixpanelLib.prototype._init = function(token, config, name) {
7111
7606
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
7112
7607
  }));
7113
7608
 
7609
+ this.recorderManager = new RecorderManager({
7610
+ mixpanelInstance: this,
7611
+ getConfigFunc: _.bind(this.get_config, this),
7612
+ setConfigFunc: _.bind(this.set_config, this),
7613
+ getTabIdFunc: _.bind(this.get_tab_id, this),
7614
+ reportErrorFunc: _.bind(this.report_error, this),
7615
+ getDistinctIdFunc: _.bind(this.get_distinct_id, this),
7616
+ recorderSrc: this.get_config('recorder_src'),
7617
+ targetingSrc: this.get_config('targeting_src'),
7618
+ libBasePath: this.get_config('lib_base_path'),
7619
+ loadExtraBundle: load_extra_bundle
7620
+ });
7621
+
7114
7622
  this['_jsc'] = NOOP_FUNC;
7115
7623
 
7116
7624
  this.__dom_loaded_queue = [];
@@ -7187,7 +7695,9 @@ MixpanelLib.prototype._init = function(token, config, name) {
7187
7695
  getConfigFunc: _.bind(this.get_config, this),
7188
7696
  setConfigFunc: _.bind(this.set_config, this),
7189
7697
  getPropertyFunc: _.bind(this.get_property, this),
7190
- trackingFunc: _.bind(this.track, this)
7698
+ trackingFunc: _.bind(this.track, this),
7699
+ loadExtraBundle: load_extra_bundle,
7700
+ targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
7191
7701
  });
7192
7702
  this.flags.init();
7193
7703
  this['flags'] = this.flags;
@@ -7200,11 +7710,11 @@ MixpanelLib.prototype._init = function(token, config, name) {
7200
7710
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
7201
7711
  var mode = this.get_config('remote_settings_mode');
7202
7712
  if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
7203
- this._fetch_remote_settings(mode).then(_.bind(function() {
7204
- this._check_and_start_session_recording();
7713
+ this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
7714
+ return this._check_and_start_session_recording();
7205
7715
  }, this));
7206
7716
  } else {
7207
- this._check_and_start_session_recording();
7717
+ this.__session_recording_init_promise = this._check_and_start_session_recording();
7208
7718
  }
7209
7719
  };
7210
7720
 
@@ -7248,132 +7758,50 @@ MixpanelLib.prototype.get_tab_id = function () {
7248
7758
  return this.tab_id || null;
7249
7759
  };
7250
7760
 
7251
- MixpanelLib.prototype._should_load_recorder = function () {
7252
- if (this.get_config('disable_persistence')) {
7253
- console.log('Load recorder check skipped due to disable_persistence config');
7254
- return Promise.resolve(false);
7255
- }
7256
-
7257
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
7258
- var tab_id = this.get_tab_id();
7259
- return recording_registry_idb.init()
7260
- .then(function () {
7261
- return recording_registry_idb.getAll();
7262
- })
7263
- .then(function (recordings) {
7264
- for (var i = 0; i < recordings.length; i++) {
7265
- // if there are expired recordings in the registry, we should load the recorder to flush them
7266
- // if there's a recording for this tab id, we should load the recorder to continue the recording
7267
- if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
7268
- return true;
7269
- }
7270
- }
7271
- return false;
7272
- })
7273
- .catch(_.bind(function (err) {
7274
- this.report_error('Error checking recording registry', err);
7275
- }, this));
7276
- };
7277
-
7278
7761
  MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
7279
- if (!win['MutationObserver']) {
7280
- console.critical('Browser does not support MutationObserver; skipping session recording');
7281
- return;
7282
- }
7283
-
7284
- var loadRecorder = _.bind(function(startNewIfInactive) {
7285
- var handleLoadedRecorder = _.bind(function() {
7286
- this._recorder = this._recorder || new win['__mp_recorder'](this);
7287
- this._recorder['resumeRecording'](startNewIfInactive);
7288
- }, this);
7289
-
7290
- if (_.isUndefined(win['__mp_recorder'])) {
7291
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
7292
- } else {
7293
- handleLoadedRecorder();
7294
- }
7295
- }, this);
7296
-
7297
- /**
7298
- * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
7299
- * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
7300
- */
7301
- var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
7302
- if (force_start || is_sampled) {
7303
- loadRecorder(true);
7304
- } else {
7305
- this._should_load_recorder()
7306
- .then(function (shouldLoad) {
7307
- if (shouldLoad) {
7308
- loadRecorder(false);
7309
- }
7310
- });
7311
- }
7762
+ return this.recorderManager.checkAndStartSessionRecording(force_start);
7312
7763
  });
7313
7764
 
7765
+ MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
7766
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
7767
+ };
7768
+
7314
7769
  MixpanelLib.prototype.start_session_recording = function () {
7315
- this._check_and_start_session_recording(true);
7770
+ return this._check_and_start_session_recording(true);
7316
7771
  };
7317
7772
 
7318
7773
  MixpanelLib.prototype.stop_session_recording = function () {
7319
- if (this._recorder) {
7320
- return this._recorder['stopRecording']();
7321
- }
7322
- return Promise.resolve();
7774
+ return this.recorderManager.stopSessionRecording();
7323
7775
  };
7324
7776
 
7325
7777
  MixpanelLib.prototype.pause_session_recording = function () {
7326
- if (this._recorder) {
7327
- return this._recorder['pauseRecording']();
7328
- }
7329
- return Promise.resolve();
7778
+ return this.recorderManager.pauseSessionRecording();
7330
7779
  };
7331
7780
 
7332
7781
  MixpanelLib.prototype.resume_session_recording = function () {
7333
- if (this._recorder) {
7334
- return this._recorder['resumeRecording']();
7335
- }
7336
- return Promise.resolve();
7782
+ return this.recorderManager.resumeSessionRecording();
7337
7783
  };
7338
7784
 
7339
7785
  MixpanelLib.prototype.is_recording_heatmap_data = function () {
7340
- return this._get_session_replay_id() && this.get_config('record_heatmap_data');
7786
+ return this.recorderManager.isRecordingHeatmapData();
7341
7787
  };
7342
7788
 
7343
7789
  MixpanelLib.prototype.get_session_recording_properties = function () {
7344
- var props = {};
7345
- var replay_id = this._get_session_replay_id();
7346
- if (replay_id) {
7347
- props['$mp_replay_id'] = replay_id;
7348
- }
7349
- return props;
7790
+ return this.recorderManager.getSessionRecordingProperties();
7350
7791
  };
7351
7792
 
7352
7793
  MixpanelLib.prototype.get_session_replay_url = function () {
7353
- var replay_url = null;
7354
- var replay_id = this._get_session_replay_id();
7355
- if (replay_id) {
7356
- var query_params = _.HTTPBuildQuery({
7357
- 'replay_id': replay_id,
7358
- 'distinct_id': this.get_distinct_id(),
7359
- 'token': this.get_config('token')
7360
- });
7361
- replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
7362
- }
7363
- return replay_url;
7364
- };
7365
-
7366
- MixpanelLib.prototype._get_session_replay_id = function () {
7367
- var replay_id = null;
7368
- if (this._recorder) {
7369
- replay_id = this._recorder['replayId'];
7370
- }
7371
- return replay_id || null;
7794
+ return this.recorderManager.getSessionReplayUrl();
7372
7795
  };
7373
7796
 
7374
7797
  // "private" public method to reach into the recorder in test cases
7375
7798
  MixpanelLib.prototype.__get_recorder = function () {
7376
- return this._recorder;
7799
+ return this.recorderManager.getRecorder();
7800
+ };
7801
+
7802
+ // "private" public method to get session recording init promise in test cases
7803
+ MixpanelLib.prototype.__get_recording_init_promise = function () {
7804
+ return this.__session_recording_init_promise;
7377
7805
  };
7378
7806
 
7379
7807
  // Private methods
@@ -7631,6 +8059,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
7631
8059
  };
7632
8060
 
7633
8061
  MixpanelLib.prototype._fetch_remote_settings = function(mode) {
8062
+ var self = this;
7634
8063
  var disableRecordingIfStrict = function() {
7635
8064
  if (mode === 'strict') {
7636
8065
  self.set_config({'record_sessions_percent': 0});
@@ -7651,7 +8080,6 @@ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
7651
8080
  };
7652
8081
  var query_string = _.HTTPBuildQuery(request_params);
7653
8082
  var full_url = settings_endpoint + '?' + query_string;
7654
- var self = this;
7655
8083
 
7656
8084
  var abortController = new AbortController();
7657
8085
  var timeout_id = setTimeout(function() {
@@ -7843,6 +8271,34 @@ MixpanelLib.prototype.push = function(item) {
7843
8271
  this._execute_array([item]);
7844
8272
  };
7845
8273
 
8274
+ /**
8275
+ * Enables events on the Mixpanel object. If passed no arguments,
8276
+ * this function enable tracking of all events. If passed an
8277
+ * array of event names, those events will be enabled, but other
8278
+ * existing disabled events will continue to be not tracked.
8279
+ *
8280
+ * @param {Array} [events] An array of event names to enable
8281
+ */
8282
+ MixpanelLib.prototype.enable = function(events) {
8283
+ var keys, new_disabled_events, i, j;
8284
+
8285
+ if (typeof(events) === 'undefined') {
8286
+ this._flags.disable_all_events = false;
8287
+ } else {
8288
+ keys = {};
8289
+ new_disabled_events = [];
8290
+ for (i = 0; i < events.length; i++) {
8291
+ keys[events[i]] = true;
8292
+ }
8293
+ for (j = 0; j < this.__disabled_events.length; j++) {
8294
+ if (!keys[this.__disabled_events[j]]) {
8295
+ new_disabled_events.push(this.__disabled_events[j]);
8296
+ }
8297
+ }
8298
+ this.__disabled_events = new_disabled_events;
8299
+ }
8300
+ };
8301
+
7846
8302
  /**
7847
8303
  * Disable events on the Mixpanel object. If passed no arguments,
7848
8304
  * this function disables tracking of any event. If passed an
@@ -8016,6 +8472,8 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
8016
8472
  this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
8017
8473
  }
8018
8474
 
8475
+ this._start_recording_on_event(event_name, properties);
8476
+
8019
8477
  var data = {
8020
8478
  'event': event_name,
8021
8479
  'properties': properties
@@ -8029,6 +8487,11 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
8029
8487
  send_request_options: options
8030
8488
  }, callback);
8031
8489
 
8490
+ // Check for first-time event matches
8491
+ if (this.flags && this.flags.checkFirstTimeEvents) {
8492
+ this.flags.checkFirstTimeEvents(event_name, properties);
8493
+ }
8494
+
8032
8495
  return ret;
8033
8496
  });
8034
8497
 
@@ -9219,6 +9682,7 @@ MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
9219
9682
  // MixpanelLib Exports
9220
9683
  MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
9221
9684
  MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
9685
+ MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
9222
9686
  MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
9223
9687
  MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
9224
9688
  MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
@@ -9262,6 +9726,7 @@ MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES
9262
9726
 
9263
9727
  // Exports intended only for testing
9264
9728
  MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
9729
+ MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
9265
9730
 
9266
9731
  // MixpanelPersistence Exports
9267
9732
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;