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