mixpanel-browser 2.75.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 +14 -0
- package/.github/workflows/integration-tests.yml +2 -2
- package/.github/workflows/unit-tests.yml +2 -2
- package/CHANGELOG.md +10 -0
- 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 +550 -383
- package/dist/mixpanel-recorder.js +708 -32
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-targeting.js +6 -62
- package/dist/mixpanel-targeting.min.js +1 -1
- package/dist/mixpanel-targeting.min.js.map +1 -1
- package/dist/mixpanel-with-async-modules.cjs.d.ts +68 -0
- package/dist/mixpanel-with-async-modules.cjs.js +550 -383
- package/dist/mixpanel-with-async-recorder.cjs.d.ts +68 -0
- package/dist/mixpanel-with-async-recorder.cjs.js +550 -383
- package/dist/mixpanel-with-recorder.d.ts +68 -0
- package/dist/mixpanel-with-recorder.js +1036 -197
- 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 +1038 -251
- package/dist/mixpanel.cjs.d.ts +68 -0
- package/dist/mixpanel.cjs.js +1038 -251
- package/dist/mixpanel.globals.js +550 -383
- package/dist/mixpanel.min.js +184 -181
- package/dist/mixpanel.module.d.ts +68 -0
- package/dist/mixpanel.module.js +1038 -251
- package/dist/mixpanel.umd.d.ts +68 -0
- package/dist/mixpanel.umd.js +1038 -251
- package/logo.svg +5 -0
- package/package.json +2 -1
- package/rollup.config.mjs +163 -46
- package/src/autocapture/index.js +10 -27
- package/src/config.js +9 -3
- package/src/flags/index.js +1 -2
- package/src/index.d.ts +68 -0
- package/src/mixpanel-core.js +76 -111
- package/src/recorder/index.js +1 -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 +2 -57
- package/src/targeting/index.js +1 -1
- package/src/targeting/loader.js +1 -1
- package/src/utils.js +13 -1
- package/testServer.js +55 -0
- package/src/globals.js +0 -14
|
@@ -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 = '__MP_RECORDER_FILENAME__';
|
|
14
|
+
var TARGETING_FILENAME = '__MP_TARGETING_FILENAME__';
|
|
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') {
|
|
@@ -2121,6 +2129,17 @@ var isOnline = function() {
|
|
|
2121
2129
|
|
|
2122
2130
|
var NOOP_FUNC = function () {};
|
|
2123
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
|
+
|
|
2124
2143
|
var JSONStringify = null, JSONParse = null;
|
|
2125
2144
|
if (typeof JSON !== 'undefined') {
|
|
2126
2145
|
JSONStringify = JSON.stringify;
|
|
@@ -2143,25 +2162,6 @@ _['JSONEncode'] = _.JSONEncode;
|
|
|
2143
2162
|
_['toArray'] = _.toArray;
|
|
2144
2163
|
_['NPO'] = NpoPromise;
|
|
2145
2164
|
|
|
2146
|
-
/**
|
|
2147
|
-
* @param {import('./session-recording').SerializedRecording} serializedRecording
|
|
2148
|
-
* @returns {boolean}
|
|
2149
|
-
*/
|
|
2150
|
-
var isRecordingExpired = function(serializedRecording) {
|
|
2151
|
-
var now = Date.now();
|
|
2152
|
-
return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
|
|
2153
|
-
};
|
|
2154
|
-
|
|
2155
|
-
/**
|
|
2156
|
-
* Shared global window property names used across modules
|
|
2157
|
-
*/
|
|
2158
|
-
|
|
2159
|
-
// Targeting library global (used by flags and targeting modules)
|
|
2160
|
-
var TARGETING_GLOBAL_NAME = '__mp_targeting';
|
|
2161
|
-
|
|
2162
|
-
// Recorder library global (used by recorder and mixpanel-core)
|
|
2163
|
-
var RECORDER_GLOBAL_NAME = '__mp_recorder';
|
|
2164
|
-
|
|
2165
2165
|
// stateless utils
|
|
2166
2166
|
// mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
|
|
2167
2167
|
|
|
@@ -3426,27 +3426,15 @@ Autocapture.prototype.getConfig = function(key) {
|
|
|
3426
3426
|
};
|
|
3427
3427
|
|
|
3428
3428
|
Autocapture.prototype.currentUrlBlocked = function() {
|
|
3429
|
-
var i;
|
|
3430
3429
|
var currentUrl = _.info.currentUrl();
|
|
3431
3430
|
|
|
3432
3431
|
var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
|
|
3433
3432
|
if (allowUrlRegexes.length) {
|
|
3434
3433
|
// we're using an allowlist, only track if current URL matches
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
if (currentUrl.match(allowRegex)) {
|
|
3440
|
-
allowed = true;
|
|
3441
|
-
break;
|
|
3442
|
-
}
|
|
3443
|
-
} catch (err) {
|
|
3444
|
-
logger$4.critical('Error while checking block URL regex: ' + allowRegex, err);
|
|
3445
|
-
return true;
|
|
3446
|
-
}
|
|
3447
|
-
}
|
|
3448
|
-
if (!allowed) {
|
|
3449
|
-
// 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);
|
|
3450
3438
|
return true;
|
|
3451
3439
|
}
|
|
3452
3440
|
}
|
|
@@ -3456,17 +3444,12 @@ Autocapture.prototype.currentUrlBlocked = function() {
|
|
|
3456
3444
|
return false;
|
|
3457
3445
|
}
|
|
3458
3446
|
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
} catch (err) {
|
|
3465
|
-
logger$4.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
|
|
3466
|
-
return true;
|
|
3467
|
-
}
|
|
3447
|
+
try {
|
|
3448
|
+
return urlMatchesRegexList(currentUrl, blockUrlRegexes);
|
|
3449
|
+
} catch (err) {
|
|
3450
|
+
logger$4.critical('Error while checking block URL regexes: ', err);
|
|
3451
|
+
return true;
|
|
3468
3452
|
}
|
|
3469
|
-
return false;
|
|
3470
3453
|
};
|
|
3471
3454
|
|
|
3472
3455
|
Autocapture.prototype.pageviewTrackingConfig = function() {
|
|
@@ -4460,146 +4443,490 @@ FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.
|
|
|
4460
4443
|
// Exports intended only for testing
|
|
4461
4444
|
FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
|
|
4462
4445
|
|
|
4463
|
-
|
|
4446
|
+
var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
|
|
4447
|
+
|
|
4448
|
+
var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
|
|
4449
|
+
var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
|
|
4464
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];
|
|
4465
4454
|
|
|
4466
4455
|
/**
|
|
4467
|
-
*
|
|
4468
|
-
* @constructor
|
|
4456
|
+
* @type {import('./wrapper').StorageWrapper}
|
|
4469
4457
|
*/
|
|
4470
|
-
var
|
|
4458
|
+
var IDBStorageWrapper = function (storeName) {
|
|
4459
|
+
/**
|
|
4460
|
+
* @type {Promise<IDBDatabase>|null}
|
|
4461
|
+
*/
|
|
4462
|
+
this.dbPromise = null;
|
|
4463
|
+
this.storeName = storeName;
|
|
4464
|
+
};
|
|
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
|
+
};
|
|
4471
4472
|
|
|
4473
|
+
openRequest['onsuccess'] = function () {
|
|
4474
|
+
resolve(openRequest.result);
|
|
4475
|
+
};
|
|
4472
4476
|
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
DomTracker.prototype.event_handler = function() {};
|
|
4476
|
-
DomTracker.prototype.after_track_handler = function() {};
|
|
4477
|
+
openRequest['onupgradeneeded'] = function (ev) {
|
|
4478
|
+
var db = ev.target.result;
|
|
4477
4479
|
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4480
|
+
OBJECT_STORES.forEach(function (storeName) {
|
|
4481
|
+
db.createObjectStore(storeName);
|
|
4482
|
+
});
|
|
4483
|
+
};
|
|
4484
|
+
});
|
|
4481
4485
|
};
|
|
4482
4486
|
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
* @param {Object=} properties
|
|
4487
|
-
* @param {function=} user_callback
|
|
4488
|
-
*/
|
|
4489
|
-
DomTracker.prototype.track = function(query, event_name, properties, user_callback) {
|
|
4490
|
-
var that = this;
|
|
4491
|
-
var elements = _.dom_query(query);
|
|
4492
|
-
|
|
4493
|
-
if (elements.length === 0) {
|
|
4494
|
-
console.error('The DOM query (' + query + ') returned 0 elements');
|
|
4495
|
-
return;
|
|
4487
|
+
IDBStorageWrapper.prototype.init = function () {
|
|
4488
|
+
if (!win.indexedDB) {
|
|
4489
|
+
return PromisePolyfill.reject('indexedDB is not supported in this browser');
|
|
4496
4490
|
}
|
|
4497
4491
|
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
var props = that.create_properties(properties, this);
|
|
4502
|
-
var timeout = that.mp.get_config('track_links_timeout');
|
|
4503
|
-
|
|
4504
|
-
that.event_handler(e, this, options);
|
|
4505
|
-
|
|
4506
|
-
// in case the mixpanel servers don't get back to us in time
|
|
4507
|
-
window.setTimeout(that.track_callback(user_callback, props, options, true), timeout);
|
|
4492
|
+
if (!this.dbPromise) {
|
|
4493
|
+
this.dbPromise = this._openDb();
|
|
4494
|
+
}
|
|
4508
4495
|
|
|
4509
|
-
|
|
4510
|
-
|
|
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
|
+
}
|
|
4511
4503
|
});
|
|
4512
|
-
|
|
4504
|
+
};
|
|
4513
4505
|
|
|
4514
|
-
|
|
4506
|
+
IDBStorageWrapper.prototype.isInitialized = function () {
|
|
4507
|
+
return !!this.dbPromise;
|
|
4515
4508
|
};
|
|
4516
4509
|
|
|
4517
4510
|
/**
|
|
4518
|
-
* @param {
|
|
4519
|
-
* @param {
|
|
4520
|
-
* @param {boolean=} timeout_occured
|
|
4511
|
+
* @param {IDBTransactionMode} mode
|
|
4512
|
+
* @param {function(IDBObjectStore): void} storeCb
|
|
4521
4513
|
*/
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
var
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
// user can prevent the default functionality by
|
|
4534
|
-
// returning false from their callback
|
|
4535
|
-
return;
|
|
4536
|
-
}
|
|
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
|
+
};
|
|
4537
4525
|
|
|
4538
|
-
|
|
4526
|
+
storeCb(transaction.objectStore(storeName));
|
|
4527
|
+
});
|
|
4539
4528
|
};
|
|
4540
|
-
};
|
|
4541
|
-
|
|
4542
|
-
DomTracker.prototype.create_properties = function(properties, element) {
|
|
4543
|
-
var props;
|
|
4544
4529
|
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
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));
|
|
4552
4541
|
};
|
|
4553
4542
|
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
*/
|
|
4559
|
-
var LinkTracker = function() {
|
|
4560
|
-
this.override_event = 'click';
|
|
4543
|
+
IDBStorageWrapper.prototype.setItem = function (key, value) {
|
|
4544
|
+
return this.makeTransaction('readwrite', function (objectStore) {
|
|
4545
|
+
objectStore.put(value, key);
|
|
4546
|
+
});
|
|
4561
4547
|
};
|
|
4562
|
-
_.inherit(LinkTracker, DomTracker);
|
|
4563
|
-
|
|
4564
|
-
LinkTracker.prototype.create_properties = function(properties, element) {
|
|
4565
|
-
var props = LinkTracker.superclass.create_properties.apply(this, arguments);
|
|
4566
4548
|
|
|
4567
|
-
|
|
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
|
+
};
|
|
4568
4557
|
|
|
4569
|
-
|
|
4558
|
+
IDBStorageWrapper.prototype.removeItem = function (key) {
|
|
4559
|
+
return this.makeTransaction('readwrite', function (objectStore) {
|
|
4560
|
+
objectStore.delete(key);
|
|
4561
|
+
});
|
|
4570
4562
|
};
|
|
4571
4563
|
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
);
|
|
4579
|
-
|
|
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
|
+
};
|
|
4580
4572
|
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
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'];
|
|
4584
4580
|
};
|
|
4585
4581
|
|
|
4586
|
-
|
|
4587
|
-
if (options.new_tab) { return; }
|
|
4582
|
+
/* eslint camelcase: "off" */
|
|
4588
4583
|
|
|
4589
|
-
setTimeout(function() {
|
|
4590
|
-
window.location = options.href;
|
|
4591
|
-
}, 0);
|
|
4592
|
-
};
|
|
4593
4584
|
|
|
4594
4585
|
/**
|
|
4595
|
-
*
|
|
4586
|
+
* RecorderManager: manages session recording initialization, lifecycle and state
|
|
4596
4587
|
* @constructor
|
|
4597
|
-
* @extends DomTracker
|
|
4598
4588
|
*/
|
|
4599
|
-
var
|
|
4600
|
-
|
|
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;
|
|
4601
4604
|
};
|
|
4602
|
-
|
|
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) {
|
|
4860
|
+
// user can prevent the default functionality by
|
|
4861
|
+
// returning false from their callback
|
|
4862
|
+
return;
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
that.after_track_handler(props, options, timeout_occured);
|
|
4866
|
+
};
|
|
4867
|
+
};
|
|
4868
|
+
|
|
4869
|
+
DomTracker.prototype.create_properties = function(properties, element) {
|
|
4870
|
+
var props;
|
|
4871
|
+
|
|
4872
|
+
if (typeof(properties) === 'function') {
|
|
4873
|
+
props = properties(element);
|
|
4874
|
+
} else {
|
|
4875
|
+
props = _.extend({}, properties);
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4878
|
+
return props;
|
|
4879
|
+
};
|
|
4880
|
+
|
|
4881
|
+
/**
|
|
4882
|
+
* LinkTracker Object
|
|
4883
|
+
* @constructor
|
|
4884
|
+
* @extends DomTracker
|
|
4885
|
+
*/
|
|
4886
|
+
var LinkTracker = function() {
|
|
4887
|
+
this.override_event = 'click';
|
|
4888
|
+
};
|
|
4889
|
+
_.inherit(LinkTracker, DomTracker);
|
|
4890
|
+
|
|
4891
|
+
LinkTracker.prototype.create_properties = function(properties, element) {
|
|
4892
|
+
var props = LinkTracker.superclass.create_properties.apply(this, arguments);
|
|
4893
|
+
|
|
4894
|
+
if (element.href) { props['url'] = element.href; }
|
|
4895
|
+
|
|
4896
|
+
return props;
|
|
4897
|
+
};
|
|
4898
|
+
|
|
4899
|
+
LinkTracker.prototype.event_handler = function(evt, element, options) {
|
|
4900
|
+
options.new_tab = (
|
|
4901
|
+
evt.which === 2 ||
|
|
4902
|
+
evt.metaKey ||
|
|
4903
|
+
evt.ctrlKey ||
|
|
4904
|
+
element.target === '_blank'
|
|
4905
|
+
);
|
|
4906
|
+
options.href = element.href;
|
|
4907
|
+
|
|
4908
|
+
if (!options.new_tab) {
|
|
4909
|
+
evt.preventDefault();
|
|
4910
|
+
}
|
|
4911
|
+
};
|
|
4912
|
+
|
|
4913
|
+
LinkTracker.prototype.after_track_handler = function(props, options) {
|
|
4914
|
+
if (options.new_tab) { return; }
|
|
4915
|
+
|
|
4916
|
+
setTimeout(function() {
|
|
4917
|
+
window.location = options.href;
|
|
4918
|
+
}, 0);
|
|
4919
|
+
};
|
|
4920
|
+
|
|
4921
|
+
/**
|
|
4922
|
+
* FormTracker Object
|
|
4923
|
+
* @constructor
|
|
4924
|
+
* @extends DomTracker
|
|
4925
|
+
*/
|
|
4926
|
+
var FormTracker = function() {
|
|
4927
|
+
this.override_event = 'submit';
|
|
4928
|
+
};
|
|
4929
|
+
_.inherit(FormTracker, DomTracker);
|
|
4603
4930
|
|
|
4604
4931
|
FormTracker.prototype.event_handler = function(evt, element, options) {
|
|
4605
4932
|
options.element = element;
|
|
@@ -6981,133 +7308,6 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
|
|
|
6981
7308
|
return timestamp;
|
|
6982
7309
|
};
|
|
6983
7310
|
|
|
6984
|
-
var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
|
|
6985
|
-
|
|
6986
|
-
var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
|
|
6987
|
-
var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
|
|
6988
|
-
|
|
6989
|
-
// note: increment the version number when adding new object stores
|
|
6990
|
-
var DB_VERSION = 1;
|
|
6991
|
-
var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
|
|
6992
|
-
|
|
6993
|
-
/**
|
|
6994
|
-
* @type {import('./wrapper').StorageWrapper}
|
|
6995
|
-
*/
|
|
6996
|
-
var IDBStorageWrapper = function (storeName) {
|
|
6997
|
-
/**
|
|
6998
|
-
* @type {Promise<IDBDatabase>|null}
|
|
6999
|
-
*/
|
|
7000
|
-
this.dbPromise = null;
|
|
7001
|
-
this.storeName = storeName;
|
|
7002
|
-
};
|
|
7003
|
-
|
|
7004
|
-
IDBStorageWrapper.prototype._openDb = function () {
|
|
7005
|
-
return new PromisePolyfill(function (resolve, reject) {
|
|
7006
|
-
var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
|
|
7007
|
-
openRequest['onerror'] = function () {
|
|
7008
|
-
reject(openRequest.error);
|
|
7009
|
-
};
|
|
7010
|
-
|
|
7011
|
-
openRequest['onsuccess'] = function () {
|
|
7012
|
-
resolve(openRequest.result);
|
|
7013
|
-
};
|
|
7014
|
-
|
|
7015
|
-
openRequest['onupgradeneeded'] = function (ev) {
|
|
7016
|
-
var db = ev.target.result;
|
|
7017
|
-
|
|
7018
|
-
OBJECT_STORES.forEach(function (storeName) {
|
|
7019
|
-
db.createObjectStore(storeName);
|
|
7020
|
-
});
|
|
7021
|
-
};
|
|
7022
|
-
});
|
|
7023
|
-
};
|
|
7024
|
-
|
|
7025
|
-
IDBStorageWrapper.prototype.init = function () {
|
|
7026
|
-
if (!win.indexedDB) {
|
|
7027
|
-
return PromisePolyfill.reject('indexedDB is not supported in this browser');
|
|
7028
|
-
}
|
|
7029
|
-
|
|
7030
|
-
if (!this.dbPromise) {
|
|
7031
|
-
this.dbPromise = this._openDb();
|
|
7032
|
-
}
|
|
7033
|
-
|
|
7034
|
-
return this.dbPromise
|
|
7035
|
-
.then(function (dbOrError) {
|
|
7036
|
-
if (dbOrError instanceof win['IDBDatabase']) {
|
|
7037
|
-
return PromisePolyfill.resolve();
|
|
7038
|
-
} else {
|
|
7039
|
-
return PromisePolyfill.reject(dbOrError);
|
|
7040
|
-
}
|
|
7041
|
-
});
|
|
7042
|
-
};
|
|
7043
|
-
|
|
7044
|
-
IDBStorageWrapper.prototype.isInitialized = function () {
|
|
7045
|
-
return !!this.dbPromise;
|
|
7046
|
-
};
|
|
7047
|
-
|
|
7048
|
-
/**
|
|
7049
|
-
* @param {IDBTransactionMode} mode
|
|
7050
|
-
* @param {function(IDBObjectStore): void} storeCb
|
|
7051
|
-
*/
|
|
7052
|
-
IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
|
|
7053
|
-
var storeName = this.storeName;
|
|
7054
|
-
var doTransaction = function (db) {
|
|
7055
|
-
return new PromisePolyfill(function (resolve, reject) {
|
|
7056
|
-
var transaction = db.transaction(storeName, mode);
|
|
7057
|
-
transaction.oncomplete = function () {
|
|
7058
|
-
resolve(transaction);
|
|
7059
|
-
};
|
|
7060
|
-
transaction.onabort = transaction.onerror = function () {
|
|
7061
|
-
reject(transaction.error);
|
|
7062
|
-
};
|
|
7063
|
-
|
|
7064
|
-
storeCb(transaction.objectStore(storeName));
|
|
7065
|
-
});
|
|
7066
|
-
};
|
|
7067
|
-
|
|
7068
|
-
return this.dbPromise
|
|
7069
|
-
.then(doTransaction)
|
|
7070
|
-
.catch(function (err) {
|
|
7071
|
-
if (err && err['name'] === 'InvalidStateError') {
|
|
7072
|
-
// try reopening the DB if the connection is closed
|
|
7073
|
-
this.dbPromise = this._openDb();
|
|
7074
|
-
return this.dbPromise.then(doTransaction);
|
|
7075
|
-
} else {
|
|
7076
|
-
return PromisePolyfill.reject(err);
|
|
7077
|
-
}
|
|
7078
|
-
}.bind(this));
|
|
7079
|
-
};
|
|
7080
|
-
|
|
7081
|
-
IDBStorageWrapper.prototype.setItem = function (key, value) {
|
|
7082
|
-
return this.makeTransaction('readwrite', function (objectStore) {
|
|
7083
|
-
objectStore.put(value, key);
|
|
7084
|
-
});
|
|
7085
|
-
};
|
|
7086
|
-
|
|
7087
|
-
IDBStorageWrapper.prototype.getItem = function (key) {
|
|
7088
|
-
var req;
|
|
7089
|
-
return this.makeTransaction('readonly', function (objectStore) {
|
|
7090
|
-
req = objectStore.get(key);
|
|
7091
|
-
}).then(function () {
|
|
7092
|
-
return req.result;
|
|
7093
|
-
});
|
|
7094
|
-
};
|
|
7095
|
-
|
|
7096
|
-
IDBStorageWrapper.prototype.removeItem = function (key) {
|
|
7097
|
-
return this.makeTransaction('readwrite', function (objectStore) {
|
|
7098
|
-
objectStore.delete(key);
|
|
7099
|
-
});
|
|
7100
|
-
};
|
|
7101
|
-
|
|
7102
|
-
IDBStorageWrapper.prototype.getAll = function () {
|
|
7103
|
-
var req;
|
|
7104
|
-
return this.makeTransaction('readonly', function (objectStore) {
|
|
7105
|
-
req = objectStore.getAll();
|
|
7106
|
-
}).then(function () {
|
|
7107
|
-
return req.result;
|
|
7108
|
-
});
|
|
7109
|
-
};
|
|
7110
|
-
|
|
7111
7311
|
/* eslint camelcase: "off" */
|
|
7112
7312
|
|
|
7113
7313
|
/*
|
|
@@ -7242,13 +7442,17 @@ var DEFAULT_CONFIG = {
|
|
|
7242
7442
|
'record_collect_fonts': false,
|
|
7243
7443
|
'record_console': true,
|
|
7244
7444
|
'record_heatmap_data': false,
|
|
7445
|
+
'recording_event_triggers': {},
|
|
7245
7446
|
'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
|
|
7246
7447
|
'record_mask_inputs': true,
|
|
7247
7448
|
'record_max_ms': MAX_RECORDING_MS,
|
|
7248
7449
|
'record_min_ms': 0,
|
|
7450
|
+
'record_network': false,
|
|
7451
|
+
'record_network_options': {},
|
|
7249
7452
|
'record_sessions_percent': 0,
|
|
7250
|
-
'recorder_src':
|
|
7251
|
-
'targeting_src':
|
|
7453
|
+
'recorder_src': null,
|
|
7454
|
+
'targeting_src': null,
|
|
7455
|
+
'lib_base_path': 'https://cdn.mxpnl.com/libs/',
|
|
7252
7456
|
'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
|
|
7253
7457
|
};
|
|
7254
7458
|
|
|
@@ -7402,6 +7606,19 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
7402
7606
|
'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
|
|
7403
7607
|
}));
|
|
7404
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
|
+
|
|
7405
7622
|
this['_jsc'] = NOOP_FUNC;
|
|
7406
7623
|
|
|
7407
7624
|
this.__dom_loaded_queue = [];
|
|
@@ -7480,7 +7697,7 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
7480
7697
|
getPropertyFunc: _.bind(this.get_property, this),
|
|
7481
7698
|
trackingFunc: _.bind(this.track, this),
|
|
7482
7699
|
loadExtraBundle: load_extra_bundle,
|
|
7483
|
-
targetingSrc: this.get_config('targeting_src')
|
|
7700
|
+
targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
|
|
7484
7701
|
});
|
|
7485
7702
|
this.flags.init();
|
|
7486
7703
|
this['flags'] = this.flags;
|
|
@@ -7493,11 +7710,11 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
7493
7710
|
// Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
|
|
7494
7711
|
var mode = this.get_config('remote_settings_mode');
|
|
7495
7712
|
if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
|
|
7496
|
-
this._fetch_remote_settings(mode).then(_.bind(function() {
|
|
7497
|
-
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();
|
|
7498
7715
|
}, this));
|
|
7499
7716
|
} else {
|
|
7500
|
-
this._check_and_start_session_recording();
|
|
7717
|
+
this.__session_recording_init_promise = this._check_and_start_session_recording();
|
|
7501
7718
|
}
|
|
7502
7719
|
};
|
|
7503
7720
|
|
|
@@ -7541,132 +7758,50 @@ MixpanelLib.prototype.get_tab_id = function () {
|
|
|
7541
7758
|
return this.tab_id || null;
|
|
7542
7759
|
};
|
|
7543
7760
|
|
|
7544
|
-
MixpanelLib.prototype._should_load_recorder = function () {
|
|
7545
|
-
if (this.get_config('disable_persistence')) {
|
|
7546
|
-
console.log('Load recorder check skipped due to disable_persistence config');
|
|
7547
|
-
return Promise.resolve(false);
|
|
7548
|
-
}
|
|
7549
|
-
|
|
7550
|
-
var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
7551
|
-
var tab_id = this.get_tab_id();
|
|
7552
|
-
return recording_registry_idb.init()
|
|
7553
|
-
.then(function () {
|
|
7554
|
-
return recording_registry_idb.getAll();
|
|
7555
|
-
})
|
|
7556
|
-
.then(function (recordings) {
|
|
7557
|
-
for (var i = 0; i < recordings.length; i++) {
|
|
7558
|
-
// if there are expired recordings in the registry, we should load the recorder to flush them
|
|
7559
|
-
// if there's a recording for this tab id, we should load the recorder to continue the recording
|
|
7560
|
-
if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
|
|
7561
|
-
return true;
|
|
7562
|
-
}
|
|
7563
|
-
}
|
|
7564
|
-
return false;
|
|
7565
|
-
})
|
|
7566
|
-
.catch(_.bind(function (err) {
|
|
7567
|
-
this.report_error('Error checking recording registry', err);
|
|
7568
|
-
}, this));
|
|
7569
|
-
};
|
|
7570
|
-
|
|
7571
7761
|
MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
|
|
7572
|
-
|
|
7573
|
-
console.critical('Browser does not support MutationObserver; skipping session recording');
|
|
7574
|
-
return;
|
|
7575
|
-
}
|
|
7576
|
-
|
|
7577
|
-
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
7578
|
-
var handleLoadedRecorder = _.bind(function() {
|
|
7579
|
-
this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this);
|
|
7580
|
-
this._recorder['resumeRecording'](startNewIfInactive);
|
|
7581
|
-
}, this);
|
|
7582
|
-
|
|
7583
|
-
if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
|
|
7584
|
-
load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
|
|
7585
|
-
} else {
|
|
7586
|
-
handleLoadedRecorder();
|
|
7587
|
-
}
|
|
7588
|
-
}, this);
|
|
7589
|
-
|
|
7590
|
-
/**
|
|
7591
|
-
* If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
|
|
7592
|
-
* 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.
|
|
7593
|
-
*/
|
|
7594
|
-
var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
|
|
7595
|
-
if (force_start || is_sampled) {
|
|
7596
|
-
loadRecorder(true);
|
|
7597
|
-
} else {
|
|
7598
|
-
this._should_load_recorder()
|
|
7599
|
-
.then(function (shouldLoad) {
|
|
7600
|
-
if (shouldLoad) {
|
|
7601
|
-
loadRecorder(false);
|
|
7602
|
-
}
|
|
7603
|
-
});
|
|
7604
|
-
}
|
|
7762
|
+
return this.recorderManager.checkAndStartSessionRecording(force_start);
|
|
7605
7763
|
});
|
|
7606
7764
|
|
|
7765
|
+
MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
|
|
7766
|
+
return this.recorderManager.startRecordingOnEvent(event_name, properties);
|
|
7767
|
+
};
|
|
7768
|
+
|
|
7607
7769
|
MixpanelLib.prototype.start_session_recording = function () {
|
|
7608
|
-
this._check_and_start_session_recording(true);
|
|
7770
|
+
return this._check_and_start_session_recording(true);
|
|
7609
7771
|
};
|
|
7610
7772
|
|
|
7611
7773
|
MixpanelLib.prototype.stop_session_recording = function () {
|
|
7612
|
-
|
|
7613
|
-
return this._recorder['stopRecording']();
|
|
7614
|
-
}
|
|
7615
|
-
return Promise.resolve();
|
|
7774
|
+
return this.recorderManager.stopSessionRecording();
|
|
7616
7775
|
};
|
|
7617
7776
|
|
|
7618
7777
|
MixpanelLib.prototype.pause_session_recording = function () {
|
|
7619
|
-
|
|
7620
|
-
return this._recorder['pauseRecording']();
|
|
7621
|
-
}
|
|
7622
|
-
return Promise.resolve();
|
|
7778
|
+
return this.recorderManager.pauseSessionRecording();
|
|
7623
7779
|
};
|
|
7624
7780
|
|
|
7625
7781
|
MixpanelLib.prototype.resume_session_recording = function () {
|
|
7626
|
-
|
|
7627
|
-
return this._recorder['resumeRecording']();
|
|
7628
|
-
}
|
|
7629
|
-
return Promise.resolve();
|
|
7782
|
+
return this.recorderManager.resumeSessionRecording();
|
|
7630
7783
|
};
|
|
7631
7784
|
|
|
7632
7785
|
MixpanelLib.prototype.is_recording_heatmap_data = function () {
|
|
7633
|
-
return this.
|
|
7786
|
+
return this.recorderManager.isRecordingHeatmapData();
|
|
7634
7787
|
};
|
|
7635
7788
|
|
|
7636
7789
|
MixpanelLib.prototype.get_session_recording_properties = function () {
|
|
7637
|
-
|
|
7638
|
-
var replay_id = this._get_session_replay_id();
|
|
7639
|
-
if (replay_id) {
|
|
7640
|
-
props['$mp_replay_id'] = replay_id;
|
|
7641
|
-
}
|
|
7642
|
-
return props;
|
|
7790
|
+
return this.recorderManager.getSessionRecordingProperties();
|
|
7643
7791
|
};
|
|
7644
7792
|
|
|
7645
7793
|
MixpanelLib.prototype.get_session_replay_url = function () {
|
|
7646
|
-
|
|
7647
|
-
var replay_id = this._get_session_replay_id();
|
|
7648
|
-
if (replay_id) {
|
|
7649
|
-
var query_params = _.HTTPBuildQuery({
|
|
7650
|
-
'replay_id': replay_id,
|
|
7651
|
-
'distinct_id': this.get_distinct_id(),
|
|
7652
|
-
'token': this.get_config('token')
|
|
7653
|
-
});
|
|
7654
|
-
replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
|
|
7655
|
-
}
|
|
7656
|
-
return replay_url;
|
|
7657
|
-
};
|
|
7658
|
-
|
|
7659
|
-
MixpanelLib.prototype._get_session_replay_id = function () {
|
|
7660
|
-
var replay_id = null;
|
|
7661
|
-
if (this._recorder) {
|
|
7662
|
-
replay_id = this._recorder['replayId'];
|
|
7663
|
-
}
|
|
7664
|
-
return replay_id || null;
|
|
7794
|
+
return this.recorderManager.getSessionReplayUrl();
|
|
7665
7795
|
};
|
|
7666
7796
|
|
|
7667
7797
|
// "private" public method to reach into the recorder in test cases
|
|
7668
7798
|
MixpanelLib.prototype.__get_recorder = function () {
|
|
7669
|
-
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;
|
|
7670
7805
|
};
|
|
7671
7806
|
|
|
7672
7807
|
// Private methods
|
|
@@ -7924,6 +8059,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
|
|
|
7924
8059
|
};
|
|
7925
8060
|
|
|
7926
8061
|
MixpanelLib.prototype._fetch_remote_settings = function(mode) {
|
|
8062
|
+
var self = this;
|
|
7927
8063
|
var disableRecordingIfStrict = function() {
|
|
7928
8064
|
if (mode === 'strict') {
|
|
7929
8065
|
self.set_config({'record_sessions_percent': 0});
|
|
@@ -7944,7 +8080,6 @@ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
|
|
|
7944
8080
|
};
|
|
7945
8081
|
var query_string = _.HTTPBuildQuery(request_params);
|
|
7946
8082
|
var full_url = settings_endpoint + '?' + query_string;
|
|
7947
|
-
var self = this;
|
|
7948
8083
|
|
|
7949
8084
|
var abortController = new AbortController();
|
|
7950
8085
|
var timeout_id = setTimeout(function() {
|
|
@@ -8136,6 +8271,34 @@ MixpanelLib.prototype.push = function(item) {
|
|
|
8136
8271
|
this._execute_array([item]);
|
|
8137
8272
|
};
|
|
8138
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
|
+
|
|
8139
8302
|
/**
|
|
8140
8303
|
* Disable events on the Mixpanel object. If passed no arguments,
|
|
8141
8304
|
* this function disables tracking of any event. If passed an
|
|
@@ -8309,6 +8472,8 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
|
|
|
8309
8472
|
this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
|
|
8310
8473
|
}
|
|
8311
8474
|
|
|
8475
|
+
this._start_recording_on_event(event_name, properties);
|
|
8476
|
+
|
|
8312
8477
|
var data = {
|
|
8313
8478
|
'event': event_name,
|
|
8314
8479
|
'properties': properties
|
|
@@ -9517,6 +9682,7 @@ MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
|
|
|
9517
9682
|
// MixpanelLib Exports
|
|
9518
9683
|
MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
|
|
9519
9684
|
MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
|
|
9685
|
+
MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
|
|
9520
9686
|
MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
|
|
9521
9687
|
MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
|
|
9522
9688
|
MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
|
|
@@ -9560,6 +9726,7 @@ MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES
|
|
|
9560
9726
|
|
|
9561
9727
|
// Exports intended only for testing
|
|
9562
9728
|
MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
|
|
9729
|
+
MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
|
|
9563
9730
|
|
|
9564
9731
|
// MixpanelPersistence Exports
|
|
9565
9732
|
MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
|