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
|
@@ -4510,7 +4510,7 @@
|
|
|
4510
4510
|
|
|
4511
4511
|
var Config = {
|
|
4512
4512
|
DEBUG: false,
|
|
4513
|
-
LIB_VERSION: '2.
|
|
4513
|
+
LIB_VERSION: '2.56.0'
|
|
4514
4514
|
};
|
|
4515
4515
|
|
|
4516
4516
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
@@ -4522,7 +4522,7 @@
|
|
|
4522
4522
|
hostname: ''
|
|
4523
4523
|
};
|
|
4524
4524
|
win = {
|
|
4525
|
-
navigator: { userAgent: '' },
|
|
4525
|
+
navigator: { userAgent: '', onLine: true },
|
|
4526
4526
|
document: {
|
|
4527
4527
|
location: loc,
|
|
4528
4528
|
referrer: ''
|
|
@@ -4536,6 +4536,8 @@
|
|
|
4536
4536
|
|
|
4537
4537
|
// Maximum allowed session recording length
|
|
4538
4538
|
var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
4539
|
+
// Maximum allowed value for minimum session recording length
|
|
4540
|
+
var MAX_VALUE_FOR_MIN_RECORDING_MS = 8 * 1000; // 8 seconds
|
|
4539
4541
|
|
|
4540
4542
|
/*
|
|
4541
4543
|
* Saved references to long variable names, so that closure compiler can
|
|
@@ -5447,7 +5449,7 @@
|
|
|
5447
5449
|
_.getQueryParam = function(url, param) {
|
|
5448
5450
|
// Expects a raw URL
|
|
5449
5451
|
|
|
5450
|
-
param = param.replace(/[[]
|
|
5452
|
+
param = param.replace(/[[]/g, '\\[').replace(/[\]]/g, '\\]');
|
|
5451
5453
|
var regexS = '[\\?&]' + param + '=([^&#]*)',
|
|
5452
5454
|
regex = new RegExp(regexS),
|
|
5453
5455
|
results = regex.exec(url);
|
|
@@ -5904,8 +5906,8 @@
|
|
|
5904
5906
|
};
|
|
5905
5907
|
})();
|
|
5906
5908
|
|
|
5907
|
-
var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
|
|
5908
|
-
var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid'];
|
|
5909
|
+
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'];
|
|
5910
|
+
var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid'];
|
|
5909
5911
|
|
|
5910
5912
|
_.info = {
|
|
5911
5913
|
campaignParams: function(default_value) {
|
|
@@ -6187,6 +6189,15 @@
|
|
|
6187
6189
|
return matches ? matches[0] : '';
|
|
6188
6190
|
};
|
|
6189
6191
|
|
|
6192
|
+
/**
|
|
6193
|
+
* Check whether we have network connection. default to true for browsers that don't support navigator.onLine (IE)
|
|
6194
|
+
* @returns {boolean}
|
|
6195
|
+
*/
|
|
6196
|
+
var isOnline = function() {
|
|
6197
|
+
var onLine = win.navigator['onLine'];
|
|
6198
|
+
return _.isUndefined(onLine) || onLine;
|
|
6199
|
+
};
|
|
6200
|
+
|
|
6190
6201
|
var JSONStringify = null, JSONParse = null;
|
|
6191
6202
|
if (typeof JSON !== 'undefined') {
|
|
6192
6203
|
JSONStringify = JSON.stringify;
|
|
@@ -6375,7 +6386,7 @@
|
|
|
6375
6386
|
};
|
|
6376
6387
|
}
|
|
6377
6388
|
|
|
6378
|
-
var logger$
|
|
6389
|
+
var logger$4 = console_with_prefix('lock');
|
|
6379
6390
|
|
|
6380
6391
|
/**
|
|
6381
6392
|
* SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
|
|
@@ -6432,7 +6443,7 @@
|
|
|
6432
6443
|
|
|
6433
6444
|
var delay = function(cb) {
|
|
6434
6445
|
if (new Date().getTime() - startTime > timeoutMS) {
|
|
6435
|
-
logger$
|
|
6446
|
+
logger$4.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
|
|
6436
6447
|
storage.removeItem(keyZ);
|
|
6437
6448
|
storage.removeItem(keyY);
|
|
6438
6449
|
loop();
|
|
@@ -6521,7 +6532,7 @@
|
|
|
6521
6532
|
}
|
|
6522
6533
|
};
|
|
6523
6534
|
|
|
6524
|
-
var logger$
|
|
6535
|
+
var logger$3 = console_with_prefix('batch');
|
|
6525
6536
|
|
|
6526
6537
|
/**
|
|
6527
6538
|
* RequestQueue: queue for batching API requests with localStorage backup for retries.
|
|
@@ -6542,11 +6553,13 @@
|
|
|
6542
6553
|
var RequestQueue = function(storageKey, options) {
|
|
6543
6554
|
options = options || {};
|
|
6544
6555
|
this.storageKey = storageKey;
|
|
6545
|
-
this.storage = options.storage || window.localStorage;
|
|
6546
|
-
this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2);
|
|
6547
|
-
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
6548
|
-
|
|
6549
6556
|
this.usePersistence = options.usePersistence;
|
|
6557
|
+
if (this.usePersistence) {
|
|
6558
|
+
this.storage = options.storage || window.localStorage;
|
|
6559
|
+
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
6560
|
+
}
|
|
6561
|
+
this.reportError = options.errorReporter || _.bind(logger$3.error, logger$3);
|
|
6562
|
+
|
|
6550
6563
|
this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
|
|
6551
6564
|
|
|
6552
6565
|
this.memQueue = [];
|
|
@@ -6825,7 +6838,7 @@
|
|
|
6825
6838
|
// maximum interval between request retries after exponential backoff
|
|
6826
6839
|
var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
|
|
6827
6840
|
|
|
6828
|
-
var logger$
|
|
6841
|
+
var logger$2 = console_with_prefix('batch');
|
|
6829
6842
|
|
|
6830
6843
|
/**
|
|
6831
6844
|
* RequestBatcher: manages the queueing, flushing, retry etc of requests of one
|
|
@@ -6939,7 +6952,7 @@
|
|
|
6939
6952
|
try {
|
|
6940
6953
|
|
|
6941
6954
|
if (this.requestInProgress) {
|
|
6942
|
-
logger$
|
|
6955
|
+
logger$2.log('Flush: Request already in progress');
|
|
6943
6956
|
return;
|
|
6944
6957
|
}
|
|
6945
6958
|
|
|
@@ -7018,7 +7031,12 @@
|
|
|
7018
7031
|
this.flush();
|
|
7019
7032
|
} else if (
|
|
7020
7033
|
_.isObject(res) &&
|
|
7021
|
-
(
|
|
7034
|
+
(
|
|
7035
|
+
res.httpStatusCode >= 500
|
|
7036
|
+
|| res.httpStatusCode === 429
|
|
7037
|
+
|| (res.httpStatusCode <= 0 && !isOnline())
|
|
7038
|
+
|| res.error === 'timeout'
|
|
7039
|
+
)
|
|
7022
7040
|
) {
|
|
7023
7041
|
// network or API error, or 429 Too Many Requests, retry
|
|
7024
7042
|
var retryMS = this.flushInterval * 2;
|
|
@@ -7102,7 +7120,7 @@
|
|
|
7102
7120
|
if (options.unloading) {
|
|
7103
7121
|
requestOptions.transport = 'sendBeacon';
|
|
7104
7122
|
}
|
|
7105
|
-
logger$
|
|
7123
|
+
logger$2.log('MIXPANEL REQUEST:', dataForRequest);
|
|
7106
7124
|
this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
|
|
7107
7125
|
} catch(err) {
|
|
7108
7126
|
this.reportError('Error flushing request queue', err);
|
|
@@ -7114,7 +7132,7 @@
|
|
|
7114
7132
|
* Log error to global logger and optional user-defined logger.
|
|
7115
7133
|
*/
|
|
7116
7134
|
RequestBatcher.prototype.reportError = function(msg, err) {
|
|
7117
|
-
logger$
|
|
7135
|
+
logger$2.error.apply(logger$2.error, arguments);
|
|
7118
7136
|
if (this.errorReporter) {
|
|
7119
7137
|
try {
|
|
7120
7138
|
if (!(err instanceof Error)) {
|
|
@@ -7122,12 +7140,12 @@
|
|
|
7122
7140
|
}
|
|
7123
7141
|
this.errorReporter(msg, err);
|
|
7124
7142
|
} catch(err) {
|
|
7125
|
-
logger$
|
|
7143
|
+
logger$2.error(err);
|
|
7126
7144
|
}
|
|
7127
7145
|
}
|
|
7128
7146
|
};
|
|
7129
7147
|
|
|
7130
|
-
var logger = console_with_prefix('recorder');
|
|
7148
|
+
var logger$1 = console_with_prefix('recorder');
|
|
7131
7149
|
var CompressionStream = win['CompressionStream'];
|
|
7132
7150
|
|
|
7133
7151
|
var RECORDER_BATCHER_LIB_CONFIG = {
|
|
@@ -7153,63 +7171,85 @@
|
|
|
7153
7171
|
return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
|
|
7154
7172
|
}
|
|
7155
7173
|
|
|
7156
|
-
|
|
7157
|
-
|
|
7174
|
+
/**
|
|
7175
|
+
* This class encapsulates a single session recording and its lifecycle.
|
|
7176
|
+
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7177
|
+
* @param {String} [options.replayId] - unique uuid for a single replay
|
|
7178
|
+
* @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
7179
|
+
* @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
7180
|
+
* @param {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
7181
|
+
*/
|
|
7182
|
+
var SessionRecording = function(options) {
|
|
7183
|
+
this._mixpanel = options.mixpanelInstance;
|
|
7184
|
+
this._onIdleTimeout = options.onIdleTimeout;
|
|
7185
|
+
this._onMaxLengthReached = options.onMaxLengthReached;
|
|
7186
|
+
this._rrwebRecord = options.rrwebRecord;
|
|
7187
|
+
|
|
7188
|
+
this.replayId = options.replayId;
|
|
7158
7189
|
|
|
7159
7190
|
// internal rrweb stopRecording function
|
|
7160
7191
|
this._stopRecording = null;
|
|
7161
7192
|
|
|
7162
|
-
this.recEvents = [];
|
|
7163
7193
|
this.seqNo = 0;
|
|
7164
|
-
this.replayId = null;
|
|
7165
7194
|
this.replayStartTime = null;
|
|
7166
|
-
this.
|
|
7195
|
+
this.batchStartUrl = null;
|
|
7167
7196
|
|
|
7168
7197
|
this.idleTimeoutId = null;
|
|
7169
7198
|
this.maxTimeoutId = null;
|
|
7170
7199
|
|
|
7171
7200
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7172
|
-
this.
|
|
7173
|
-
};
|
|
7174
|
-
|
|
7201
|
+
this.recordMinMs = 0;
|
|
7175
7202
|
|
|
7176
|
-
|
|
7177
|
-
this
|
|
7178
|
-
|
|
7179
|
-
|
|
7203
|
+
// each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
|
|
7204
|
+
// this will be important when persistence is introduced
|
|
7205
|
+
var batcherKey = '__mprec_' + this.getConfig('token') + '_' + this.replayId;
|
|
7206
|
+
this.batcher = new RequestBatcher(batcherKey, {
|
|
7180
7207
|
errorReporter: _.bind(this.reportError, this),
|
|
7181
7208
|
flushOnlyOnInterval: true,
|
|
7209
|
+
libConfig: RECORDER_BATCHER_LIB_CONFIG,
|
|
7210
|
+
sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
|
|
7182
7211
|
usePersistence: false
|
|
7183
7212
|
});
|
|
7184
7213
|
};
|
|
7185
7214
|
|
|
7186
|
-
|
|
7187
|
-
MixpanelRecorder.prototype.get_config = function(configVar) {
|
|
7215
|
+
SessionRecording.prototype.getConfig = function(configVar) {
|
|
7188
7216
|
return this._mixpanel.get_config(configVar);
|
|
7189
7217
|
};
|
|
7190
7218
|
|
|
7191
|
-
|
|
7219
|
+
// Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
|
|
7220
|
+
// reaches into this class instance and expects the snake case version of the function.
|
|
7221
|
+
// eslint-disable-next-line camelcase
|
|
7222
|
+
SessionRecording.prototype.get_config = function(configVar) {
|
|
7223
|
+
return this.getConfig(configVar);
|
|
7224
|
+
};
|
|
7225
|
+
|
|
7226
|
+
SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
7192
7227
|
if (this._stopRecording !== null) {
|
|
7193
|
-
logger.log('Recording already in progress, skipping startRecording.');
|
|
7228
|
+
logger$1.log('Recording already in progress, skipping startRecording.');
|
|
7194
7229
|
return;
|
|
7195
7230
|
}
|
|
7196
7231
|
|
|
7197
|
-
this.recordMaxMs = this.
|
|
7232
|
+
this.recordMaxMs = this.getConfig('record_max_ms');
|
|
7198
7233
|
if (this.recordMaxMs > MAX_RECORDING_MS) {
|
|
7199
7234
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7200
|
-
logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7235
|
+
logger$1.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7201
7236
|
}
|
|
7202
7237
|
|
|
7203
|
-
this.
|
|
7204
|
-
this.
|
|
7205
|
-
|
|
7238
|
+
this.recordMinMs = this.getConfig('record_min_ms');
|
|
7239
|
+
if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
|
|
7240
|
+
this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
|
|
7241
|
+
logger$1.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
7242
|
+
}
|
|
7206
7243
|
|
|
7207
|
-
this.
|
|
7244
|
+
this.replayStartTime = new Date().getTime();
|
|
7245
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
7208
7246
|
|
|
7209
|
-
if (shouldStopBatcher) {
|
|
7210
|
-
//
|
|
7247
|
+
if (shouldStopBatcher || this.recordMinMs > 0) {
|
|
7248
|
+
// the primary case for shouldStopBatcher is when we're starting recording after a reset
|
|
7211
7249
|
// and don't want to send anything over the network until there's
|
|
7212
7250
|
// actual user activity
|
|
7251
|
+
// this also applies if the minimum recording length has not been hit yet
|
|
7252
|
+
// so that we don't send data until we know the recording will be long enough
|
|
7213
7253
|
this.batcher.stop();
|
|
7214
7254
|
} else {
|
|
7215
7255
|
this.batcher.start();
|
|
@@ -7217,45 +7257,52 @@
|
|
|
7217
7257
|
|
|
7218
7258
|
var resetIdleTimeout = _.bind(function () {
|
|
7219
7259
|
clearTimeout(this.idleTimeoutId);
|
|
7220
|
-
this.idleTimeoutId = setTimeout(
|
|
7221
|
-
logger.log('Idle timeout reached, restarting recording.');
|
|
7222
|
-
this.resetRecording();
|
|
7223
|
-
}, this), this.get_config('record_idle_timeout_ms'));
|
|
7260
|
+
this.idleTimeoutId = setTimeout(this._onIdleTimeout, this.getConfig('record_idle_timeout_ms'));
|
|
7224
7261
|
}, this);
|
|
7225
7262
|
|
|
7226
|
-
|
|
7263
|
+
var blockSelector = this.getConfig('record_block_selector');
|
|
7264
|
+
if (blockSelector === '' || blockSelector === null) {
|
|
7265
|
+
blockSelector = undefined;
|
|
7266
|
+
}
|
|
7267
|
+
|
|
7268
|
+
this._stopRecording = this._rrwebRecord({
|
|
7227
7269
|
'emit': _.bind(function (ev) {
|
|
7228
7270
|
this.batcher.enqueue(ev);
|
|
7229
7271
|
if (isUserEvent(ev)) {
|
|
7230
|
-
if (this.batcher.stopped) {
|
|
7272
|
+
if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
|
|
7231
7273
|
// start flushing again after user activity
|
|
7232
7274
|
this.batcher.start();
|
|
7233
7275
|
}
|
|
7234
7276
|
resetIdleTimeout();
|
|
7235
7277
|
}
|
|
7236
7278
|
}, this),
|
|
7237
|
-
'blockClass': this.
|
|
7238
|
-
'blockSelector':
|
|
7239
|
-
'collectFonts': this.
|
|
7240
|
-
'inlineImages': this.get_config('record_inline_images'),
|
|
7279
|
+
'blockClass': this.getConfig('record_block_class'),
|
|
7280
|
+
'blockSelector': blockSelector,
|
|
7281
|
+
'collectFonts': this.getConfig('record_collect_fonts'),
|
|
7241
7282
|
'maskAllInputs': true,
|
|
7242
|
-
'maskTextClass': this.
|
|
7243
|
-
'maskTextSelector': this.
|
|
7283
|
+
'maskTextClass': this.getConfig('record_mask_text_class'),
|
|
7284
|
+
'maskTextSelector': this.getConfig('record_mask_text_selector')
|
|
7244
7285
|
});
|
|
7245
7286
|
|
|
7246
|
-
|
|
7287
|
+
if (typeof this._stopRecording !== 'function') {
|
|
7288
|
+
this.reportError('rrweb failed to start, skipping this recording.');
|
|
7289
|
+
this._stopRecording = null;
|
|
7290
|
+
this.stopRecording(); // stop batcher looping and any timeouts
|
|
7291
|
+
return;
|
|
7292
|
+
}
|
|
7247
7293
|
|
|
7248
|
-
|
|
7249
|
-
};
|
|
7294
|
+
resetIdleTimeout();
|
|
7250
7295
|
|
|
7251
|
-
|
|
7252
|
-
this.stopRecording();
|
|
7253
|
-
this.startRecording(true);
|
|
7296
|
+
this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
|
|
7254
7297
|
};
|
|
7255
7298
|
|
|
7256
|
-
|
|
7257
|
-
if (this.
|
|
7258
|
-
|
|
7299
|
+
SessionRecording.prototype.stopRecording = function () {
|
|
7300
|
+
if (!this.isRrwebStopped()) {
|
|
7301
|
+
try {
|
|
7302
|
+
this._stopRecording();
|
|
7303
|
+
} catch (err) {
|
|
7304
|
+
this.reportError('Error with rrweb stopRecording', err);
|
|
7305
|
+
}
|
|
7259
7306
|
this._stopRecording = null;
|
|
7260
7307
|
}
|
|
7261
7308
|
|
|
@@ -7267,36 +7314,39 @@
|
|
|
7267
7314
|
this.batcher.flush();
|
|
7268
7315
|
this.batcher.stop();
|
|
7269
7316
|
}
|
|
7270
|
-
this.replayId = null;
|
|
7271
7317
|
|
|
7272
7318
|
clearTimeout(this.idleTimeoutId);
|
|
7273
7319
|
clearTimeout(this.maxTimeoutId);
|
|
7274
7320
|
};
|
|
7275
7321
|
|
|
7322
|
+
SessionRecording.prototype.isRrwebStopped = function () {
|
|
7323
|
+
return this._stopRecording === null;
|
|
7324
|
+
};
|
|
7325
|
+
|
|
7276
7326
|
/**
|
|
7277
7327
|
* Flushes the current batch of events to the server, but passes an opt-out callback to make sure
|
|
7278
7328
|
* we stop recording and dump any queued events if the user has opted out.
|
|
7279
7329
|
*/
|
|
7280
|
-
|
|
7330
|
+
SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
7281
7331
|
this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
|
|
7282
7332
|
};
|
|
7283
7333
|
|
|
7284
|
-
|
|
7334
|
+
SessionRecording.prototype._onOptOut = function (code) {
|
|
7285
7335
|
// addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
|
|
7286
7336
|
if (code === 0) {
|
|
7287
|
-
this.recEvents = [];
|
|
7288
7337
|
this.stopRecording();
|
|
7289
7338
|
}
|
|
7290
7339
|
};
|
|
7291
7340
|
|
|
7292
|
-
|
|
7341
|
+
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
7293
7342
|
var onSuccess = _.bind(function (response, responseBody) {
|
|
7294
|
-
//
|
|
7343
|
+
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
7295
7344
|
// RequestBatcher will always flush the next batch after the previous one succeeds.
|
|
7296
|
-
if
|
|
7345
|
+
// extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
|
|
7346
|
+
if (response.status === 200 && this.replayId === currentReplayId) {
|
|
7297
7347
|
this.seqNo++;
|
|
7348
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
7298
7349
|
}
|
|
7299
|
-
|
|
7300
7350
|
callback({
|
|
7301
7351
|
status: 0,
|
|
7302
7352
|
httpStatusCode: response.status,
|
|
@@ -7305,10 +7355,10 @@
|
|
|
7305
7355
|
});
|
|
7306
7356
|
}, this);
|
|
7307
7357
|
|
|
7308
|
-
win['fetch'](this.
|
|
7358
|
+
win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
7309
7359
|
'method': 'POST',
|
|
7310
7360
|
'headers': {
|
|
7311
|
-
'Authorization': 'Basic ' + btoa(this.
|
|
7361
|
+
'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
|
|
7312
7362
|
'Content-Type': 'application/octet-stream'
|
|
7313
7363
|
},
|
|
7314
7364
|
'body': reqBody,
|
|
@@ -7319,28 +7369,37 @@
|
|
|
7319
7369
|
callback({error: error});
|
|
7320
7370
|
});
|
|
7321
7371
|
}).catch(function (error) {
|
|
7322
|
-
callback({error: error});
|
|
7372
|
+
callback({error: error, httpStatusCode: 0});
|
|
7323
7373
|
});
|
|
7324
7374
|
};
|
|
7325
7375
|
|
|
7326
|
-
|
|
7376
|
+
SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
7327
7377
|
const numEvents = data.length;
|
|
7328
7378
|
|
|
7329
7379
|
if (numEvents > 0) {
|
|
7380
|
+
var replayId = this.replayId;
|
|
7330
7381
|
// each rrweb event has a timestamp - leverage those to get time properties
|
|
7331
7382
|
var batchStartTime = data[0].timestamp;
|
|
7332
|
-
if (this.seqNo === 0) {
|
|
7383
|
+
if (this.seqNo === 0 || !this.replayStartTime) {
|
|
7384
|
+
// extra safety net so that we don't send a null replay start time
|
|
7385
|
+
if (this.seqNo !== 0) {
|
|
7386
|
+
this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
|
|
7387
|
+
}
|
|
7388
|
+
|
|
7333
7389
|
this.replayStartTime = batchStartTime;
|
|
7334
7390
|
}
|
|
7335
7391
|
var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
|
|
7336
7392
|
|
|
7337
7393
|
var reqParams = {
|
|
7338
|
-
'
|
|
7339
|
-
'
|
|
7394
|
+
'$current_url': this.batchStartUrl,
|
|
7395
|
+
'$lib_version': Config.LIB_VERSION,
|
|
7340
7396
|
'batch_start_time': batchStartTime / 1000,
|
|
7341
|
-
'
|
|
7397
|
+
'distinct_id': String(this._mixpanel.get_distinct_id()),
|
|
7398
|
+
'mp_lib': 'web',
|
|
7399
|
+
'replay_id': replayId,
|
|
7342
7400
|
'replay_length_ms': replayLengthMs,
|
|
7343
|
-
'replay_start_time': this.replayStartTime / 1000
|
|
7401
|
+
'replay_start_time': this.replayStartTime / 1000,
|
|
7402
|
+
'seq': this.seqNo
|
|
7344
7403
|
};
|
|
7345
7404
|
var eventsJson = _.JSONEncode(data);
|
|
7346
7405
|
|
|
@@ -7361,28 +7420,93 @@
|
|
|
7361
7420
|
.blob()
|
|
7362
7421
|
.then(_.bind(function(compressedBlob) {
|
|
7363
7422
|
reqParams['format'] = 'gzip';
|
|
7364
|
-
this._sendRequest(reqParams, compressedBlob, callback);
|
|
7423
|
+
this._sendRequest(replayId, reqParams, compressedBlob, callback);
|
|
7365
7424
|
}, this));
|
|
7366
7425
|
} else {
|
|
7367
7426
|
reqParams['format'] = 'body';
|
|
7368
|
-
this._sendRequest(reqParams, eventsJson, callback);
|
|
7427
|
+
this._sendRequest(replayId, reqParams, eventsJson, callback);
|
|
7369
7428
|
}
|
|
7370
7429
|
}
|
|
7371
7430
|
});
|
|
7372
7431
|
|
|
7373
7432
|
|
|
7374
|
-
|
|
7375
|
-
logger.error.apply(logger.error, arguments);
|
|
7433
|
+
SessionRecording.prototype.reportError = function(msg, err) {
|
|
7434
|
+
logger$1.error.apply(logger$1.error, arguments);
|
|
7376
7435
|
try {
|
|
7377
7436
|
if (!err && !(msg instanceof Error)) {
|
|
7378
7437
|
msg = new Error(msg);
|
|
7379
7438
|
}
|
|
7380
|
-
this.
|
|
7439
|
+
this.getConfig('error_reporter')(msg, err);
|
|
7381
7440
|
} catch(err) {
|
|
7382
|
-
logger.error(err);
|
|
7441
|
+
logger$1.error(err);
|
|
7442
|
+
}
|
|
7443
|
+
};
|
|
7444
|
+
|
|
7445
|
+
var logger = console_with_prefix('recorder');
|
|
7446
|
+
|
|
7447
|
+
/**
|
|
7448
|
+
* Recorder API: manages recordings and exposes methods public to the core Mixpanel library.
|
|
7449
|
+
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7450
|
+
*/
|
|
7451
|
+
var MixpanelRecorder = function(mixpanelInstance) {
|
|
7452
|
+
this._mixpanel = mixpanelInstance;
|
|
7453
|
+
this.activeRecording = null;
|
|
7454
|
+
};
|
|
7455
|
+
|
|
7456
|
+
MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
|
|
7457
|
+
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
7458
|
+
logger.log('Recording already in progress, skipping startRecording.');
|
|
7459
|
+
return;
|
|
7460
|
+
}
|
|
7461
|
+
|
|
7462
|
+
var onIdleTimeout = _.bind(function () {
|
|
7463
|
+
logger.log('Idle timeout reached, restarting recording.');
|
|
7464
|
+
this.resetRecording();
|
|
7465
|
+
}, this);
|
|
7466
|
+
|
|
7467
|
+
var onMaxLengthReached = _.bind(function () {
|
|
7468
|
+
logger.log('Max recording length reached, stopping recording.');
|
|
7469
|
+
this.resetRecording();
|
|
7470
|
+
}, this);
|
|
7471
|
+
|
|
7472
|
+
this.activeRecording = new SessionRecording({
|
|
7473
|
+
mixpanelInstance: this._mixpanel,
|
|
7474
|
+
onIdleTimeout: onIdleTimeout,
|
|
7475
|
+
onMaxLengthReached: onMaxLengthReached,
|
|
7476
|
+
replayId: _.UUID(),
|
|
7477
|
+
rrwebRecord: record
|
|
7478
|
+
});
|
|
7479
|
+
|
|
7480
|
+
this.activeRecording.startRecording(shouldStopBatcher);
|
|
7481
|
+
};
|
|
7482
|
+
|
|
7483
|
+
MixpanelRecorder.prototype.stopRecording = function() {
|
|
7484
|
+
if (this.activeRecording) {
|
|
7485
|
+
this.activeRecording.stopRecording();
|
|
7486
|
+
this.activeRecording = null;
|
|
7383
7487
|
}
|
|
7384
7488
|
};
|
|
7385
7489
|
|
|
7490
|
+
MixpanelRecorder.prototype.resetRecording = function () {
|
|
7491
|
+
this.stopRecording();
|
|
7492
|
+
this.startRecording(true);
|
|
7493
|
+
};
|
|
7494
|
+
|
|
7495
|
+
MixpanelRecorder.prototype.getActiveReplayId = function () {
|
|
7496
|
+
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
7497
|
+
return this.activeRecording.replayId;
|
|
7498
|
+
} else {
|
|
7499
|
+
return null;
|
|
7500
|
+
}
|
|
7501
|
+
};
|
|
7502
|
+
|
|
7503
|
+
// getter so that older mixpanel-core versions can still retrieve the replay ID
|
|
7504
|
+
// when pulling the latest recorder bundle from the CDN
|
|
7505
|
+
Object.defineProperty(MixpanelRecorder.prototype, 'replayId', {
|
|
7506
|
+
get: function () {
|
|
7507
|
+
return this.getActiveReplayId();
|
|
7508
|
+
}
|
|
7509
|
+
});
|
|
7386
7510
|
|
|
7387
7511
|
win['__mp_recorder'] = MixpanelRecorder;
|
|
7388
7512
|
|