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.umd.js
CHANGED
|
@@ -4513,7 +4513,7 @@
|
|
|
4513
4513
|
|
|
4514
4514
|
var Config = {
|
|
4515
4515
|
DEBUG: false,
|
|
4516
|
-
LIB_VERSION: '2.
|
|
4516
|
+
LIB_VERSION: '2.56.0'
|
|
4517
4517
|
};
|
|
4518
4518
|
|
|
4519
4519
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
@@ -4525,7 +4525,7 @@
|
|
|
4525
4525
|
hostname: ''
|
|
4526
4526
|
};
|
|
4527
4527
|
win = {
|
|
4528
|
-
navigator: { userAgent: '' },
|
|
4528
|
+
navigator: { userAgent: '', onLine: true },
|
|
4529
4529
|
document: {
|
|
4530
4530
|
location: loc,
|
|
4531
4531
|
referrer: ''
|
|
@@ -4539,6 +4539,8 @@
|
|
|
4539
4539
|
|
|
4540
4540
|
// Maximum allowed session recording length
|
|
4541
4541
|
var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
4542
|
+
// Maximum allowed value for minimum session recording length
|
|
4543
|
+
var MAX_VALUE_FOR_MIN_RECORDING_MS = 8 * 1000; // 8 seconds
|
|
4542
4544
|
|
|
4543
4545
|
/*
|
|
4544
4546
|
* Saved references to long variable names, so that closure compiler can
|
|
@@ -5479,7 +5481,7 @@
|
|
|
5479
5481
|
_.getQueryParam = function(url, param) {
|
|
5480
5482
|
// Expects a raw URL
|
|
5481
5483
|
|
|
5482
|
-
param = param.replace(/[[]
|
|
5484
|
+
param = param.replace(/[[]/g, '\\[').replace(/[\]]/g, '\\]');
|
|
5483
5485
|
var regexS = '[\\?&]' + param + '=([^&#]*)',
|
|
5484
5486
|
regex = new RegExp(regexS),
|
|
5485
5487
|
results = regex.exec(url);
|
|
@@ -5936,8 +5938,8 @@
|
|
|
5936
5938
|
};
|
|
5937
5939
|
})();
|
|
5938
5940
|
|
|
5939
|
-
var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
|
|
5940
|
-
var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid'];
|
|
5941
|
+
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'];
|
|
5942
|
+
var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid'];
|
|
5941
5943
|
|
|
5942
5944
|
_.info = {
|
|
5943
5945
|
campaignParams: function(default_value) {
|
|
@@ -6219,6 +6221,15 @@
|
|
|
6219
6221
|
return matches ? matches[0] : '';
|
|
6220
6222
|
};
|
|
6221
6223
|
|
|
6224
|
+
/**
|
|
6225
|
+
* Check whether we have network connection. default to true for browsers that don't support navigator.onLine (IE)
|
|
6226
|
+
* @returns {boolean}
|
|
6227
|
+
*/
|
|
6228
|
+
var isOnline = function() {
|
|
6229
|
+
var onLine = win.navigator['onLine'];
|
|
6230
|
+
return _.isUndefined(onLine) || onLine;
|
|
6231
|
+
};
|
|
6232
|
+
|
|
6222
6233
|
var JSONStringify = null, JSONParse = null;
|
|
6223
6234
|
if (typeof JSON !== 'undefined') {
|
|
6224
6235
|
JSONStringify = JSON.stringify;
|
|
@@ -6539,7 +6550,7 @@
|
|
|
6539
6550
|
};
|
|
6540
6551
|
}
|
|
6541
6552
|
|
|
6542
|
-
var logger$
|
|
6553
|
+
var logger$4 = console_with_prefix('lock');
|
|
6543
6554
|
|
|
6544
6555
|
/**
|
|
6545
6556
|
* SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
|
|
@@ -6596,7 +6607,7 @@
|
|
|
6596
6607
|
|
|
6597
6608
|
var delay = function(cb) {
|
|
6598
6609
|
if (new Date().getTime() - startTime > timeoutMS) {
|
|
6599
|
-
logger$
|
|
6610
|
+
logger$4.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
|
|
6600
6611
|
storage.removeItem(keyZ);
|
|
6601
6612
|
storage.removeItem(keyY);
|
|
6602
6613
|
loop();
|
|
@@ -6685,7 +6696,7 @@
|
|
|
6685
6696
|
}
|
|
6686
6697
|
};
|
|
6687
6698
|
|
|
6688
|
-
var logger$
|
|
6699
|
+
var logger$3 = console_with_prefix('batch');
|
|
6689
6700
|
|
|
6690
6701
|
/**
|
|
6691
6702
|
* RequestQueue: queue for batching API requests with localStorage backup for retries.
|
|
@@ -6706,11 +6717,13 @@
|
|
|
6706
6717
|
var RequestQueue = function(storageKey, options) {
|
|
6707
6718
|
options = options || {};
|
|
6708
6719
|
this.storageKey = storageKey;
|
|
6709
|
-
this.storage = options.storage || window.localStorage;
|
|
6710
|
-
this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2);
|
|
6711
|
-
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
6712
|
-
|
|
6713
6720
|
this.usePersistence = options.usePersistence;
|
|
6721
|
+
if (this.usePersistence) {
|
|
6722
|
+
this.storage = options.storage || window.localStorage;
|
|
6723
|
+
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
6724
|
+
}
|
|
6725
|
+
this.reportError = options.errorReporter || _.bind(logger$3.error, logger$3);
|
|
6726
|
+
|
|
6714
6727
|
this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
|
|
6715
6728
|
|
|
6716
6729
|
this.memQueue = [];
|
|
@@ -6989,7 +7002,7 @@
|
|
|
6989
7002
|
// maximum interval between request retries after exponential backoff
|
|
6990
7003
|
var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
|
|
6991
7004
|
|
|
6992
|
-
var logger$
|
|
7005
|
+
var logger$2 = console_with_prefix('batch');
|
|
6993
7006
|
|
|
6994
7007
|
/**
|
|
6995
7008
|
* RequestBatcher: manages the queueing, flushing, retry etc of requests of one
|
|
@@ -7103,7 +7116,7 @@
|
|
|
7103
7116
|
try {
|
|
7104
7117
|
|
|
7105
7118
|
if (this.requestInProgress) {
|
|
7106
|
-
logger$
|
|
7119
|
+
logger$2.log('Flush: Request already in progress');
|
|
7107
7120
|
return;
|
|
7108
7121
|
}
|
|
7109
7122
|
|
|
@@ -7182,7 +7195,12 @@
|
|
|
7182
7195
|
this.flush();
|
|
7183
7196
|
} else if (
|
|
7184
7197
|
_.isObject(res) &&
|
|
7185
|
-
(
|
|
7198
|
+
(
|
|
7199
|
+
res.httpStatusCode >= 500
|
|
7200
|
+
|| res.httpStatusCode === 429
|
|
7201
|
+
|| (res.httpStatusCode <= 0 && !isOnline())
|
|
7202
|
+
|| res.error === 'timeout'
|
|
7203
|
+
)
|
|
7186
7204
|
) {
|
|
7187
7205
|
// network or API error, or 429 Too Many Requests, retry
|
|
7188
7206
|
var retryMS = this.flushInterval * 2;
|
|
@@ -7266,7 +7284,7 @@
|
|
|
7266
7284
|
if (options.unloading) {
|
|
7267
7285
|
requestOptions.transport = 'sendBeacon';
|
|
7268
7286
|
}
|
|
7269
|
-
logger$
|
|
7287
|
+
logger$2.log('MIXPANEL REQUEST:', dataForRequest);
|
|
7270
7288
|
this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
|
|
7271
7289
|
} catch(err) {
|
|
7272
7290
|
this.reportError('Error flushing request queue', err);
|
|
@@ -7278,7 +7296,7 @@
|
|
|
7278
7296
|
* Log error to global logger and optional user-defined logger.
|
|
7279
7297
|
*/
|
|
7280
7298
|
RequestBatcher.prototype.reportError = function(msg, err) {
|
|
7281
|
-
logger$
|
|
7299
|
+
logger$2.error.apply(logger$2.error, arguments);
|
|
7282
7300
|
if (this.errorReporter) {
|
|
7283
7301
|
try {
|
|
7284
7302
|
if (!(err instanceof Error)) {
|
|
@@ -7286,12 +7304,12 @@
|
|
|
7286
7304
|
}
|
|
7287
7305
|
this.errorReporter(msg, err);
|
|
7288
7306
|
} catch(err) {
|
|
7289
|
-
logger$
|
|
7307
|
+
logger$2.error(err);
|
|
7290
7308
|
}
|
|
7291
7309
|
}
|
|
7292
7310
|
};
|
|
7293
7311
|
|
|
7294
|
-
var logger = console_with_prefix('recorder');
|
|
7312
|
+
var logger$1 = console_with_prefix('recorder');
|
|
7295
7313
|
var CompressionStream = win['CompressionStream'];
|
|
7296
7314
|
|
|
7297
7315
|
var RECORDER_BATCHER_LIB_CONFIG = {
|
|
@@ -7317,63 +7335,85 @@
|
|
|
7317
7335
|
return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
|
|
7318
7336
|
}
|
|
7319
7337
|
|
|
7320
|
-
|
|
7321
|
-
|
|
7338
|
+
/**
|
|
7339
|
+
* This class encapsulates a single session recording and its lifecycle.
|
|
7340
|
+
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7341
|
+
* @param {String} [options.replayId] - unique uuid for a single replay
|
|
7342
|
+
* @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
7343
|
+
* @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
7344
|
+
* @param {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
7345
|
+
*/
|
|
7346
|
+
var SessionRecording = function(options) {
|
|
7347
|
+
this._mixpanel = options.mixpanelInstance;
|
|
7348
|
+
this._onIdleTimeout = options.onIdleTimeout;
|
|
7349
|
+
this._onMaxLengthReached = options.onMaxLengthReached;
|
|
7350
|
+
this._rrwebRecord = options.rrwebRecord;
|
|
7351
|
+
|
|
7352
|
+
this.replayId = options.replayId;
|
|
7322
7353
|
|
|
7323
7354
|
// internal rrweb stopRecording function
|
|
7324
7355
|
this._stopRecording = null;
|
|
7325
7356
|
|
|
7326
|
-
this.recEvents = [];
|
|
7327
7357
|
this.seqNo = 0;
|
|
7328
|
-
this.replayId = null;
|
|
7329
7358
|
this.replayStartTime = null;
|
|
7330
|
-
this.
|
|
7359
|
+
this.batchStartUrl = null;
|
|
7331
7360
|
|
|
7332
7361
|
this.idleTimeoutId = null;
|
|
7333
7362
|
this.maxTimeoutId = null;
|
|
7334
7363
|
|
|
7335
7364
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7336
|
-
this.
|
|
7337
|
-
};
|
|
7338
|
-
|
|
7365
|
+
this.recordMinMs = 0;
|
|
7339
7366
|
|
|
7340
|
-
|
|
7341
|
-
this
|
|
7342
|
-
|
|
7343
|
-
|
|
7367
|
+
// each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
|
|
7368
|
+
// this will be important when persistence is introduced
|
|
7369
|
+
var batcherKey = '__mprec_' + this.getConfig('token') + '_' + this.replayId;
|
|
7370
|
+
this.batcher = new RequestBatcher(batcherKey, {
|
|
7344
7371
|
errorReporter: _.bind(this.reportError, this),
|
|
7345
7372
|
flushOnlyOnInterval: true,
|
|
7373
|
+
libConfig: RECORDER_BATCHER_LIB_CONFIG,
|
|
7374
|
+
sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
|
|
7346
7375
|
usePersistence: false
|
|
7347
7376
|
});
|
|
7348
7377
|
};
|
|
7349
7378
|
|
|
7350
|
-
|
|
7351
|
-
MixpanelRecorder.prototype.get_config = function(configVar) {
|
|
7379
|
+
SessionRecording.prototype.getConfig = function(configVar) {
|
|
7352
7380
|
return this._mixpanel.get_config(configVar);
|
|
7353
7381
|
};
|
|
7354
7382
|
|
|
7355
|
-
|
|
7383
|
+
// Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
|
|
7384
|
+
// reaches into this class instance and expects the snake case version of the function.
|
|
7385
|
+
// eslint-disable-next-line camelcase
|
|
7386
|
+
SessionRecording.prototype.get_config = function(configVar) {
|
|
7387
|
+
return this.getConfig(configVar);
|
|
7388
|
+
};
|
|
7389
|
+
|
|
7390
|
+
SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
7356
7391
|
if (this._stopRecording !== null) {
|
|
7357
|
-
logger.log('Recording already in progress, skipping startRecording.');
|
|
7392
|
+
logger$1.log('Recording already in progress, skipping startRecording.');
|
|
7358
7393
|
return;
|
|
7359
7394
|
}
|
|
7360
7395
|
|
|
7361
|
-
this.recordMaxMs = this.
|
|
7396
|
+
this.recordMaxMs = this.getConfig('record_max_ms');
|
|
7362
7397
|
if (this.recordMaxMs > MAX_RECORDING_MS) {
|
|
7363
7398
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7364
|
-
logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7399
|
+
logger$1.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7365
7400
|
}
|
|
7366
7401
|
|
|
7367
|
-
this.
|
|
7368
|
-
this.
|
|
7369
|
-
|
|
7402
|
+
this.recordMinMs = this.getConfig('record_min_ms');
|
|
7403
|
+
if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
|
|
7404
|
+
this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
|
|
7405
|
+
logger$1.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
7406
|
+
}
|
|
7370
7407
|
|
|
7371
|
-
this.
|
|
7408
|
+
this.replayStartTime = new Date().getTime();
|
|
7409
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
7372
7410
|
|
|
7373
|
-
if (shouldStopBatcher) {
|
|
7374
|
-
//
|
|
7411
|
+
if (shouldStopBatcher || this.recordMinMs > 0) {
|
|
7412
|
+
// the primary case for shouldStopBatcher is when we're starting recording after a reset
|
|
7375
7413
|
// and don't want to send anything over the network until there's
|
|
7376
7414
|
// actual user activity
|
|
7415
|
+
// this also applies if the minimum recording length has not been hit yet
|
|
7416
|
+
// so that we don't send data until we know the recording will be long enough
|
|
7377
7417
|
this.batcher.stop();
|
|
7378
7418
|
} else {
|
|
7379
7419
|
this.batcher.start();
|
|
@@ -7381,45 +7421,52 @@
|
|
|
7381
7421
|
|
|
7382
7422
|
var resetIdleTimeout = _.bind(function () {
|
|
7383
7423
|
clearTimeout(this.idleTimeoutId);
|
|
7384
|
-
this.idleTimeoutId = setTimeout(
|
|
7385
|
-
logger.log('Idle timeout reached, restarting recording.');
|
|
7386
|
-
this.resetRecording();
|
|
7387
|
-
}, this), this.get_config('record_idle_timeout_ms'));
|
|
7424
|
+
this.idleTimeoutId = setTimeout(this._onIdleTimeout, this.getConfig('record_idle_timeout_ms'));
|
|
7388
7425
|
}, this);
|
|
7389
7426
|
|
|
7390
|
-
|
|
7427
|
+
var blockSelector = this.getConfig('record_block_selector');
|
|
7428
|
+
if (blockSelector === '' || blockSelector === null) {
|
|
7429
|
+
blockSelector = undefined;
|
|
7430
|
+
}
|
|
7431
|
+
|
|
7432
|
+
this._stopRecording = this._rrwebRecord({
|
|
7391
7433
|
'emit': _.bind(function (ev) {
|
|
7392
7434
|
this.batcher.enqueue(ev);
|
|
7393
7435
|
if (isUserEvent(ev)) {
|
|
7394
|
-
if (this.batcher.stopped) {
|
|
7436
|
+
if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
|
|
7395
7437
|
// start flushing again after user activity
|
|
7396
7438
|
this.batcher.start();
|
|
7397
7439
|
}
|
|
7398
7440
|
resetIdleTimeout();
|
|
7399
7441
|
}
|
|
7400
7442
|
}, this),
|
|
7401
|
-
'blockClass': this.
|
|
7402
|
-
'blockSelector':
|
|
7403
|
-
'collectFonts': this.
|
|
7404
|
-
'inlineImages': this.get_config('record_inline_images'),
|
|
7443
|
+
'blockClass': this.getConfig('record_block_class'),
|
|
7444
|
+
'blockSelector': blockSelector,
|
|
7445
|
+
'collectFonts': this.getConfig('record_collect_fonts'),
|
|
7405
7446
|
'maskAllInputs': true,
|
|
7406
|
-
'maskTextClass': this.
|
|
7407
|
-
'maskTextSelector': this.
|
|
7447
|
+
'maskTextClass': this.getConfig('record_mask_text_class'),
|
|
7448
|
+
'maskTextSelector': this.getConfig('record_mask_text_selector')
|
|
7408
7449
|
});
|
|
7409
7450
|
|
|
7410
|
-
|
|
7451
|
+
if (typeof this._stopRecording !== 'function') {
|
|
7452
|
+
this.reportError('rrweb failed to start, skipping this recording.');
|
|
7453
|
+
this._stopRecording = null;
|
|
7454
|
+
this.stopRecording(); // stop batcher looping and any timeouts
|
|
7455
|
+
return;
|
|
7456
|
+
}
|
|
7411
7457
|
|
|
7412
|
-
|
|
7413
|
-
};
|
|
7458
|
+
resetIdleTimeout();
|
|
7414
7459
|
|
|
7415
|
-
|
|
7416
|
-
this.stopRecording();
|
|
7417
|
-
this.startRecording(true);
|
|
7460
|
+
this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
|
|
7418
7461
|
};
|
|
7419
7462
|
|
|
7420
|
-
|
|
7421
|
-
if (this.
|
|
7422
|
-
|
|
7463
|
+
SessionRecording.prototype.stopRecording = function () {
|
|
7464
|
+
if (!this.isRrwebStopped()) {
|
|
7465
|
+
try {
|
|
7466
|
+
this._stopRecording();
|
|
7467
|
+
} catch (err) {
|
|
7468
|
+
this.reportError('Error with rrweb stopRecording', err);
|
|
7469
|
+
}
|
|
7423
7470
|
this._stopRecording = null;
|
|
7424
7471
|
}
|
|
7425
7472
|
|
|
@@ -7431,36 +7478,39 @@
|
|
|
7431
7478
|
this.batcher.flush();
|
|
7432
7479
|
this.batcher.stop();
|
|
7433
7480
|
}
|
|
7434
|
-
this.replayId = null;
|
|
7435
7481
|
|
|
7436
7482
|
clearTimeout(this.idleTimeoutId);
|
|
7437
7483
|
clearTimeout(this.maxTimeoutId);
|
|
7438
7484
|
};
|
|
7439
7485
|
|
|
7486
|
+
SessionRecording.prototype.isRrwebStopped = function () {
|
|
7487
|
+
return this._stopRecording === null;
|
|
7488
|
+
};
|
|
7489
|
+
|
|
7440
7490
|
/**
|
|
7441
7491
|
* Flushes the current batch of events to the server, but passes an opt-out callback to make sure
|
|
7442
7492
|
* we stop recording and dump any queued events if the user has opted out.
|
|
7443
7493
|
*/
|
|
7444
|
-
|
|
7494
|
+
SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
7445
7495
|
this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
|
|
7446
7496
|
};
|
|
7447
7497
|
|
|
7448
|
-
|
|
7498
|
+
SessionRecording.prototype._onOptOut = function (code) {
|
|
7449
7499
|
// addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
|
|
7450
7500
|
if (code === 0) {
|
|
7451
|
-
this.recEvents = [];
|
|
7452
7501
|
this.stopRecording();
|
|
7453
7502
|
}
|
|
7454
7503
|
};
|
|
7455
7504
|
|
|
7456
|
-
|
|
7505
|
+
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
7457
7506
|
var onSuccess = _.bind(function (response, responseBody) {
|
|
7458
|
-
//
|
|
7507
|
+
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
7459
7508
|
// RequestBatcher will always flush the next batch after the previous one succeeds.
|
|
7460
|
-
if
|
|
7509
|
+
// extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
|
|
7510
|
+
if (response.status === 200 && this.replayId === currentReplayId) {
|
|
7461
7511
|
this.seqNo++;
|
|
7512
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
7462
7513
|
}
|
|
7463
|
-
|
|
7464
7514
|
callback({
|
|
7465
7515
|
status: 0,
|
|
7466
7516
|
httpStatusCode: response.status,
|
|
@@ -7469,10 +7519,10 @@
|
|
|
7469
7519
|
});
|
|
7470
7520
|
}, this);
|
|
7471
7521
|
|
|
7472
|
-
win['fetch'](this.
|
|
7522
|
+
win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
7473
7523
|
'method': 'POST',
|
|
7474
7524
|
'headers': {
|
|
7475
|
-
'Authorization': 'Basic ' + btoa(this.
|
|
7525
|
+
'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
|
|
7476
7526
|
'Content-Type': 'application/octet-stream'
|
|
7477
7527
|
},
|
|
7478
7528
|
'body': reqBody,
|
|
@@ -7483,28 +7533,37 @@
|
|
|
7483
7533
|
callback({error: error});
|
|
7484
7534
|
});
|
|
7485
7535
|
}).catch(function (error) {
|
|
7486
|
-
callback({error: error});
|
|
7536
|
+
callback({error: error, httpStatusCode: 0});
|
|
7487
7537
|
});
|
|
7488
7538
|
};
|
|
7489
7539
|
|
|
7490
|
-
|
|
7540
|
+
SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
7491
7541
|
const numEvents = data.length;
|
|
7492
7542
|
|
|
7493
7543
|
if (numEvents > 0) {
|
|
7544
|
+
var replayId = this.replayId;
|
|
7494
7545
|
// each rrweb event has a timestamp - leverage those to get time properties
|
|
7495
7546
|
var batchStartTime = data[0].timestamp;
|
|
7496
|
-
if (this.seqNo === 0) {
|
|
7547
|
+
if (this.seqNo === 0 || !this.replayStartTime) {
|
|
7548
|
+
// extra safety net so that we don't send a null replay start time
|
|
7549
|
+
if (this.seqNo !== 0) {
|
|
7550
|
+
this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
|
|
7551
|
+
}
|
|
7552
|
+
|
|
7497
7553
|
this.replayStartTime = batchStartTime;
|
|
7498
7554
|
}
|
|
7499
7555
|
var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
|
|
7500
7556
|
|
|
7501
7557
|
var reqParams = {
|
|
7502
|
-
'
|
|
7503
|
-
'
|
|
7558
|
+
'$current_url': this.batchStartUrl,
|
|
7559
|
+
'$lib_version': Config.LIB_VERSION,
|
|
7504
7560
|
'batch_start_time': batchStartTime / 1000,
|
|
7505
|
-
'
|
|
7561
|
+
'distinct_id': String(this._mixpanel.get_distinct_id()),
|
|
7562
|
+
'mp_lib': 'web',
|
|
7563
|
+
'replay_id': replayId,
|
|
7506
7564
|
'replay_length_ms': replayLengthMs,
|
|
7507
|
-
'replay_start_time': this.replayStartTime / 1000
|
|
7565
|
+
'replay_start_time': this.replayStartTime / 1000,
|
|
7566
|
+
'seq': this.seqNo
|
|
7508
7567
|
};
|
|
7509
7568
|
var eventsJson = _.JSONEncode(data);
|
|
7510
7569
|
|
|
@@ -7525,28 +7584,93 @@
|
|
|
7525
7584
|
.blob()
|
|
7526
7585
|
.then(_.bind(function(compressedBlob) {
|
|
7527
7586
|
reqParams['format'] = 'gzip';
|
|
7528
|
-
this._sendRequest(reqParams, compressedBlob, callback);
|
|
7587
|
+
this._sendRequest(replayId, reqParams, compressedBlob, callback);
|
|
7529
7588
|
}, this));
|
|
7530
7589
|
} else {
|
|
7531
7590
|
reqParams['format'] = 'body';
|
|
7532
|
-
this._sendRequest(reqParams, eventsJson, callback);
|
|
7591
|
+
this._sendRequest(replayId, reqParams, eventsJson, callback);
|
|
7533
7592
|
}
|
|
7534
7593
|
}
|
|
7535
7594
|
});
|
|
7536
7595
|
|
|
7537
7596
|
|
|
7538
|
-
|
|
7539
|
-
logger.error.apply(logger.error, arguments);
|
|
7597
|
+
SessionRecording.prototype.reportError = function(msg, err) {
|
|
7598
|
+
logger$1.error.apply(logger$1.error, arguments);
|
|
7540
7599
|
try {
|
|
7541
7600
|
if (!err && !(msg instanceof Error)) {
|
|
7542
7601
|
msg = new Error(msg);
|
|
7543
7602
|
}
|
|
7544
|
-
this.
|
|
7603
|
+
this.getConfig('error_reporter')(msg, err);
|
|
7545
7604
|
} catch(err) {
|
|
7546
|
-
logger.error(err);
|
|
7605
|
+
logger$1.error(err);
|
|
7547
7606
|
}
|
|
7548
7607
|
};
|
|
7549
7608
|
|
|
7609
|
+
var logger = console_with_prefix('recorder');
|
|
7610
|
+
|
|
7611
|
+
/**
|
|
7612
|
+
* Recorder API: manages recordings and exposes methods public to the core Mixpanel library.
|
|
7613
|
+
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7614
|
+
*/
|
|
7615
|
+
var MixpanelRecorder = function(mixpanelInstance) {
|
|
7616
|
+
this._mixpanel = mixpanelInstance;
|
|
7617
|
+
this.activeRecording = null;
|
|
7618
|
+
};
|
|
7619
|
+
|
|
7620
|
+
MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
|
|
7621
|
+
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
7622
|
+
logger.log('Recording already in progress, skipping startRecording.');
|
|
7623
|
+
return;
|
|
7624
|
+
}
|
|
7625
|
+
|
|
7626
|
+
var onIdleTimeout = _.bind(function () {
|
|
7627
|
+
logger.log('Idle timeout reached, restarting recording.');
|
|
7628
|
+
this.resetRecording();
|
|
7629
|
+
}, this);
|
|
7630
|
+
|
|
7631
|
+
var onMaxLengthReached = _.bind(function () {
|
|
7632
|
+
logger.log('Max recording length reached, stopping recording.');
|
|
7633
|
+
this.resetRecording();
|
|
7634
|
+
}, this);
|
|
7635
|
+
|
|
7636
|
+
this.activeRecording = new SessionRecording({
|
|
7637
|
+
mixpanelInstance: this._mixpanel,
|
|
7638
|
+
onIdleTimeout: onIdleTimeout,
|
|
7639
|
+
onMaxLengthReached: onMaxLengthReached,
|
|
7640
|
+
replayId: _.UUID(),
|
|
7641
|
+
rrwebRecord: record
|
|
7642
|
+
});
|
|
7643
|
+
|
|
7644
|
+
this.activeRecording.startRecording(shouldStopBatcher);
|
|
7645
|
+
};
|
|
7646
|
+
|
|
7647
|
+
MixpanelRecorder.prototype.stopRecording = function() {
|
|
7648
|
+
if (this.activeRecording) {
|
|
7649
|
+
this.activeRecording.stopRecording();
|
|
7650
|
+
this.activeRecording = null;
|
|
7651
|
+
}
|
|
7652
|
+
};
|
|
7653
|
+
|
|
7654
|
+
MixpanelRecorder.prototype.resetRecording = function () {
|
|
7655
|
+
this.stopRecording();
|
|
7656
|
+
this.startRecording(true);
|
|
7657
|
+
};
|
|
7658
|
+
|
|
7659
|
+
MixpanelRecorder.prototype.getActiveReplayId = function () {
|
|
7660
|
+
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
7661
|
+
return this.activeRecording.replayId;
|
|
7662
|
+
} else {
|
|
7663
|
+
return null;
|
|
7664
|
+
}
|
|
7665
|
+
};
|
|
7666
|
+
|
|
7667
|
+
// getter so that older mixpanel-core versions can still retrieve the replay ID
|
|
7668
|
+
// when pulling the latest recorder bundle from the CDN
|
|
7669
|
+
Object.defineProperty(MixpanelRecorder.prototype, 'replayId', {
|
|
7670
|
+
get: function () {
|
|
7671
|
+
return this.getActiveReplayId();
|
|
7672
|
+
}
|
|
7673
|
+
});
|
|
7550
7674
|
|
|
7551
7675
|
win['__mp_recorder'] = MixpanelRecorder;
|
|
7552
7676
|
|
|
@@ -9013,10 +9137,10 @@
|
|
|
9013
9137
|
'record_block_selector': 'img, video',
|
|
9014
9138
|
'record_collect_fonts': false,
|
|
9015
9139
|
'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
|
|
9016
|
-
'record_inline_images': false,
|
|
9017
9140
|
'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
|
|
9018
9141
|
'record_mask_text_selector': '*',
|
|
9019
9142
|
'record_max_ms': MAX_RECORDING_MS,
|
|
9143
|
+
'record_min_ms': 0,
|
|
9020
9144
|
'record_sessions_percent': 0,
|
|
9021
9145
|
'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
|
|
9022
9146
|
};
|
|
@@ -9265,15 +9389,35 @@
|
|
|
9265
9389
|
|
|
9266
9390
|
MixpanelLib.prototype.get_session_recording_properties = function () {
|
|
9267
9391
|
var props = {};
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
props['$mp_replay_id'] = replay_id;
|
|
9272
|
-
}
|
|
9392
|
+
var replay_id = this._get_session_replay_id();
|
|
9393
|
+
if (replay_id) {
|
|
9394
|
+
props['$mp_replay_id'] = replay_id;
|
|
9273
9395
|
}
|
|
9274
9396
|
return props;
|
|
9275
9397
|
};
|
|
9276
9398
|
|
|
9399
|
+
MixpanelLib.prototype.get_session_replay_url = function () {
|
|
9400
|
+
var replay_url = null;
|
|
9401
|
+
var replay_id = this._get_session_replay_id();
|
|
9402
|
+
if (replay_id) {
|
|
9403
|
+
var query_params = _.HTTPBuildQuery({
|
|
9404
|
+
'replay_id': replay_id,
|
|
9405
|
+
'distinct_id': this.get_distinct_id(),
|
|
9406
|
+
'token': this.get_config('token')
|
|
9407
|
+
});
|
|
9408
|
+
replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
|
|
9409
|
+
}
|
|
9410
|
+
return replay_url;
|
|
9411
|
+
};
|
|
9412
|
+
|
|
9413
|
+
MixpanelLib.prototype._get_session_replay_id = function () {
|
|
9414
|
+
var replay_id = null;
|
|
9415
|
+
if (this._recorder) {
|
|
9416
|
+
replay_id = this._recorder['replayId'];
|
|
9417
|
+
}
|
|
9418
|
+
return replay_id || null;
|
|
9419
|
+
};
|
|
9420
|
+
|
|
9277
9421
|
// Private methods
|
|
9278
9422
|
|
|
9279
9423
|
MixpanelLib.prototype._loaded = function() {
|
|
@@ -11000,6 +11144,7 @@
|
|
|
11000
11144
|
MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
|
|
11001
11145
|
MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
|
|
11002
11146
|
MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
|
|
11147
|
+
MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
|
|
11003
11148
|
MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
|
|
11004
11149
|
|
|
11005
11150
|
// MixpanelPersistence Exports
|
package/package.json
CHANGED