mixpanel-browser 2.55.0 → 2.56.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/.eslintrc.json +31 -1
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/mixpanel-core.cjs.js +52 -15
- package/dist/mixpanel-recorder.js +211 -87
- package/dist/mixpanel-recorder.min.js +10 -9
- package/dist/mixpanel-recorder.min.js.map +1 -0
- package/dist/mixpanel-with-async-recorder.cjs.js +52 -15
- package/dist/mixpanel.amd.js +238 -93
- package/dist/mixpanel.cjs.js +238 -93
- package/dist/mixpanel.globals.js +52 -15
- package/dist/mixpanel.min.js +109 -107
- package/dist/mixpanel.min.js.map +8 -0
- package/dist/mixpanel.module.js +238 -93
- package/dist/mixpanel.umd.js +238 -93
- package/package.json +1 -1
- package/src/config.js +1 -1
- package/src/mixpanel-core.js +27 -6
- package/src/recorder/index.js +39 -232
- package/src/recorder/rollup.config.js +2 -1
- package/src/recorder/session-recording.js +305 -0
- package/src/request-batcher.js +7 -2
- package/src/request-queue.js +5 -3
- package/src/utils.js +26 -13
package/dist/mixpanel.cjs.js
CHANGED
|
@@ -4509,7 +4509,7 @@ var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
|
|
|
4509
4509
|
|
|
4510
4510
|
var Config = {
|
|
4511
4511
|
DEBUG: false,
|
|
4512
|
-
LIB_VERSION: '2.
|
|
4512
|
+
LIB_VERSION: '2.56.0'
|
|
4513
4513
|
};
|
|
4514
4514
|
|
|
4515
4515
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
@@ -4521,7 +4521,7 @@ if (typeof(window) === 'undefined') {
|
|
|
4521
4521
|
hostname: ''
|
|
4522
4522
|
};
|
|
4523
4523
|
win = {
|
|
4524
|
-
navigator: { userAgent: '' },
|
|
4524
|
+
navigator: { userAgent: '', onLine: true },
|
|
4525
4525
|
document: {
|
|
4526
4526
|
location: loc,
|
|
4527
4527
|
referrer: ''
|
|
@@ -4535,6 +4535,8 @@ if (typeof(window) === 'undefined') {
|
|
|
4535
4535
|
|
|
4536
4536
|
// Maximum allowed session recording length
|
|
4537
4537
|
var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
4538
|
+
// Maximum allowed value for minimum session recording length
|
|
4539
|
+
var MAX_VALUE_FOR_MIN_RECORDING_MS = 8 * 1000; // 8 seconds
|
|
4538
4540
|
|
|
4539
4541
|
/*
|
|
4540
4542
|
* Saved references to long variable names, so that closure compiler can
|
|
@@ -5475,7 +5477,7 @@ _.HTTPBuildQuery = function(formdata, arg_separator) {
|
|
|
5475
5477
|
_.getQueryParam = function(url, param) {
|
|
5476
5478
|
// Expects a raw URL
|
|
5477
5479
|
|
|
5478
|
-
param = param.replace(/[[]
|
|
5480
|
+
param = param.replace(/[[]/g, '\\[').replace(/[\]]/g, '\\]');
|
|
5479
5481
|
var regexS = '[\\?&]' + param + '=([^&#]*)',
|
|
5480
5482
|
regex = new RegExp(regexS),
|
|
5481
5483
|
results = regex.exec(url);
|
|
@@ -5932,8 +5934,8 @@ _.dom_query = (function() {
|
|
|
5932
5934
|
};
|
|
5933
5935
|
})();
|
|
5934
5936
|
|
|
5935
|
-
var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
|
|
5936
|
-
var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid'];
|
|
5937
|
+
var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform','utm_campaign_id', 'utm_creative_format', 'utm_marketing_tactic'];
|
|
5938
|
+
var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid'];
|
|
5937
5939
|
|
|
5938
5940
|
_.info = {
|
|
5939
5941
|
campaignParams: function(default_value) {
|
|
@@ -6215,6 +6217,15 @@ var extract_domain = function(hostname) {
|
|
|
6215
6217
|
return matches ? matches[0] : '';
|
|
6216
6218
|
};
|
|
6217
6219
|
|
|
6220
|
+
/**
|
|
6221
|
+
* Check whether we have network connection. default to true for browsers that don't support navigator.onLine (IE)
|
|
6222
|
+
* @returns {boolean}
|
|
6223
|
+
*/
|
|
6224
|
+
var isOnline = function() {
|
|
6225
|
+
var onLine = win.navigator['onLine'];
|
|
6226
|
+
return _.isUndefined(onLine) || onLine;
|
|
6227
|
+
};
|
|
6228
|
+
|
|
6218
6229
|
var JSONStringify = null, JSONParse = null;
|
|
6219
6230
|
if (typeof JSON !== 'undefined') {
|
|
6220
6231
|
JSONStringify = JSON.stringify;
|
|
@@ -6535,7 +6546,7 @@ function _addOptOutCheck(method, getConfigValue) {
|
|
|
6535
6546
|
};
|
|
6536
6547
|
}
|
|
6537
6548
|
|
|
6538
|
-
var logger$
|
|
6549
|
+
var logger$4 = console_with_prefix('lock');
|
|
6539
6550
|
|
|
6540
6551
|
/**
|
|
6541
6552
|
* SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
|
|
@@ -6592,7 +6603,7 @@ SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) {
|
|
|
6592
6603
|
|
|
6593
6604
|
var delay = function(cb) {
|
|
6594
6605
|
if (new Date().getTime() - startTime > timeoutMS) {
|
|
6595
|
-
logger$
|
|
6606
|
+
logger$4.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
|
|
6596
6607
|
storage.removeItem(keyZ);
|
|
6597
6608
|
storage.removeItem(keyY);
|
|
6598
6609
|
loop();
|
|
@@ -6681,7 +6692,7 @@ SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) {
|
|
|
6681
6692
|
}
|
|
6682
6693
|
};
|
|
6683
6694
|
|
|
6684
|
-
var logger$
|
|
6695
|
+
var logger$3 = console_with_prefix('batch');
|
|
6685
6696
|
|
|
6686
6697
|
/**
|
|
6687
6698
|
* RequestQueue: queue for batching API requests with localStorage backup for retries.
|
|
@@ -6702,11 +6713,13 @@ var logger$2 = console_with_prefix('batch');
|
|
|
6702
6713
|
var RequestQueue = function(storageKey, options) {
|
|
6703
6714
|
options = options || {};
|
|
6704
6715
|
this.storageKey = storageKey;
|
|
6705
|
-
this.storage = options.storage || window.localStorage;
|
|
6706
|
-
this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2);
|
|
6707
|
-
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
6708
|
-
|
|
6709
6716
|
this.usePersistence = options.usePersistence;
|
|
6717
|
+
if (this.usePersistence) {
|
|
6718
|
+
this.storage = options.storage || window.localStorage;
|
|
6719
|
+
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
6720
|
+
}
|
|
6721
|
+
this.reportError = options.errorReporter || _.bind(logger$3.error, logger$3);
|
|
6722
|
+
|
|
6710
6723
|
this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
|
|
6711
6724
|
|
|
6712
6725
|
this.memQueue = [];
|
|
@@ -6985,7 +6998,7 @@ RequestQueue.prototype.clear = function() {
|
|
|
6985
6998
|
// maximum interval between request retries after exponential backoff
|
|
6986
6999
|
var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
|
|
6987
7000
|
|
|
6988
|
-
var logger$
|
|
7001
|
+
var logger$2 = console_with_prefix('batch');
|
|
6989
7002
|
|
|
6990
7003
|
/**
|
|
6991
7004
|
* RequestBatcher: manages the queueing, flushing, retry etc of requests of one
|
|
@@ -7099,7 +7112,7 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
7099
7112
|
try {
|
|
7100
7113
|
|
|
7101
7114
|
if (this.requestInProgress) {
|
|
7102
|
-
logger$
|
|
7115
|
+
logger$2.log('Flush: Request already in progress');
|
|
7103
7116
|
return;
|
|
7104
7117
|
}
|
|
7105
7118
|
|
|
@@ -7178,7 +7191,12 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
7178
7191
|
this.flush();
|
|
7179
7192
|
} else if (
|
|
7180
7193
|
_.isObject(res) &&
|
|
7181
|
-
(
|
|
7194
|
+
(
|
|
7195
|
+
res.httpStatusCode >= 500
|
|
7196
|
+
|| res.httpStatusCode === 429
|
|
7197
|
+
|| (res.httpStatusCode <= 0 && !isOnline())
|
|
7198
|
+
|| res.error === 'timeout'
|
|
7199
|
+
)
|
|
7182
7200
|
) {
|
|
7183
7201
|
// network or API error, or 429 Too Many Requests, retry
|
|
7184
7202
|
var retryMS = this.flushInterval * 2;
|
|
@@ -7262,7 +7280,7 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
7262
7280
|
if (options.unloading) {
|
|
7263
7281
|
requestOptions.transport = 'sendBeacon';
|
|
7264
7282
|
}
|
|
7265
|
-
logger$
|
|
7283
|
+
logger$2.log('MIXPANEL REQUEST:', dataForRequest);
|
|
7266
7284
|
this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
|
|
7267
7285
|
} catch(err) {
|
|
7268
7286
|
this.reportError('Error flushing request queue', err);
|
|
@@ -7274,7 +7292,7 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
7274
7292
|
* Log error to global logger and optional user-defined logger.
|
|
7275
7293
|
*/
|
|
7276
7294
|
RequestBatcher.prototype.reportError = function(msg, err) {
|
|
7277
|
-
logger$
|
|
7295
|
+
logger$2.error.apply(logger$2.error, arguments);
|
|
7278
7296
|
if (this.errorReporter) {
|
|
7279
7297
|
try {
|
|
7280
7298
|
if (!(err instanceof Error)) {
|
|
@@ -7282,12 +7300,12 @@ RequestBatcher.prototype.reportError = function(msg, err) {
|
|
|
7282
7300
|
}
|
|
7283
7301
|
this.errorReporter(msg, err);
|
|
7284
7302
|
} catch(err) {
|
|
7285
|
-
logger$
|
|
7303
|
+
logger$2.error(err);
|
|
7286
7304
|
}
|
|
7287
7305
|
}
|
|
7288
7306
|
};
|
|
7289
7307
|
|
|
7290
|
-
var logger = console_with_prefix('recorder');
|
|
7308
|
+
var logger$1 = console_with_prefix('recorder');
|
|
7291
7309
|
var CompressionStream = win['CompressionStream'];
|
|
7292
7310
|
|
|
7293
7311
|
var RECORDER_BATCHER_LIB_CONFIG = {
|
|
@@ -7313,63 +7331,85 @@ function isUserEvent(ev) {
|
|
|
7313
7331
|
return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
|
|
7314
7332
|
}
|
|
7315
7333
|
|
|
7316
|
-
|
|
7317
|
-
|
|
7334
|
+
/**
|
|
7335
|
+
* This class encapsulates a single session recording and its lifecycle.
|
|
7336
|
+
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7337
|
+
* @param {String} [options.replayId] - unique uuid for a single replay
|
|
7338
|
+
* @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
7339
|
+
* @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
7340
|
+
* @param {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
7341
|
+
*/
|
|
7342
|
+
var SessionRecording = function(options) {
|
|
7343
|
+
this._mixpanel = options.mixpanelInstance;
|
|
7344
|
+
this._onIdleTimeout = options.onIdleTimeout;
|
|
7345
|
+
this._onMaxLengthReached = options.onMaxLengthReached;
|
|
7346
|
+
this._rrwebRecord = options.rrwebRecord;
|
|
7347
|
+
|
|
7348
|
+
this.replayId = options.replayId;
|
|
7318
7349
|
|
|
7319
7350
|
// internal rrweb stopRecording function
|
|
7320
7351
|
this._stopRecording = null;
|
|
7321
7352
|
|
|
7322
|
-
this.recEvents = [];
|
|
7323
7353
|
this.seqNo = 0;
|
|
7324
|
-
this.replayId = null;
|
|
7325
7354
|
this.replayStartTime = null;
|
|
7326
|
-
this.
|
|
7355
|
+
this.batchStartUrl = null;
|
|
7327
7356
|
|
|
7328
7357
|
this.idleTimeoutId = null;
|
|
7329
7358
|
this.maxTimeoutId = null;
|
|
7330
7359
|
|
|
7331
7360
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7332
|
-
this.
|
|
7333
|
-
};
|
|
7334
|
-
|
|
7361
|
+
this.recordMinMs = 0;
|
|
7335
7362
|
|
|
7336
|
-
|
|
7337
|
-
this
|
|
7338
|
-
|
|
7339
|
-
|
|
7363
|
+
// each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
|
|
7364
|
+
// this will be important when persistence is introduced
|
|
7365
|
+
var batcherKey = '__mprec_' + this.getConfig('token') + '_' + this.replayId;
|
|
7366
|
+
this.batcher = new RequestBatcher(batcherKey, {
|
|
7340
7367
|
errorReporter: _.bind(this.reportError, this),
|
|
7341
7368
|
flushOnlyOnInterval: true,
|
|
7369
|
+
libConfig: RECORDER_BATCHER_LIB_CONFIG,
|
|
7370
|
+
sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
|
|
7342
7371
|
usePersistence: false
|
|
7343
7372
|
});
|
|
7344
7373
|
};
|
|
7345
7374
|
|
|
7346
|
-
|
|
7347
|
-
MixpanelRecorder.prototype.get_config = function(configVar) {
|
|
7375
|
+
SessionRecording.prototype.getConfig = function(configVar) {
|
|
7348
7376
|
return this._mixpanel.get_config(configVar);
|
|
7349
7377
|
};
|
|
7350
7378
|
|
|
7351
|
-
|
|
7379
|
+
// Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
|
|
7380
|
+
// reaches into this class instance and expects the snake case version of the function.
|
|
7381
|
+
// eslint-disable-next-line camelcase
|
|
7382
|
+
SessionRecording.prototype.get_config = function(configVar) {
|
|
7383
|
+
return this.getConfig(configVar);
|
|
7384
|
+
};
|
|
7385
|
+
|
|
7386
|
+
SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
7352
7387
|
if (this._stopRecording !== null) {
|
|
7353
|
-
logger.log('Recording already in progress, skipping startRecording.');
|
|
7388
|
+
logger$1.log('Recording already in progress, skipping startRecording.');
|
|
7354
7389
|
return;
|
|
7355
7390
|
}
|
|
7356
7391
|
|
|
7357
|
-
this.recordMaxMs = this.
|
|
7392
|
+
this.recordMaxMs = this.getConfig('record_max_ms');
|
|
7358
7393
|
if (this.recordMaxMs > MAX_RECORDING_MS) {
|
|
7359
7394
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7360
|
-
logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7395
|
+
logger$1.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7361
7396
|
}
|
|
7362
7397
|
|
|
7363
|
-
this.
|
|
7364
|
-
this.
|
|
7365
|
-
|
|
7398
|
+
this.recordMinMs = this.getConfig('record_min_ms');
|
|
7399
|
+
if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
|
|
7400
|
+
this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
|
|
7401
|
+
logger$1.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
7402
|
+
}
|
|
7366
7403
|
|
|
7367
|
-
this.
|
|
7404
|
+
this.replayStartTime = new Date().getTime();
|
|
7405
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
7368
7406
|
|
|
7369
|
-
if (shouldStopBatcher) {
|
|
7370
|
-
//
|
|
7407
|
+
if (shouldStopBatcher || this.recordMinMs > 0) {
|
|
7408
|
+
// the primary case for shouldStopBatcher is when we're starting recording after a reset
|
|
7371
7409
|
// and don't want to send anything over the network until there's
|
|
7372
7410
|
// actual user activity
|
|
7411
|
+
// this also applies if the minimum recording length has not been hit yet
|
|
7412
|
+
// so that we don't send data until we know the recording will be long enough
|
|
7373
7413
|
this.batcher.stop();
|
|
7374
7414
|
} else {
|
|
7375
7415
|
this.batcher.start();
|
|
@@ -7377,45 +7417,52 @@ MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7377
7417
|
|
|
7378
7418
|
var resetIdleTimeout = _.bind(function () {
|
|
7379
7419
|
clearTimeout(this.idleTimeoutId);
|
|
7380
|
-
this.idleTimeoutId = setTimeout(
|
|
7381
|
-
logger.log('Idle timeout reached, restarting recording.');
|
|
7382
|
-
this.resetRecording();
|
|
7383
|
-
}, this), this.get_config('record_idle_timeout_ms'));
|
|
7420
|
+
this.idleTimeoutId = setTimeout(this._onIdleTimeout, this.getConfig('record_idle_timeout_ms'));
|
|
7384
7421
|
}, this);
|
|
7385
7422
|
|
|
7386
|
-
|
|
7423
|
+
var blockSelector = this.getConfig('record_block_selector');
|
|
7424
|
+
if (blockSelector === '' || blockSelector === null) {
|
|
7425
|
+
blockSelector = undefined;
|
|
7426
|
+
}
|
|
7427
|
+
|
|
7428
|
+
this._stopRecording = this._rrwebRecord({
|
|
7387
7429
|
'emit': _.bind(function (ev) {
|
|
7388
7430
|
this.batcher.enqueue(ev);
|
|
7389
7431
|
if (isUserEvent(ev)) {
|
|
7390
|
-
if (this.batcher.stopped) {
|
|
7432
|
+
if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
|
|
7391
7433
|
// start flushing again after user activity
|
|
7392
7434
|
this.batcher.start();
|
|
7393
7435
|
}
|
|
7394
7436
|
resetIdleTimeout();
|
|
7395
7437
|
}
|
|
7396
7438
|
}, this),
|
|
7397
|
-
'blockClass': this.
|
|
7398
|
-
'blockSelector':
|
|
7399
|
-
'collectFonts': this.
|
|
7400
|
-
'inlineImages': this.get_config('record_inline_images'),
|
|
7439
|
+
'blockClass': this.getConfig('record_block_class'),
|
|
7440
|
+
'blockSelector': blockSelector,
|
|
7441
|
+
'collectFonts': this.getConfig('record_collect_fonts'),
|
|
7401
7442
|
'maskAllInputs': true,
|
|
7402
|
-
'maskTextClass': this.
|
|
7403
|
-
'maskTextSelector': this.
|
|
7443
|
+
'maskTextClass': this.getConfig('record_mask_text_class'),
|
|
7444
|
+
'maskTextSelector': this.getConfig('record_mask_text_selector')
|
|
7404
7445
|
});
|
|
7405
7446
|
|
|
7406
|
-
|
|
7447
|
+
if (typeof this._stopRecording !== 'function') {
|
|
7448
|
+
this.reportError('rrweb failed to start, skipping this recording.');
|
|
7449
|
+
this._stopRecording = null;
|
|
7450
|
+
this.stopRecording(); // stop batcher looping and any timeouts
|
|
7451
|
+
return;
|
|
7452
|
+
}
|
|
7407
7453
|
|
|
7408
|
-
|
|
7409
|
-
};
|
|
7454
|
+
resetIdleTimeout();
|
|
7410
7455
|
|
|
7411
|
-
|
|
7412
|
-
this.stopRecording();
|
|
7413
|
-
this.startRecording(true);
|
|
7456
|
+
this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
|
|
7414
7457
|
};
|
|
7415
7458
|
|
|
7416
|
-
|
|
7417
|
-
if (this.
|
|
7418
|
-
|
|
7459
|
+
SessionRecording.prototype.stopRecording = function () {
|
|
7460
|
+
if (!this.isRrwebStopped()) {
|
|
7461
|
+
try {
|
|
7462
|
+
this._stopRecording();
|
|
7463
|
+
} catch (err) {
|
|
7464
|
+
this.reportError('Error with rrweb stopRecording', err);
|
|
7465
|
+
}
|
|
7419
7466
|
this._stopRecording = null;
|
|
7420
7467
|
}
|
|
7421
7468
|
|
|
@@ -7427,36 +7474,39 @@ MixpanelRecorder.prototype.stopRecording = function () {
|
|
|
7427
7474
|
this.batcher.flush();
|
|
7428
7475
|
this.batcher.stop();
|
|
7429
7476
|
}
|
|
7430
|
-
this.replayId = null;
|
|
7431
7477
|
|
|
7432
7478
|
clearTimeout(this.idleTimeoutId);
|
|
7433
7479
|
clearTimeout(this.maxTimeoutId);
|
|
7434
7480
|
};
|
|
7435
7481
|
|
|
7482
|
+
SessionRecording.prototype.isRrwebStopped = function () {
|
|
7483
|
+
return this._stopRecording === null;
|
|
7484
|
+
};
|
|
7485
|
+
|
|
7436
7486
|
/**
|
|
7437
7487
|
* Flushes the current batch of events to the server, but passes an opt-out callback to make sure
|
|
7438
7488
|
* we stop recording and dump any queued events if the user has opted out.
|
|
7439
7489
|
*/
|
|
7440
|
-
|
|
7490
|
+
SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
7441
7491
|
this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
|
|
7442
7492
|
};
|
|
7443
7493
|
|
|
7444
|
-
|
|
7494
|
+
SessionRecording.prototype._onOptOut = function (code) {
|
|
7445
7495
|
// addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
|
|
7446
7496
|
if (code === 0) {
|
|
7447
|
-
this.recEvents = [];
|
|
7448
7497
|
this.stopRecording();
|
|
7449
7498
|
}
|
|
7450
7499
|
};
|
|
7451
7500
|
|
|
7452
|
-
|
|
7501
|
+
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
7453
7502
|
var onSuccess = _.bind(function (response, responseBody) {
|
|
7454
|
-
//
|
|
7503
|
+
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
7455
7504
|
// RequestBatcher will always flush the next batch after the previous one succeeds.
|
|
7456
|
-
if
|
|
7505
|
+
// extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
|
|
7506
|
+
if (response.status === 200 && this.replayId === currentReplayId) {
|
|
7457
7507
|
this.seqNo++;
|
|
7508
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
7458
7509
|
}
|
|
7459
|
-
|
|
7460
7510
|
callback({
|
|
7461
7511
|
status: 0,
|
|
7462
7512
|
httpStatusCode: response.status,
|
|
@@ -7465,10 +7515,10 @@ MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback)
|
|
|
7465
7515
|
});
|
|
7466
7516
|
}, this);
|
|
7467
7517
|
|
|
7468
|
-
win['fetch'](this.
|
|
7518
|
+
win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
7469
7519
|
'method': 'POST',
|
|
7470
7520
|
'headers': {
|
|
7471
|
-
'Authorization': 'Basic ' + btoa(this.
|
|
7521
|
+
'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
|
|
7472
7522
|
'Content-Type': 'application/octet-stream'
|
|
7473
7523
|
},
|
|
7474
7524
|
'body': reqBody,
|
|
@@ -7479,28 +7529,37 @@ MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback)
|
|
|
7479
7529
|
callback({error: error});
|
|
7480
7530
|
});
|
|
7481
7531
|
}).catch(function (error) {
|
|
7482
|
-
callback({error: error});
|
|
7532
|
+
callback({error: error, httpStatusCode: 0});
|
|
7483
7533
|
});
|
|
7484
7534
|
};
|
|
7485
7535
|
|
|
7486
|
-
|
|
7536
|
+
SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
7487
7537
|
const numEvents = data.length;
|
|
7488
7538
|
|
|
7489
7539
|
if (numEvents > 0) {
|
|
7540
|
+
var replayId = this.replayId;
|
|
7490
7541
|
// each rrweb event has a timestamp - leverage those to get time properties
|
|
7491
7542
|
var batchStartTime = data[0].timestamp;
|
|
7492
|
-
if (this.seqNo === 0) {
|
|
7543
|
+
if (this.seqNo === 0 || !this.replayStartTime) {
|
|
7544
|
+
// extra safety net so that we don't send a null replay start time
|
|
7545
|
+
if (this.seqNo !== 0) {
|
|
7546
|
+
this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
|
|
7547
|
+
}
|
|
7548
|
+
|
|
7493
7549
|
this.replayStartTime = batchStartTime;
|
|
7494
7550
|
}
|
|
7495
7551
|
var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
|
|
7496
7552
|
|
|
7497
7553
|
var reqParams = {
|
|
7498
|
-
'
|
|
7499
|
-
'
|
|
7554
|
+
'$current_url': this.batchStartUrl,
|
|
7555
|
+
'$lib_version': Config.LIB_VERSION,
|
|
7500
7556
|
'batch_start_time': batchStartTime / 1000,
|
|
7501
|
-
'
|
|
7557
|
+
'distinct_id': String(this._mixpanel.get_distinct_id()),
|
|
7558
|
+
'mp_lib': 'web',
|
|
7559
|
+
'replay_id': replayId,
|
|
7502
7560
|
'replay_length_ms': replayLengthMs,
|
|
7503
|
-
'replay_start_time': this.replayStartTime / 1000
|
|
7561
|
+
'replay_start_time': this.replayStartTime / 1000,
|
|
7562
|
+
'seq': this.seqNo
|
|
7504
7563
|
};
|
|
7505
7564
|
var eventsJson = _.JSONEncode(data);
|
|
7506
7565
|
|
|
@@ -7521,28 +7580,93 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
|
|
|
7521
7580
|
.blob()
|
|
7522
7581
|
.then(_.bind(function(compressedBlob) {
|
|
7523
7582
|
reqParams['format'] = 'gzip';
|
|
7524
|
-
this._sendRequest(reqParams, compressedBlob, callback);
|
|
7583
|
+
this._sendRequest(replayId, reqParams, compressedBlob, callback);
|
|
7525
7584
|
}, this));
|
|
7526
7585
|
} else {
|
|
7527
7586
|
reqParams['format'] = 'body';
|
|
7528
|
-
this._sendRequest(reqParams, eventsJson, callback);
|
|
7587
|
+
this._sendRequest(replayId, reqParams, eventsJson, callback);
|
|
7529
7588
|
}
|
|
7530
7589
|
}
|
|
7531
7590
|
});
|
|
7532
7591
|
|
|
7533
7592
|
|
|
7534
|
-
|
|
7535
|
-
logger.error.apply(logger.error, arguments);
|
|
7593
|
+
SessionRecording.prototype.reportError = function(msg, err) {
|
|
7594
|
+
logger$1.error.apply(logger$1.error, arguments);
|
|
7536
7595
|
try {
|
|
7537
7596
|
if (!err && !(msg instanceof Error)) {
|
|
7538
7597
|
msg = new Error(msg);
|
|
7539
7598
|
}
|
|
7540
|
-
this.
|
|
7599
|
+
this.getConfig('error_reporter')(msg, err);
|
|
7541
7600
|
} catch(err) {
|
|
7542
|
-
logger.error(err);
|
|
7601
|
+
logger$1.error(err);
|
|
7543
7602
|
}
|
|
7544
7603
|
};
|
|
7545
7604
|
|
|
7605
|
+
var logger = console_with_prefix('recorder');
|
|
7606
|
+
|
|
7607
|
+
/**
|
|
7608
|
+
* Recorder API: manages recordings and exposes methods public to the core Mixpanel library.
|
|
7609
|
+
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7610
|
+
*/
|
|
7611
|
+
var MixpanelRecorder = function(mixpanelInstance) {
|
|
7612
|
+
this._mixpanel = mixpanelInstance;
|
|
7613
|
+
this.activeRecording = null;
|
|
7614
|
+
};
|
|
7615
|
+
|
|
7616
|
+
MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
|
|
7617
|
+
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
7618
|
+
logger.log('Recording already in progress, skipping startRecording.');
|
|
7619
|
+
return;
|
|
7620
|
+
}
|
|
7621
|
+
|
|
7622
|
+
var onIdleTimeout = _.bind(function () {
|
|
7623
|
+
logger.log('Idle timeout reached, restarting recording.');
|
|
7624
|
+
this.resetRecording();
|
|
7625
|
+
}, this);
|
|
7626
|
+
|
|
7627
|
+
var onMaxLengthReached = _.bind(function () {
|
|
7628
|
+
logger.log('Max recording length reached, stopping recording.');
|
|
7629
|
+
this.resetRecording();
|
|
7630
|
+
}, this);
|
|
7631
|
+
|
|
7632
|
+
this.activeRecording = new SessionRecording({
|
|
7633
|
+
mixpanelInstance: this._mixpanel,
|
|
7634
|
+
onIdleTimeout: onIdleTimeout,
|
|
7635
|
+
onMaxLengthReached: onMaxLengthReached,
|
|
7636
|
+
replayId: _.UUID(),
|
|
7637
|
+
rrwebRecord: record
|
|
7638
|
+
});
|
|
7639
|
+
|
|
7640
|
+
this.activeRecording.startRecording(shouldStopBatcher);
|
|
7641
|
+
};
|
|
7642
|
+
|
|
7643
|
+
MixpanelRecorder.prototype.stopRecording = function() {
|
|
7644
|
+
if (this.activeRecording) {
|
|
7645
|
+
this.activeRecording.stopRecording();
|
|
7646
|
+
this.activeRecording = null;
|
|
7647
|
+
}
|
|
7648
|
+
};
|
|
7649
|
+
|
|
7650
|
+
MixpanelRecorder.prototype.resetRecording = function () {
|
|
7651
|
+
this.stopRecording();
|
|
7652
|
+
this.startRecording(true);
|
|
7653
|
+
};
|
|
7654
|
+
|
|
7655
|
+
MixpanelRecorder.prototype.getActiveReplayId = function () {
|
|
7656
|
+
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
7657
|
+
return this.activeRecording.replayId;
|
|
7658
|
+
} else {
|
|
7659
|
+
return null;
|
|
7660
|
+
}
|
|
7661
|
+
};
|
|
7662
|
+
|
|
7663
|
+
// getter so that older mixpanel-core versions can still retrieve the replay ID
|
|
7664
|
+
// when pulling the latest recorder bundle from the CDN
|
|
7665
|
+
Object.defineProperty(MixpanelRecorder.prototype, 'replayId', {
|
|
7666
|
+
get: function () {
|
|
7667
|
+
return this.getActiveReplayId();
|
|
7668
|
+
}
|
|
7669
|
+
});
|
|
7546
7670
|
|
|
7547
7671
|
win['__mp_recorder'] = MixpanelRecorder;
|
|
7548
7672
|
|
|
@@ -9009,10 +9133,10 @@ var DEFAULT_CONFIG = {
|
|
|
9009
9133
|
'record_block_selector': 'img, video',
|
|
9010
9134
|
'record_collect_fonts': false,
|
|
9011
9135
|
'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
|
|
9012
|
-
'record_inline_images': false,
|
|
9013
9136
|
'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
|
|
9014
9137
|
'record_mask_text_selector': '*',
|
|
9015
9138
|
'record_max_ms': MAX_RECORDING_MS,
|
|
9139
|
+
'record_min_ms': 0,
|
|
9016
9140
|
'record_sessions_percent': 0,
|
|
9017
9141
|
'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
|
|
9018
9142
|
};
|
|
@@ -9261,15 +9385,35 @@ MixpanelLib.prototype.stop_session_recording = function () {
|
|
|
9261
9385
|
|
|
9262
9386
|
MixpanelLib.prototype.get_session_recording_properties = function () {
|
|
9263
9387
|
var props = {};
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
props['$mp_replay_id'] = replay_id;
|
|
9268
|
-
}
|
|
9388
|
+
var replay_id = this._get_session_replay_id();
|
|
9389
|
+
if (replay_id) {
|
|
9390
|
+
props['$mp_replay_id'] = replay_id;
|
|
9269
9391
|
}
|
|
9270
9392
|
return props;
|
|
9271
9393
|
};
|
|
9272
9394
|
|
|
9395
|
+
MixpanelLib.prototype.get_session_replay_url = function () {
|
|
9396
|
+
var replay_url = null;
|
|
9397
|
+
var replay_id = this._get_session_replay_id();
|
|
9398
|
+
if (replay_id) {
|
|
9399
|
+
var query_params = _.HTTPBuildQuery({
|
|
9400
|
+
'replay_id': replay_id,
|
|
9401
|
+
'distinct_id': this.get_distinct_id(),
|
|
9402
|
+
'token': this.get_config('token')
|
|
9403
|
+
});
|
|
9404
|
+
replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
|
|
9405
|
+
}
|
|
9406
|
+
return replay_url;
|
|
9407
|
+
};
|
|
9408
|
+
|
|
9409
|
+
MixpanelLib.prototype._get_session_replay_id = function () {
|
|
9410
|
+
var replay_id = null;
|
|
9411
|
+
if (this._recorder) {
|
|
9412
|
+
replay_id = this._recorder['replayId'];
|
|
9413
|
+
}
|
|
9414
|
+
return replay_id || null;
|
|
9415
|
+
};
|
|
9416
|
+
|
|
9273
9417
|
// Private methods
|
|
9274
9418
|
|
|
9275
9419
|
MixpanelLib.prototype._loaded = function() {
|
|
@@ -10996,6 +11140,7 @@ MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.protot
|
|
|
10996
11140
|
MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
|
|
10997
11141
|
MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
|
|
10998
11142
|
MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
|
|
11143
|
+
MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
|
|
10999
11144
|
MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
|
|
11000
11145
|
|
|
11001
11146
|
// MixpanelPersistence Exports
|