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