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
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
var Config = {
|
|
4
4
|
DEBUG: false,
|
|
5
|
-
LIB_VERSION: '2.
|
|
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
|
-
|
|
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
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
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
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
4178
|
-
* @constructor
|
|
4456
|
+
* @type {import('./wrapper').StorageWrapper}
|
|
4179
4457
|
*/
|
|
4180
|
-
var
|
|
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
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
DomTracker.prototype.after_track_handler = function() {};
|
|
4473
|
+
openRequest['onsuccess'] = function () {
|
|
4474
|
+
resolve(openRequest.result);
|
|
4475
|
+
};
|
|
4187
4476
|
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
return this;
|
|
4191
|
-
};
|
|
4477
|
+
openRequest['onupgradeneeded'] = function (ev) {
|
|
4478
|
+
var db = ev.target.result;
|
|
4192
4479
|
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
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
|
-
|
|
4204
|
-
|
|
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
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
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
|
-
|
|
4220
|
-
|
|
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
|
-
|
|
4504
|
+
};
|
|
4223
4505
|
|
|
4224
|
-
|
|
4506
|
+
IDBStorageWrapper.prototype.isInitialized = function () {
|
|
4507
|
+
return !!this.dbPromise;
|
|
4225
4508
|
};
|
|
4226
4509
|
|
|
4227
4510
|
/**
|
|
4228
|
-
* @param {
|
|
4229
|
-
* @param {
|
|
4230
|
-
* @param {boolean=} timeout_occured
|
|
4511
|
+
* @param {IDBTransactionMode} mode
|
|
4512
|
+
* @param {function(IDBObjectStore): void} storeCb
|
|
4231
4513
|
*/
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
var
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
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':
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
7786
|
+
return this.recorderManager.isRecordingHeatmapData();
|
|
7341
7787
|
};
|
|
7342
7788
|
|
|
7343
7789
|
MixpanelLib.prototype.get_session_recording_properties = function () {
|
|
7344
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|