|
4510 | 4510 |
|
4511 | 4511 | var Config = {
|
4512 | 4512 | DEBUG: false,
|
4513 |
| - LIB_VERSION: '2.55.0' |
| 4513 | + LIB_VERSION: '2.55.1' |
4514 | 4514 | };
|
4515 | 4515 |
|
4516 | 4516 | /* eslint camelcase: "off", eqeqeq: "off" */
|
|
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 | 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 | 5449 | _.getQueryParam = function(url, param) {
|
5448 | 5450 | // Expects a raw URL
|
5449 | 5451 |
|
5450 |
| - param = param.replace(/[[]/, '\\[').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 | 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 | 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;
|
|
7018 | 7029 | this.flush();
|
7019 | 7030 | } else if (
|
7020 | 7031 | _.isObject(res) &&
|
7021 |
| - (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') |
| 7032 | + ( |
| 7033 | + res.httpStatusCode >= 500 |
| 7034 | + || res.httpStatusCode === 429 |
| 7035 | + || (res.httpStatusCode <= 0 && !isOnline()) |
| 7036 | + || res.error === 'timeout' |
| 7037 | + ) |
7022 | 7038 | ) {
|
7023 | 7039 | // network or API error, or 429 Too Many Requests, retry
|
7024 | 7040 | var retryMS = this.flushInterval * 2;
|
|
7169 | 7185 | this.maxTimeoutId = null;
|
7170 | 7186 |
|
7171 | 7187 | this.recordMaxMs = MAX_RECORDING_MS;
|
| 7188 | + this.recordMinMs = 0; |
7172 | 7189 | this._initBatcher();
|
7173 | 7190 | };
|
7174 | 7191 |
|
|
7200 | 7217 | logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
7201 | 7218 | }
|
7202 | 7219 |
|
| 7220 | + this.recordMinMs = this.get_config('record_min_ms'); |
| 7221 | + if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) { |
| 7222 | + this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS; |
| 7223 | + logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.'); |
| 7224 | + } |
| 7225 | + |
7203 | 7226 | this.recEvents = [];
|
7204 | 7227 | this.seqNo = 0;
|
7205 |
| - this.replayStartTime = null; |
| 7228 | + this.replayStartTime = new Date().getTime(); |
7206 | 7229 |
|
7207 | 7230 | this.replayId = _.UUID();
|
7208 | 7231 |
|
7209 |
| - if (shouldStopBatcher) { |
7210 |
| - // this is the case when we're starting recording after a reset |
| 7232 | + if (shouldStopBatcher || this.recordMinMs > 0) { |
| 7233 | + // the primary case for shouldStopBatcher is when we're starting recording after a reset |
7211 | 7234 | // and don't want to send anything over the network until there's
|
7212 | 7235 | // actual user activity
|
| 7236 | + // this also applies if the minimum recording length has not been hit yet |
| 7237 | + // so that we don't send data until we know the recording will be long enough |
7213 | 7238 | this.batcher.stop();
|
7214 | 7239 | } else {
|
7215 | 7240 | this.batcher.start();
|
|
7223 | 7248 | }, this), this.get_config('record_idle_timeout_ms'));
|
7224 | 7249 | }, this);
|
7225 | 7250 |
|
| 7251 | + var blockSelector = this.get_config('record_block_selector'); |
| 7252 | + if (blockSelector === '' || blockSelector === null) { |
| 7253 | + blockSelector = undefined; |
| 7254 | + } |
| 7255 | + |
7226 | 7256 | this._stopRecording = record({
|
7227 | 7257 | 'emit': _.bind(function (ev) {
|
7228 | 7258 | this.batcher.enqueue(ev);
|
7229 | 7259 | if (isUserEvent(ev)) {
|
7230 |
| - if (this.batcher.stopped) { |
| 7260 | + if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) { |
7231 | 7261 | // start flushing again after user activity
|
7232 | 7262 | this.batcher.start();
|
7233 | 7263 | }
|
7234 | 7264 | resetIdleTimeout();
|
7235 | 7265 | }
|
7236 | 7266 | }, this),
|
7237 | 7267 | 'blockClass': this.get_config('record_block_class'),
|
7238 |
| - 'blockSelector': this.get_config('record_block_selector'), |
| 7268 | + 'blockSelector': blockSelector, |
7239 | 7269 | 'collectFonts': this.get_config('record_collect_fonts'),
|
7240 | 7270 | 'inlineImages': this.get_config('record_inline_images'),
|
7241 | 7271 | 'maskAllInputs': true,
|
|
7289 | 7319 | }
|
7290 | 7320 | };
|
7291 | 7321 |
|
7292 |
| - MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { |
| 7322 | + MixpanelRecorder.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) { |
7293 | 7323 | var onSuccess = _.bind(function (response, responseBody) {
|
7294 | 7324 | // Increment sequence counter only if the request was successful to guarantee ordering.
|
7295 | 7325 | // RequestBatcher will always flush the next batch after the previous one succeeds.
|
7296 |
| - if (response.status === 200) { |
| 7326 | + // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay |
| 7327 | + if (response.status === 200 && this.replayId === currentReplayId) { |
7297 | 7328 | this.seqNo++;
|
7298 | 7329 | }
|
7299 |
| - |
7300 | 7330 | callback({
|
7301 | 7331 | status: 0,
|
7302 | 7332 | httpStatusCode: response.status,
|
|
7319 | 7349 | callback({error: error});
|
7320 | 7350 | });
|
7321 | 7351 | }).catch(function (error) {
|
7322 |
| - callback({error: error}); |
| 7352 | + callback({error: error, httpStatusCode: 0}); |
7323 | 7353 | });
|
7324 | 7354 | };
|
7325 | 7355 |
|
7326 | 7356 | MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
7327 | 7357 | const numEvents = data.length;
|
7328 | 7358 |
|
7329 | 7359 | if (numEvents > 0) {
|
| 7360 | + var replayId = this.replayId; |
7330 | 7361 | // each rrweb event has a timestamp - leverage those to get time properties
|
7331 | 7362 | var batchStartTime = data[0].timestamp;
|
7332 |
| - if (this.seqNo === 0) { |
| 7363 | + if (this.seqNo === 0 || !this.replayStartTime) { |
| 7364 | + // extra safety net so that we don't send a null replay start time |
| 7365 | + if (this.seqNo !== 0) { |
| 7366 | + this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.'); |
| 7367 | + } |
| 7368 | + |
7333 | 7369 | this.replayStartTime = batchStartTime;
|
7334 | 7370 | }
|
7335 | 7371 | var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
|
|
7338 | 7374 | 'distinct_id': String(this._mixpanel.get_distinct_id()),
|
7339 | 7375 | 'seq': this.seqNo,
|
7340 | 7376 | 'batch_start_time': batchStartTime / 1000,
|
7341 |
| - 'replay_id': this.replayId, |
| 7377 | + 'replay_id': replayId, |
7342 | 7378 | 'replay_length_ms': replayLengthMs,
|
7343 | 7379 | 'replay_start_time': this.replayStartTime / 1000
|
7344 | 7380 | };
|
|
7361 | 7397 | .blob()
|
7362 | 7398 | .then(_.bind(function(compressedBlob) {
|
7363 | 7399 | reqParams['format'] = 'gzip';
|
7364 |
| - this._sendRequest(reqParams, compressedBlob, callback); |
| 7400 | + this._sendRequest(replayId, reqParams, compressedBlob, callback); |
7365 | 7401 | }, this));
|
7366 | 7402 | } else {
|
7367 | 7403 | reqParams['format'] = 'body';
|
7368 |
| - this._sendRequest(reqParams, eventsJson, callback); |
| 7404 | + this._sendRequest(replayId, reqParams, eventsJson, callback); |
7369 | 7405 | }
|
7370 | 7406 | }
|
7371 | 7407 | });
|
|
0 commit comments